mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Add Device to Device Transfer UI.
This commit is contained in:
committed by
Greyson Parrelli
parent
6f8be3260c
commit
75aab4c031
@@ -15,6 +15,12 @@ public abstract class LoggingFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(LoggingFragment.class);
|
||||
|
||||
public LoggingFragment() { }
|
||||
|
||||
public LoggingFragment(int contentLayoutId) {
|
||||
super(contentLayoutId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
|
||||
@@ -11,10 +11,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
@@ -45,6 +48,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_ENTER_SIGNAL_PIN = 5;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -146,6 +150,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent();
|
||||
case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent();
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -165,6 +170,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (userMustCreateSignalPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) {
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
@@ -219,6 +226,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(EditProfileActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getOldDeviceTransferIntent() {
|
||||
Intent intent = new Intent(this, OldDeviceTransferActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
|
||||
@@ -26,8 +26,6 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
@@ -85,7 +83,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = new FileOutputStream(output)) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,15 +96,26 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void transfer(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream outputStream,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, false);
|
||||
}
|
||||
|
||||
private static void internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase)
|
||||
@NonNull String passphrase,
|
||||
boolean closeOutputStream)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
@@ -155,7 +164,9 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
outputStream.writeEnd();
|
||||
} finally {
|
||||
outputStream.close();
|
||||
if (closeOutputStream) {
|
||||
outputStream.close();
|
||||
}
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,19 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
try (InputStream is = getInputStream(context, uri)) {
|
||||
importFile(context, attachmentSecret, db, is, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull InputStream is, @NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
try (InputStream is = getInputStream(context, uri)) {
|
||||
try {
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
@@ -200,7 +200,7 @@ public class ApplicationDependencies {
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeConnectionsAfterProxyFailure() {
|
||||
public static void closeConnections() {
|
||||
synchronized (LOCK) {
|
||||
if (incomingMessageObserver != null) {
|
||||
incomingMessageObserver.terminateAsync();
|
||||
@@ -220,7 +220,7 @@ public class ApplicationDependencies {
|
||||
public static void resetNetworkConnectionsAfterProxyChange() {
|
||||
synchronized (LOCK) {
|
||||
getPipeListener().reset();
|
||||
closeConnectionsAfterProxyFailure();
|
||||
closeConnections();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() { }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public final class LocalBackupJob extends BaseJob {
|
||||
|
||||
private static final String TAG = Log.tag(LocalBackupJob.class);
|
||||
|
||||
private static final String QUEUE = "__LOCAL_BACKUP__";
|
||||
public static final String QUEUE = "__LOCAL_BACKUP__";
|
||||
|
||||
public static final String TEMP_BACKUP_FILE_PREFIX = ".backup";
|
||||
public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp";
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* Provide a way to block network access while performing a device transfer.
|
||||
*/
|
||||
public final class DeviceTransferBlockingInterceptor implements Interceptor {
|
||||
|
||||
private static final String TAG = Log.tag(DeviceTransferBlockingInterceptor.class);
|
||||
|
||||
private static final DeviceTransferBlockingInterceptor INSTANCE = new DeviceTransferBlockingInterceptor();
|
||||
|
||||
private volatile boolean blockNetworking = false;
|
||||
|
||||
public static DeviceTransferBlockingInterceptor getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
|
||||
if (!blockNetworking) {
|
||||
return chain.proceed(chain.request());
|
||||
}
|
||||
|
||||
Log.w(TAG, "Preventing request because in transfer mode.");
|
||||
return new Response.Builder().request(chain.request())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.receivedResponseAtMillis(System.currentTimeMillis())
|
||||
.message("")
|
||||
.body(ResponseBody.create(null, ""))
|
||||
.code(500)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void blockNetwork() {
|
||||
blockNetworking = true;
|
||||
ApplicationDependencies.closeConnections();
|
||||
}
|
||||
|
||||
public void unblockNetwork() {
|
||||
blockNetworking = false;
|
||||
ApplicationDependencies.getIncomingMessageObserver();
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class PipeConnectivityListener implements ConnectivityListener {
|
||||
|
||||
if (SignalStore.proxy().isProxyEnabled()) {
|
||||
Log.w(TAG, "Encountered an error while we had a proxy set! Terminating the connection to prevent retry spam.");
|
||||
ApplicationDependencies.closeConnectionsAfterProxyFailure();
|
||||
ApplicationDependencies.closeConnections();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
|
||||
@@ -10,6 +10,7 @@ public final class NotificationIds {
|
||||
public static final int PRE_REGISTRATION_SMS = 5050;
|
||||
public static final int THREAD = 50000;
|
||||
public static final int USER_NOTIFICATION_MIGRATION = 525600;
|
||||
public static final int DEVICE_TRANSFER = 625420;
|
||||
|
||||
private NotificationIds() { }
|
||||
|
||||
|
||||
@@ -61,8 +61,9 @@ public class Permissions {
|
||||
private Consumer<List<String>> someDeniedListener;
|
||||
private Consumer<List<String>> somePermanentlyDeniedListener;
|
||||
|
||||
private @DrawableRes int[] rationalDialogHeader;
|
||||
private String rationaleDialogMessage;
|
||||
private @DrawableRes int[] rationalDialogHeader;
|
||||
private String rationaleDialogMessage;
|
||||
private boolean rationaleDialogCancelable;
|
||||
|
||||
private boolean ifNecesary;
|
||||
|
||||
@@ -89,8 +90,13 @@ public class Permissions {
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) {
|
||||
this.rationalDialogHeader = headers;
|
||||
this.rationaleDialogMessage = message;
|
||||
return withRationaleDialog(message, true, headers);
|
||||
}
|
||||
|
||||
public PermissionsBuilder withRationaleDialog(@NonNull String message, boolean cancelable, @NonNull @DrawableRes int... headers) {
|
||||
this.rationalDialogHeader = headers;
|
||||
this.rationaleDialogMessage = message;
|
||||
this.rationaleDialogCancelable = cancelable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -159,6 +165,7 @@ public class Permissions {
|
||||
RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader)
|
||||
.setPositiveButton(R.string.Permissions_continue, (dialog, which) -> executePermissionsRequest(request))
|
||||
.setNegativeButton(R.string.Permissions_not_now, (dialog, which) -> executeNoPermissionsRequest(request))
|
||||
.setCancelable(rationaleDialogCancelable)
|
||||
.show()
|
||||
.getWindow()
|
||||
.setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@@ -248,7 +255,7 @@ public class Permissions {
|
||||
resultListener.onResult(permissions, grantResults, shouldShowRationaleDialog);
|
||||
}
|
||||
|
||||
private static Intent getApplicationSettingsIntent(@NonNull Context context) {
|
||||
public static Intent getApplicationSettingsIntent(@NonNull Context context) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", context.getPackageName(), null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -10,6 +11,7 @@ import androidx.preference.ListPreference;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
@@ -34,6 +36,11 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
findPreference(TextSecurePreferences.TRANSFER).setOnPreferenceClickListener(unused -> {
|
||||
goToTransferAccount();
|
||||
return true;
|
||||
});
|
||||
|
||||
findPreference(PREFER_SYSTEM_CONTACT_PHOTOS)
|
||||
.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
SignalStore.settings().setPreferSystemContactPhotos(newValue == Boolean.TRUE);
|
||||
@@ -71,6 +78,10 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(new BackupsPreferenceFragment());
|
||||
}
|
||||
|
||||
private void goToTransferAccount() {
|
||||
requireContext().startActivity(new Intent(requireContext(), OldDeviceTransferActivity.class));
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.CustomDns;
|
||||
import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor;
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||
import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor;
|
||||
import org.thoughtcrime.securesms.net.SequentialDns;
|
||||
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
|
||||
@@ -180,7 +181,10 @@ public class SignalServiceNetworkAccess {
|
||||
|
||||
final String[] fastUrls = {"https://cdn.sstatic.net", "https://github.githubassets.com", "https://pinterest.com", "https://open.scdn.co", "https://www.redditstatic.com"};
|
||||
|
||||
final List<Interceptor> interceptors = Arrays.asList(new StandardUserAgentInterceptor(), new RemoteDeprecationDetectorInterceptor(), new DeprecatedClientPreventionInterceptor());
|
||||
final List<Interceptor> interceptors = Arrays.asList(new StandardUserAgentInterceptor(),
|
||||
new RemoteDeprecationDetectorInterceptor(),
|
||||
new DeprecatedClientPreventionInterceptor(),
|
||||
DeviceTransferBlockingInterceptor.getInstance());
|
||||
final Optional<Dns> dns = Optional.of(DNS);
|
||||
|
||||
final byte[] zkGroupServerPublicParams;
|
||||
|
||||
@@ -42,17 +42,12 @@ public class ChooseBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext())) {
|
||||
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
|
||||
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
|
||||
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
} else {
|
||||
Log.i(TAG, "User Selection is not required. Skipping.");
|
||||
Navigation.findNavController(requireView()).navigate(ChooseBackupFragmentDirections.actionSkip());
|
||||
}
|
||||
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
|
||||
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -19,12 +20,16 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.devicetransfer.DeviceToDeviceTransferService;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
@@ -61,7 +66,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
|
||||
|
||||
private CircularProgressButton continueButton;
|
||||
private View restoreFromBackup;
|
||||
private Button restoreFromBackup;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
@@ -104,15 +109,14 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
continueButton = view.findViewById(R.id.welcome_continue_button);
|
||||
continueButton.setOnClickListener(this::continueClicked);
|
||||
|
||||
restoreFromBackup = view.findViewById(R.id.welcome_restore_backup);
|
||||
restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
|
||||
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
|
||||
|
||||
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
|
||||
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
|
||||
|
||||
if (canUserSelectBackup()) {
|
||||
restoreFromBackup.setVisibility(View.VISIBLE);
|
||||
welcomeTermsButton.setTextColor(ContextCompat.getColor(requireActivity(), R.color.core_grey_60));
|
||||
if (!canUserSelectBackup()) {
|
||||
restoreFromBackup.setText(R.string.registration_activity__transfer_account);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +126,17 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null) {
|
||||
Log.i(TAG, "Found existing transferStatus, redirect to transfer flow");
|
||||
NavHostFragment.findNavController(this).navigate(R.id.action_welcomeFragment_to_deviceTransferSetup);
|
||||
} else {
|
||||
DeviceToDeviceTransferService.stop(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private void continueClicked(@NonNull View view) {
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
|
||||
|
||||
@@ -177,7 +192,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
initializeNumber();
|
||||
|
||||
Navigation.findNavController(view)
|
||||
.navigate(WelcomeFragmentDirections.actionChooseBackup());
|
||||
.navigate(WelcomeFragmentDirections.actionTransferOrRestore());
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class SignalProxyUtil {
|
||||
public static void startListeningToWebsocket() {
|
||||
if (SignalStore.proxy().isProxyEnabled() && ApplicationDependencies.getPipeListener().getState().getValue() == PipeConnectivityListener.State.FAILURE) {
|
||||
Log.w(TAG, "Proxy is in a failed state. Restarting.");
|
||||
ApplicationDependencies.closeConnectionsAfterProxyFailure();
|
||||
ApplicationDependencies.closeConnections();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getIncomingMessageObserver();
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.DynamicDrawableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class SpanUtil {
|
||||
|
||||
@@ -45,6 +54,17 @@ public class SpanUtil {
|
||||
return spannable;
|
||||
}
|
||||
|
||||
public static CharSequence boldSubstring(CharSequence fullString, CharSequence substring) {
|
||||
SpannableString spannable = new SpannableString(fullString);
|
||||
int start = TextUtils.indexOf(fullString, substring);
|
||||
int end = start + substring.length();
|
||||
|
||||
if (start >= 0 && end <= fullString.length()) {
|
||||
spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
return spannable;
|
||||
}
|
||||
|
||||
public static CharSequence color(int color, CharSequence sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new ForegroundColorSpan(color), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@@ -52,8 +72,12 @@ public class SpanUtil {
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence bullet(@NonNull CharSequence sequence) {
|
||||
return bullet(sequence, BulletSpan.STANDARD_GAP_WIDTH);
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence bullet(@NonNull CharSequence sequence, int gapWidth) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new BulletSpan(), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new BulletSpan(gapWidth), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
@@ -66,4 +90,30 @@ public class SpanUtil {
|
||||
|
||||
return imageSpan;
|
||||
}
|
||||
|
||||
public static CharSequence clickSubstring(@NonNull Context context, @NonNull CharSequence fullString, @NonNull CharSequence substring, @NonNull View.OnClickListener clickListener) {
|
||||
ClickableSpan clickable = new ClickableSpan() {
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setUnderlineText(false);
|
||||
ds.setColor(ContextCompat.getColor(context, R.color.signal_accent_primary));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
clickListener.onClick(widget);
|
||||
}
|
||||
};
|
||||
|
||||
SpannableString spannable = new SpannableString(fullString);
|
||||
int start = TextUtils.indexOf(fullString, substring);
|
||||
int end = start + substring.length();
|
||||
|
||||
if (start >= 0 && end <= fullString.length()) {
|
||||
spannable.setSpan(clickable, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
return spannable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,8 @@ public class TextSecurePreferences {
|
||||
private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase";
|
||||
private static final String BACKUP_TIME = "pref_backup_next_time";
|
||||
|
||||
public static final String TRANSFER = "pref_transfer";
|
||||
|
||||
public static final String SCREEN_LOCK = "pref_android_screen_lock";
|
||||
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import com.annimon.stream.function.Predicate;
|
||||
|
||||
@@ -76,6 +77,13 @@ public final class LiveDataUtil {
|
||||
return outputLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a map operation on the source observable and then only emits the mapped item if it has changed since the previous emission.
|
||||
*/
|
||||
public static <A, B> LiveData<B> mapDistinct(@NonNull LiveData<A> source, @NonNull androidx.arch.core.util.Function<A, B> mapFunction) {
|
||||
return Transformations.distinctUntilChanged(Transformations.map(source, mapFunction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Once there is non-null data on both input {@link LiveData}, the {@link Combine} function is run
|
||||
* and produces a live data of the combined data.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.util.livedata;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
|
||||
import com.annimon.stream.function.Function;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Manages a state to be updated from a view model and provide direct and live access. Updates
|
||||
* occur serially on the same executor to allow updating in a thread safe way. While not
|
||||
* every state update is guaranteed to be emitted, no update action will be dropped and state
|
||||
* that is emitted will be accurate.
|
||||
*/
|
||||
public class Store<State> {
|
||||
private final LiveDataStore liveStore;
|
||||
|
||||
public Store(@NonNull State state) {
|
||||
this.liveStore = new LiveDataStore(state);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<State> getStateLiveData() {
|
||||
return liveStore;
|
||||
}
|
||||
|
||||
public @NonNull State getState() {
|
||||
return liveStore.getState();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public void update(@NonNull Function<State, State> updater) {
|
||||
liveStore.update(updater);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public <Input> void update(@NonNull LiveData<Input> source, @NonNull Action<Input, State> action) {
|
||||
liveStore.update(source, action);
|
||||
}
|
||||
|
||||
private final class LiveDataStore extends MediatorLiveData<State> {
|
||||
private State state;
|
||||
private final Executor stateUpdater;
|
||||
|
||||
LiveDataStore(@NonNull State state) {
|
||||
this.stateUpdater = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
setState(state);
|
||||
}
|
||||
|
||||
synchronized @NonNull State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
private synchronized void setState(@NonNull State state) {
|
||||
this.state = state;
|
||||
postValue(this.state);
|
||||
}
|
||||
|
||||
<Input> void update(@NonNull LiveData<Input> source, @NonNull Action<Input, State> action) {
|
||||
addSource(source, input -> stateUpdater.execute(() -> setState(action.apply(input, getState()))));
|
||||
}
|
||||
|
||||
void update(@NonNull Function<State, State> updater) {
|
||||
stateUpdater.execute(() -> setState(updater.apply(getState())));
|
||||
}
|
||||
}
|
||||
|
||||
public interface Action<Input, State> {
|
||||
State apply(Input input, State current);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user