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.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||
|
@ -191,7 +192,16 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
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)
|
||||
.setTitle(R.string.OldDeviceTransferLockedDialog__complete_registration_on_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();
|
||||
}
|
||||
|
||||
if (SignalStore.misc().getShouldShowLinkedDevicesReminder()) {
|
||||
SignalStore.misc().setShouldShowLinkedDevicesReminder(false);
|
||||
RelinkDevicesReminderBottomSheetFragment.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
updateTabVisibility();
|
||||
|
||||
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.keys.KeysApi
|
||||
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.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
|
@ -298,6 +299,10 @@ object AppDependencies {
|
|||
val linkDeviceApi: LinkDeviceApi
|
||||
get() = networkModule.linkDeviceApi
|
||||
|
||||
@JvmStatic
|
||||
val registrationApi: RegistrationApi
|
||||
get() = networkModule.registrationApi
|
||||
|
||||
@JvmStatic
|
||||
val okHttpClient: OkHttpClient
|
||||
get() = networkModule.okHttpClient
|
||||
|
@ -361,5 +366,6 @@ object AppDependencies {
|
|||
fun provideKeysApi(pushServiceSocket: PushServiceSocket): KeysApi
|
||||
fun provideAttachmentApi(signalWebSocket: SignalWebSocket, pushServiceSocket: PushServiceSocket): AttachmentApi
|
||||
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.push.ServiceId.ACI;
|
||||
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.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
|
@ -473,6 +474,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||
return new LinkDeviceApi(pushServiceSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RegistrationApi provideRegistrationApi(@NonNull PushServiceSocket pushServiceSocket) {
|
||||
return new RegistrationApi(pushServiceSocket);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
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.link.LinkDeviceApi
|
||||
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.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
|
@ -143,6 +144,10 @@ class NetworkDependenciesModule(
|
|||
provider.provideLinkDeviceApi(pushServiceSocket)
|
||||
}
|
||||
|
||||
val registrationApi: RegistrationApi by lazy {
|
||||
provider.provideRegistrationApi(pushServiceSocket)
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.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 LOCAL_REGISTRATION_DATA = "registration.local_registration_data"
|
||||
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
|
||||
|
@ -58,6 +60,10 @@ class RegistrationValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
var hasUploadedProfile: Boolean by booleanValue(HAS_UPLOADED_PROFILE, true)
|
||||
var sessionId: String? by stringValue(SESSION_ID, 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 {
|
||||
return getBoolean(SKIPPED_TRANSFER_OR_RESTORE, false)
|
||||
|
|
|
@ -36,6 +36,7 @@ public final class RegistrationUtil {
|
|||
Log.i(TAG, "Marking registration completed.", new Throwable());
|
||||
SignalStore.registration().markRegistrationComplete();
|
||||
SignalStore.registration().setLocalRegistrationMetadata(null);
|
||||
SignalStore.registration().setRestoreMethodToken(null);
|
||||
|
||||
if (SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode() == PhoneNumberDiscoverabilityMode.UNDECIDED) {
|
||||
Log.w(TAG, "Phone number discoverability mode is still UNDECIDED. Setting to DISCOVERABLE.");
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
package org.thoughtcrime.securesms.registrationv3.data
|
||||
|
||||
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.Hex
|
||||
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.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.registration.RestoreMethod
|
||||
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.
|
||||
|
@ -41,7 +49,7 @@ object QuickRegistrationRepository {
|
|||
/**
|
||||
* Send registration provisioning message to new device.
|
||||
*/
|
||||
fun transferAccount(reRegisterUri: String): TransferAccountResult {
|
||||
fun transferAccount(reRegisterUri: String, restoreMethodToken: String): TransferAccountResult {
|
||||
if (!isValidReRegistrationQr(reRegisterUri)) {
|
||||
Log.w(TAG, "Invalid quick re-register qr data")
|
||||
return TransferAccountResult.FAILED
|
||||
|
@ -81,7 +89,8 @@ object QuickRegistrationRepository {
|
|||
MessageBackupTier.PAID -> RegistrationProvisionMessage.Tier.PAID
|
||||
MessageBackupTier.FREE,
|
||||
null -> RegistrationProvisionMessage.Tier.FREE
|
||||
}
|
||||
},
|
||||
restoreMethodToken = restoreMethodToken
|
||||
)
|
||||
)
|
||||
.successOrThrow()
|
||||
|
@ -98,6 +107,60 @@ object QuickRegistrationRepository {
|
|||
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 {
|
||||
SUCCESS,
|
||||
FAILED
|
||||
|
|
|
@ -41,6 +41,10 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.Buttons
|
||||
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.thoughtcrime.securesms.BiometricDeviceAuthentication
|
||||
import org.thoughtcrime.securesms.BiometricDeviceLockContract
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
|
||||
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.SpanUtil
|
||||
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
|
||||
|
@ -121,6 +128,29 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
|||
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 {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
|
@ -128,7 +158,11 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
|||
TransferToNewDevice(
|
||||
state = state,
|
||||
onTransferAccount = this::authenticate,
|
||||
clearReRegisterResult = viewModel::clearReRegisterResult,
|
||||
onContinueOnOtherDeviceDismiss = {
|
||||
finish()
|
||||
viewModel.clearReRegisterResult()
|
||||
},
|
||||
onErrorDismiss = viewModel::clearReRegisterResult,
|
||||
onBackClicked = { finish() }
|
||||
)
|
||||
}
|
||||
|
@ -184,7 +218,8 @@ class TransferAccountActivity : PassphraseRequiredActivity() {
|
|||
fun TransferToNewDevice(
|
||||
state: TransferAccountViewModel.TransferAccountState,
|
||||
onTransferAccount: () -> Unit = {},
|
||||
clearReRegisterResult: () -> Unit = {},
|
||||
onContinueOnOtherDeviceDismiss: () -> Unit = {},
|
||||
onErrorDismiss: () -> Unit = {},
|
||||
onBackClicked: () -> Unit = {}
|
||||
) {
|
||||
Scaffold(
|
||||
|
@ -248,7 +283,7 @@ fun TransferToNewDevice(
|
|||
QuickRegistrationRepository.TransferAccountResult.SUCCESS -> {
|
||||
ModalBottomSheet(
|
||||
dragHandle = null,
|
||||
onDismissRequest = clearReRegisterResult,
|
||||
onDismissRequest = onContinueOnOtherDeviceDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
ContinueOnOtherDevice()
|
||||
|
@ -256,12 +291,10 @@ fun TransferToNewDevice(
|
|||
}
|
||||
|
||||
QuickRegistrationRepository.TransferAccountResult.FAILED -> {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = Dialogs.NoTitle,
|
||||
body = stringResource(R.string.RegistrationActivity_unable_to_connect_to_service),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
onConfirm = clearReRegisterResult,
|
||||
onDismiss = clearReRegisterResult
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = stringResource(R.string.RegistrationActivity_unable_to_connect_to_service),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = onErrorDismiss
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
|
||||
import org.whispersystems.signalservice.api.registration.RestoreMethod
|
||||
import java.util.UUID
|
||||
|
||||
class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
||||
|
||||
|
@ -22,9 +25,18 @@ class TransferAccountViewModel(reRegisterUri: String) : ViewModel() {
|
|||
|
||||
fun transferAccount() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val restoreMethodToken = UUID.randomUUID().toString()
|
||||
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) }
|
||||
|
||||
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(
|
||||
val reRegisterUri: String,
|
||||
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.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.registration.proto.RegistrationProvisionMessage
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.registration.ProvisioningSocket
|
||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
||||
import java.io.Closeable
|
||||
|
||||
class RestoreViaQrViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RestoreViaQrViewModel::class)
|
||||
}
|
||||
|
||||
private val store: MutableStateFlow<RestoreViaQrState> = MutableStateFlow(RestoreViaQrState())
|
||||
|
||||
val state: StateFlow<RestoreViaQrState> = store
|
||||
|
@ -58,6 +64,7 @@ class RestoreViaQrViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun start(): Closeable {
|
||||
SignalStore.registration.restoreMethodToken = null
|
||||
store.update { it.copy(qrState = QrState.Loading) }
|
||||
|
||||
return ProvisioningSocket.start(
|
||||
|
@ -69,7 +76,10 @@ class RestoreViaQrViewModel : ViewModel() {
|
|||
store.update { it.copy(qrState = QrState.Loaded(qrData = QrCodeData.forData(data = url, supportIconOverlay = false))) }
|
||||
|
||||
val result = socket.getRegistrationProvisioningMessage()
|
||||
|
||||
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) }
|
||||
} else {
|
||||
store.update { it.copy(showProvisioningError = true, qrState = QrState.Scanned) }
|
||||
|
|
|
@ -7,14 +7,18 @@ package org.thoughtcrime.securesms.restore.selection
|
|||
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
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.RestoreMethod
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.SelectRestoreMethodScreen
|
||||
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.
|
||||
|
@ -27,6 +31,11 @@ class SelectRestoreMethodFragment : ComposeFragment() {
|
|||
onRestoreMethodClicked = this::startRestoreMethod,
|
||||
onSkip = {
|
||||
SignalStore.registration.markSkippedTransferOrRestore()
|
||||
|
||||
lifecycleScope.launch {
|
||||
QuickRegistrationRepository.setRestoreMethodForOldDevice(ApiRestoreMethod.DECLINE)
|
||||
}
|
||||
|
||||
startActivity(MainActivity.clearTop(requireContext()))
|
||||
activity?.finish()
|
||||
}
|
||||
|
@ -34,6 +43,16 @@ class SelectRestoreMethodFragment : ComposeFragment() {
|
|||
}
|
||||
|
||||
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) {
|
||||
RestoreMethod.FROM_SIGNAL_BACKUPS -> startActivity(Intent(requireContext(), RemoteRestoreActivity::class.java))
|
||||
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 -->
|
||||
<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 -->
|
||||
</resources>
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
|
|||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
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.DonationsService
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
|
@ -222,4 +223,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
|||
override fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi {
|
||||
return mockk()
|
||||
}
|
||||
|
||||
override fun provideRegistrationApi(pushServiceSocket: PushServiceSocket): RegistrationApi {
|
||||
return mockk()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,4 +161,22 @@ class RegistrationApi(
|
|||
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.UsernameMalformedException;
|
||||
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.subscriptions.ActiveSubscription;
|
||||
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 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 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 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 ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
|
||||
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);
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
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, (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);
|
||||
|
||||
String response = makeServiceRequest(String.format(Locale.US, WAIT_FOR_DEVICES_PATH, token, timeoutSeconds), "GET", null, NO_HEADERS, LONG_POLL_HANDLER, SealedSenderAccess.NONE);
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 CredentialResponse retrieveGroupsV2Credentials(long todaySeconds)
|
||||
|
|
|
@ -26,5 +26,6 @@ message RegistrationProvisionMessage {
|
|||
Platform platform = 5;
|
||||
uint64 backupTimestampMs = 6;
|
||||
Tier tier = 7;
|
||||
reserved 8; // iOSDeviceTransferMessage
|
||||
string restoreMethodToken = 8;
|
||||
reserved 9; // iOSDeviceTransferMessage
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue