Skip to content

Commit c4b39f4

Browse files
feat: cross platform backup (#WPB-10579) (#4006)
Co-authored-by: Vitor Hugo Schwaab <vitor@schwaab.dev>
1 parent 16c8252 commit c4b39f4

File tree

11 files changed

+161
-85
lines changed

11 files changed

+161
-85
lines changed

app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ class UseCaseModule {
362362
): ObserveSecurityClassificationLabelUseCase =
363363
coreLogic.getSessionScope(currentAccount).observeSecurityClassificationLabel
364364

365+
@ViewModelScoped
366+
@Provides
367+
fun provideCreateMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
368+
coreLogic.getSessionScope(currentAccount).multiPlatformBackup.create
369+
370+
@ViewModelScoped
371+
@Provides
372+
fun provideRestoreMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
373+
coreLogic.getSessionScope(currentAccount).multiPlatformBackup.restore
374+
365375
@ViewModelScoped
366376
@Provides
367377
fun provideUpdateApiVersionsScheduler(@KaliumCoreLogic coreLogic: CoreLogic) =

app/src/main/kotlin/com/wire/android/di/accountScoped/BackupModule.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
*/
1818
package com.wire.android.di.accountScoped
1919

20+
import com.wire.android.BuildConfig
2021
import com.wire.android.di.CurrentAccount
2122
import com.wire.android.di.KaliumCoreLogic
23+
import com.wire.android.ui.home.settings.backup.MPBackupSettings
2224
import com.wire.kalium.logic.CoreLogic
2325
import com.wire.kalium.logic.data.user.UserId
2426
import com.wire.kalium.logic.feature.backup.BackupScope
@@ -53,6 +55,13 @@ class BackupModule {
5355
fun provideRestoreBackupUseCase(backupScope: BackupScope) =
5456
backupScope.restore
5557

58+
@Provides
59+
fun provideMpBackupSettings() = if (BuildConfig.ENABLE_CROSSPLATFORM_BACKUP) {
60+
MPBackupSettings.Enabled
61+
} else {
62+
MPBackupSettings.Disabled
63+
}
64+
5665
@OptIn(DelicateKaliumApi::class)
5766
@ViewModelScoped
5867
@Provides

app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject
8383
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
8484
result.backupFilePath,
8585
result.backupFileName,
86-
result.backupFileSize,
8786
false
8887
)
8988
}

app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreState.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package com.wire.android.ui.home.settings.backup
2020

2121
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
22+
import com.wire.kalium.logic.feature.backup.BackupFileFormat
2223
import okio.Path
2324

2425
data class BackupAndRestoreState(
@@ -27,10 +28,11 @@ data class BackupAndRestoreState(
2728
val restorePasswordValidation: PasswordValidation,
2829
val backupCreationProgress: BackupCreationProgress,
2930
val lastBackupData: Long?,
30-
val passwordValidation: ValidatePasswordResult
31+
val passwordValidation: ValidatePasswordResult,
32+
val backupFileFormat: BackupFileFormat,
3133
) {
3234

33-
data class CreatedBackup(val path: Path, val assetName: String, val assetSize: Long, val isEncrypted: Boolean)
35+
data class CreatedBackup(val path: Path, val assetName: String, val isEncrypted: Boolean)
3436

3537
companion object {
3638
val INITIAL_STATE = BackupAndRestoreState(
@@ -39,7 +41,8 @@ data class BackupAndRestoreState(
3941
backupCreationProgress = BackupCreationProgress.InProgress(),
4042
restorePasswordValidation = PasswordValidation.NotVerified,
4143
passwordValidation = ValidatePasswordResult.Valid,
42-
lastBackupData = null
44+
lastBackupData = null,
45+
backupFileFormat = BackupFileFormat.ANDROID,
4346
)
4447
}
4548
}

app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import androidx.compose.runtime.mutableStateOf
2727
import androidx.compose.runtime.setValue
2828
import androidx.lifecycle.ViewModel
2929
import androidx.lifecycle.viewModelScope
30-
import com.wire.android.BuildConfig
3130
import com.wire.android.appLogger
3231
import com.wire.android.datastore.UserDataStore
3332
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
@@ -38,15 +37,18 @@ import com.wire.android.util.dispatchers.DispatcherProvider
3837
import com.wire.kalium.logic.data.asset.KaliumFileSystem
3938
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
4039
import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
40+
import com.wire.kalium.logic.feature.backup.BackupFileFormat
4141
import com.wire.kalium.logic.feature.backup.CreateBackupResult
4242
import com.wire.kalium.logic.feature.backup.CreateBackupUseCase
43+
import com.wire.kalium.logic.feature.backup.CreateMPBackupUseCase
4344
import com.wire.kalium.logic.feature.backup.RestoreBackupResult
4445
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.BackupIOFailure
4546
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.DecryptionFailure
4647
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.IncompatibleBackup
4748
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.InvalidPassword
4849
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.InvalidUserId
4950
import com.wire.kalium.logic.feature.backup.RestoreBackupUseCase
51+
import com.wire.kalium.logic.feature.backup.RestoreMPBackupUseCase
5052
import com.wire.kalium.logic.feature.backup.VerifyBackupResult
5153
import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase
5254
import com.wire.kalium.util.DateTimeUtil
@@ -60,16 +62,18 @@ import javax.inject.Inject
6062

6163
@Suppress("LongParameterList", "TooManyFunctions")
6264
@HiltViewModel
63-
class BackupAndRestoreViewModel
64-
@Inject constructor(
65+
class BackupAndRestoreViewModel @Inject constructor(
6566
private val importBackup: RestoreBackupUseCase,
67+
private val importMpBackup: RestoreMPBackupUseCase,
6668
private val createBackupFile: CreateBackupUseCase,
69+
private val createMpBackupFile: CreateMPBackupUseCase,
6770
private val verifyBackup: VerifyBackupUseCase,
6871
private val validatePassword: ValidatePasswordUseCase,
6972
private val kaliumFileSystem: KaliumFileSystem,
7073
private val fileManager: FileManager,
7174
private val userDataStore: UserDataStore,
72-
private val dispatcher: DispatcherProvider
75+
private val dispatcher: DispatcherProvider,
76+
private val mpBackupSettings: MPBackupSettings,
7377
) : ViewModel() {
7478

7579
val createBackupPasswordState: TextFieldState = TextFieldState()
@@ -111,13 +115,20 @@ class BackupAndRestoreViewModel
111115
updateCreationProgress(PROGRESS_50)
112116
delay(SMALL_DELAY)
113117

114-
when (val result = createBackupFile(createBackupPasswordState.text.toString())) {
118+
val password = createBackupPasswordState.text.toString()
119+
120+
val result = if (mpBackupSettings is MPBackupSettings.Enabled) {
121+
createMpBackupFile(password)
122+
} else {
123+
createBackupFile(password)
124+
}
125+
126+
when (result) {
115127
is CreateBackupResult.Success -> {
116128
state = state.copy(backupCreationProgress = BackupCreationProgress.Finished(result.backupFileName))
117129
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
118130
result.backupFilePath,
119131
result.backupFileName,
120-
result.backupFileSize,
121132
createBackupPasswordState.text.isNotEmpty()
122133
)
123134
createBackupPasswordState.clearText()
@@ -170,27 +181,25 @@ class BackupAndRestoreViewModel
170181
fun chooseBackupFileToRestore(uri: Uri) = viewModelScope.launch {
171182
latestImportedBackupTempPath = kaliumFileSystem.tempFilePath(TEMP_IMPORTED_BACKUP_FILE_NAME)
172183
fileManager.copyToPath(uri, latestImportedBackupTempPath)
173-
checkIfBackupEncrypted(latestImportedBackupTempPath)
184+
verifyBackupFile(latestImportedBackupTempPath)
174185
}
175186

176187
private fun showPasswordDialog() {
177188
state = state.copy(restoreFileValidation = RestoreFileValidation.PasswordRequired)
178189
}
179190

180-
private suspend fun checkIfBackupEncrypted(importedBackupPath: Path) = withContext(dispatcher.main()) {
191+
private suspend fun verifyBackupFile(importedBackupPath: Path) = withContext(dispatcher.main()) {
181192
when (val result = verifyBackup(importedBackupPath)) {
182193
is VerifyBackupResult.Success -> {
183-
when (result) {
184-
is VerifyBackupResult.Success.Encrypted -> showPasswordDialog()
185-
is VerifyBackupResult.Success.NotEncrypted -> importDatabase(importedBackupPath)
186-
VerifyBackupResult.Success.Web -> {
187-
if (BuildConfig.DEVELOPER_FEATURES_ENABLED) {
188-
importDatabase(importedBackupPath)
189-
} else {
190-
state = state.copy(restoreFileValidation = RestoreFileValidation.IncompatibleBackup)
191-
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
192-
}
193-
}
194+
state = state.copy(backupFileFormat = result.format)
195+
if (result.isEncrypted) {
196+
showPasswordDialog()
197+
} else {
198+
state = state.copy(
199+
restoreFileValidation = RestoreFileValidation.ValidNonEncryptedBackup,
200+
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_75)
201+
)
202+
restoreBackup(importedBackupPath, null)
194203
}
195204
}
196205

@@ -199,6 +208,15 @@ class BackupAndRestoreViewModel
199208
val errorMessage = when (result) {
200209
is VerifyBackupResult.Failure.Generic -> result.error.toString()
201210
VerifyBackupResult.Failure.InvalidBackupFile -> "No valid files found in the backup"
211+
is VerifyBackupResult.Failure.UnsupportedVersion -> "Unsupported backup version: ${result.version}"
212+
VerifyBackupResult.Failure.InvalidUserId -> {
213+
state = state.copy(
214+
backupRestoreProgress = BackupRestoreProgress.Failed,
215+
restoreFileValidation = RestoreFileValidation.WrongBackup,
216+
restorePasswordValidation = PasswordValidation.Valid
217+
)
218+
"Invalid user ID"
219+
}
202220
}
203221

204222
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
@@ -207,30 +225,6 @@ class BackupAndRestoreViewModel
207225
}
208226
}
209227

210-
private suspend fun importDatabase(importedBackupPath: Path) {
211-
state = state.copy(
212-
restoreFileValidation = RestoreFileValidation.ValidNonEncryptedBackup,
213-
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_75)
214-
)
215-
when (val result = importBackup(importedBackupPath, null)) {
216-
RestoreBackupResult.Success -> {
217-
updateCreationProgress(PROGRESS_75)
218-
delay(SMALL_DELAY)
219-
state = state.copy(backupRestoreProgress = BackupRestoreProgress.Finished)
220-
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
221-
}
222-
223-
is RestoreBackupResult.Failure -> {
224-
appLogger.e("Error when restoring the backup db file caused by: ${result.failure.cause}")
225-
state = state.copy(
226-
restoreFileValidation = RestoreFileValidation.IncompatibleBackup,
227-
backupRestoreProgress = BackupRestoreProgress.Failed
228-
)
229-
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
230-
}
231-
}
232-
}
233-
234228
fun restorePasswordProtectedBackup() = viewModelScope.launch(dispatcher.main()) {
235229
state = state.copy(
236230
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_50),
@@ -240,21 +234,8 @@ class BackupAndRestoreViewModel
240234
val fileValidationState = state.restoreFileValidation
241235
if (fileValidationState is RestoreFileValidation.PasswordRequired) {
242236
state = state.copy(restorePasswordValidation = PasswordValidation.Entered)
243-
when (val result = importBackup(latestImportedBackupTempPath, restoreBackupPasswordState.text.toString())) {
244-
RestoreBackupResult.Success -> {
245-
state = state.copy(
246-
backupRestoreProgress = BackupRestoreProgress.Finished,
247-
restorePasswordValidation = PasswordValidation.Valid
248-
)
249-
restoreBackupPasswordState.clearText()
250-
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
251-
}
252237

253-
is RestoreBackupResult.Failure -> {
254-
mapBackupRestoreFailure(result.failure)
255-
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
256-
}
257-
}
238+
restoreBackup(latestImportedBackupTempPath, restoreBackupPasswordState.text.toString())
258239
} else {
259240
state = state.copy(backupRestoreProgress = BackupRestoreProgress.Failed)
260241
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
@@ -317,6 +298,30 @@ class BackupAndRestoreViewModel
317298
}
318299
}
319300

301+
private fun restoreBackup(backupFilePath: Path, password: String?) = viewModelScope.launch {
302+
val result = when (state.backupFileFormat) {
303+
BackupFileFormat.ANDROID -> importBackup(backupFilePath, password)
304+
BackupFileFormat.MULTIPLATFORM -> importMpBackup(backupFilePath, password)
305+
}
306+
when (result) {
307+
RestoreBackupResult.Success -> {
308+
updateCreationProgress(PROGRESS_75)
309+
delay(SMALL_DELAY)
310+
state = state.copy(
311+
backupRestoreProgress = BackupRestoreProgress.Finished,
312+
restorePasswordValidation = PasswordValidation.Valid
313+
)
314+
restoreBackupPasswordState.clearText()
315+
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
316+
}
317+
318+
is RestoreBackupResult.Failure -> {
319+
mapBackupRestoreFailure(result.failure)
320+
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
321+
}
322+
}
323+
}
324+
320325
private suspend fun updateCreationProgress(progress: Float) = withContext(dispatcher.main()) {
321326
state = state.copy(backupCreationProgress = BackupCreationProgress.InProgress(progress))
322327
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.ui.home.settings.backup
19+
20+
sealed interface MPBackupSettings {
21+
data object Disabled : MPBackupSettings
22+
data object Enabled : MPBackupSettings
23+
}

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,7 @@ In group conversations, the group admin can overwrite this setting.</string>
13431343
<string name="backup_label_conversation_restored">Conversations have been restored</string>
13441344
<string name="backup_label_loading_conversations">Loading conversations</string>
13451345
<string name="backup_dialog_restore_backup_title">Restore from backup </string>
1346-
<string name="backup_dialog_restore_backup_message">The backup contents will replace the conversation history on this device. You can only restore history from a backup of the same platform.</string>
1346+
<string name="backup_dialog_restore_backup_message">The existing history on this device remains and will be completed by the new backup. You can restore history from all your devices and different platforms but not from another account.</string>
13471347
<string name="backup_dialog_choose_backup_file_option">Choose Backup File</string>
13481348
<string name="backup_label_enter_password">Enter password</string>
13491349
<string name="backup_dialog_restore_backup_password_message">This backup is password protected.</string>

0 commit comments

Comments
 (0)