Update registration for new restore flows.

This commit is contained in:
Cody Henthorne
2024-11-07 10:42:54 -05:00
committed by Greyson Parrelli
parent aad2624bd5
commit 22c4e2d084
140 changed files with 8364 additions and 2679 deletions

View File

@@ -1,170 +0,0 @@
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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
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 static final String TRANSFER_FINISHED_KEY = "transfer_finished";
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;
protected boolean transferFinished;
public DeviceTransferFragment() {
super(R.layout.fragment_device_transfer);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
transferFinished = savedInstanceState.getBoolean(TRANSFER_FINISHED_KEY);
}
}
@Override
public void onStart() {
super.onStart();
if (transferFinished) {
navigateToTransferComplete();
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(TRANSFER_FINISHED_KEY, transferFinished);
}
@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 MaterialAlertDialogBuilder(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();
protected abstract void navigateToTransferComplete();
private class TransferModeListener {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull TransferStatus event) {
if (event.getTransferMode() != TransferStatus.TransferMode.SERVICE_CONNECTED) {
abort();
}
}
}
protected void abort() {
abort(R.string.DeviceTransfer__transfer_failed);
}
protected void abort(@StringRes int errorMessage) {
EventBus.getDefault().unregister(transferModeListener);
DeviceToDeviceTransferService.stop(requireContext());
progress.setVisibility(View.GONE);
alert.setVisibility(View.VISIBLE);
tryAgain.setVisibility(View.VISIBLE);
title.setText(R.string.DeviceTransfer__unable_to_transfer);
status.setText(errorMessage);
cancel.setText(R.string.DeviceTransfer__cancel);
cancel.setOnClickListener(v -> navigateAwayFromTransfer());
onBackPressed.isActiveTransfer = false;
}
protected class OnBackPressed extends OnBackPressedCallback {
private boolean isActiveTransfer = true;
public OnBackPressed() {
super(true);
}
@Override
public void handleOnBackPressed() {
if (isActiveTransfer) {
cancelActiveTransfer();
} else {
navigateAwayFromTransfer();
}
}
}
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.moreoptions
/**
* Allows component opening sheet to specify mode
*/
enum class MoreTransferOrRestoreOptionsMode {
/**
* Only display the option to log in without transferring. Selection
* will be disabled.
*/
SKIP_ONLY,
/**
* Display transfer/restore local/skip as well as a next and cancel button
*/
SELECTION
}

View File

@@ -1,339 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.moreoptions
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
import org.signal.core.ui.R as CoreUiR
/**
* Lists a set of options the user can choose from for restoring backup or skipping restoration
*/
class MoreTransferOrRestoreOptionsSheet : ComposeBottomSheetDialogFragment() {
private val args by navArgs<MoreTransferOrRestoreOptionsSheetArgs>()
@Composable
override fun SheetContent() {
var selectedOption by remember {
mutableStateOf<BackupRestorationType?>(null)
}
MoreOptionsSheetContent(
mode = args.mode,
selectedOption = selectedOption,
onOptionSelected = { selectedOption = it },
onCancelClick = { findNavController().popBackStack() },
onNextClick = {
this.onNextClicked(selectedOption ?: BackupRestorationType.NONE)
}
)
}
private fun onNextClicked(selectedOption: BackupRestorationType) {
// TODO [message-requests] -- Launch next screen based off user choice
}
}
@Preview
@Composable
private fun MoreOptionsSheetContentPreview() {
Previews.BottomSheetPreview {
MoreOptionsSheetContent(
mode = MoreTransferOrRestoreOptionsMode.SKIP_ONLY,
selectedOption = null,
onOptionSelected = {},
onCancelClick = {},
onNextClick = {}
)
}
}
@Preview
@Composable
private fun MoreOptionsSheetSelectableContentPreview() {
Previews.BottomSheetPreview {
MoreOptionsSheetContent(
mode = MoreTransferOrRestoreOptionsMode.SELECTION,
selectedOption = null,
onOptionSelected = {},
onCancelClick = {},
onNextClick = {}
)
}
}
@Composable
private fun MoreOptionsSheetContent(
mode: MoreTransferOrRestoreOptionsMode,
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit,
onCancelClick: () -> Unit,
onNextClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
) {
BottomSheets.Handle()
Spacer(modifier = Modifier.size(42.dp))
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
TransferFromAndroidDeviceOption(
selectedOption = selectedOption,
onOptionSelected = onOptionSelected
)
Spacer(modifier = Modifier.size(16.dp))
RestoreLocalBackupOption(
selectedOption = selectedOption,
onOptionSelected = onOptionSelected
)
Spacer(modifier = Modifier.size(16.dp))
}
LogInWithoutTransferringOption(
selectedOption = selectedOption,
onOptionSelected = when (mode) {
MoreTransferOrRestoreOptionsMode.SKIP_ONLY -> { _ -> onNextClick() }
MoreTransferOrRestoreOptionsMode.SELECTION -> onOptionSelected
}
)
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp, bottom = 24.dp)
) {
TextButton(
onClick = onCancelClick
) {
Text(text = stringResource(id = android.R.string.cancel))
}
Spacer(modifier = Modifier.weight(1f))
Buttons.LargeTonal(
enabled = selectedOption != null,
onClick = onNextClick
) {
Text(text = stringResource(id = R.string.RegistrationActivity_next))
}
}
} else {
Spacer(modifier = Modifier.size(45.dp))
}
}
}
@Preview
@Composable
private fun LogInWithoutTransferringOptionPreview() {
Previews.BottomSheetPreview {
LogInWithoutTransferringOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun LogInWithoutTransferringOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.NONE,
title = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__log_in_without_transferring),
subtitle = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__continue_without_transferring),
onClick = { onOptionSelected(BackupRestorationType.NONE) }
)
}
@Preview
@Composable
private fun TransferFromAndroidDeviceOptionPreview() {
Previews.BottomSheetPreview {
TransferFromAndroidDeviceOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun TransferFromAndroidDeviceOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.DEVICE_TRANSFER,
title = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__transfer_from_android_device),
subtitle = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__transfer_your_account_and_messages),
onClick = { onOptionSelected(BackupRestorationType.DEVICE_TRANSFER) }
)
}
@Preview
@Composable
private fun RestoreLocalBackupOptionPreview() {
Previews.BottomSheetPreview {
RestoreLocalBackupOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun RestoreLocalBackupOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.LOCAL_BACKUP,
title = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__restore_local_backup),
subtitle = stringResource(id = R.string.MoreTransferOrRestoreOptionsSheet__restore_your_messages),
onClick = { onOptionSelected(BackupRestorationType.LOCAL_BACKUP) }
)
}
@Preview
@Composable
private fun OptionPreview() {
Previews.BottomSheetPreview {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = false,
title = "Option Preview Title",
subtitle = "Option Preview Subtitle",
onClick = {}
)
}
}
@Composable
private fun Option(
icon: @Composable () -> Unit,
isSelected: Boolean,
title: String,
subtitle: String,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(12.dp)
)
.border(
width = if (isSelected) 2.dp else 0.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
)
.clip(RoundedCornerShape(12.dp))
.clickable { onClick() }
.padding(vertical = 21.dp)
) {
icon()
Column {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.newdevice
/**
* What kind of backup restore the user wishes to perform.
*/
enum class BackupRestorationType {
DEVICE_TRANSFER,
LOCAL_BACKUP,
REMOTE_BACKUP,
NONE
}

View File

@@ -73,6 +73,8 @@ final class NewDeviceServerTask implements ServerTask {
long end = System.currentTimeMillis();
Log.i(TAG, "Receive took: " + (end - start));
EventBus.getDefault().post(new Status(0, Status.State.RESTORE_COMPLETE));
}
@Subscribe(threadMode = ThreadMode.POSTING)
@@ -80,7 +82,7 @@ final class NewDeviceServerTask implements ServerTask {
if (event.getType() == BackupEvent.Type.PROGRESS) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.IN_PROGRESS));
} else if (event.getType() == BackupEvent.Type.FINISHED) {
EventBus.getDefault().post(new Status(event.getCount(), Status.State.SUCCESS));
EventBus.getDefault().post(new Status(event.getCount(), Status.State.TRANSFER_COMPLETE));
}
}
@@ -103,7 +105,8 @@ final class NewDeviceServerTask implements ServerTask {
public enum State {
IN_PROGRESS,
SUCCESS,
TRANSFER_COMPLETE,
RESTORE_COMPLETE,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN

View File

@@ -6,11 +6,10 @@ 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;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.restore.RestoreActivity;
/**
* Shown after the new device successfully completes receiving a backup from the old device.
@@ -23,8 +22,7 @@ public final class NewDeviceTransferCompleteFragment extends LoggingFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.new_device_transfer_complete_fragment_continue_registration)
.setOnClickListener(v -> SafeNavigation.safeNavigate(NavHostFragment.findNavController(this),
R.id.action_newDeviceTransferComplete_to_enterPhoneNumberFragment));
.setOnClickListener(v -> ((RestoreActivity) requireActivity()).onBackupCompletedSuccessfully());
}
@Override

View File

@@ -1,80 +0,0 @@
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;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
/**
* 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() {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferInstructions());
}
@Override
protected void navigateAwayFromTransfer() {
EventBus.getDefault().unregister(serverTaskListener);
requireActivity().finish();
}
@Override
protected void navigateToTransferComplete() {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferComplete());
}
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:
transferFinished = true;
DeviceToDeviceTransferService.stop(requireContext());
SignalStore.registration().markRestoreCompleted();
navigateToTransferComplete();
break;
case FAILURE_VERSION_DOWNGRADE:
abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal);
break;
case FAILURE_FOREIGN_KEY:
abort(R.string.NewDeviceTransfer__failure_foreign_key);
break;
case FAILURE_UNKNOWN:
abort();
break;
}
}
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
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.keyvalue.SignalStore
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.restore.devicetransfer.DeviceTransferFragment
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Shows transfer progress on the new device. Most logic is in [DeviceTransferFragment]
* and it delegates to this class for strings, navigation, and updating progress.
*/
class NewDeviceTransferFragment : DeviceTransferFragment() {
private val viewModel: NewDeviceTransferViewModel by viewModels()
private val serverTaskListener = ServerTaskListener()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EventBus.getDefault().register(serverTaskListener)
}
override fun onDestroyView() {
EventBus.getDefault().unregister(serverTaskListener)
super.onDestroyView()
}
override fun navigateToRestartTransfer() {
findNavController().safeNavigate(NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferInstructions())
}
override fun navigateAwayFromTransfer() {
EventBus.getDefault().unregister(serverTaskListener)
requireActivity().finish()
}
override fun navigateToTransferComplete() {
if (SignalStore.account.isRegistered) {
(requireActivity() as RestoreActivity).onBackupCompletedSuccessfully()
} else {
findNavController().safeNavigate(NewDeviceTransferFragmentDirections.actionNewDeviceTransferToNewDeviceTransferComplete())
}
}
private fun onRestoreComplete() {
ignoreTransferStatusEvents()
DeviceToDeviceTransferService.stop(requireContext())
viewModel.onRestoreComplete(requireContext()) {
transferFinished = true
navigateToTransferComplete()
}
}
private inner class ServerTaskListener {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: NewDeviceServerTask.Status) {
status.text = getString(R.string.DeviceTransfer__d_messages_so_far, event.messageCount)
when (event.state) {
NewDeviceServerTask.Status.State.IN_PROGRESS,
NewDeviceServerTask.Status.State.TRANSFER_COMPLETE -> Unit
NewDeviceServerTask.Status.State.RESTORE_COMPLETE -> onRestoreComplete()
NewDeviceServerTask.Status.State.FAILURE_VERSION_DOWNGRADE -> abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal)
NewDeviceServerTask.Status.State.FAILURE_FOREIGN_KEY -> abort(R.string.NewDeviceTransfer__failure_foreign_key)
NewDeviceServerTask.Status.State.FAILURE_UNKNOWN -> abort()
}
}
}
}

View File

@@ -27,7 +27,7 @@ public final class NewDeviceTransferSetupFragment extends DeviceTransferSetupFra
@Override
protected void navigateAwayFromTransfer() {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_deviceTransferSetup_to_transferOrRestore);
requireActivity().onNavigateUp();
}
@Override
@@ -78,7 +78,7 @@ public final class NewDeviceTransferSetupFragment extends DeviceTransferSetupFra
@Override
protected void navigateWhenWifiDirectUnavailable() {
SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_deviceTransferSetup_to_transferOrRestore);
requireActivity().onNavigateUp();
}
@Override

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.newdevice
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
class NewDeviceTransferViewModel : ViewModel() {
fun onRestoreComplete(context: Context, onComplete: () -> Unit) {
viewModelScope.launch {
SignalStore.registration.localRegistrationMetadata?.let { metadata ->
RegistrationRepository.registerAccountLocally(context, metadata)
SignalStore.registration.clearLocalRegistrationMetadata()
RegistrationUtil.maybeMarkRegistrationComplete()
}
SignalStore.registration.markRestoreCompleted()
withContext(Dispatchers.Main) {
onComplete()
}
}
}
}

View File

@@ -1,71 +0,0 @@
package org.thoughtcrime.securesms.devicetransfer.newdevice;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreBinding;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
/**
* Simple jumping off menu to starts a device-to-device transfer or restore a backup.
*/
public final class TransferOrRestoreFragment extends LoggingFragment {
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
private FragmentTransferRestoreBinding binding;
public TransferOrRestoreFragment() {
super(R.layout.fragment_transfer_restore);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
binding = FragmentTransferRestoreBinding.bind(view);
TransferOrRestoreViewModel viewModel = new ViewModelProvider(this).get(TransferOrRestoreViewModel.class);
binding.transferOrRestoreFragmentTransfer.setOnClickListener(v -> viewModel.onTransferFromAndroidDeviceSelected());
binding.transferOrRestoreFragmentRestore.setOnClickListener(v -> viewModel.onRestoreFromLocalBackupSelected());
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener(v -> viewModel.onRestoreFromRemoteBackupSelected());
binding.transferOrRestoreFragmentNext.setOnClickListener(v -> launchSelection(viewModel.getStateSnapshot()));
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transferOrRestore_to_moreOptions));
int visibility = RemoteConfig.messageBackups() ? View.VISIBLE : View.GONE;
binding.transferOrRestoreFragmentRestoreRemoteCard.setVisibility(visibility);
binding.transferOrRestoreFragmentMoreOptions.setVisibility(visibility);
String description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device);
String toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device);
binding.transferOrRestoreFragmentTransferDescription.setText(SpanUtil.boldSubstring(description, toBold));
lifecycleDisposable.bindTo(getViewLifecycleOwner());
lifecycleDisposable.add(viewModel.getState().subscribe(this::updateSelection));
}
private void updateSelection(BackupRestorationType restorationType) {
binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == BackupRestorationType.DEVICE_TRANSFER);
binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == BackupRestorationType.LOCAL_BACKUP);
binding.transferOrRestoreFragmentRestoreRemoteCard.setSelected(restorationType == BackupRestorationType.REMOTE_BACKUP);
}
private void launchSelection(BackupRestorationType restorationType) {
switch (restorationType) {
case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions);
case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transfer_or_restore_to_local_restore);
case REMOTE_BACKUP -> {}
default -> throw new IllegalArgumentException();
}
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.newdevice
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
/**
* Maintains state of the TransferOrRestoreFragment
*/
class TransferOrRestoreViewModel : ViewModel() {
private val internalState = BehaviorProcessor.createDefault(BackupRestorationType.DEVICE_TRANSFER)
val state: Flowable<BackupRestorationType> = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
val stateSnapshot: BackupRestorationType get() = internalState.value!!
fun onTransferFromAndroidDeviceSelected() {
internalState.onNext(BackupRestorationType.DEVICE_TRANSFER)
}
fun onRestoreFromLocalBackupSelected() {
internalState.onNext(BackupRestorationType.LOCAL_BACKUP)
}
fun onRestoreFromRemoteBackupSelected() {
internalState.onNext(BackupRestorationType.REMOTE_BACKUP)
}
}

View File

@@ -13,7 +13,7 @@ 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;
import org.thoughtcrime.securesms.restore.devicetransfer.DeviceTransferFragment;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import java.text.NumberFormat;
@@ -66,16 +66,16 @@ public final class OldDeviceTransferFragment extends DeviceTransferFragment {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull OldDeviceClientTask.Status event) {
if (event.isDone()) {
transferFinished = true;
setTransferFinished(true);
ignoreTransferStatusEvents();
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
DeviceToDeviceTransferService.stop(requireContext());
SafeNavigation.safeNavigate(NavHostFragment.findNavController(OldDeviceTransferFragment.this), R.id.action_oldDeviceTransfer_to_oldDeviceTransferComplete);
} else {
if (event.getEstimatedMessageCount() == 0) {
status.setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount()));
getStatus().setText(getString(R.string.DeviceTransfer__d_messages_so_far, event.getMessageCount()));
} else {
status.setText(getString(R.string.DeviceTransfer__s_of_messages_so_far, formatter.format(event.getCompletionPercentage())));
getStatus().setText(getString(R.string.DeviceTransfer__s_of_messages_so_far, formatter.format(event.getCompletionPercentage())));
}
}
}