Add Device to Device Transfer UI.

This commit is contained in:
Cody Henthorne
2021-03-11 13:27:25 -05:00
committed by Greyson Parrelli
parent 6f8be3260c
commit 75aab4c031
75 changed files with 3494 additions and 200 deletions

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.devicetransfer;
import androidx.annotation.NonNull;
import static org.thoughtcrime.securesms.devicetransfer.SetupStep.VERIFY;
/**
* State representation of the current {@link SetupStep} in the setup flow and
* the SAS if one has been provided.
*/
public final class DeviceSetupState {
private final SetupStep currentSetupStep;
private final int authenticationCode;
public DeviceSetupState() {
this(SetupStep.INITIAL, 0);
}
public DeviceSetupState(@NonNull SetupStep currentSetupStep, int authenticationCode) {
this.currentSetupStep = currentSetupStep;
this.authenticationCode = authenticationCode;
}
public @NonNull SetupStep getCurrentSetupStep() {
return currentSetupStep;
}
public int getAuthenticationCode() {
return authenticationCode;
}
public @NonNull DeviceSetupState updateStep(@NonNull SetupStep currentSetupStep) {
return new DeviceSetupState(currentSetupStep, this.authenticationCode);
}
public @NonNull DeviceSetupState updateVerificationRequired(int authenticationCode) {
return new DeviceSetupState(VERIFY, authenticationCode);
}
}

View File

@@ -0,0 +1,142 @@
package org.thoughtcrime.securesms.devicetransfer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
/**
* Drives the UI for the actual device transfer progress. Shown after setup is complete
* and the two devices are transferring.
* <p>
* Handles show progress and error state.
*/
public abstract class DeviceTransferFragment extends LoggingFragment {
private final OnBackPressed onBackPressed = new OnBackPressed();
private final TransferModeListener transferModeListener = new TransferModeListener();
protected TextView title;
protected View tryAgain;
protected Button cancel;
protected View progress;
protected View alert;
protected TextView status;
public DeviceTransferFragment() {
super(R.layout.device_transfer_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
title = view.findViewById(R.id.device_transfer_fragment_title);
tryAgain = view.findViewById(R.id.device_transfer_fragment_try_again);
cancel = view.findViewById(R.id.device_transfer_fragment_cancel);
progress = view.findViewById(R.id.device_transfer_fragment_progress);
alert = view.findViewById(R.id.device_transfer_fragment_alert);
status = view.findViewById(R.id.device_transfer_fragment_status);
cancel.setOnClickListener(v -> cancelActiveTransfer());
tryAgain.setOnClickListener(v -> {
EventBus.getDefault().unregister(transferModeListener);
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
navigateToRestartTransfer();
});
EventBus.getDefault().register(transferModeListener);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressed);
}
@Override
public void onDestroyView() {
EventBus.getDefault().unregister(transferModeListener);
super.onDestroyView();
}
private void cancelActiveTransfer() {
new AlertDialog.Builder(requireContext()).setTitle(R.string.DeviceTransfer__stop_transfer)
.setMessage(R.string.DeviceTransfer__all_transfer_progress_will_be_lost)
.setPositiveButton(R.string.DeviceTransfer__stop_transfer, (d, w) -> {
EventBus.getDefault().unregister(transferModeListener);
DeviceToDeviceTransferService.stop(requireContext());
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
navigateAwayFromTransfer();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
protected void ignoreTransferStatusEvents() {
EventBus.getDefault().unregister(transferModeListener);
}
protected abstract void navigateToRestartTransfer();
protected abstract void navigateAwayFromTransfer();
private class TransferModeListener {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull TransferStatus event) {
if (event.getTransferMode() != TransferStatus.TransferMode.SERVICE_CONNECTED) {
abort();
}
}
}
protected void abort() {
abort(R.string.DeviceTransfer__transfer_failed);
}
protected void abort(@StringRes int errorMessage) {
EventBus.getDefault().unregister(transferModeListener);
DeviceToDeviceTransferService.stop(requireContext());
progress.setVisibility(View.GONE);
alert.setVisibility(View.VISIBLE);
tryAgain.setVisibility(View.VISIBLE);
title.setText(R.string.DeviceTransfer__unable_to_transfer);
status.setText(errorMessage);
cancel.setText(R.string.DeviceTransfer__cancel);
cancel.setOnClickListener(v -> navigateAwayFromTransfer());
onBackPressed.isActiveTransfer = false;
}
protected class OnBackPressed extends OnBackPressedCallback {
private boolean isActiveTransfer = true;
public OnBackPressed() {
super(true);
}
@Override
public void handleOnBackPressed() {
if (isActiveTransfer) {
cancelActiveTransfer();
} else {
navigateAwayFromTransfer();
}
}
}
}

View File

@@ -0,0 +1,393 @@
package org.thoughtcrime.securesms.devicetransfer;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.location.LocationManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.provider.Settings;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.button.MaterialButton;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.signal.devicetransfer.WifiDirect;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Responsible for driving the UI of all the legwork to startup Wi-Fi Direct and
* establish the connection between the two devices. It's capable of being used by both
* the new and old device, but delegates some of the UI (mostly strings and navigation) to
* a subclass for old or new device.
* <p>
* Handles showing setup progress, verification codes, connecting states, error states, and troubleshooting.
* <p>
* It's state driven by the view model so it's easy to transition from step to step in the
* process.
*/
public abstract class DeviceTransferSetupFragment extends LoggingFragment {
private static final String TAG = Log.tag(DeviceTransferSetupFragment.class);
private static final long PREPARE_TAKING_TOO_LONG_TIME = TimeUnit.SECONDS.toMillis(30);
private static final long WAITING_TAKING_TOO_LONG_TIME = TimeUnit.SECONDS.toMillis(90);
private final OnBackPressed onBackPressed = new OnBackPressed();
private DeviceTransferSetupViewModel viewModel;
private Runnable takingTooLong;
public DeviceTransferSetupFragment() {
super(R.layout.device_transfer_setup_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Group progressGroup = view.findViewById(R.id.device_transfer_setup_fragment_progress_group);
Group errorGroup = view.findViewById(R.id.device_transfer_setup_fragment_error_group);
Group verifyGroup = view.findViewById(R.id.device_transfer_setup_fragment_verify_group);
View troubleshooting = view.findViewById(R.id.device_transfer_setup_fragment_troubleshooting);
TextView status = view.findViewById(R.id.device_transfer_setup_fragment_status);
TextView error = view.findViewById(R.id.device_transfer_setup_fragment_error);
MaterialButton errorResolve = view.findViewById(R.id.device_transfer_setup_fragment_error_resolve);
TextView sasNumber = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_code);
MaterialButton verifyNo = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_no);
MaterialButton verifyYes = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_yes);
viewModel = ViewModelProviders.of(this).get(DeviceTransferSetupViewModel.class);
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
SetupStep step = state.getCurrentSetupStep();
progressGroup.setVisibility(step.isProgress() ? View.VISIBLE : View.GONE);
errorGroup.setVisibility(step.isError() ? View.VISIBLE : View.GONE);
verifyGroup.setVisibility(step == SetupStep.VERIFY ? View.VISIBLE : View.GONE);
troubleshooting.setVisibility(step == SetupStep.TROUBLESHOOTING ? View.VISIBLE : View.GONE);
Log.i(TAG, "Handling step: " + step.name());
switch (step) {
case INITIAL:
status.setText("");
case PERMISSIONS_CHECK:
requestLocationPermission();
break;
case PERMISSIONS_DENIED:
error.setText(getErrorTextForStep(step));
errorResolve.setText(R.string.DeviceTransferSetup__grant_location_permission);
errorResolve.setOnClickListener(v -> viewModel.checkPermissions());
break;
case LOCATION_CHECK:
verifyLocationEnabled();
break;
case LOCATION_DISABLED:
error.setText(getErrorTextForStep(step));
errorResolve.setText(R.string.DeviceTransferSetup__turn_on_location_services);
errorResolve.setOnClickListener(v -> openLocationServices());
break;
case WIFI_CHECK:
verifyWifiEnabled();
break;
case WIFI_DISABLED:
error.setText(getErrorTextForStep(step));
errorResolve.setText(R.string.DeviceTransferSetup__turn_on_wifi);
errorResolve.setOnClickListener(v -> openWifiSettings());
break;
case WIFI_DIRECT_CHECK:
verifyWifiDirectAvailable();
break;
case WIFI_DIRECT_UNAVAILABLE:
error.setText(getErrorTextForStep(step));
errorResolve.setText(getErrorResolveButtonTextForStep(step));
errorResolve.setOnClickListener(v -> navigateWhenWifiDirectUnavailable());
break;
case START:
status.setText(getStatusTextForStep(SetupStep.SETTING_UP, false));
startTransfer();
break;
case SETTING_UP:
status.setText(getStatusTextForStep(step, false));
startTakingTooLong(() -> status.setText(getStatusTextForStep(step, true)), PREPARE_TAKING_TOO_LONG_TIME);
break;
case WAITING:
status.setText(getStatusTextForStep(step, false));
cancelTakingTooLong();
startTakingTooLong(() -> {
DeviceToDeviceTransferService.stop(requireContext());
viewModel.onWaitingTookTooLong();
}, WAITING_TAKING_TOO_LONG_TIME);
break;
case VERIFY:
cancelTakingTooLong();
sasNumber.setText(String.format(Locale.US, "%07d", state.getAuthenticationCode()));
//noinspection CodeBlock2Expr
verifyNo.setOnClickListener(v -> {
new AlertDialog.Builder(requireContext()).setTitle(R.string.DeviceTransferSetup__the_numbers_do_not_match)
.setMessage(R.string.DeviceTransferSetup__if_the_numbers_on_your_devices_do_not_match_its_possible_you_connected_to_the_wrong_device)
.setPositiveButton(R.string.DeviceTransferSetup__stop_transfer, (d, w) -> {
EventBus.getDefault().unregister(this);
DeviceToDeviceTransferService.setAuthenticationCodeVerified(requireContext(), false);
DeviceToDeviceTransferService.stop(requireContext());
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
navigateAwayFromTransfer();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
});
verifyYes.setOnClickListener(v -> {
DeviceToDeviceTransferService.setAuthenticationCodeVerified(requireContext(), true);
viewModel.onVerified();
});
break;
case CONNECTING:
status.setText(getStatusTextForStep(step, false));
break;
case CONNECTED:
Log.d(TAG, "Connected! isNotShutdown: " + viewModel.isNotShutdown());
if (viewModel.isNotShutdown()) {
navigateToTransferConnected();
}
break;
case TROUBLESHOOTING:
TextView title = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_title);
title.setText(getStatusTextForStep(step, false));
int gapWidth = ViewUtil.dpToPx(12);
TextView step1 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step1);
step1.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__make_sure_the_following_permissions_are_enabled), gapWidth));
TextView step2 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step2);
step2.setMovementMethod(LinkMovementMethod.getInstance());
step2.setText(SpanUtil.clickSubstring(requireContext(),
SpanUtil.bullet(getString(R.string.DeviceTransferSetup__on_the_wifi_direct_screen_remove_all_remembered_groups_and_unlink_any_invited_or_connected_devices), gapWidth),
getString(R.string.DeviceTransferSetup__wifi_direct_screen),
v -> openWifiDirectSettings()));
TextView step3 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step3);
step3.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__try_turning_wifi_off_and_on_on_both_devices), gapWidth));
TextView step4 = troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_step4);
step4.setText(SpanUtil.bullet(getString(R.string.DeviceTransferSetup__make_sure_both_devices_are_in_transfer_mode), gapWidth));
troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_location_permission)
.setOnClickListener(v -> openApplicationSystemSettings());
troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_location_services)
.setOnClickListener(v -> openLocationServices());
troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_wifi)
.setOnClickListener(v -> openWifiSettings());
troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_go_to_support)
.setOnClickListener(v -> gotoSupport());
troubleshooting.findViewById(R.id.device_transfer_setup_fragment_troubleshooting_try_again)
.setOnClickListener(v -> viewModel.checkPermissions());
break;
case ERROR:
error.setText(getErrorTextForStep(step));
errorResolve.setText(R.string.DeviceTransferSetup__retry);
errorResolve.setOnClickListener(v -> viewModel.checkPermissions());
DeviceToDeviceTransferService.stop(requireContext());
cancelTakingTooLong();
new AlertDialog.Builder(requireContext()).setTitle(R.string.DeviceTransferSetup__error_connecting)
.setMessage(getStatusTextForStep(step, false))
.setPositiveButton(R.string.DeviceTransferSetup__retry, (d, w) -> viewModel.checkPermissions())
.setNegativeButton(android.R.string.cancel, (d, w) -> {
EventBus.getDefault().unregister(this);
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
navigateAwayFromTransfer();
})
.setNeutralButton(R.string.DeviceTransferSetup__submit_debug_logs, (d, w) -> {
EventBus.getDefault().unregister(this);
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
navigateAwayFromTransfer();
startActivity(new Intent(requireContext(), SubmitDebugLogActivity.class));
})
.setCancelable(false)
.show();
break;
}
});
}
protected abstract @StringRes int getStatusTextForStep(@NonNull SetupStep step, boolean takingTooLongInStep);
protected abstract @StringRes int getErrorTextForStep(@NonNull SetupStep step);
protected abstract @StringRes int getErrorResolveButtonTextForStep(@NonNull SetupStep step);
protected abstract void navigateWhenWifiDirectUnavailable();
protected abstract void startTransfer();
protected abstract void navigateToTransferConnected();
protected abstract void navigateAwayFromTransfer();
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressed);
TransferStatus event = EventBus.getDefault().getStickyEvent(TransferStatus.class);
if (event == null) {
viewModel.checkPermissions();
} else {
Log.i(TAG, "Sticky event already exists for transfer, assuming service is running and we are reattaching");
}
EventBus.getDefault().register(this);
}
@Override
public void onResume() {
super.onResume();
viewModel.onResume();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onDestroyView() {
cancelTakingTooLong();
EventBus.getDefault().unregister(this);
super.onDestroyView();
}
private void requestLocationPermission() {
Permissions.with(this)
.request(Manifest.permission.ACCESS_FINE_LOCATION)
.ifNecessary()
.withRationaleDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)), false, R.drawable.ic_location_on_white_24dp)
.withPermanentDenialDialog(getString(getErrorTextForStep(SetupStep.PERMISSIONS_DENIED)))
.onAllGranted(() -> viewModel.onPermissionsGranted())
.onAnyDenied(() -> viewModel.onLocationPermissionDenied())
.execute();
}
private void openApplicationSystemSettings() {
startActivity(Permissions.getApplicationSettingsIntent(requireContext()));
}
private void verifyLocationEnabled() {
LocationManager locationManager = ContextCompat.getSystemService(requireContext(), LocationManager.class);
if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
viewModel.onLocationEnabled();
} else {
viewModel.onLocationDisabled();
}
}
private void openLocationServices() {
try {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No location settings", e);
Toast.makeText(requireContext(), R.string.DeviceTransferSetup__unable_to_open_wifi_settings, Toast.LENGTH_LONG).show();
}
}
private void verifyWifiEnabled() {
WifiManager wifiManager = ContextCompat.getSystemService(requireContext(), WifiManager.class);
if (wifiManager != null && wifiManager.isWifiEnabled()) {
viewModel.onWifiEnabled();
} else {
viewModel.onWifiDisabled(wifiManager == null);
}
}
private void openWifiSettings() {
try {
startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No wifi settings", e);
Toast.makeText(requireContext(), R.string.DeviceTransferSetup__unable_to_open_wifi_settings, Toast.LENGTH_LONG).show();
}
}
private void openWifiDirectSettings() {
try {
Intent wifiDirect = new Intent(Intent.ACTION_MAIN);
wifiDirect.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setClassName("com.android.settings", "com.android.settings.Settings$WifiP2pSettingsActivity");
startActivity(wifiDirect);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open wifi direct settings", e);
openWifiSettings();
}
}
private void verifyWifiDirectAvailable() {
WifiDirect.AvailableStatus availability = WifiDirect.getAvailability(requireContext());
if (availability != WifiDirect.AvailableStatus.AVAILABLE) {
viewModel.onWifiDirectUnavailable(availability);
} else {
viewModel.onWifiDirectAvailable();
}
}
private void gotoSupport() {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.transfer_support_url));
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull TransferStatus event) {
viewModel.onTransferEvent(event);
}
private void startTakingTooLong(@NonNull Runnable runnable, long tooLong) {
if (takingTooLong == null) {
takingTooLong = () -> {
takingTooLong = null;
runnable.run();
};
ThreadUtil.runOnMainDelayed(takingTooLong, tooLong);
}
}
private void cancelTakingTooLong() {
if (takingTooLong != null) {
ThreadUtil.cancelRunnableOnMain(takingTooLong);
takingTooLong = null;
}
}
private class OnBackPressed extends OnBackPressedCallback {
public OnBackPressed() {
super(true);
}
@Override
public void handleOnBackPressed() {
DeviceToDeviceTransferService.stop(requireContext());
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
NavHostFragment.findNavController(DeviceTransferSetupFragment.this).popBackStack();
}
}
}

