diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt
index 7db281c195..bf942d0dc8 100644
--- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt
+++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt
@@ -1537,7 +1537,7 @@ class ImportExportTest {
val frameReader = EncryptedBackupReader(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = selfData.aci,
- streamLength = import.size.toLong(),
+ length = import.size.toLong(),
dataStream = inputFactory
)
val frames = ArrayList()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 06ec3e43c7..9f7a4905a1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -988,8 +988,8 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
-
{
@@ -220,6 +224,7 @@ object BackupRepository {
else -> Log.w(TAG, "Unrecognized frame")
}
+ EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, frameReader.getBytesRead(), totalLength))
}
if (chatItemInserter.flush()) {
@@ -263,6 +268,21 @@ object BackupRepository {
}
}
+ private fun getBackupTier(): NetworkResult {
+ val api = AppDependencies.signalServiceAccountManager.archiveApi
+ val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
+
+ return initBackupAndFetchAuth(backupKey)
+ .map { credential ->
+ val zkCredential = api.getZkCredential(backupKey, credential)
+ if (zkCredential.backupLevel == BackupLevel.MEDIA) {
+ MessageBackupTier.PAID
+ } else {
+ MessageBackupTier.FREE
+ }
+ }
+ }
+
/**
* Returns an object with details about the remote backup state.
*/
@@ -331,6 +351,24 @@ object BackupRepository {
} is NetworkResult.Success
}
+ fun checkForBackupFile(): Boolean {
+ val api = AppDependencies.signalServiceAccountManager.archiveApi
+ val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
+
+ return initBackupAndFetchAuth(backupKey)
+ .then { credential ->
+ api.getBackupInfo(backupKey, credential)
+ }
+ .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
+ .then { pair ->
+ val (cdnCredentials, info) = pair
+ val messageReceiver = AppDependencies.signalServiceMessageReceiver
+ NetworkResult.fromFetch {
+ messageReceiver.checkBackupExistence(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
+ }
+ } is NetworkResult.Success
+ }
+
/**
* Returns an object with details about the remote backup state.
*/
@@ -560,6 +598,24 @@ object BackupRepository {
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
}
+ fun restoreBackupTier(): MessageBackupTier? {
+ // TODO: more complete error handling
+ try {
+ checkForBackupFile()
+ } catch (e: Exception) {
+ Log.i(TAG, "Could not check for backup file.", e)
+ SignalStore.backup().backupTier = null
+ return null
+ }
+ SignalStore.backup().backupTier = try {
+ getBackupTier().successOrThrow()
+ } catch (e: Exception) {
+ Log.i(TAG, "Could not retrieve backup tier.", e)
+ null
+ }
+ return SignalStore.backup().backupTier
+ }
+
/**
* Retrieves backupDir and mediaDir, preferring cached value if available.
*
@@ -693,13 +749,13 @@ enum class MessageBackupTier(val value: Int) {
FREE(0),
PAID(1);
- companion object Serializer : LongSerializer {
- override fun serialize(data: MessageBackupTier): Long {
- return data.value.toLong()
+ companion object Serializer : LongSerializer {
+ override fun serialize(data: MessageBackupTier?): Long {
+ return data?.value?.toLong() ?: -1
}
- override fun deserialize(data: Long): MessageBackupTier {
- return values().firstOrNull { it.value == data.toInt() } ?: FREE
+ override fun deserialize(data: Long): MessageBackupTier? {
+ return values().firstOrNull { it.value == data.toInt() } ?: null
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/RestoreV2Event.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/RestoreV2Event.kt
new file mode 100644
index 0000000000..8183c5173e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/RestoreV2Event.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2
+
+class RestoreV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
+ enum class Type {
+ PROGRESS_DOWNLOAD,
+ PROGRESS_RESTORE,
+ PROGRESS_MEDIA_RESTORE,
+ FINISHED
+ }
+
+ fun getProgress(): Float {
+ if (estimatedTotalCount == 0L) {
+ return 0f
+ }
+ return count.toFloat() / estimatedTotalCount.toFloat()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/BackupImportReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/BackupImportReader.kt
index f5b5e4d24c..80f83893eb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/BackupImportReader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/BackupImportReader.kt
@@ -10,4 +10,6 @@ import org.thoughtcrime.securesms.backup.v2.proto.Frame
interface BackupImportReader : Iterator, AutoCloseable {
fun getHeader(): BackupInfo?
+ fun getBytesRead(): Long
+ fun getStreamLength(): Long
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt
index 7031bca71f..7e8701d898 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt
@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.backup.v2.stream
+import com.google.common.io.CountingInputStream
import org.signal.core.util.readFully
import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.readVarInt32
@@ -32,21 +33,22 @@ import javax.crypto.spec.SecretKeySpec
class EncryptedBackupReader(
key: BackupKey,
aci: ACI,
- streamLength: Long,
+ val length: Long,
dataStream: () -> InputStream
) : BackupImportReader {
val backupInfo: BackupInfo?
var next: Frame? = null
val stream: InputStream
+ val countingStream: CountingInputStream
init {
val keyMaterial = key.deriveBackupSecrets(aci)
- validateMac(keyMaterial.macKey, streamLength, dataStream())
+ validateMac(keyMaterial.macKey, length, dataStream())
- val inputStream = dataStream()
- val iv = inputStream.readNBytesOrThrow(16)
+ countingStream = CountingInputStream(dataStream())
+ val iv = countingStream.readNBytesOrThrow(16)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
@@ -55,8 +57,8 @@ class EncryptedBackupReader(
stream = GZIPInputStream(
CipherInputStream(
TruncatingInputStream(
- wrapped = inputStream,
- maxBytes = streamLength - MAC_SIZE
+ wrapped = countingStream,
+ maxBytes = length - MAC_SIZE
),
cipher
)
@@ -69,6 +71,10 @@ class EncryptedBackupReader(
return backupInfo
}
+ override fun getBytesRead() = countingStream.count
+
+ override fun getStreamLength() = length
+
override fun hasNext(): Boolean {
return next != null
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/PlainTextBackupReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/PlainTextBackupReader.kt
index 136ee50dd0..8ada418766 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/PlainTextBackupReader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/PlainTextBackupReader.kt
@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.backup.v2.stream
+import com.google.common.io.CountingInputStream
import org.signal.core.util.readNBytesOrThrow
import org.signal.core.util.readVarInt32
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
@@ -15,12 +16,14 @@ import java.io.InputStream
/**
* Reads a plaintext backup import stream one frame at a time.
*/
-class PlainTextBackupReader(val inputStream: InputStream) : BackupImportReader {
+class PlainTextBackupReader(val dataStream: InputStream, val length: Long) : BackupImportReader {
val backupInfo: BackupInfo?
var next: Frame? = null
+ val inputStream: CountingInputStream
init {
+ inputStream = CountingInputStream(dataStream)
backupInfo = readHeader()
next = read()
}
@@ -29,6 +32,10 @@ class PlainTextBackupReader(val inputStream: InputStream) : BackupImportReader {
return backupInfo
}
+ override fun getBytesRead() = inputStream.count
+
+ override fun getStreamLength() = length
+
override fun hasNext(): Boolean {
return next != null
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreActivity.kt
deleted file mode 100644
index 1ce5bccc51..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreActivity.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2024 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-package org.thoughtcrime.securesms.backup.v2.ui.subscription
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-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.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import org.signal.core.ui.Buttons
-import org.signal.core.ui.Dividers
-import org.signal.core.util.getLength
-import org.thoughtcrime.securesms.BaseActivity
-import org.thoughtcrime.securesms.MainActivity
-import org.thoughtcrime.securesms.dependencies.AppDependencies
-import org.thoughtcrime.securesms.jobs.ProfileUploadJob
-import org.thoughtcrime.securesms.profiles.AvatarHelper
-import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
-import org.thoughtcrime.securesms.recipients.Recipient
-import org.thoughtcrime.securesms.registration.RegistrationUtil
-
-class MessageBackupsTestRestoreActivity : BaseActivity() {
- companion object {
- fun getIntent(context: Context): Intent {
- return Intent(context, MessageBackupsTestRestoreActivity::class.java)
- }
- }
-
- private val viewModel: MessageBackupsTestRestoreViewModel by viewModels()
- private lateinit var importFileLauncher: ActivityResultLauncher
-
- private fun onPlaintextClicked() {
- viewModel.onPlaintextToggled()
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- result.data?.data?.let { uri ->
- contentResolver.getLength(uri)?.let { length ->
- viewModel.import(length) { contentResolver.openInputStream(uri)!! }
- }
- } ?: Toast.makeText(this, "No URI selected", Toast.LENGTH_SHORT).show()
- }
- }
-
- setContent {
- val state by viewModel.state
- Surface {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp)
- ) {
- Buttons.LargePrimary(
- onClick = this@MessageBackupsTestRestoreActivity::restoreFromServer,
- enabled = !state.importState.inProgress
- ) {
- Text("Restore")
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- StateLabel(text = "Plaintext?")
- Spacer(modifier = Modifier.width(8.dp))
- Switch(
- checked = state.plaintext,
- onCheckedChange = { onPlaintextClicked() }
- )
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Buttons.LargePrimary(
- onClick = {
- val intent = Intent().apply {
- action = Intent.ACTION_GET_CONTENT
- type = "application/octet-stream"
- addCategory(Intent.CATEGORY_OPENABLE)
- }
-
- importFileLauncher.launch(intent)
- },
- enabled = !state.importState.inProgress
- ) {
- Text("Import from file")
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Dividers.Default()
-
- Buttons.LargeTonal(
- onClick = { continueRegistration() },
- enabled = !state.importState.inProgress
- ) {
- Text("Continue Reg Flow")
- }
- }
- }
- if (state.importState == MessageBackupsTestRestoreViewModel.ImportState.RESTORED) {
- SideEffect {
- RegistrationUtil.maybeMarkRegistrationComplete()
- AppDependencies.jobManager.add(ProfileUploadJob())
- startActivity(MainActivity.clearTop(this))
- }
- }
- }
- }
-
- 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
- )
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt
similarity index 78%
rename from app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreViewModel.kt
rename to app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt
index 40d41b965a..29cb2dfcf8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTestRestoreViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/RemoteRestoreViewModel.kt
@@ -17,18 +17,23 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.BackupRepository
+import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.registration.RegistrationUtil
import java.io.InputStream
import kotlin.time.Duration.Companion.seconds
-class MessageBackupsTestRestoreViewModel : ViewModel() {
+class RemoteRestoreViewModel : ViewModel() {
val disposables = CompositeDisposable()
- private val _state: MutableState = mutableStateOf(ScreenState(importState = ImportState.NONE, plaintext = false))
+ private val _state: MutableState = mutableStateOf(ScreenState(backupTier = SignalStore.backup().backupTier, importState = ImportState.NONE, restoreProgress = null))
+
val state: State = _state
fun import(length: Long, inputStreamFactory: () -> InputStream) {
@@ -37,7 +42,7 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
val self = Recipient.self()
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
- disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = _state.value.plaintext) }
+ disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
@@ -54,6 +59,7 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
.then(SyncArchivedMediaJob())
.then(BackupRestoreMediaJob())
.enqueueAndBlockUntilCompletion(120.seconds.inWholeMilliseconds)
+ RegistrationUtil.maybeMarkRegistrationComplete()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@@ -62,8 +68,8 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
}
}
- fun onPlaintextToggled() {
- _state.value = _state.value.copy(plaintext = !_state.value.plaintext)
+ fun updateRestoreProgress(restoreEvent: RestoreV2Event) {
+ _state.value = _state.value.copy(restoreProgress = restoreEvent)
}
override fun onCleared() {
@@ -71,8 +77,9 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
}
data class ScreenState(
+ val backupTier: MessageBackupTier?,
val importState: ImportState,
- val plaintext: Boolean
+ val restoreProgress: RestoreV2Event?
)
enum class ImportState(val inProgress: Boolean = false) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
index a69dbd2a99..73aa7933d8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
@@ -816,6 +816,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
SignalStore.account().setRegistered(false)
SignalStore.registrationValues().clearRegistrationComplete()
SignalStore.registrationValues().clearHasUploadedProfile()
+ SignalStore.registrationValues().clearSkippedTransferOrRestore()
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreJob.kt
index 69da6dd7b9..9c946f0858 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreJob.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreJob.kt
@@ -5,11 +5,13 @@
package org.thoughtcrime.securesms.jobs
+import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.RestoreState
import org.thoughtcrime.securesms.backup.v2.BackupRepository
+import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@@ -71,6 +73,7 @@ class BackupRestoreJob private constructor(parameters: Parameters) : BaseJob(par
progress = progress.toFloat() / total.toFloat(),
indeterminate = false
)
+ EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_DOWNLOAD, progress, total))
}
override fun shouldCancel() = isCanceled
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
index a8cab496a6..0d8362e86e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
@@ -60,7 +60,7 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
- var backupTier: MessageBackupTier by enumValue(KEY_BACKUP_TIER, MessageBackupTier.FREE, MessageBackupTier.Serializer)
+ var backupTier: MessageBackupTier? by enumValue(KEY_BACKUP_TIER, null, MessageBackupTier.Serializer)
val totalBackupSize: Long get() = lastBackupProtoSize + usedBackupMediaSpace
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java
index fca0bdb1ce..0982b162cc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java
@@ -199,14 +199,6 @@ public final class InternalValues extends SignalStoreValues {
return FeatureFlags.internalUser() && getBoolean(CONVERSATION_ITEM_V2_MEDIA, false);
}
- public void setForceEnterRestoreV2Flow(boolean enter) {
- putBoolean(FORCE_ENTER_RESTORE_V2_FLOW, enter);
- }
-
- public boolean enterRestoreV2Flow() {
- return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
- }
-
public synchronized void setWebSocketShadowingStats(byte[] bytes) {
putBlob(WEB_SOCKET_SHADOWING_STATS, bytes);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java
index 273cff76f5..0e116e10c9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java
@@ -9,11 +9,12 @@ import java.util.List;
public final class RegistrationValues extends SignalStoreValues {
- private static final String REGISTRATION_COMPLETE = "registration.complete";
- private static final String PIN_REQUIRED = "registration.pin_required";
- private static final String HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile";
- private static final String SESSION_E164 = "registration.session_e164";
- private static final String SESSION_ID = "registration.session_id";
+ private static final String REGISTRATION_COMPLETE = "registration.complete";
+ private static final String PIN_REQUIRED = "registration.pin_required";
+ private static final String HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile";
+ private static final String SESSION_E164 = "registration.session_e164";
+ private static final String SESSION_ID = "registration.session_id";
+ private static final String SKIPPED_TRANSFER_OR_RESTORE = "registration.has_skipped_transfer_or_restore";
RegistrationValues(@NonNull KeyValueStore store) {
super(store);
@@ -24,6 +25,7 @@ public final class RegistrationValues extends SignalStoreValues {
.putBoolean(HAS_UPLOADED_PROFILE, false)
.putBoolean(REGISTRATION_COMPLETE, false)
.putBoolean(PIN_REQUIRED, true)
+ .putBoolean(SKIPPED_TRANSFER_OR_RESTORE, false)
.commit();
}
@@ -68,6 +70,18 @@ public final class RegistrationValues extends SignalStoreValues {
putString(SESSION_ID, sessionId);
}
+ public boolean hasSkippedTransferOrRestore() {
+ return getBoolean(SKIPPED_TRANSFER_OR_RESTORE, false);
+ }
+
+ public void markSkippedTransferOrRestore() {
+ putBoolean(SKIPPED_TRANSFER_OR_RESTORE, true);
+ }
+
+ public void clearSkippedTransferOrRestore() {
+ putBoolean(SKIPPED_TRANSFER_OR_RESTORE, false);
+ }
+
@Nullable
public String getSessionId() {
return getString(SESSION_ID, null);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
index 3b74148976..8643676443 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java
@@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTestRestoreActivity;
+import org.thoughtcrime.securesms.backup.v2.MessageBackupTier;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -38,7 +38,9 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate;
+import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
+import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
@@ -238,8 +240,8 @@ public class PinRestoreEntryFragment extends LoggingFragment {
Activity activity = requireActivity();
- if (BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED) {
- startActivity(MessageBackupsTestRestoreActivity.Companion.getIntent(activity));
+ if (FeatureFlags.messageBackups()) {
+ startActivity(RestoreActivity.getIntentForTransferOrRestore(activity));
} else if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) {
final Intent main = MainActivity.clearTop(activity);
final Intent profile = CreateProfileActivity.getIntentForUserProfile(activity);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt
index 08d7556bec..ce5c9b61f9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.kt
@@ -7,6 +7,7 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.util.DefaultValueLiveData
@@ -35,7 +36,11 @@ class PinRestoreViewModel : ViewModel() {
}
disposables += Single
- .fromCallable { repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType) }
+ .fromCallable {
+ val response = repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType)
+ BackupRepository.restoreBackupTier()
+ response
+ }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt
index 2d62e9ea32..9a24a41420 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt
@@ -23,7 +23,9 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
+import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
+import org.thoughtcrime.securesms.util.FeatureFlags
/**
* Activity to hold the entire registration process.
@@ -76,8 +78,6 @@ class RegistrationV2Activity : BaseActivity() {
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
- SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
-
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
@@ -86,9 +86,9 @@ class RegistrationV2Activity : BaseActivity() {
val startIntent = MainActivity.clearTop(this).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
- }
-
- if (needsProfile) {
+ } else if (!SignalStore.registrationValues().hasSkippedTransferOrRestore() && FeatureFlags.messageBackups()) {
+ putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity))
+ } else if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt
index 9dd5d51344..f7fdff2d5a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.withContext
import org.signal.core.util.Stopwatch
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
@@ -748,6 +749,8 @@ class RegistrationV2ViewModel : ViewModel() {
Log.v(TAG, "onSuccessfulRegistration()")
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
+ restoreBackupTier()
+
if (reglockEnabled) {
SignalStore.onboarding().clearAll()
val stopwatch = Stopwatch("RegistrationLockRestore")
@@ -838,6 +841,12 @@ class RegistrationV2ViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
+ private suspend fun restoreBackupTier() = withContext(Dispatchers.IO) {
+ val startTime = System.currentTimeMillis()
+ BackupRepository.restoreBackupTier()
+ Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to restore the backup tier..")
+ }
+
private suspend fun refreshFeatureFlags() = withContext(Dispatchers.IO) {
val startTime = System.currentTimeMillis()
try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt
index 232f615dfc..e365981e1f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt
@@ -104,7 +104,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
when (welcomeAction) {
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
WelcomeAction.RESTORE_BACKUP -> {
- val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
+ val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt
new file mode 100644
index 0000000000..649742078a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/restore/RemoteRestoreActivity.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.registration.v2.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.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.profiles.AvatarHelper
+import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.registration.RegistrationUtil
+import org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreMoreOptionsDialog
+import org.thoughtcrime.securesms.util.Util
+
+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.backupTier != MessageBackupTier.PAID
+ )
+ if (state.importState == RemoteRestoreViewModel.ImportState.RESTORED) {
+ SideEffect {
+ 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)
+ }
+
+ private fun getFeatureList(tier: MessageBackupTier?): ImmutableList {
+ return when (tier) {
+ null -> persistentListOf()
+ MessageBackupTier.PAID -> {
+ persistentListOf(
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_thread_compact_bold_16,
+ label = "All of your media"
+ ),
+ MessageBackupsTypeFeature(
+ iconResourceId = R.drawable.symbol_recent_compact_bold_16,
+ label = "All of your text messages"
+ )
+ )
+ }
+ MessageBackupTier.FREE -> {
+ 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"
+ )
+ )
+ }
+ }
+ }
+
+ /**
+ * 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)
+ )
+ }
+
+ // TODO [message-backups] Finalized copy.
+ val progressText = when (restoreProgress?.type) {
+ RestoreV2Event.Type.PROGRESS_DOWNLOAD -> "Downloading backup..."
+ RestoreV2Event.Type.PROGRESS_RESTORE -> "Restoring messages..."
+ else -> "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 = "$progressBytes of $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,
+ true
+ )
+ }
+ }
+
+ @Composable
+ private fun RestoreFromBackupContent(
+ features: ImmutableList,
+ onRestoreBackupClick: () -> Unit,
+ onCancelClick: () -> Unit,
+ onMoreOptionsClick: () -> Unit,
+ tier: MessageBackupTier?,
+ cancelable: Boolean
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
+ .padding(top = 40.dp, bottom = 24.dp)
+ ) {
+ Text(
+ text = "Restore from backup", // TODO [message-backups] Finalized copy.
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(bottom = 12.dp)
+ )
+
+ val yourLastBackupText = buildAnnotatedString {
+ append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy.
+ append(" ")
+ if (tier != MessageBackupTier.PAID) {
+ withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
+ append("Only media sent or received in the past 30 days is included.") // TODO [message-backups] Finalized copy.
+ }
+ }
+ }
+
+ 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 = "Your backup includes:", // TODO [message-backups] Finalized copy.
+ 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 = "Restore backup" // TODO [message-backups] Finalized copy.
+ )
+ }
+
+ 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
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt
index da93a068b2..56abd4e0b3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt
@@ -28,8 +28,10 @@ import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermi
import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions
+import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.navigation.safeNavigate
+import org.thoughtcrime.securesms.util.visible
/**
* First screen that is displayed on the very first app launch.
@@ -59,6 +61,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
+ binding.welcomeTransferOrRestore.visible = !FeatureFlags.restoreAfterRegistration()
}
private fun onContinueClicked() {
@@ -86,7 +89,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
} else {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
- val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
+ val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
launchRestoreActivity.launch(restoreIntent)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
index eb85a2fd4d..38311bad2d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt
@@ -9,10 +9,14 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
+import androidx.navigation.findNavController
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
/**
@@ -33,6 +37,13 @@ class RestoreActivity : BaseActivity() {
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
sharedViewModel.setNextIntent(it)
}
+
+ val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.NONE.value))
+ when (navTarget) {
+ NavTarget.LOCAL_RESTORE -> findNavController(R.id.nav_host_fragment).navigate(R.id.choose_local_backup_fragment)
+ NavTarget.TRANSFER -> findNavController(R.id.nav_host_fragment).navigate(R.id.newDeviceTransferInstructions)
+ else -> Unit
+ }
}
override fun onResume() {
@@ -46,8 +57,41 @@ class RestoreActivity : BaseActivity() {
}
companion object {
+
+ enum class NavTarget(val value: Int) {
+ NONE(0),
+ TRANSFER(1),
+ LOCAL_RESTORE(2);
+
+ companion object {
+ fun deserialize(value: Int): NavTarget {
+ return values().firstOrNull { it.value == value } ?: NONE
+ }
+ }
+ }
+
+ private const val EXTRA_NAV_TARGET = "nav_target"
+
@JvmStatic
- fun getIntentForRestore(context: Context): Intent {
+ fun getIntentForTransfer(context: Context): Intent {
+ return Intent(context, RestoreActivity::class.java).apply {
+ putExtra(EXTRA_NAV_TARGET, NavTarget.TRANSFER.value)
+ }
+ }
+
+ @JvmStatic
+ fun getIntentForLocalRestore(context: Context): Intent {
+ return Intent(context, RestoreActivity::class.java).apply {
+ putExtra(EXTRA_NAV_TARGET, NavTarget.LOCAL_RESTORE.value)
+ }
+ }
+
+ @JvmStatic
+ fun getIntentForTransferOrRestore(context: Context): Intent {
+ val tier = SignalStore.backup().backupTier
+ if (tier == MessageBackupTier.PAID) {
+ return Intent(context, RemoteRestoreActivity::class.java)
+ }
return Intent(context, RestoreActivity::class.java)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt
index 2aa6f67135..661d8ddedd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorecomplete/RestoreCompleteV2Fragment.kt
@@ -10,7 +10,6 @@ import android.view.View
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.restore.RestoreActivity
/**
@@ -29,7 +28,6 @@ class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration
private fun onBackupCompletedSuccessfully() {
Log.d(TAG, "onBackupCompletedSuccessfully()")
- SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
val activity = requireActivity() as RestoreActivity
activity.finishActivitySuccessfully()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt
index 2e46bbf11a..d01d4053c1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt
@@ -111,7 +111,6 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
private fun onBackupCompletedSuccessfully() {
Log.d(TAG, "onBackupCompletedSuccessfully()")
- SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
val activity = requireActivity() as RestoreActivity
navigationViewModel.getNextIntent()?.let {
Log.d(TAG, "Launching ${it.component}")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt
new file mode 100644
index 0000000000..39f5f23fdc
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreMoreOptionsDialog.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.restore.transferorrestore
+
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.viewModels
+import org.thoughtcrime.securesms.MainActivity
+import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
+import org.thoughtcrime.securesms.databinding.TransferOrRestoreOptionsBottomSheetDialogFragmentBinding
+import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
+import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
+import org.thoughtcrime.securesms.restore.RestoreActivity
+import org.thoughtcrime.securesms.util.visible
+
+class TransferOrRestoreMoreOptionsDialog : FixedRoundedCornerBottomSheetDialogFragment() {
+
+ override val peekHeightPercentage: Float = 1f
+
+ private val viewModel by viewModels()
+ private lateinit var binding: TransferOrRestoreOptionsBottomSheetDialogFragmentBinding
+
+ companion object {
+
+ const val TAG = "TRANSFER_OR_RESTORE_OPTIONS_DIALOG_FRAGMENT"
+ const val ARG_SKIP_ONLY = "skip_only"
+
+ fun show(fragmentManager: FragmentManager, skipOnly: Boolean) {
+ TransferOrRestoreMoreOptionsDialog().apply {
+ arguments = bundleOf(ARG_SKIP_ONLY to skipOnly)
+ }.show(fragmentManager, TAG)
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = TransferOrRestoreOptionsBottomSheetDialogFragmentBinding.inflate(inflater.cloneInContext(ContextThemeWrapper(inflater.context, themeResId)), container, false)
+ if (arguments?.getBoolean(ARG_SKIP_ONLY, false) ?: false) {
+ binding.transferCard.visible = false
+ binding.localRestoreCard.visible = false
+ }
+ binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(viewModel.getBackupRestorationType()) }
+ binding.transferCard.setOnClickListener { viewModel.onTransferFromAndroidDeviceSelected() }
+ binding.localRestoreCard.setOnClickListener { viewModel.onRestoreFromLocalBackupSelected() }
+ binding.skipCard.setOnClickListener { viewModel.onSkipRestoreOrTransferSelected() }
+ binding.cancel.setOnClickListener { dismiss() }
+
+ viewModel.uiState.observe(viewLifecycleOwner) { state ->
+ updateSelection(state.restorationType)
+ }
+
+ return binding.root
+ }
+
+ private fun launchSelection(restorationType: BackupRestorationType?) {
+ when (restorationType) {
+ BackupRestorationType.DEVICE_TRANSFER -> {
+ startActivity(RestoreActivity.getIntentForTransfer(requireContext()))
+ }
+ BackupRestorationType.LOCAL_BACKUP -> {
+ startActivity(RestoreActivity.getIntentForLocalRestore(requireContext()))
+ }
+ BackupRestorationType.REMOTE_BACKUP -> {
+ startActivity(RemoteRestoreActivity.getIntent(requireContext()))
+ }
+ BackupRestorationType.NONE -> {
+ SignalStore.registrationValues().markSkippedTransferOrRestore()
+ val startIntent = MainActivity.clearTop(requireContext()).apply {
+ putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(requireContext()))
+ }
+ startActivity(startIntent)
+ }
+ else -> {
+ return
+ }
+ }
+ dismiss()
+ }
+
+ private fun updateSelection(restorationType: BackupRestorationType?) {
+ binding.transferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
+ binding.localRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
+ binding.skipCard.isSelected = restorationType == BackupRestorationType.NONE
+ binding.transferOrRestoreFragmentNext.isEnabled = restorationType != null
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt
index 7622f03771..81aaf595fa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.restore.transferorrestore
import android.os.Bundle
import android.view.View
-import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log
@@ -16,7 +15,9 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreV2Binding
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
+import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil
@@ -39,7 +40,11 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener { sharedViewModel.onRestoreFromRemoteBackupSelected() }
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener {
- Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
+ TransferOrRestoreMoreOptionsDialog.show(fragmentManager = childFragmentManager, skipOnly = true)
+ }
+
+ if (SignalStore.backup().backupTier == null) {
+ binding.transferOrRestoreFragmentRestoreRemoteCard.visible = false
}
binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups()
@@ -72,9 +77,7 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToRestore())
}
BackupRestorationType.REMOTE_BACKUP -> {
- // TODO [regv2]
- Log.w(TAG, "Not yet implemented!", NotImplementedError())
- Toast.makeText(requireContext(), "Not yet implemented!", Toast.LENGTH_LONG).show()
+ startActivity(RemoteRestoreActivity.getIntent(requireContext()))
}
else -> {
throw IllegalArgumentException()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreViewModel.kt
new file mode 100644
index 0000000000..d309c35da1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.restore.transferorrestore
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
+
+class TransferOrRestoreViewModel : ViewModel() {
+
+ private val store = MutableStateFlow(State())
+ val uiState = store.asLiveData()
+
+ fun onSkipRestoreOrTransferSelected() {
+ store.update {
+ it.copy(restorationType = BackupRestorationType.NONE)
+ }
+ }
+
+ fun onTransferFromAndroidDeviceSelected() {
+ store.update {
+ it.copy(restorationType = BackupRestorationType.DEVICE_TRANSFER)
+ }
+ }
+
+ fun onRestoreFromLocalBackupSelected() {
+ store.update {
+ it.copy(restorationType = BackupRestorationType.LOCAL_BACKUP)
+ }
+ }
+
+ fun getBackupRestorationType(): BackupRestorationType? {
+ return store.value.restorationType
+ }
+}
+
+data class State(val restorationType: BackupRestorationType? = null)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index eb7676df42..1d9a649383 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -733,7 +733,7 @@ public final class FeatureFlags {
/** Whether or not to launch the restore activity after registration is complete, rather than before. */
public static boolean restoreAfterRegistration() {
- return getBoolean(RESTORE_POST_REGISTRATION, false);
+ return BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || getBoolean(RESTORE_POST_REGISTRATION, false);
}
/**
diff --git a/app/src/main/res/drawable/ic_continue_no_restore_48.xml b/app/src/main/res/drawable/ic_continue_no_restore_48.xml
new file mode 100644
index 0000000000..b930186f63
--- /dev/null
+++ b/app/src/main/res/drawable/ic_continue_no_restore_48.xml
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_transfer_local_48.xml b/app/src/main/res/drawable/ic_transfer_local_48.xml
new file mode 100644
index 0000000000..b290196c53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_transfer_local_48.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_remote_restore.xml b/app/src/main/res/layout/activity_remote_restore.xml
new file mode 100644
index 0000000000..883b564737
--- /dev/null
+++ b/app/src/main/res/layout/activity_remote_restore.xml
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_transfer_restore_v2.xml b/app/src/main/res/layout/fragment_transfer_restore_v2.xml
index 883b564737..9d2cd64ebd 100644
--- a/app/src/main/res/layout/fragment_transfer_restore_v2.xml
+++ b/app/src/main/res/layout/fragment_transfer_restore_v2.xml
@@ -129,7 +129,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="16dp"
- android:text="@string/TransferOrRestoreFragment__restore_from_backup"
+ android:text="@string/TransferOrRestoreFragment__restore_from_signal_backup"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_remote_description"
app:layout_constraintEnd_toEndOf="parent"
@@ -143,7 +143,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
- android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
+ android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_signal_backup"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -173,22 +173,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 75cae3272f..b8ebc063ca 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4143,9 +4143,16 @@
You need access to your old device.
Restore from backup
Restore your messages from a local backup. If you don’t restore now, you won\'t be able to restore later.
+ Restore local backup
+ Restore Signal backup
+ Restore all your text messages + your last 30 days of media
More options
+ Cancel
+ Log in without transferring
+ Continue without transferring your messages and media
+
Open Signal on your old Android phone
Continue
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
index 32cf046f8c..540c66236d 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
@@ -28,6 +28,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@@ -224,6 +225,10 @@ public class SignalServiceMessageReceiver {
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
}
+ public boolean checkBackupExistence(int cdnNumber, Map headers, String cdnPath) throws MissingConfigurationException, IOException {
+ return socket.checkForBackup(cdnNumber, headers, cdnPath);
+ }
+
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
throws IOException, InvalidMessageException
{
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt
index 5ca7baa7b1..e05d72eb40 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt
@@ -257,7 +257,7 @@ class ArchiveApi(
}
}
- private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
+ fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index 06657cdd5e..bece777efd 100644
--- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -948,6 +948,10 @@ public class PushServiceSocket {
downloadFromCdn(destination, cdnNumber, headers, cdnPath, maxSizeBytes, listener);
}
+ public boolean checkForBackup(int cdnNumber, Map headers, String cdnPath) throws PushNetworkException, MissingConfigurationException, NonSuccessfulResponseCodeException {
+ return checkExistsOnCdn(cdnNumber, headers, cdnPath);
+ }
+
public void retrieveAttachment(int cdnNumber, Map headers, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
throws IOException, MissingConfigurationException
{
@@ -1709,6 +1713,51 @@ public class PushServiceSocket {
}
}
+ private boolean checkExistsOnCdn(int cdnNumber, Map headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException {
+ ConnectionHolder[] cdnNumberClients = cdnClientsMap.get(cdnNumber);
+ if (cdnNumberClients == null) {
+ throw new MissingConfigurationException("Attempted to download from unsupported CDN number: " + cdnNumber + ", Our configuration supports: " + cdnClientsMap.keySet());
+ }
+ ConnectionHolder connectionHolder = getRandom(cdnNumberClients, random);
+ OkHttpClient okHttpClient = connectionHolder.getClient()
+ .newBuilder()
+ .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
+ .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
+ .build();
+
+ Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + "/" + path).get();
+
+ if (connectionHolder.getHostHeader().isPresent()) {
+ request.addHeader("Host", connectionHolder.getHostHeader().get());
+ }
+
+ for (Map.Entry header : headers.entrySet()) {
+ request.addHeader(header.getKey(), header.getValue());
+ }
+
+ Call call = okHttpClient.newCall(request.build());
+
+ synchronized (connections) {
+ connections.add(call);
+ }
+
+ try (Response response = call.execute()) {
+ if (response.isSuccessful()) {
+ return true;
+ } else {
+ throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
+ }
+ } catch (NonSuccessfulResponseCodeException | PushNetworkException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new PushNetworkException(e);
+ } finally {
+ synchronized (connections) {
+ connections.remove(call);
+ }
+ }
+ }
+
private AttachmentDigest uploadToCdn0(String path, String acl, String key, String policy, String algorithm,
String credential, String date, String signature,
InputStream data, String contentType, long length, boolean incremental,