mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Update registration for new restore flows.
This commit is contained in:
committed by
Greyson Parrelli
parent
aad2624bd5
commit
22c4e2d084
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.data
|
||||
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
|
||||
data class AccountRegistrationResult(
|
||||
val uuid: String,
|
||||
val pni: String,
|
||||
val storageCapable: Boolean,
|
||||
val number: String,
|
||||
val masterKey: MasterKey?,
|
||||
val pin: String?,
|
||||
val aciPreKeyCollection: PreKeyCollection,
|
||||
val pniPreKeyCollection: PreKeyCollection
|
||||
)
|
||||
@@ -17,7 +17,7 @@ import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
* and combines them into a proto-backed class [LocalRegistrationMetadata] so they can be serialized & stored.
|
||||
*/
|
||||
object LocalRegistrationMetadataUtil {
|
||||
fun createLocalRegistrationMetadata(localAciIdentityKeyPair: IdentityKeyPair, localPniIdentityKeyPair: IdentityKeyPair, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean): LocalRegistrationMetadata {
|
||||
fun createLocalRegistrationMetadata(localAciIdentityKeyPair: IdentityKeyPair, localPniIdentityKeyPair: IdentityKeyPair, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean): LocalRegistrationMetadata {
|
||||
return LocalRegistrationMetadata.Builder().apply {
|
||||
aciIdentityKeyPair = localAciIdentityKeyPair.serialize().toByteString()
|
||||
aciSignedPreKey = remoteResult.aciPreKeyCollection.signedPreKey.serialize().toByteString()
|
||||
|
||||
@@ -622,15 +622,4 @@ object RegistrationRepository {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
data class AccountRegistrationResult(
|
||||
val uuid: String,
|
||||
val pni: String,
|
||||
val storageCapable: Boolean,
|
||||
val number: String,
|
||||
val masterKey: MasterKey?,
|
||||
val pin: String?,
|
||||
val aciPreKeyCollection: PreKeyCollection,
|
||||
val pniPreKeyCollection: PreKeyCollection
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.data.AccountRegistrationResult
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
@@ -23,7 +23,7 @@ import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
*/
|
||||
sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause) {
|
||||
companion object {
|
||||
fun from(networkResult: NetworkResult<RegistrationRepository.AccountRegistrationResult>): RegisterAccountResult {
|
||||
fun from(networkResult: NetworkResult<AccountRegistrationResult>): RegisterAccountResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> Success(networkResult.result)
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
@@ -55,7 +55,7 @@ sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause
|
||||
}
|
||||
}
|
||||
}
|
||||
class Success(val accountRegistrationResult: RegistrationRepository.AccountRegistrationResult) : RegisterAccountResult(null)
|
||||
class Success(val accountRegistrationResult: AccountRegistrationResult) : RegisterAccountResult(null)
|
||||
class IncorrectRecoveryPassword(cause: Throwable) : RegisterAccountResult(cause)
|
||||
class AuthorizationFailed(cause: Throwable) : RegisterAccountResult(cause)
|
||||
class MalformedRequest(cause: Throwable) : RegisterAccountResult(cause)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.secondary
|
||||
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.protocol.kdf.HKDF
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionEnvelope
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.UUID
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Used to decrypt a secondary/link device provisioning message from the primary device.
|
||||
*/
|
||||
class SecondaryProvisioningCipher private constructor(private val secondaryIdentityKeyPair: IdentityKeyPair) {
|
||||
|
||||
val secondaryDevicePublicKey: IdentityKey = secondaryIdentityKeyPair.publicKey
|
||||
|
||||
fun decrypt(envelope: ProvisionEnvelope): ProvisionDecryptResult {
|
||||
val primaryEphemeralPublicKey = envelope.publicKey!!.toByteArray()
|
||||
val body = envelope.body!!.toByteArray()
|
||||
|
||||
val provisionMessageLength = body.size - VERSION_LENGTH - IV_LENGTH - MAC_LENGTH
|
||||
|
||||
if (provisionMessageLength <= 0) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val version = body[0].toInt()
|
||||
if (version != 1) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val iv = body.sliceArray(1 until (1 + IV_LENGTH))
|
||||
val theirMac = body.sliceArray(body.size - MAC_LENGTH until body.size)
|
||||
val message = body.sliceArray(0 until body.size - MAC_LENGTH)
|
||||
val cipherText = body.sliceArray((1 + IV_LENGTH) until body.size - MAC_LENGTH)
|
||||
|
||||
val sharedSecret = Curve.calculateAgreement(ECPublicKey(primaryEphemeralPublicKey), secondaryIdentityKeyPair.privateKey)
|
||||
val derivedSecret: ByteArray = HKDF.deriveSecrets(sharedSecret, PrimaryProvisioningCipher.PROVISIONING_MESSAGE.toByteArray(), 64)
|
||||
|
||||
val cipherKey = derivedSecret.sliceArray(0 until 32)
|
||||
val macKey = derivedSecret.sliceArray(32 until 64)
|
||||
|
||||
val ourHmac = getMac(macKey, message)
|
||||
|
||||
if (!MessageDigest.isEqual(theirMac, ourHmac)) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val plaintext = try {
|
||||
getPlaintext(cipherKey, iv, cipherText)
|
||||
} catch (e: Exception) {
|
||||
return ProvisionDecryptResult.Error
|
||||
}
|
||||
|
||||
val provisioningMessage = ProvisionMessage.ADAPTER.decode(plaintext)
|
||||
|
||||
return ProvisionDecryptResult.Success(
|
||||
uuid = UuidUtil.parseOrThrow(provisioningMessage.aci),
|
||||
e164 = provisioningMessage.number!!,
|
||||
identityKeyPair = IdentityKeyPair(IdentityKey(provisioningMessage.aciIdentityKeyPublic!!.toByteArray()), Curve.decodePrivatePoint(provisioningMessage.aciIdentityKeyPrivate!!.toByteArray())),
|
||||
profileKey = ProfileKey(provisioningMessage.profileKey!!.toByteArray()),
|
||||
areReadReceiptsEnabled = provisioningMessage.readReceipts == true,
|
||||
primaryUserAgent = provisioningMessage.userAgent,
|
||||
provisioningCode = provisioningMessage.provisioningCode!!,
|
||||
provisioningVersion = provisioningMessage.provisioningVersion!!
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMac(key: ByteArray, message: ByteArray): ByteArray? {
|
||||
return try {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(key, "HmacSHA256"))
|
||||
mac.doFinal(message)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw AssertionError(e)
|
||||
} catch (e: InvalidKeyException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPlaintext(key: ByteArray, iv: ByteArray, message: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
return cipher.doFinal(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VERSION_LENGTH = 1
|
||||
private const val IV_LENGTH = 16
|
||||
private const val MAC_LENGTH = 32
|
||||
|
||||
fun generate(): SecondaryProvisioningCipher {
|
||||
return SecondaryProvisioningCipher(IdentityKeyUtil.generateIdentityKeyPair())
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ProvisionDecryptResult {
|
||||
object Error : ProvisionDecryptResult()
|
||||
|
||||
data class Success(
|
||||
val uuid: UUID,
|
||||
val e164: String,
|
||||
val identityKeyPair: IdentityKeyPair,
|
||||
val profileKey: ProfileKey,
|
||||
val areReadReceiptsEnabled: Boolean,
|
||||
val primaryUserAgent: String?,
|
||||
val provisioningCode: String,
|
||||
val provisioningVersion: Int
|
||||
) : ProvisionDecryptResult()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
@@ -119,7 +119,7 @@ class RegistrationActivity : BaseActivity() {
|
||||
|
||||
@JvmStatic
|
||||
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
|
||||
return Intent(context, RegistrationActivity::class.java).apply {
|
||||
return Intent(context, getRegistrationClass()).apply {
|
||||
putExtra(RE_REGISTRATION_EXTRA, false)
|
||||
setData(originalIntent.data)
|
||||
}
|
||||
@@ -127,9 +127,13 @@ class RegistrationActivity : BaseActivity() {
|
||||
|
||||
@JvmStatic
|
||||
fun newIntentForReRegistration(context: Context): Intent {
|
||||
return Intent(context, RegistrationActivity::class.java).apply {
|
||||
return Intent(context, getRegistrationClass()).apply {
|
||||
putExtra(RE_REGISTRATION_EXTRA, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRegistrationClass(): Class<*> {
|
||||
return if (RemoteConfig.restoreAfterRegistration) org.thoughtcrime.securesms.registrationv3.ui.RegistrationActivity::class.java else RegistrationActivity::class.java
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
import org.thoughtcrime.securesms.registration.data.AccountRegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
@@ -827,7 +828,7 @@ class RegistrationViewModel : ViewModel() {
|
||||
handleRegistrationResult(context, registrationData, registrationResponse, false)
|
||||
}
|
||||
|
||||
private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) {
|
||||
private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean) {
|
||||
Log.v(TAG, "onSuccessfulRegistration()")
|
||||
val metadata = LocalRegistrationMetadataUtil.createLocalRegistrationMetadata(SignalStore.account.aciIdentityKey, SignalStore.account.pniIdentityKey, registrationData, remoteResult, reglockEnabled)
|
||||
RegistrationRepository.registerAccountLocally(context, metadata)
|
||||
|
||||
@@ -104,7 +104,7 @@ class GrantPermissionsFragment : ComposeFragment() {
|
||||
when (welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsFragmentDirections.actionEnterPhoneNumber())
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
|
||||
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.RemoteRestoreViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreMoreOptionsDialog
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import java.util.Locale
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
class RemoteRestoreActivity : BaseActivity() {
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, RemoteRestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: RemoteRestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
SignalTheme {
|
||||
Surface {
|
||||
RestoreFromBackupContent(
|
||||
features = getFeatureList(state.backupTier),
|
||||
onRestoreBackupClick = {
|
||||
viewModel.restore()
|
||||
},
|
||||
onCancelClick = {
|
||||
finish()
|
||||
},
|
||||
onMoreOptionsClick = {
|
||||
TransferOrRestoreMoreOptionsDialog.show(fragmentManager = supportFragmentManager, skipOnly = false)
|
||||
},
|
||||
state.backupTier,
|
||||
state.backupTime,
|
||||
state.backupTier != MessageBackupTier.PAID
|
||||
)
|
||||
if (state.importState == RemoteRestoreViewModel.ImportState.RESTORED) {
|
||||
SideEffect {
|
||||
SignalStore.registration.markRestoreCompleted()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
} else if (state.importState == RemoteRestoreViewModel.ImportState.IN_PROGRESS) {
|
||||
ProgressDialog(state.restoreProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = this)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(restoreEvent: RestoreV2Event) {
|
||||
viewModel.updateRestoreProgress(restoreEvent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getFeatureList(tier: MessageBackupTier?): ImmutableList<MessageBackupsTypeFeature> {
|
||||
return when (tier) {
|
||||
null -> persistentListOf()
|
||||
MessageBackupTier.PAID -> {
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(id = R.string.RemoteRestoreActivity__all_of_your_media)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = stringResource(id = R.string.RemoteRestoreActivity__all_of_your_messages)
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageBackupTier.FREE -> {
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(id = R.string.RemoteRestoreActivity__your_last_d_days_of_media, 30)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = stringResource(id = R.string.RemoteRestoreActivity__all_of_your_messages)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog that *just* shows a spinner. Useful for short actions where you need to
|
||||
* let the user know that some action is completing.
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressDialog(restoreProgress: RestoreV2Event?) {
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = {},
|
||||
confirmButton = {},
|
||||
dismissButton = {},
|
||||
text = {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
if (restoreProgress == null) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(top = 55.dp, bottom = 16.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = restoreProgress.getProgress(),
|
||||
modifier = Modifier
|
||||
.padding(top = 55.dp, bottom = 16.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val progressText = when (restoreProgress?.type) {
|
||||
RestoreV2Event.Type.PROGRESS_DOWNLOAD -> stringResource(id = R.string.RemoteRestoreActivity__downloading_backup)
|
||||
RestoreV2Event.Type.PROGRESS_RESTORE -> stringResource(id = R.string.RemoteRestoreActivity__downloading_backup)
|
||||
else -> stringResource(id = R.string.RemoteRestoreActivity__restoring)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = progressText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
if (restoreProgress != null) {
|
||||
val progressBytes = Util.getPrettyFileSize(restoreProgress.count)
|
||||
val totalBytes = Util.getPrettyFileSize(restoreProgress.estimatedTotalCount)
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteRestoreActivity__s_of_s_s, progressBytes, totalBytes, "%.2f%%".format(restoreProgress.getProgress())),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.width(212.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ProgressDialogPreview() {
|
||||
Previews.Preview {
|
||||
ProgressDialog(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, 10, 1000))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RestoreFromBackupContentPreview() {
|
||||
Previews.Preview {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Your last 30 days of media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
),
|
||||
onRestoreBackupClick = {},
|
||||
onCancelClick = {},
|
||||
onMoreOptionsClick = {},
|
||||
MessageBackupTier.PAID,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreFromBackupContent(
|
||||
features: ImmutableList<MessageBackupsTypeFeature>,
|
||||
onRestoreBackupClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onMoreOptionsClick: () -> Unit,
|
||||
tier: MessageBackupTier?,
|
||||
lastBackupTime: Long,
|
||||
cancelable: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteRestoreActivity__restore_from_backup),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
val yourLastBackupText = buildAnnotatedString {
|
||||
append(
|
||||
stringResource(
|
||||
id = R.string.RemoteRestoreActivity__backup_created_at,
|
||||
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), lastBackupTime),
|
||||
DateUtils.getOnlyTimeString(LocalContext.current, lastBackupTime)
|
||||
)
|
||||
|
||||
)
|
||||
append(" ")
|
||||
if (tier != MessageBackupTier.PAID) {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
append(stringResource(id = R.string.RemoteRestoreActivity__only_media_sent_or_received))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = yourLastBackupText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 28.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 18.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteRestoreActivity__your_backup_includes),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
features.forEach {
|
||||
MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature = it,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onRestoreBackupClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteRestoreActivity__restore_backup)
|
||||
)
|
||||
}
|
||||
|
||||
if (cancelable) {
|
||||
TextButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = onMoreOptionsClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.TransferOrRestoreFragment__more_options)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreFromServer() {
|
||||
viewModel.restore()
|
||||
}
|
||||
|
||||
private fun continueRegistration() {
|
||||
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
|
||||
val main = MainActivity.clearTop(this)
|
||||
val profile = CreateProfileActivity.getIntentForUserProfile(this)
|
||||
profile.putExtra("next_intent", main)
|
||||
startActivity(profile)
|
||||
} else {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome)
|
||||
} else {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
|
||||
val restoreIntent = RestoreActivity.getRestoreIntent(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
|
||||
public final class RegistrationUtil {
|
||||
|
||||
@@ -29,7 +30,8 @@ public final class RegistrationUtil {
|
||||
if (!SignalStore.registration().isRegistrationComplete() &&
|
||||
SignalStore.account().isRegistered() &&
|
||||
!Recipient.self().getProfileName().isEmpty() &&
|
||||
(SignalStore.svr().hasPin() || SignalStore.svr().hasOptedOut()))
|
||||
(SignalStore.svr().hasOptedInWithAccess() || SignalStore.svr().hasOptedOut()) &&
|
||||
(RemoteConfig.restoreAfterRegistration() && (SignalStore.registration().hasSkippedTransferOrRestore() || SignalStore.registration().hasCompletedRestore())))
|
||||
{
|
||||
Log.i(TAG, "Marking registration completed.", new Throwable());
|
||||
SignalStore.registration().setRegistrationComplete();
|
||||
|
||||
Reference in New Issue
Block a user