View File

@@ -0,0 +1,137 @@
package org.thoughtcrime.securesms.devicetransfer;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.TransferStatus;
import org.signal.devicetransfer.WifiDirect;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.livedata.Store;
/**
* Drives and wraps the state of the transfer setup process.
*/
public final class DeviceTransferSetupViewModel extends ViewModel {
private static final String TAG = Log.tag(DeviceTransferSetupViewModel.class);
private final Store<DeviceSetupState> store;
private final LiveData<DeviceSetupState> distinctStepChanges;
private boolean shutdown;
public DeviceTransferSetupViewModel() {
this.store = new Store<>(new DeviceSetupState());
this.distinctStepChanges = LiveDataUtil.distinctUntilChanged(this.store.getStateLiveData(), (current, next) -> current.getCurrentSetupStep() == next.getCurrentSetupStep());
}
public @NonNull LiveData<DeviceSetupState> getState() {
return distinctStepChanges;
}
public boolean isNotShutdown() {
return !shutdown;
}
public void onTransferEvent(@NonNull TransferStatus event) {
if (shutdown) {
return;
}
Log.i(TAG, "Handling transferStatus: " + event.getTransferMode());
switch (event.getTransferMode()) {
case UNAVAILABLE:
case NETWORK_CONNECTED:
Log.d(TAG, "Ignore event: " + event.getTransferMode());
break;
case READY:
case STARTING_UP:
store.update(s -> s.updateStep(SetupStep.SETTING_UP));
break;
case DISCOVERY:
store.update(s -> s.updateStep(SetupStep.WAITING));
break;
case VERIFICATION_REQUIRED:
store.update(s -> s.updateVerificationRequired(event.getAuthenticationCode()));
break;
case SERVICE_CONNECTED:
store.update(s -> s.updateStep(SetupStep.CONNECTED));
break;
case FAILED:
store.update(s -> s.updateStep(SetupStep.ERROR));
break;
}
}
public void onLocationPermissionDenied() {
Log.i(TAG, "Location permissions denied");
store.update(s -> s.updateStep(SetupStep.PERMISSIONS_DENIED));
}
public void onWifiDisabled(boolean wifiManagerNotAvailable) {
Log.i(TAG, "Wifi disabled manager: " + wifiManagerNotAvailable);
store.update(s -> s.updateStep(SetupStep.WIFI_DISABLED));
}
public void onWifiDirectUnavailable(WifiDirect.AvailableStatus availability) {
Log.i(TAG, "Wifi Direct unavailable: " + availability);
if (availability == WifiDirect.AvailableStatus.FINE_LOCATION_PERMISSION_NOT_GRANTED) {
store.update(s -> s.updateStep(SetupStep.PERMISSIONS_CHECK));
} else {
store.update(s -> s.updateStep(SetupStep.WIFI_DIRECT_UNAVAILABLE));
}
}
public void checkPermissions() {
Log.d(TAG, "Check for permissions");
shutdown = false;
store.update(s -> s.updateStep(SetupStep.PERMISSIONS_CHECK));
}
public void onPermissionsGranted() {
Log.d(TAG, "Permissions granted");
store.update(s -> s.updateStep(SetupStep.LOCATION_CHECK));
}
public void onLocationEnabled() {
Log.d(TAG, "Location enabled");
store.update(s -> s.updateStep(SetupStep.WIFI_CHECK));
}
public void onLocationDisabled() {
Log.d(TAG, "Location disabled");
store.update(s -> s.updateStep(SetupStep.LOCATION_DISABLED));
}
public void onWifiEnabled() {
Log.d(TAG, "Wifi enabled");
store.update(s -> s.updateStep(SetupStep.WIFI_DIRECT_CHECK));
}
public void onWifiDirectAvailable() {
Log.d(TAG, "Wifi direct available");
store.update(s -> s.updateStep(SetupStep.START));
}
public void onVerified() {
store.update(s -> s.updateStep(SetupStep.CONNECTING));
}
public void onResume() {
store.update(s -> {
if (s.getCurrentSetupStep() == SetupStep.WIFI_DISABLED) {
return s.updateStep(SetupStep.WIFI_CHECK);
} else if (s.getCurrentSetupStep() == SetupStep.LOCATION_DISABLED) {
return s.updateStep(SetupStep.LOCATION_CHECK);
}
return s;
});
}
public void onWaitingTookTooLong() {
shutdown = true;
store.update(s -> s.updateStep(SetupStep.TROUBLESHOOTING));
}
}

