Add user restore method selection plumbing to old device.
This commit is contained in:
parent
b6bb3928e7
commit
75f0d3363b
22 changed files with 457 additions and 72 deletions
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
|
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
|
||||||
|
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog;
|
||||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||||
|
@ -191,7 +192,16 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
dynamicTheme.onResume(this);
|
dynamicTheme.onResume(this);
|
||||||
if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
|
||||||
|
if (SignalStore.misc().getShouldShowLinkedDevicesReminder()) {
|
||||||
|
SignalStore.misc().setShouldShowLinkedDevicesReminder(false);
|
||||||
|
RelinkDevicesReminderBottomSheetFragment.show(getSupportFragmentManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SignalStore.registration().isRestoringOnNewDevice()) {
|
||||||
|
SignalStore.registration().setRestoringOnNewDevice(false);
|
||||||
|
RestoreCompleteBottomSheetDialog.show(getSupportFragmentManager());
|
||||||
|
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||||
new MaterialAlertDialogBuilder(this)
|
new MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.OldDeviceTransferLockedDialog__complete_registration_on_your_new_device)
|
.setTitle(R.string.OldDeviceTransferLockedDialog__complete_registration_on_your_new_device)
|
||||||
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
|
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
|
||||||
|
@ -204,11 +214,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SignalStore.misc().getShouldShowLinkedDevicesReminder()) {
|
|
||||||
SignalStore.misc().setShouldShowLinkedDevicesReminder(false);
|
|
||||||
RelinkDevicesReminderBottomSheetFragment.show(getSupportFragmentManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTabVisibility();
|
updateTabVisibility();
|
||||||
|
|
||||||
vitalsViewModel.checkSlowNotificationHeuristics();
|
vitalsViewModel.checkSlowNotificationHeuristics();
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.conversationlist
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import org.signal.core.ui.BottomSheets
|
||||||
|
import org.signal.core.ui.Buttons
|
||||||
|
import org.signal.core.ui.Previews
|
||||||
|
import org.signal.core.ui.SignalPreview
|
||||||
|
import org.signal.core.ui.horizontalGutters
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet dialog shown on an old device after the user has decided to transfer/restore to a new device.
|
||||||
|
*/
|
||||||
|
class RestoreCompleteBottomSheetDialog : ComposeBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(fragmentManager: FragmentManager) {
|
||||||
|
RestoreCompleteBottomSheetDialog().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val peekHeightPercentage: Float = 1f
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun SheetContent() {
|
||||||
|
RestoreCompleteContent(
|
||||||
|
isAfterDeviceTransfer = SignalStore.misc.isOldDeviceTransferLocked,
|
||||||
|
onOkayClick = this::dismissAllowingStateLoss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RestoreCompleteContent(
|
||||||
|
isAfterDeviceTransfer: Boolean = false,
|
||||||
|
onOkayClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalGutters()
|
||||||
|
.padding(bottom = 54.dp)
|
||||||
|
) {
|
||||||
|
BottomSheets.Handle()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.symbol_check_circle_40),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val title = if (isAfterDeviceTransfer) R.string.RestoreCompleteBottomSheet_transfer_complete else R.string.RestoreCompleteBottomSheet_restore_complete
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val message = if (isAfterDeviceTransfer) R.string.RestoreCompleteBottomSheet_transfer_complete_message else R.string.RestoreCompleteBottomSheet_restore_complete_message
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = message),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(54.dp))
|
||||||
|
|
||||||
|
Buttons.LargeTonal(
|
||||||
|
onClick = onOkayClick,
|
||||||
|
modifier = Modifier.widthIn(min = 220.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.RestoreCompleteBottomSheet_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun RestoreCompleteContentPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
RestoreCompleteContent(isAfterDeviceTransfer = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SignalPreview
|
||||||
|
@Composable
|
||||||
|
private fun RestoreCompleteContentAfterDeviceTransferPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
RestoreCompleteContent(isAfterDeviceTransfer = true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||||
|
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||||
import org.whispersystems.signalservice.api.services.CallLinksService
|
import org.whispersystems.signalservice.api.services.CallLinksService
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService
|
import org.whispersystems.signalservice.api.services.ProfileService
|
||||||
|
@ -298,6 +299,10 @@ object AppDependencies {
|
||||||
val linkDeviceApi: LinkDeviceApi
|
val linkDeviceApi: LinkDeviceApi
|
||||||
get() = networkModule.linkDeviceApi
|
get() = networkModule.linkDeviceApi
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val registrationApi: RegistrationApi
|
||||||
|
get() = networkModule.registrationApi
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val okHttpClient: OkHttpClient
|
val okHttpClient: OkHttpClient
|
||||||
get() = networkModule.okHttpClient
|
get() = networkModule.okHttpClient
|
||||||
|
@ -361,5 +366,6 @@ object AppDependencies {
|
||||||
fun provideKeysApi(pushServiceSocket: PushServiceSocket): KeysApi
|
fun provideKeysApi(pushServiceSocket: PushServiceSocket): KeysApi
|
||||||
fun provideAttachmentApi(signalWebSocket: SignalWebSocket, pushServiceSocket: PushServiceSocket): AttachmentApi
|
fun provideAttachmentApi(signalWebSocket: SignalWebSocket, pushServiceSocket: PushServiceSocket): AttachmentApi
|
||||||
fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi
|
fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi
|
||||||
|
fun provideRegistrationApi(pushServiceSocket: PushServiceSocket): RegistrationApi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi;
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
|
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||||
import org.whispersystems.signalservice.api.services.CallLinksService;
|
import org.whispersystems.signalservice.api.services.CallLinksService;
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||||
|
@ -473,6 +474,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||||
return new LinkDeviceApi(pushServiceSocket);
|
return new LinkDeviceApi(pushServiceSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull RegistrationApi provideRegistrationApi(@NonNull PushServiceSocket pushServiceSocket) {
|
||||||
|
return new RegistrationApi(pushServiceSocket);
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore
|
import org.whispersystems.signalservice.api.push.TrustStore
|
||||||
|
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||||
import org.whispersystems.signalservice.api.services.CallLinksService
|
import org.whispersystems.signalservice.api.services.CallLinksService
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService
|
import org.whispersystems.signalservice.api.services.ProfileService
|
||||||
|
@ -143,6 +144,10 @@ class NetworkDependenciesModule(
|
||||||
provider.provideLinkDeviceApi(pushServiceSocket)
|
provider.provideLinkDeviceApi(pushServiceSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val registrationApi: RegistrationApi by lazy {
|
||||||
|
provider.provideRegistrationApi(pushServiceSocket)
|
||||||
|
}
|
||||||
|
|
||||||
val okHttpClient: OkHttpClient by lazy {
|
val okHttpClient: OkHttpClient by lazy {
|
||||||
OkHttpClient.Builder()
|
OkHttpClient.Builder()
|
||||||
.addInterceptor(StandardUserAgentInterceptor())
|
.addInterceptor(StandardUserAgentInterceptor())
|
||||||
|
|
|
@ -1,35 +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.Navigation;
|
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
|
||||||
import org.signal.devicetransfer.TransferStatus;
|
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 -> SafeNavigation.safeNavigate(Navigation.findNavController(v), R.id.action_device_transfer_setup));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
EventBus.getDefault().removeStickyEvent(TransferStatus.class);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.thoughtcrime.securesms.devicetransfer.newdevice
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.signal.devicetransfer.TransferStatus
|
||||||
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows instructions for new device to being transfer.
|
||||||
|
*/
|
||||||
|
class NewDeviceTransferInstructionsFragment : LoggingFragment(R.layout.new_device_transfer_instructions_fragment) {
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
view
|
||||||
|
.findViewById<View>(R.id.new_device_transfer_instructions_fragment_continue)
|
||||||
|
.setOnClickListener { findNavController().safeNavigate(R.id.action_device_transfer_setup) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
EventBus.getDefault().removeStickyEvent(TransferStatus::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
|
||||||
private const val SKIPPED_TRANSFER_OR_RESTORE = "registration.has_skipped_transfer_or_restore"
|
private const val SKIPPED_TRANSFER_OR_RESTORE = "registration.has_skipped_transfer_or_restore"
|
||||||
private const val LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
|
private const val LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
|
||||||
private const val RESTORE_COMPLETED = "registration.backup_restore_completed"
|
private const val RESTORE_COMPLETED = "registration.backup_restore_completed"
|
||||||
|
private const val RESTORE_METHOD_TOKEN = "registration.restore_method_token"
|
||||||
|
private const val RESTORING_ON_NEW_DEVICE = "registration.restoring_on_new_device"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -58,6 +60,10 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
|
||||||
var hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
|
var hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
|
||||||
var sessionId: String? by stringValue(SESSION_ID, null)
|
var sessionId: String? by stringValue(SESSION_ID, null)
|
||||||
var sessionE164: String? by stringValue(SESSION_E164, null)
|
var sessionE164: String? by stringValue(SESSION_E164, null)
|
||||||
|
var restoreMethodToken: String? by stringValue(RESTORE_METHOD_TOKEN, null)
|
||||||
|
|
||||||
|
@get:JvmName("isRestoringOnNewDevice")
|
||||||
|
var restoringOnNewDevice: Boolean by booleanValue(RESTORING_ON_NEW_DEVICE, false)
|
||||||
|
|
||||||
fun hasSkippedTransferOrRestore(): Boolean {
|
fun hasSkippedTransferOrRestore(): Boolean {
|
||||||
return getBoolean(SKIPPED_TRANSFER_OR_RESTORE, false)
|
return getBoolean(SKIPPED_TRANSFER_OR_RESTORE, false)
|
||||||
|
|
|
@ -36,6 +36,7 @@ public final class RegistrationUtil {
|
||||||
Log.i(TAG, "Marking registration completed.", new Throwable());
|
Log.i(TAG, "Marking registration completed.", new Throwable());
|
||||||
SignalStore.registration().markRegistrationComplete();
|
SignalStore.registration().markRegistrationComplete();
|
||||||
SignalStore.registration().setLocalRegistrationMetadata(null);
|
SignalStore.registration().setLocalRegistrationMetadata(null);
|
||||||
|
SignalStore.registration().setRestoreMethodToken(null);
|
||||||
|
|
||||||
if (SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode() == PhoneNumberDiscoverabilityMode.UNDECIDED) {
|
if (SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode() == PhoneNumberDiscoverabilityMode.UNDECIDED) {
|
||||||
Log.w(TAG, "Phone number discoverability mode is still UNDECIDED. Setting to DISCOVERABLE.");
|
Log.w(TAG, "Phone number discoverability mode is still UNDECIDED. Setting to DISCOVERABLE.");
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
package org.thoughtcrime.securesms.registrationv3.data
|
package org.thoughtcrime.securesms.registrationv3.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.signal.core.util.Base64.decode
|
import org.signal.core.util.Base64.decode
|
||||||
import org.signal.core.util.Hex
|
import org.signal.core.util.Hex
|
||||||
import org.signal.core.util.isNotNullOrBlank
|
import org.signal.core.util.isNotNullOrBlank
|
||||||
|
@ -16,7 +20,11 @@ import org.signal.registration.proto.RegistrationProvisionMessage
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import org.whispersystems.signalservice.api.registration.RestoreMethod
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers for quickly re-registering on a new device with the old device.
|
* Helpers for quickly re-registering on a new device with the old device.
|
||||||
|
@ -41,7 +49,7 @@ object QuickRegistrationRepository {
|
||||||
/**
|
/**
|
||||||
* Send registration provisioning message to new device.
|
* Send registration provisioning message to new device.
|
||||||
*/
|
*/
|
||||||
fun transferAccount(reRegisterUri: String): TransferAccountResult {
|
fun transferAccount(reRegisterUri: String, restoreMethodToken: String): TransferAccountResult {
|
||||||
if (!isValidReRegistrationQr(reRegisterUri)) {
|
if (!isValidReRegistrationQr(reRegisterUri)) {
|
||||||
Log.w(TAG, "Invalid quick re-register qr data")
|
Log.w(TAG, "Invalid quick re-register qr data")
|
||||||
return TransferAccountResult.FAILED
|
return TransferAccountResult.FAILED
|
||||||
|
@ -81,7 +89,8 @@ object QuickRegistrationRepository {
|
||||||
MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID
|
MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID
|
||||||
MessageBackupTier.FREE,
|
MessageBackupTier.FREE,
|
||||||
null -> RegistrationProvisionMessage.Tier.FREE
|
null -> RegistrationProvisionMessage.Tier.FREE
|
||||||
}
|
},
|
||||||
|
restoreMethodToken = restoreMethodToken
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.successOrThrow()
|
.successOrThrow()
|
||||||
|
@ -98,6 +107,60 @@ object QuickRegistrationRepository {
|
||||||
return TransferAccountResult.SUCCESS
|
return TransferAccountResult.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the restore method enum for the old device to retrieve and update their UI with.
|
||||||
|
*/
|
||||||
|
suspend fun setRestoreMethodForOldDevice(restoreMethod: RestoreMethod) {
|
||||||
|
val restoreMethodToken = SignalStore.registration.restoreMethodToken
|
||||||
|
|
||||||
|
if (restoreMethodToken != null) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Setting restore method ***${restoreMethodToken.takeLast(4)}: $restoreMethod")
|
||||||
|
var retries = 3
|
||||||
|
var result: NetworkResult<Unit>? = null
|
||||||
|
while (retries-- > 0 && result !is NetworkResult.Success) {
|
||||||
|
Log.d(TAG, "Setting method, retries remaining: $retries")
|
||||||
|
result = AppDependencies.registrationApi.setRestoreMethod(restoreMethodToken, restoreMethod)
|
||||||
|
|
||||||
|
if (result !is NetworkResult.Success) {
|
||||||
|
delay(1.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result is NetworkResult.Success) {
|
||||||
|
Log.i(TAG, "Restore method set successfully")
|
||||||
|
SignalStore.registration.restoreMethodToken = null
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Restore method set failed", result?.getCause())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the restore method used by the new device to update UI with. This is a long polling operation.
|
||||||
|
*/
|
||||||
|
suspend fun waitForRestoreMethodSelectionOnNewDevice(restoreMethodToken: String): RestoreMethod {
|
||||||
|
var retries = 5
|
||||||
|
var result: NetworkResult<RestoreMethod>? = null
|
||||||
|
|
||||||
|
Log.d(TAG, "Waiting for restore method with token: ***${restoreMethodToken.takeLast(4)}")
|
||||||
|
while (retries-- > 0 && result !is NetworkResult.Success && coroutineContext.isActive) {
|
||||||
|
Log.d(TAG, "Remaining tries $retries...")
|
||||||
|
val api = AppDependencies.registrationApi
|
||||||
|
result = api.waitForRestoreMethod(restoreMethodToken)
|
||||||
|
Log.d(TAG, "Result: $result")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result is NetworkResult.Success) {
|
||||||
|
Log.i(TAG, "Restore method selected on new device ${result.result}")
|
||||||
|
return result.result
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to determine restore method, using default")
|
||||||
|
return RestoreMethod.DECLINE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class TransferAccountResult {
|
enum class TransferAccountResult {
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
FAILED
|
FAILED
|
||||||
|
|
|
@ -41,6 +41,10 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.signal.core.ui.BottomSheets
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
import org.signal.core.ui.Dialogs
|
import org.signal.core.ui.Dialogs
|
||||||
|
@ -52,8 +56,10 @@ import org.signal.core.ui.theme.SignalTheme
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
|
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
|
||||||
import org.thoughtcrime.securesms.BiometricDeviceLockContract
|
import org.thoughtcrime.securesms.BiometricDeviceLockContract
|
||||||
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity
|
||||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
@ -63,6 +69,7 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
import org.thoughtcrime.securesms.util.viewModel
|
||||||
|
import org.whispersystems.signalservice.api.registration.RestoreMethod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launched after scanning QR code from new device to start the transfer/reregistration process from
|
* Launched after scanning QR code from new device to start the transfer/reregistration process from
|
||||||
|
@ -121,6 +128,29 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
||||||
promptInfo
|
promptInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val restoreMethodSelected = viewModel
|
||||||
|
.state
|
||||||
|
.mapNotNull { it.restoreMethodSelected }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
when (restoreMethodSelected) {
|
||||||
|
RestoreMethod.DEVICE_TRANSFER -> {
|
||||||
|
startActivities(
|
||||||
|
arrayOf(
|
||||||
|
MainActivity.clearTop(this@TransferAccountActivity),
|
||||||
|
Intent(this@TransferAccountActivity, OldDeviceTransferActivity::class.java)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreMethod.REMOTE_BACKUP,
|
||||||
|
RestoreMethod.LOCAL_BACKUP,
|
||||||
|
RestoreMethod.DECLINE,
|
||||||
|
null -> startActivity(MainActivity.clearTop(this@TransferAccountActivity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
@ -128,7 +158,11 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
||||||
TransferToNewDevice(
|
TransferToNewDevice(
|
||||||
state = state,
|
state = state,
|
||||||
onTransferAccount = this::authenticate,
|
onTransferAccount = this::authenticate,
|
||||||
clearReRegisterResult = viewModel::clearReRegisterResult,
|
onContinueOnOtherDeviceDismiss = {
|
||||||
|
finish()
|
||||||
|
viewModel.clearReRegisterResult()
|
||||||
|
},
|
||||||
|
onErrorDismiss = viewModel::clearReRegisterResult,
|
||||||
onBackClicked = { finish() }
|
onBackClicked = { finish() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -184,7 +218,8 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
||||||
fun TransferToNewDevice(
|
fun TransferToNewDevice(
|
||||||
state: TransferAccountViewModel.TransferAccountState,
|
state: TransferAccountViewModel.TransferAccountState,
|
||||||
onTransferAccount: () -> Unit = {},
|
onTransferAccount: () -> Unit = {},
|
||||||
clearReRegisterResult: () -> Unit = {},
|
onContinueOnOtherDeviceDismiss: () -> Unit = {},
|
||||||
|
onErrorDismiss: () -> Unit = {},
|
||||||
onBackClicked: () -> Unit = {}
|
onBackClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -248,7 +283,7 @@ fun TransferToNewDevice(
|
||||||
QuickRegistrationRepository.TransferAccountResult.SUCCESS -> {
|
QuickRegistrationRepository.TransferAccountResult.SUCCESS -> {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
dragHandle = null,
|
dragHandle = null,
|
||||||
onDismissRequest = clearReRegisterResult,
|
onDismissRequest = onContinueOnOtherDeviceDismiss,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
) {
|
) {
|
||||||
ContinueOnOtherDevice()
|
ContinueOnOtherDevice()
|
||||||
|
@ -256,12 +291,10 @@ fun TransferToNewDevice(
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickRegistrationRepository.TransferAccountResult.FAILED -> {
|
QuickRegistrationRepository.TransferAccountResult.FAILED -> {
|
||||||
Dialogs.SimpleAlertDialog(
|
Dialogs.SimpleMessageDialog(
|
||||||
title = Dialogs.NoTitle,
|
message = stringResource(R.string.RegistrationActivity_unable_to_connect_to_service),
|
||||||
body = stringResource(R.string.RegistrationActivity_unable_to_connect_to_service),
|
dismiss = stringResource(android.R.string.ok),
|
||||||
confirm = stringResource(android.R.string.ok),
|
onDismiss = onErrorDismiss
|
||||||
onConfirm = clearReRegisterResult,
|
|
||||||
onDismiss = clearReRegisterResult
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
|
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
|
||||||
|
import org.whispersystems.signalservice.api.registration.RestoreMethod
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
||||||
|
|
||||||
|
@ -22,9 +25,18 @@ class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
||||||
|
|
||||||
fun transferAccount() {
|
fun transferAccount() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val restoreMethodToken = UUID.randomUUID().toString()
|
||||||
store.update { it.copy(inProgress = true) }
|
store.update { it.copy(inProgress = true) }
|
||||||
val result = QuickRegistrationRepository.transferAccount(store.value.reRegisterUri)
|
val result = QuickRegistrationRepository.transferAccount(store.value.reRegisterUri, restoreMethodToken)
|
||||||
store.update { it.copy(reRegisterResult = result, inProgress = false) }
|
store.update { it.copy(reRegisterResult = result, inProgress = false) }
|
||||||
|
|
||||||
|
val restoreMethod = QuickRegistrationRepository.waitForRestoreMethodSelectionOnNewDevice(restoreMethodToken)
|
||||||
|
|
||||||
|
if (restoreMethod != RestoreMethod.DECLINE) {
|
||||||
|
SignalStore.registration.restoringOnNewDevice = true
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update { it.copy(restoreMethodSelected = restoreMethod) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +47,7 @@ class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
||||||
data class TransferAccountState(
|
data class TransferAccountState(
|
||||||
val reRegisterUri: String,
|
val reRegisterUri: String,
|
||||||
val inProgress: Boolean = false,
|
val inProgress: Boolean = false,
|
||||||
val reRegisterResult: QuickRegistrationRepository.TransferAccountResult? = null
|
val reRegisterResult: QuickRegistrationRepository.TransferAccountResult? = null,
|
||||||
|
val restoreMethodSelected: RestoreMethod? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,22 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.registration.proto.RegistrationProvisionMessage
|
import org.signal.registration.proto.RegistrationProvisionMessage
|
||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.whispersystems.signalservice.api.registration.ProvisioningSocket
|
import org.whispersystems.signalservice.api.registration.ProvisioningSocket
|
||||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
class RestoreViaQrViewModel : ViewModel() {
|
class RestoreViaQrViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(RestoreViaQrViewModel::class)
|
||||||
|
}
|
||||||
|
|
||||||
private val store: MutableStateFlow<RestoreViaQrState> = MutableStateFlow(RestoreViaQrState())
|
private val store: MutableStateFlow<RestoreViaQrState> = MutableStateFlow(RestoreViaQrState())
|
||||||
|
|
||||||
val state: StateFlow<RestoreViaQrState> = store
|
val state: StateFlow<RestoreViaQrState> = store
|
||||||
|
@ -58,6 +64,7 @@ class RestoreViaQrViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start(): Closeable {
|
private fun start(): Closeable {
|
||||||
|
SignalStore.registration.restoreMethodToken = null
|
||||||
store.update { it.copy(qrState = QrState.Loading) }
|
store.update { it.copy(qrState = QrState.Loading) }
|
||||||
|
|
||||||
return ProvisioningSocket.start(
|
return ProvisioningSocket.start(
|
||||||
|
@ -69,7 +76,10 @@ class RestoreViaQrViewModel : ViewModel() {
|
||||||
store.update { it.copy(qrState = QrState.Loaded(qrData = QrCodeData.forData(data = url, supportIconOverlay = false))) }
|
store.update { it.copy(qrState = QrState.Loaded(qrData = QrCodeData.forData(data = url, supportIconOverlay = false))) }
|
||||||
|
|
||||||
val result = socket.getRegistrationProvisioningMessage()
|
val result = socket.getRegistrationProvisioningMessage()
|
||||||
|
|
||||||
if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) {
|
if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) {
|
||||||
|
Log.i(TAG, "Saving restore method token: ***${result.message.restoreMethodToken.takeLast(4)}")
|
||||||
|
SignalStore.registration.restoreMethodToken = result.message.restoreMethodToken
|
||||||
store.update { it.copy(isRegistering = true, provisioningMessage = result.message, qrState = QrState.Scanned) }
|
store.update { it.copy(isRegistering = true, provisioningMessage = result.message, qrState = QrState.Scanned) }
|
||||||
} else {
|
} else {
|
||||||
store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) }
|
store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) }
|
||||||
|
|
|
@ -7,14 +7,18 @@ package org.thoughtcrime.securesms.restore.selection
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.thoughtcrime.securesms.MainActivity
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
|
||||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
|
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
|
||||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
|
import org.thoughtcrime.securesms.registrationv3.ui.restore.RestoreMethod
|
||||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen
|
import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
|
import org.whispersystems.signalservice.api.registration.RestoreMethod as ApiRestoreMethod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide options to select restore/transfer operation and flow during quick registration.
|
* Provide options to select restore/transfer operation and flow during quick registration.
|
||||||
|
@ -27,6 +31,11 @@ class SelectRestoreMethodFragment : ComposeFragment() {
|
||||||
onRestoreMethodClicked = this::startRestoreMethod,
|
onRestoreMethodClicked = this::startRestoreMethod,
|
||||||
onSkip = {
|
onSkip = {
|
||||||
SignalStore.registration.markSkippedTransferOrRestore()
|
SignalStore.registration.markSkippedTransferOrRestore()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
QuickRegistrationRepository.setRestoreMethodForOldDevice(ApiRestoreMethod.DECLINE)
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(MainActivity.clearTop(requireContext()))
|
startActivity(MainActivity.clearTop(requireContext()))
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
@ -34,6 +43,16 @@ class SelectRestoreMethodFragment : ComposeFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRestoreMethod(method: RestoreMethod) {
|
private fun startRestoreMethod(method: RestoreMethod) {
|
||||||
|
val apiRestoreMethod = when (method) {
|
||||||
|
RestoreMethod.FROM_SIGNAL_BACKUPS -> ApiRestoreMethod.REMOTE_BACKUP
|
||||||
|
RestoreMethod.FROM_LOCAL_BACKUP_V1, RestoreMethod.FROM_LOCAL_BACKUP_V2 -> ApiRestoreMethod.LOCAL_BACKUP
|
||||||
|
RestoreMethod.FROM_OLD_DEVICE -> ApiRestoreMethod.DEVICE_TRANSFER
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
QuickRegistrationRepository.setRestoreMethodForOldDevice(apiRestoreMethod)
|
||||||
|
}
|
||||||
|
|
||||||
when (method) {
|
when (method) {
|
||||||
RestoreMethod.FROM_SIGNAL_BACKUPS -> startActivity(Intent(requireContext(), RemoteRestoreActivity::class.java))
|
RestoreMethod.FROM_SIGNAL_BACKUPS -> startActivity(Intent(requireContext(), RemoteRestoreActivity::class.java))
|
||||||
RestoreMethod.FROM_OLD_DEVICE -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToDeviceTransfer())
|
RestoreMethod.FROM_OLD_DEVICE -> findNavController().safeNavigate(SelectRestoreMethodFragmentDirections.goToDeviceTransfer())
|
||||||
|
|
12
app/src/main/res/drawable/symbol_check_circle_40.xml
Normal file
12
app/src/main/res/drawable/symbol_check_circle_40.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40dp"
|
||||||
|
android:height="40dp"
|
||||||
|
android:viewportWidth="40"
|
||||||
|
android:viewportHeight="40">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M27.6 14.03c0.29-0.46 0.15-1.08-0.32-1.38-0.46-0.29-1.08-0.15-1.38 0.32l-7.75 12.3-4.12-5.14c-0.34-0.44-0.97-0.5-1.4-0.16-0.44 0.34-0.5 0.97-0.16 1.4l5 6.25c0.2 0.26 0.51 0.4 0.84 0.38 0.32-0.02 0.61-0.2 0.79-0.47l8.5-13.5Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M20 2C10.06 2 2 10.06 2 20s8.06 18 18 18 18-8.06 18-18S29.94 2 20 2ZM4 20c0-8.84 7.16-16 16-16s16 7.16 16 16-7.16 16-16 16S4 28.84 4 20Z"/>
|
||||||
|
</vector>
|
|
@ -7929,5 +7929,17 @@
|
||||||
<!-- Old device Transfer account bottom sheet dialog body -->
|
<!-- Old device Transfer account bottom sheet dialog body -->
|
||||||
<string name="TransferAccount_continue_on_your_other_device_details">Continue transferring your account on your other device.</string>
|
<string name="TransferAccount_continue_on_your_other_device_details">Continue transferring your account on your other device.</string>
|
||||||
|
|
||||||
|
<!-- Restore Complete bottom sheet dialog title -->
|
||||||
|
<string name="RestoreCompleteBottomSheet_restore_complete">Restore complete</string>
|
||||||
|
<!-- Restore Complete bottom sheet dialog message -->
|
||||||
|
<string name="RestoreCompleteBottomSheet_restore_complete_message">Your Signal account and messages have started transferring to your other device. Signal is now inactive on this device.</string>
|
||||||
|
<!-- Restore Complete bottom sheet dialog title after device transfer -->
|
||||||
|
<string name="RestoreCompleteBottomSheet_transfer_complete">Transfer complete</string>
|
||||||
|
<!-- Restore Complete bottom sheet dialog message after device transfer -->
|
||||||
|
<string name="RestoreCompleteBottomSheet_transfer_complete_message">Your Signal account and messages have been transferred to your other device. Signal is now inactive on this device.</string>
|
||||||
|
<!-- Restore Complete bottom sheet dialog button text to dismiss sheet -->
|
||||||
|
<string name="RestoreCompleteBottomSheet_button">Okay</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||||
|
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||||
import org.whispersystems.signalservice.api.services.CallLinksService
|
import org.whispersystems.signalservice.api.services.CallLinksService
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
import org.whispersystems.signalservice.api.services.ProfileService
|
import org.whispersystems.signalservice.api.services.ProfileService
|
||||||
|
@ -222,4 +223,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||||
override fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi {
|
override fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi {
|
||||||
return mockk()
|
return mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun provideRegistrationApi(pushServiceSocket: PushServiceSocket): RegistrationApi {
|
||||||
|
return mockk()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,4 +161,22 @@ class RegistrationApi(
|
||||||
pushServiceSocket.sendProvisioningMessage(deviceIdentifier, cipherText)
|
pushServiceSocket.sendProvisioningMessage(deviceIdentifier, cipherText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set [RestoreMethod] enum on the server for use by the old device to update UX.
|
||||||
|
*/
|
||||||
|
fun setRestoreMethod(token: String, method: RestoreMethod): NetworkResult<Unit> {
|
||||||
|
return NetworkResult.fromFetch {
|
||||||
|
pushServiceSocket.setRestoreMethodChosen(token, RestoreMethodBody(method = method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the [RestoreMethod] to be set on the server by the new device. This is a long polling operation.
|
||||||
|
*/
|
||||||
|
fun waitForRestoreMethod(token: String, timeout: Int = 30): NetworkResult<RestoreMethod> {
|
||||||
|
return NetworkResult.fromFetch {
|
||||||
|
pushServiceSocket.waitForRestoreMethodChosen(token, timeout).method ?: RestoreMethod.DECLINE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.registration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore method chosen by user on new device after performing a quick-restore.
|
||||||
|
*/
|
||||||
|
enum class RestoreMethod {
|
||||||
|
REMOTE_BACKUP,
|
||||||
|
LOCAL_BACKUP,
|
||||||
|
DEVICE_TRANSFER,
|
||||||
|
DECLINE
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.registration
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request and response body used to communicate a quick restore method selection during registration.
|
||||||
|
*/
|
||||||
|
data class RestoreMethodBody @JsonCreator constructor(
|
||||||
|
@JsonProperty val method: RestoreMethod?
|
||||||
|
)
|
|
@ -115,6 +115,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssocia
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||||
|
import org.whispersystems.signalservice.api.registration.RestoreMethodBody;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||||
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
|
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
|
||||||
|
@ -255,6 +256,8 @@ public class PushServiceSocket {
|
||||||
private static final String DEVICE_PATH = "/v1/devices/%s";
|
private static final String DEVICE_PATH = "/v1/devices/%s";
|
||||||
private static final String WAIT_FOR_DEVICES_PATH = "/v1/devices/wait_for_linked_device/%s?timeout=%s";
|
private static final String WAIT_FOR_DEVICES_PATH = "/v1/devices/wait_for_linked_device/%s?timeout=%s";
|
||||||
private static final String TRANSFER_ARCHIVE_PATH = "/v1/devices/transfer_archive";
|
private static final String TRANSFER_ARCHIVE_PATH = "/v1/devices/transfer_archive";
|
||||||
|
private static final String SET_RESTORE_METHOD_PATH = "/v1/devices/restore_account/%s";
|
||||||
|
private static final String WAIT_RESTORE_METHOD_PATH = "/v1/devices/restore_account/%s?timeout=%s";
|
||||||
|
|
||||||
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||||
private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s";
|
private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s";
|
||||||
|
@ -347,6 +350,7 @@ public class PushServiceSocket {
|
||||||
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
|
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
|
||||||
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
|
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
|
||||||
private static final ResponseCodeHandler UNOPINIONATED_HANDLER = new UnopinionatedResponseCodeHandler();
|
private static final ResponseCodeHandler UNOPINIONATED_HANDLER = new UnopinionatedResponseCodeHandler();
|
||||||
|
private static final ResponseCodeHandler LONG_POLL_HANDLER = new LongPollingResponseCodeHandler();
|
||||||
|
|
||||||
public static final long CDN2_RESUMABLE_LINK_LIFETIME_MILLIS = TimeUnit.DAYS.toMillis(7);
|
public static final long CDN2_RESUMABLE_LINK_LIFETIME_MILLIS = TimeUnit.DAYS.toMillis(7);
|
||||||
|
|
||||||
|
@ -687,23 +691,7 @@ public class PushServiceSocket {
|
||||||
* This is a long-polling endpoint that relies on the fact that our normal connection timeout is already 30s.
|
* This is a long-polling endpoint that relies on the fact that our normal connection timeout is already 30s.
|
||||||
*/
|
*/
|
||||||
public WaitForLinkedDeviceResponse waitForLinkedDevice(String token, int timeoutSeconds) throws IOException {
|
public WaitForLinkedDeviceResponse waitForLinkedDevice(String token, int timeoutSeconds) throws IOException {
|
||||||
// Note: We consider 204 failure, since that means that we timed out before determining if a device was linked. Easier that way.
|
String response = makeServiceRequest(String.format(Locale.US, WAIT_FOR_DEVICES_PATH, token, timeoutSeconds), "GET", null, NO_HEADERS, LONG_POLL_HANDLER, SealedSenderAccess.NONE);
|
||||||
|
|
||||||
String response = makeServiceRequest(String.format(Locale.US, WAIT_FOR_DEVICES_PATH, token, timeoutSeconds), "GET", null, NO_HEADERS, (responseCode, body) -> {
|
|
||||||
if (responseCode == 204 || responseCode < 200 || responseCode > 299) {
|
|
||||||
String bodyString = null;
|
|
||||||
if (body != null) {
|
|
||||||
try {
|
|
||||||
bodyString = readBodyString(body);
|
|
||||||
} catch (MalformedResponseException e) {
|
|
||||||
Log.w(TAG, "Failed to read body string", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NonSuccessfulResponseCodeException(responseCode, "Response: " + responseCode, bodyString);
|
|
||||||
}
|
|
||||||
}, SealedSenderAccess.NONE);
|
|
||||||
|
|
||||||
return JsonUtil.fromJsonResponse(response, WaitForLinkedDeviceResponse.class);
|
return JsonUtil.fromJsonResponse(response, WaitForLinkedDeviceResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,6 +700,19 @@ public class PushServiceSocket {
|
||||||
makeServiceRequest(String.format(Locale.US, TRANSFER_ARCHIVE_PATH), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
makeServiceRequest(String.format(Locale.US, TRANSFER_ARCHIVE_PATH), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException {
|
||||||
|
String body = JsonUtil.toJson(request);
|
||||||
|
makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a long-polling endpoint that relies on the fact that our normal connection timeout is already 30s.
|
||||||
|
*/
|
||||||
|
public @Nonnull RestoreMethodBody waitForRestoreMethodChosen(@Nonnull String token, int timeoutSeconds) throws IOException {
|
||||||
|
String response = makeServiceRequest(String.format(Locale.US, WAIT_RESTORE_METHOD_PATH, urlEncode(token), timeoutSeconds), "GET", null, NO_HEADERS, LONG_POLL_HANDLER, SealedSenderAccess.NONE);
|
||||||
|
return JsonUtil.fromJsonResponse(response, RestoreMethodBody.class);
|
||||||
|
}
|
||||||
|
|
||||||
public void removeDevice(long deviceId) throws IOException {
|
public void removeDevice(long deviceId) throws IOException {
|
||||||
makeServiceRequest(String.format(DEVICE_PATH, String.valueOf(deviceId)), "DELETE", null);
|
makeServiceRequest(String.format(DEVICE_PATH, String.valueOf(deviceId)), "DELETE", null);
|
||||||
}
|
}
|
||||||
|
@ -2818,6 +2819,28 @@ public class PushServiceSocket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link UnopinionatedResponseCodeHandler} but also treats a 204 as a failure, since that means that the server intentionally
|
||||||
|
* timed out before a valid result for the long poll was returned. Easier that way.
|
||||||
|
*/
|
||||||
|
private static class LongPollingResponseCodeHandler implements ResponseCodeHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||||
|
if (responseCode == 204 || responseCode < 200 || responseCode > 299) {
|
||||||
|
String bodyString = null;
|
||||||
|
if (body != null) {
|
||||||
|
try {
|
||||||
|
bodyString = readBodyString(body);
|
||||||
|
} catch (MalformedResponseException e) {
|
||||||
|
Log.w(TAG, "Failed to read body string", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NonSuccessfulResponseCodeException(responseCode, "Response: " + responseCode, bodyString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum ClientSet { KeyBackup }
|
public enum ClientSet { KeyBackup }
|
||||||
|
|
||||||
public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds)
|
public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds)
|
||||||
|
|
|
@ -26,5 +26,6 @@ message RegistrationProvisionMessage {
|
||||||
Platform platform = 5;
|
Platform platform = 5;
|
||||||
uint64 backupTimestampMs = 6;
|
uint64 backupTimestampMs = 6;
|
||||||
Tier tier = 7;
|
Tier tier = 7;
|
||||||
reserved 8; // iOSDeviceTransferMessage
|
string restoreMethodToken = 8;
|
||||||
|
reserved 9; // iOSDeviceTransferMessage
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue