That fuck shit the fascists are using
1package org.tm.archive.devicetransfer;
2
3import android.content.ActivityNotFoundException;
4import android.content.Intent;
5import android.location.LocationManager;
6import android.net.wifi.WifiManager;
7import android.os.Bundle;
8import android.provider.Settings;
9import android.text.method.LinkMovementMethod;
10import android.view.View;
11import android.widget.TextView;
12import android.widget.Toast;
13
14import androidx.activity.OnBackPressedCallback;
15import androidx.annotation.NonNull;
16import androidx.annotation.Nullable;
17import androidx.annotation.StringRes;
18import androidx.constraintlayout.widget.Group;
19import androidx.core.content.ContextCompat;
20import androidx.lifecycle.ViewModelProvider;
21import androidx.navigation.fragment.NavHostFragment;
22
23import com.google.android.material.button.MaterialButton;
24import com.google.android.material.dialog.MaterialAlertDialogBuilder;
25
26import org.greenrobot.eventbus.EventBus;
27import org.greenrobot.eventbus.Subscribe;
28import org.greenrobot.eventbus.ThreadMode;
29import org.signal.core.util.ThreadUtil;
30import org.signal.core.util.logging.Log;
31import org.signal.devicetransfer.DeviceToDeviceTransferService;
32import org.signal.devicetransfer.TransferStatus;
33import org.signal.devicetransfer.WifiDirect;
34import org.tm.archive.LoggingFragment;
35import org.tm.archive.R;
36import org.tm.archive.logsubmit.SubmitDebugLogActivity;
37import org.tm.archive.permissions.Permissions;
38import org.tm.archive.util.CommunicationActions;
39import org.tm.archive.util.SpanUtil;
40import org.tm.archive.util.ViewUtil;
41
42import java.util.Locale;
43import java.util.concurrent.TimeUnit;
44
45/**
46 * Responsible for driving the UI of all the legwork to startup Wi-Fi Direct and
47 * establish the connection between the two devices. It's capable of being used by both
48 * the new and old device, but delegates some of the UI (mostly strings and navigation) to
49 * a subclass for old or new device.
50 * <p>
51 * Handles showing setup progress, verification codes, connecting states, error states, and troubleshooting.
52 * <p>
53 * It's state driven by the view model so it's easy to transition from step to step in the
54 * process.
55 */
56public abstract class DeviceTransferSetupFragment extends LoggingFragment {
57
58 private static final String TAG = Log.tag(DeviceTransferSetupFragment.class);
59
60 private static final long PREPARE_TAKING_TOO_LONG_TIME = TimeUnit.SECONDS.toMillis(30);
61 private static final long WAITING_TAKING_TOO_LONG_TIME = TimeUnit.SECONDS.toMillis(90);
62
63 private final OnBackPressed onBackPressed = new OnBackPressed();
64 private DeviceTransferSetupViewModel viewModel;
65 private Runnable takingTooLong;
66
67 public DeviceTransferSetupFragment() {
68 super(R.layout.device_transfer_setup_fragment);
69 }
70
71 @Override
72 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
73 Group progressGroup = view.findViewById(R.id.device_transfer_setup_fragment_progress_group);
74 Group errorGroup = view.findViewById(R.id.device_transfer_setup_fragment_error_group);
75 View verifyGroup = view.findViewById(R.id.device_transfer_setup_fragment_verify);
76 View waitingGroup = view.findViewById(R.id.device_transfer_setup_fragment_waiting);
77 View troubleshooting = view.findViewById(R.id.device_transfer_setup_fragment_troubleshooting);
78 TextView status = view.findViewById(R.id.device_transfer_setup_fragment_status);
79 TextView error = view.findViewById(R.id.device_transfer_setup_fragment_error);
80 MaterialButton errorResolve = view.findViewById(R.id.device_transfer_setup_fragment_error_resolve);
81 TextView sasNumber = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_code);
82 MaterialButton verifyNo = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_no);
83 MaterialButton verifyYes = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_yes);
84
85 viewModel = new ViewModelProvider(this).get(DeviceTransferSetupViewModel.class);
86
87 viewModel.getState().observe(getViewLifecycleOwner(), state -> {
88 SetupStep step = state.getCurrentSetupStep();
89
90 progressGroup.setVisibility(step.isProgress() ? View.VISIBLE : View.GONE);
91 errorGroup.setVisibility(step.isError() ? View.VISIBLE : View.GONE);
92 verifyGroup.setVisibility(step == SetupStep.VERIFY ? View.VISIBLE : View.GONE);
93 waitingGroup.setVisibility(step == SetupStep.WAITING_FOR_OTHER_TO_VERIFY ? View.VISIBLE : View.GONE);
94 troubleshooting.setVisibility(step == SetupStep.TROUBLESHOOTING ? View.VISIBLE : View.GONE);
95
96 Log.i(TAG, "Handling step: " + step.name());
97 switch (step) {
98 case INITIAL:
99 status.setText("");
100 case PERMISSIONS_CHECK:
101 requestRequiredPermission();
102 break;
103 case PERMISSIONS_DENIED:
104 error.setText(getErrorTextForStep(step));
105 errorResolve.setText(R.string.DeviceTransferSetup__grant_location_permission);
106 errorResolve.setOnClickListener(v -> viewModel.checkPermissions());
107 break;
108 case LOCATION_CHECK:
109 verifyLocationEnabled();
110 break;
111 case LOCATION_DISABLED:
112 error.setText(getErrorTextForStep(step));
113 errorResolve.setText(R.string.DeviceTransferSetup__turn_on_location_services);
114 errorResolve.setOnClickListener(v -> openLocationServices());
115 break;
116 case WIFI_CHECK:
117 verifyWifiEnabled();
118 break;
119 case WIFI_DISABLED:
120 error.setText(getErrorTextForStep(step));
121 errorResolve.setText(R.string.DeviceTransferSetup__turn_on_wifi);
122 errorResolve.setOnClickListener(v -> openWifiSettings());
123 break;
124 case WIFI_DIRECT_CHECK:
125 verifyWifiDirectAvailable();
126 break;
127 case WIFI_DIRECT_UNAVAILABLE:
128 error.setText(getErrorTextForStep(step));
129 errorResolve.setText(getErrorResolveButtonTextForStep(step));
130 errorResolve.setOnClickListener(v -> navigateWhenWifiDirectUnavailable());
131 break;
132 case START:
133 status.setText(getStatusTextForStep(SetupStep.SETTING_UP, false));
134 startTransfer();
135 break;
136 case SETTING_UP:
137 status.setText(getStatusTextForStep(step, false));
138 startTakingTooLong(() -> status.setText(getStatusTextForStep(step, true)), PREPARE_TAKING_TOO_LONG_TIME);
139 break;
140 case WAITING:
141 status.setText(getStatusTextForStep(step, false));
142 cancelTakingTooLong();
143 startTakingTooLong(() -> {
144 DeviceToDeviceTransferService.stop(requireContext());
145 viewModel.onWaitingTookTooLong();
146 }, WAITING_TAKING_TOO_LONG_TIME);
147 break;
148 case VERIFY:
149 cancelTakingTooLong();
150 sasNumber.setText(String.format(Locale.US, "%07d", state.getAuthenticationCode()));
151 //noinspection CodeBlock2Expr
152 verifyNo.setOnClickListener(v -> {
153 new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.DeviceTransferSetup__the_numbers_do_not_match)
154 .setMessage(R.string.DeviceTransferSetup__if_the_numbers_on_your_devices_do_not_match_its_possible_you_connected_to_the_wrong_device)
155 .setPositiveButton(R.string.DeviceTransferSetup__stop_transfer, (d, w) -> {
156 EventBus.getDefault().unregister(this);
157 DeviceToDeviceTransferService.setAuthenticationCodeVerified(requireContext(), false);
158 DeviceToDeviceTransferService.stop(requireContext());
159 EventBus.getDefault().removeStickyEvent(TransferStatus.class);
160 navigateAwayFromTransfer();
161 })
162 .setNegativeButton(android.R.string.cancel, null)
163 .show();
164 });
165 verifyYes.setOnClickListener(v -> {
166 DeviceToDeviceTransferService.setAuthenticationCodeVerified(requireContext(), true);
167 viewModel.onVerified();
168 });
169 break;
170 case WAITING_FOR_OTHER_TO_VERIFY:
171 break;
172 case CONNECTED:
173 Log.d(TAG, "Connected! isNotShutdown: " + viewModel.isNotShutdown());
174 if (viewModel.isNotShutdown()) {
175 navigateToTransferConnected();
176 }
177 break;
178 case TROUBLESHOOTING:
179 TextView title = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_title);
180 title.setText(getStatusTextForStep(step, false));
181
182 int gapWidth = ViewUtil.dpToPx(12);
183 TextView step1 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step1);
184 step1.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__make_sure_the_following_permissions_are_enabled), gapWidth));
185 TextView step2 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step2);
186 step2.setMovementMethod(LinkMovementMethod.getInstance());
187 step2.setText(SpanUtil.clickSubstring(requireContext(),
188 SpanUtil.bullet(getString(R.string.DeviceTransferSetup__on_the_wifi_direct_screen_remove_all_remembered_groups_and_unlink_any_invited_or_connected_devices), gapWidth),
189 getString(R.string.DeviceTransferSetup__wifi_direct_screen),
190 v -> openWifiDirectSettings()));
191 TextView step3 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step3);
192 step3.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__try_turning_wifi_off_and_on_on_both_devices), gapWidth));
193 TextView step4 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step4);
194 step4.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__make_sure_both_devices_are_in_transfer_mode), gapWidth));
195
196 troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_location_permission)
197 .setOnClickListener(v -> openApplicationSystemSettings());
198 troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_location_services)
199 .setOnClickListener(v -> openLocationServices());
200 troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_wifi)
201 .setOnClickListener(v -> openWifiSettings());
202 troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_go_to_support)
203 .setOnClickListener(v -> gotoSupport());
204 troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_try_again)
205 .setOnClickListener(v -> viewModel.checkPermissions());
206 break;
207 case ERROR:
208 error.setText(getErrorTextForStep(step));
209 errorResolve.setText(R.string.DeviceTransferSetup__retry);
210 errorResolve.setOnClickListener(v -> viewModel.checkPermissions());
211 DeviceToDeviceTransferService.stop(requireContext());
212 cancelTakingTooLong();
213 new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.DeviceTransferSetup__error_connecting)
214 .setMessage(getStatusTextForStep(step, false))
215 .setPositiveButton(R.string.DeviceTransferSetup__retry, (d, w) -> viewModel.checkPermissions())
216 .setNegativeButton(android.R.string.cancel, (d, w) -> {
217 EventBus.getDefault().unregister(this);
218 EventBus.getDefault().removeStickyEvent(TransferStatus.class);
219 navigateAwayFromTransfer();
220 })
221 .setNeutralButton(R.string.DeviceTransferSetup__submit_debug_logs, (d, w) -> {
222 EventBus.getDefault().unregister(this);
223 EventBus.getDefault().removeStickyEvent(TransferStatus.class);
224 navigateAwayFromTransfer();
225 startActivity(new Intent(requireContext(), SubmitDebugLogActivity.class));
226 })
227 .setCancelable(false)
228 .show();
229 break;
230 }
231 });
232 }
233
234 protected abstract @StringRes int getStatusTextForStep(@NonNull SetupStep step, boolean takingTooLongInStep);
235
236 protected abstract @StringRes int getErrorTextForStep(@NonNull SetupStep step);
237
238 protected abstract @StringRes int getErrorResolveButtonTextForStep(@NonNull SetupStep step);
239
240 protected abstract void navigateWhenWifiDirectUnavailable();
241
242 protected abstract void startTransfer();
243
244 protected abstract void navigateToTransferConnected();
245
246 protected abstract void navigateAwayFromTransfer();
247
248 @Override
249 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
250 super.onActivityCreated(savedInstanceState);
251 requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressed);
252
253 TransferStatus event = EventBus.getDefault().getStickyEvent(TransferStatus.class);
254 if (event == null) {
255 viewModel.checkPermissions();
256 } else {
257 Log.i(TAG, "Sticky event already exists for transfer, assuming service is running and we are reattaching");
258 }
259
260 EventBus.getDefault().register(this);
261 }
262
263 @Override
264 public void onResume() {
265 super.onResume();
266 viewModel.onResume();
267 }
268
269 @Override
270 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
271 Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
272 }
273
274 @Override
275 public void onDestroyView() {
276 cancelTakingTooLong();
277 EventBus.getDefault().unregister(this);
278 super.onDestroyView();
279 }
280
281 private void requestRequiredPermission() {
282 Permissions.with(this)
283 .request(WifiDirect.requiredPermission())
284 .ifNecessary()
285 .withRationaleDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)), false, R.drawable.symbol_location_white_24)
286 .withPermanentDenialDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)))
287 .onAllGranted(() -> viewModel.onPermissionsGranted())
288 .onAnyDenied(() -> viewModel.onLocationPermissionDenied())
289 .execute();
290 }
291
292 private void openApplicationSystemSettings() {
293 startActivity(Permissions.getApplicationSettingsIntent(requireContext()));
294 }
295
296 private void verifyLocationEnabled() {
297 LocationManager locationManager = ContextCompat.getSystemService(requireContext(), LocationManager.class);
298 if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
299 viewModel.onLocationEnabled();
300 } else {
301 viewModel.onLocationDisabled();
302 }
303 }
304
305 private void openLocationServices() {
306 try {
307 startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
308 } catch (ActivityNotFoundException e) {
309 Log.w(TAG, "No location settings", e);
310 Toast.makeText(requireContext(), R.string.DeviceTransferSetup__unable_to_open_wifi_settings, Toast.LENGTH_LONG).show();
311 }
312 }
313
314 private void verifyWifiEnabled() {
315 WifiManager wifiManager = ContextCompat.getSystemService(requireContext(), WifiManager.class);
316 if (wifiManager != null && wifiManager.isWifiEnabled()) {
317 viewModel.onWifiEnabled();
318 } else {
319 viewModel.onWifiDisabled(wifiManager == null);
320 }
321 }
322
323 private void openWifiSettings() {
324 try {
325 startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
326 } catch (ActivityNotFoundException e) {
327 Log.w(TAG, "No wifi settings", e);
328 Toast.makeText(requireContext(), R.string.DeviceTransferSetup__unable_to_open_wifi_settings, Toast.LENGTH_LONG).show();
329 }
330 }
331
332 private void openWifiDirectSettings() {
333 try {
334 Intent wifiDirect = new Intent(Intent.ACTION_MAIN);
335 wifiDirect.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
336 .setClassName("com.android.settings", "com.android.settings.Settings$WifiP2pSettingsActivity");
337
338 startActivity(wifiDirect);
339 } catch (ActivityNotFoundException e) {
340 Log.w(TAG, "Unable to open wifi direct settings", e);
341 openWifiSettings();
342 }
343 }
344
345 private void verifyWifiDirectAvailable() {
346 WifiDirect.AvailableStatus availability = WifiDirect.getAvailability(requireContext());
347 if (availability != WifiDirect.AvailableStatus.AVAILABLE) {
348 viewModel.onWifiDirectUnavailable(availability);
349 } else {
350 viewModel.onWifiDirectAvailable();
351 }
352 }
353
354 private void gotoSupport() {
355 CommunicationActions.openBrowserLink(requireContext(), getString(R.string.transfer_support_url));
356 }
357
358 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
359 public void onEventMainThread(@NonNull TransferStatus event) {
360 viewModel.onTransferEvent(event);
361 }
362
363 private void startTakingTooLong(@NonNull Runnable runnable, long tooLong) {
364 if (takingTooLong == null) {
365 takingTooLong = () -> {
366 takingTooLong = null;
367 runnable.run();
368 };
369 ThreadUtil.runOnMainDelayed(takingTooLong, tooLong);
370 }
371 }
372
373 private void cancelTakingTooLong() {
374 if (takingTooLong != null) {
375 ThreadUtil.cancelRunnableOnMain(takingTooLong);
376 takingTooLong = null;
377 }
378 }
379
380 private class OnBackPressed extends OnBackPressedCallback {
381
382 public OnBackPressed() {
383 super(true);
384 }
385
386 @Override
387 public void handleOnBackPressed() {
388 DeviceToDeviceTransferService.stop(requireContext());
389 EventBus.getDefault().removeStickyEvent(TransferStatus.class);
390 NavHostFragment.findNavController(DeviceTransferSetupFragment.this).popBackStack();
391 }
392 }
393}