View File

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.devicetransfer;
/**
* The various steps involved in setting up a transfer connection. Each step has a
* corresponding UI.
*/
public enum SetupStep {
INITIAL(true, false),
PERMISSIONS_CHECK(true, false),
PERMISSIONS_DENIED(false, true),
LOCATION_CHECK(true, false),
LOCATION_DISABLED(false, true),
WIFI_CHECK(true, false),
WIFI_DISABLED(false, true),
WIFI_DIRECT_CHECK(true, false),
WIFI_DIRECT_UNAVAILABLE(false, true),
START(true, false),
SETTING_UP(true, false),
WAITING(true, false),
VERIFY(false, false),
CONNECTING(true, false),
CONNECTED(true, false),
TROUBLESHOOTING(false, false),
ERROR(false, true);
private final boolean isProgress;
private final boolean isError;
SetupStep(boolean isProgress, boolean isError) {
this.isProgress = isProgress;
this.isError = isError;
}
public boolean isProgress() {
return isProgress;
}
public boolean isError() {
return isError;
}
}

View File

@@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.content.Context;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.ServerTask;
import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupBase;
import org.thoughtcrime.securesms.backup.FullBackupImporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import java.io.IOException;
import java.io.InputStream;
/**
* Performs the restore with the backup data coming in over the input stream. Used in
* conjunction with {@link org.signal.devicetransfer.DeviceToDeviceTransferService}.
*/
final class NewDeviceServerTask implements ServerTask {
private static final String TAG = Log.tag(NewDeviceServerTask.class);
@Override
public void run(@NonNull Context context, @NonNull InputStream inputStream) {
long start = System.currentTimeMillis();
Log.i(TAG, "Starting backup restore.");
EventBus.getDefault().register(this);
try {
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
String passphrase = "deadbeef";
BackupPassphrase.set(context, passphrase);
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
inputStream,
passphrase);
DatabaseFactory.upgradeRestored(context, database);
NotificationChannels.restoreContactNotificationChannels(context);
AppInitialization.onPostBackupRestore(context);
Log.i(TAG, "Backup restore complete.");
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_VERSION_DOWNGRADE));
} catch (IOException e) {
Log.w(TAG, e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_UNKNOWN));
} finally {
EventBus.getDefault().unregister(this);
}
long end = System.currentTimeMillis();
Log.i(TAG, "Receive took: " + (end - start));
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onEvent(FullBackupBase.BackupEvent event) {
if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.IN_PROGRESS));
} else if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.SUCCESS));
}
}
public static final class Status {
private final long messageCount;
private final State state;
public Status(long messageCount, State state) {
this.messageCount = messageCount;
this.state = state;
}
public long getMessageCount() {
return messageCount;
}
public @NonNull State getState() {
return state;
}
public enum State {
IN_PROGRESS,
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_UNKNOWN
}
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.os.Bundle;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
/**
* Shown after the new device successfully completes receiving a backup from the old device.
*/
public final class NewDeviceTransferCompleteFragment extends LoggingFragment {
public NewDeviceTransferCompleteFragment() {
super(R.layout.new_device_transfer_complete_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.new_device_transfer_complete_fragment_continue_registration)
.setOnClickListener(v -> NavHostFragment.findNavController(this)
.navigate(R.id.action_newDeviceTransferComplete_to_enterPhoneNumberFragment));
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() { }
});
}
}

View File

@@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.devicetransfer.DeviceTransferFragment;
/**
* Shows transfer progress on the new device. Most logic is in {@link DeviceTransferFragment}
* and it delegates to this class for strings, navigation, and updating progress.
*/
public final class NewDeviceTransferFragment extends DeviceTransferFragment {
private final ServerTaskListener serverTaskListener = new ServerTaskListener();
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
EventBus.getDefault().register(serverTaskListener);
}
@Override
public void onDestroyView() {
EventBus.getDefault().unregister(serverTaskListener);
super.onDestroyView();
}
@Override
protected void navigateToRestartTransfer() {
NavHostFragment.findNavController(this).navigate(R.id.action_newDeviceTransfer_to_newDeviceTransferInstructions);
}
@Override
protected void navigateAwayFromTransfer() {
EventBus.getDefault().unregister(serverTaskListener);
NavHostFragment.findNavController(this)
.navigate(R.id.action_restart_to_welcomeFragment);
}
private class ServerTaskListener {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull NewDeviceServerTask.Status event) {
status.setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount()));
switch (event.getState()) {
case IN_PROGRESS:
break;
case SUCCESS:
DeviceToDeviceTransferService.stop(requireContext());
NavHostFragment.findNavController(NewDeviceTransferFragment.this).navigate(R.id.action_newDeviceTransfer_to_newDeviceTransferComplete);
break;
case FAILURE_VERSION_DOWNGRADE:
abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal);
break;
case FAILURE_UNKNOWN:
abort();
break;
}
}
}
}

View File

@@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.Navigation;
import org.greenrobot.eventbus.EventBus;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
/**
* Shows instructions for new device to being transfer.
*/
public final class NewDeviceTransferInstructionsFragment extends LoggingFragment {
public NewDeviceTransferInstructionsFragment() {
super(R.layout.new_device_transfer_instructions_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.new_device_transfer_instructions_fragment_continue)
.setOnClickListener(v -> Navigation.findNavController(v).navigate(R.id.action_device_transfer_setup));
}
@Override
public void onResume() {
super.onResume();
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
}
}

View File

@@ -0,0 +1,93 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.app.PendingIntent;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.navigation.fragment.NavHostFragment;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.DeviceToDeviceTransferService.TransferNotificationData;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.devicetransfer.SetupStep;
import org.thoughtcrime.securesms.devicetransfer.DeviceTransferSetupFragment;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
/**
* Most responsibility is in {@link DeviceTransferSetupFragment} and delegates here
* for strings and behavior relevant to setting up device transfer for the new device.
*
* Also responsible for setting up {@link DeviceToDeviceTransferService}.
*/
public final class NewDeviceTransferSetupFragment extends DeviceTransferSetupFragment {
@Override
protected void navigateAwayFromTransfer() {
NavHostFragment.findNavController(this)
.navigate(R.id.action_deviceTransferSetup_to_transferOrRestore);
}
@Override
protected void navigateToTransferConnected() {
NavHostFragment.findNavController(this).navigate(R.id.action_new_device_transfer);
}
@Override
protected @StringRes int getErrorTextForStep(@NonNull SetupStep step) {
switch (step) {
case PERMISSIONS_DENIED:
return R.string.NewDeviceTransferSetup__signal_needs_the_location_permission_to_discover_and_connect_with_your_old_device;
case LOCATION_DISABLED:
return R.string.NewDeviceTransferSetup__signal_needs_location_services_enabled_to_discover_and_connect_with_your_old_device;
case WIFI_DISABLED:
return R.string.NewDeviceTransferSetup__signal_needs_wifi_on_to_discover_and_connect_with_your_old_device;
case WIFI_DIRECT_UNAVAILABLE:
return R.string.NewDeviceTransferSetup__sorry_it_appears_your_device_does_not_support_wifi_direct;
case ERROR:
return R.string.NewDeviceTransferSetup__an_unexpected_error_occurred_while_attempting_to_connect_to_your_old_device;
}
throw new AssertionError("No error text for step: " + step);
}
@Override
protected @StringRes int getErrorResolveButtonTextForStep(@NonNull SetupStep step) {
if (step == SetupStep.WIFI_DIRECT_UNAVAILABLE) {
return R.string.NewDeviceTransferSetup__restore_a_backup;
}
throw new AssertionError("No error resolve button text for step: " + step);
}
@Override
protected @StringRes int getStatusTextForStep(@NonNull SetupStep step, boolean takingTooLongInStep) {
switch (step) {
case SETTING_UP:
return takingTooLongInStep ? R.string.NewDeviceTransferSetup__take_a_moment_should_be_ready_soon
: R.string.NewDeviceTransferSetup__preparing_to_connect_to_old_android_device;
case WAITING:
return R.string.NewDeviceTransferSetup__waiting_for_old_device_to_connect;
case CONNECTING:
return R.string.NewDeviceTransferSetup__connecting_to_old_android_device;
case ERROR:
return R.string.NewDeviceTransferSetup__an_unexpected_error_occurred_while_attempting_to_connect_to_your_old_device;
case TROUBLESHOOTING:
return R.string.DeviceTransferSetup__unable_to_discover_old_device;
}
throw new AssertionError("No status text for step: " + step);
}
@Override
protected void navigateWhenWifiDirectUnavailable() {
NavHostFragment.findNavController(this)
.navigate(R.id.action_deviceTransferSetup_to_transferOrRestore);
}
@Override
protected void startTransfer() {
PendingIntent pendingIntent = PendingIntent.getActivity(requireContext(), 0, MainActivity.clearTop(requireContext()), 0);
TransferNotificationData notificationData = new TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.BACKUPS, R.drawable.ic_signal_backup);
DeviceToDeviceTransferService.startServer(requireContext(), new NewDeviceServerTask(), notificationData, pendingIntent);
}
}

View File

@@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SpanUtil;
/**
* Simple jumping off menu to starts a device-to-device transfer or restore a backup.
*/
public final class TransferOrRestoreFragment extends LoggingFragment {
public TransferOrRestoreFragment() {
super(R.layout.fragment_transfer_restore);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.transfer_or_restore_fragment_transfer)
.setOnClickListener(v -> Navigation.findNavController(v).navigate(R.id.action_new_device_transfer_instructions));
view.findViewById(R.id.transfer_or_restore_fragment_restore)
.setOnClickListener(v -> Navigation.findNavController(v).navigate(R.id.action_choose_backup));
String description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_message_history_from_your_old_android_device);
String toBold = getString(R.string.TransferOrRestoreFragment__you_must_have_access_to_your_old_device);
TextView transferDescriptionView = view.findViewById(R.id.transfer_or_restore_fragment_transfer_description);
transferDescriptionView.setText(SpanUtil.boldSubstring(description, toBold));
}
}

View File

@@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.content.Context;
import androidx.annotation.NonNull;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.ClientTask;
import org.thoughtcrime.securesms.backup.FullBackupBase;
import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
import java.io.IOException;
import java.io.OutputStream;
/**
* Create the backup stream of the old device and sends it over the wire via the output stream.
* Used in conjunction with {@link org.signal.devicetransfer.DeviceToDeviceTransferService}.
*/
final class OldDeviceClientTask implements ClientTask {
private static final String TAG = Log.tag(OldDeviceClientTask.class);
private static final long PROGRESS_UPDATE_THROTTLE = 250;
private long lastProgressUpdate = 0;
@Override
public void run(@NonNull Context context, @NonNull OutputStream outputStream) throws IOException {
DeviceTransferBlockingInterceptor.getInstance().blockNetwork();
long start = System.currentTimeMillis();
EventBus.getDefault().register(this);
try {
FullBackupExporter.transfer(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
DatabaseFactory.getBackupDatabase(context),
outputStream,
"deadbeef");
} catch (Exception e) {
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
throw e;
} finally {
EventBus.getDefault().unregister(this);
}
long end = System.currentTimeMillis();
Log.i(TAG, "Sending took: " + (end - start));
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onEvent(FullBackupBase.BackupEvent event) {
if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) {
if (System.currentTimeMillis() > lastProgressUpdate + PROGRESS_UPDATE_THROTTLE) {
EventBus.getDefault().post(new Status(event.getCount(), false));
lastProgressUpdate = System.currentTimeMillis();
}
}
}
@Override
public void success() {
EventBus.getDefault().post(new Status(0, true));
}
public static final class Status {
private final long messages;
private final boolean done;
public Status(long messages, boolean done) {
this.messages = messages;
this.done = done;
}
public long getMessageCount() {
return messages;
}
public boolean isDone() {
return done;
}
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class OldDeviceExitActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finish();
}
public static void exit(Context context) {
Intent intent = new Intent(context, OldDeviceExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Shell of an activity to hold the old device navigation graph. See the various
* fragments in this package for actual implementation.
*/
public final class OldDeviceTransferActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
dynamicTheme.onCreate(this);
setContentView(R.layout.old_device_transfer_activity);
NavController controller = Navigation.findNavController(this, R.id.nav_host_fragment);
controller.setGraph(R.navigation.old_device_transfer);
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
}

View File

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.os.Bundle;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
/**
* Shown after the old device successfully completes sending a backup to the new device.
*/
public final class OldDeviceTransferCompleteFragment extends LoggingFragment {
public OldDeviceTransferCompleteFragment() {
super(R.layout.old_device_transfer_complete_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.old_device_transfer_complete_fragment_close)
.setOnClickListener(v -> close());
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
close();
}
});
}
private void close() {
OldDeviceExitActivity.exit(requireContext());
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.fragment.NavHostFragment;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.devicetransfer.DeviceTransferFragment;
/**
* Shows transfer progress on the old device. Most logic is in {@link DeviceTransferFragment}
* and it delegates to this class for strings, navigation, and updating progress.
*/
public final class OldDeviceTransferFragment extends DeviceTransferFragment {
private final ClientTaskListener clientTaskListener = new ClientTaskListener();
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
EventBus.getDefault().register(clientTaskListener);
}
@Override
public void onDestroyView() {
EventBus.getDefault().unregister(clientTaskListener);
super.onDestroyView();
}
@Override
protected void navigateToRestartTransfer() {
NavHostFragment.findNavController(this).navigate(R.id.action_directly_to_oldDeviceTransferInstructions);
}
@Override
protected void navigateAwayFromTransfer() {
EventBus.getDefault().unregister(clientTaskListener);
requireActivity().finish();
}
private class ClientTaskListener {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull OldDeviceClientTask.Status event) {
if (event.isDone()) {
ignoreTransferStatusEvents();
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
DeviceToDeviceTransferService.stop(requireContext());
NavHostFragment.findNavController(OldDeviceTransferFragment.this).navigate(R.id.action_oldDeviceTransfer_to_oldDeviceTransferComplete);
} else {
status.setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount()));
}
}
}
}

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.greenrobot.eventbus.EventBus;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
/**
* Provides instructions for the old device on how to start a device-to-device transfer.
*/
public final class OldDeviceTransferInstructionsFragment extends LoggingFragment {
public OldDeviceTransferInstructionsFragment() {
super(R.layout.old_device_transfer_instructions_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Toolbar toolbar = view.findViewById(R.id.old_device_transfer_instructions_fragment_toolbar);
toolbar.setNavigationOnClickListener(v -> {
if (!Navigation.findNavController(v).popBackStack()) {
requireActivity().finish();
}
});
view.findViewById(R.id.old_device_transfer_instructions_fragment_continue)
.setOnClickListener(v -> Navigation.findNavController(v)
.navigate(R.id.action_oldDeviceTransferInstructions_to_oldDeviceTransferSetup));
}
@Override
public void onResume() {
super.onResume();
if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null) {
NavHostFragment.findNavController(this)
.navigate(R.id.action_oldDeviceTransferInstructions_to_oldDeviceTransferSetup);
} else {
DeviceToDeviceTransferService.stop(requireContext());
}
}
}

View File

@@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.devicetransfer.olddevice;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.navigation.fragment.NavHostFragment;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.DeviceTransferSetupFragment;
import org.thoughtcrime.securesms.devicetransfer.SetupStep;
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
/**
* Most responsibility is in {@link DeviceTransferSetupFragment} and delegates here
* for strings and behavior relevant to setting up device transfer for the old device.
*
* Also responsible for setting up {@link DeviceToDeviceTransferService}.
*/
public final class OldDeviceTransferSetupFragment extends DeviceTransferSetupFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ApplicationDependencies.getJobManager().cancelAllInQueue(LocalBackupJob.QUEUE);
}
@Override
protected void navigateAwayFromTransfer() {
NavHostFragment.findNavController(this).popBackStack();
}
@Override
protected void navigateToTransferConnected() {
NavHostFragment.findNavController(this).navigate(R.id.action_oldDeviceTransferSetup_to_oldDeviceTransfer);
}
@Override
protected void navigateWhenWifiDirectUnavailable() {
Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class);
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
startActivity(intent);
requireActivity().finish();
}
@Override
protected void startTransfer() {
Intent intent = new Intent(requireContext(), OldDeviceTransferActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(requireContext(), 0, intent, 0);
DeviceToDeviceTransferService.TransferNotificationData notificationData = new DeviceToDeviceTransferService.TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.BACKUPS, R.drawable.ic_signal_backup);
DeviceToDeviceTransferService.startClient(requireContext(), new OldDeviceClientTask(), notificationData, pendingIntent);
}
@Override
protected @StringRes int getErrorTextForStep(@NonNull SetupStep step) {
switch (step) {
case PERMISSIONS_DENIED:
return R.string.OldDeviceTransferSetup__signal_needs_the_location_permission_to_discover_and_connect_with_your_new_device;
case LOCATION_DISABLED:
return R.string.OldDeviceTransferSetup__signal_needs_location_services_enabled_to_discover_and_connect_with_your_new_device;
case WIFI_DISABLED:
return R.string.OldDeviceTransferSetup__signal_needs_wifi_on_to_discover_and_connect_with_your_new_device;
case WIFI_DIRECT_UNAVAILABLE:
return R.string.OldDeviceTransferSetup__sorry_it_appears_your_device_does_not_support_wifi_direct;
case ERROR:
return R.string.OldDeviceTransferSetup__an_unexpected_error_occurred_while_attempting_to_connect_to_your_old_device;
}
throw new AssertionError("No error text for step: " + step);
}
@Override
protected @StringRes int getErrorResolveButtonTextForStep(@NonNull SetupStep step) {
if (step == SetupStep.WIFI_DIRECT_UNAVAILABLE) {
return R.string.OldDeviceTransferSetup__create_a_backup;
}
throw new AssertionError("No error resolve button text for step: " + step);
}
@Override
protected @StringRes int getStatusTextForStep(@NonNull SetupStep step, boolean takingTooLongInStep) {
switch (step) {
case SETTING_UP:
case WAITING:
return R.string.OldDeviceTransferSetup__searching_for_your_new_android_device;
case CONNECTING:
return R.string.OldDeviceTransferSetup__connecting_to_new_android_device;
case ERROR:
return R.string.OldDeviceTransferSetup__an_unexpected_error_occurred_while_attempting_to_connect_to_your_old_device;
case TROUBLESHOOTING:
return R.string.DeviceTransferSetup__unable_to_discover_new_device;
}
throw new AssertionError("No status text for step: " + step);
}
}