Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,16 @@ class UseCaseModule {
): ObserveSecurityClassificationLabelUseCase =
coreLogic.getSessionScope(currentAccount).observeSecurityClassificationLabel

@ViewModelScoped
@Provides
fun provideCreateMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
coreLogic.getSessionScope(currentAccount).multiPlatformBackup.create

@ViewModelScoped
@Provides
fun provideRestoreMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
coreLogic.getSessionScope(currentAccount).multiPlatformBackup.restore

@ViewModelScoped
@Provides
fun provideUpdateApiVersionsScheduler(@KaliumCoreLogic coreLogic: CoreLogic) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
*/
package com.wire.android.di.accountScoped

import com.wire.android.BuildConfig
import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.ui.home.settings.backup.MPBackupSettings
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.backup.BackupScope
Expand Down Expand Up @@ -53,6 +55,13 @@ class BackupModule {
fun provideRestoreBackupUseCase(backupScope: BackupScope) =
backupScope.restore

@Provides
fun provideMpBackupSettings() = if (BuildConfig.ENABLE_CROSSPLATFORM_BACKUP) {
MPBackupSettings.Enabled
} else {
MPBackupSettings.Disabled
}

@OptIn(DelicateKaliumApi::class)
@ViewModelScoped
@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
result.backupFilePath,
result.backupFileName,
result.backupFileSize,
false
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.wire.android.ui.home.settings.backup

import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import com.wire.kalium.logic.feature.backup.BackupFileFormat
import okio.Path

data class BackupAndRestoreState(
Expand All @@ -27,10 +28,11 @@ data class BackupAndRestoreState(
val restorePasswordValidation: PasswordValidation,
val backupCreationProgress: BackupCreationProgress,
val lastBackupData: Long?,
val passwordValidation: ValidatePasswordResult
val passwordValidation: ValidatePasswordResult,
val backupFileFormat: BackupFileFormat,
) {

data class CreatedBackup(val path: Path, val assetName: String, val assetSize: Long, val isEncrypted: Boolean)
data class CreatedBackup(val path: Path, val assetName: String, val isEncrypted: Boolean)

companion object {
val INITIAL_STATE = BackupAndRestoreState(
Expand All @@ -39,7 +41,8 @@ data class BackupAndRestoreState(
backupCreationProgress = BackupCreationProgress.InProgress(),
restorePasswordValidation = PasswordValidation.NotVerified,
passwordValidation = ValidatePasswordResult.Valid,
lastBackupData = null
lastBackupData = null,
backupFileFormat = BackupFileFormat.ANDROID,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
import com.wire.android.appLogger
import com.wire.android.datastore.UserDataStore
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
Expand All @@ -38,15 +37,18 @@
import com.wire.kalium.logic.data.asset.KaliumFileSystem
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import com.wire.kalium.logic.feature.backup.BackupFileFormat
import com.wire.kalium.logic.feature.backup.CreateBackupResult
import com.wire.kalium.logic.feature.backup.CreateBackupUseCase
import com.wire.kalium.logic.feature.backup.CreateMPBackupUseCase
import com.wire.kalium.logic.feature.backup.RestoreBackupResult
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.BackupIOFailure
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.DecryptionFailure
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.IncompatibleBackup
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.InvalidPassword
import com.wire.kalium.logic.feature.backup.RestoreBackupResult.BackupRestoreFailure.InvalidUserId
import com.wire.kalium.logic.feature.backup.RestoreBackupUseCase
import com.wire.kalium.logic.feature.backup.RestoreMPBackupUseCase
import com.wire.kalium.logic.feature.backup.VerifyBackupResult
import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase
import com.wire.kalium.util.DateTimeUtil
Expand All @@ -60,16 +62,18 @@

@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class BackupAndRestoreViewModel
@Inject constructor(
class BackupAndRestoreViewModel @Inject constructor(
private val importBackup: RestoreBackupUseCase,
private val importMpBackup: RestoreMPBackupUseCase,
private val createBackupFile: CreateBackupUseCase,
private val createMpBackupFile: CreateMPBackupUseCase,
private val verifyBackup: VerifyBackupUseCase,
private val validatePassword: ValidatePasswordUseCase,
private val kaliumFileSystem: KaliumFileSystem,
private val fileManager: FileManager,
private val userDataStore: UserDataStore,
private val dispatcher: DispatcherProvider
private val dispatcher: DispatcherProvider,
private val mpBackupSettings: MPBackupSettings,
) : ViewModel() {

val createBackupPasswordState: TextFieldState = TextFieldState()
Expand Down Expand Up @@ -111,13 +115,20 @@
updateCreationProgress(PROGRESS_50)
delay(SMALL_DELAY)

when (val result = createBackupFile(createBackupPasswordState.text.toString())) {
val password = createBackupPasswordState.text.toString()

val result = if (mpBackupSettings is MPBackupSettings.Enabled) {
createMpBackupFile(password)
} else {
createBackupFile(password)

Check warning on line 123 in app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L123 was not covered by tests
}

when (result) {
is CreateBackupResult.Success -> {
state = state.copy(backupCreationProgress = BackupCreationProgress.Finished(result.backupFileName))
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
result.backupFilePath,
result.backupFileName,
result.backupFileSize,
createBackupPasswordState.text.isNotEmpty()
)
createBackupPasswordState.clearText()
Expand Down Expand Up @@ -170,27 +181,25 @@
fun chooseBackupFileToRestore(uri: Uri) = viewModelScope.launch {
latestImportedBackupTempPath = kaliumFileSystem.tempFilePath(TEMP_IMPORTED_BACKUP_FILE_NAME)
fileManager.copyToPath(uri, latestImportedBackupTempPath)
checkIfBackupEncrypted(latestImportedBackupTempPath)
verifyBackupFile(latestImportedBackupTempPath)
}

private fun showPasswordDialog() {
state = state.copy(restoreFileValidation = RestoreFileValidation.PasswordRequired)
}

private suspend fun checkIfBackupEncrypted(importedBackupPath: Path) = withContext(dispatcher.main()) {
private suspend fun verifyBackupFile(importedBackupPath: Path) = withContext(dispatcher.main()) {
when (val result = verifyBackup(importedBackupPath)) {
is VerifyBackupResult.Success -> {
when (result) {
is VerifyBackupResult.Success.Encrypted -> showPasswordDialog()
is VerifyBackupResult.Success.NotEncrypted -> importDatabase(importedBackupPath)
VerifyBackupResult.Success.Web -> {
if (BuildConfig.DEVELOPER_FEATURES_ENABLED) {
importDatabase(importedBackupPath)
} else {
state = state.copy(restoreFileValidation = RestoreFileValidation.IncompatibleBackup)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
}
}
state = state.copy(backupFileFormat = result.format)
if (result.isEncrypted) {
showPasswordDialog()
} else {
state = state.copy(
restoreFileValidation = RestoreFileValidation.ValidNonEncryptedBackup,
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_75)
)
restoreBackup(importedBackupPath, null)
}
}

Expand All @@ -199,6 +208,15 @@
val errorMessage = when (result) {
is VerifyBackupResult.Failure.Generic -> result.error.toString()
VerifyBackupResult.Failure.InvalidBackupFile -> "No valid files found in the backup"
is VerifyBackupResult.Failure.UnsupportedVersion -> "Unsupported backup version: ${result.version}"
VerifyBackupResult.Failure.InvalidUserId -> {
state = state.copy(
backupRestoreProgress = BackupRestoreProgress.Failed,
restoreFileValidation = RestoreFileValidation.WrongBackup,
restorePasswordValidation = PasswordValidation.Valid

Check warning on line 216 in app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt#L213-L216

Added lines #L213 - L216 were not covered by tests
)
"Invalid user ID"

Check warning on line 218 in app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L218 was not covered by tests
}
}

AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
Expand All @@ -207,30 +225,6 @@
}
}

private suspend fun importDatabase(importedBackupPath: Path) {
state = state.copy(
restoreFileValidation = RestoreFileValidation.ValidNonEncryptedBackup,
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_75)
)
when (val result = importBackup(importedBackupPath, null)) {
RestoreBackupResult.Success -> {
updateCreationProgress(PROGRESS_75)
delay(SMALL_DELAY)
state = state.copy(backupRestoreProgress = BackupRestoreProgress.Finished)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
}

is RestoreBackupResult.Failure -> {
appLogger.e("Error when restoring the backup db file caused by: ${result.failure.cause}")
state = state.copy(
restoreFileValidation = RestoreFileValidation.IncompatibleBackup,
backupRestoreProgress = BackupRestoreProgress.Failed
)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
}
}
}

fun restorePasswordProtectedBackup() = viewModelScope.launch(dispatcher.main()) {
state = state.copy(
backupRestoreProgress = BackupRestoreProgress.InProgress(PROGRESS_50),
Expand All @@ -240,21 +234,8 @@
val fileValidationState = state.restoreFileValidation
if (fileValidationState is RestoreFileValidation.PasswordRequired) {
state = state.copy(restorePasswordValidation = PasswordValidation.Entered)
when (val result = importBackup(latestImportedBackupTempPath, restoreBackupPasswordState.text.toString())) {
RestoreBackupResult.Success -> {
state = state.copy(
backupRestoreProgress = BackupRestoreProgress.Finished,
restorePasswordValidation = PasswordValidation.Valid
)
restoreBackupPasswordState.clearText()
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
}

is RestoreBackupResult.Failure -> {
mapBackupRestoreFailure(result.failure)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
}
}
restoreBackup(latestImportedBackupTempPath, restoreBackupPasswordState.text.toString())
} else {
state = state.copy(backupRestoreProgress = BackupRestoreProgress.Failed)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
Expand Down Expand Up @@ -317,6 +298,30 @@
}
}

private fun restoreBackup(backupFilePath: Path, password: String?) = viewModelScope.launch {
val result = when (state.backupFileFormat) {
BackupFileFormat.ANDROID -> importBackup(backupFilePath, password)
BackupFileFormat.MULTIPLATFORM -> importMpBackup(backupFilePath, password)

Check warning on line 304 in app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L304 was not covered by tests
}
when (result) {
RestoreBackupResult.Success -> {
updateCreationProgress(PROGRESS_75)
delay(SMALL_DELAY)
state = state.copy(
backupRestoreProgress = BackupRestoreProgress.Finished,
restorePasswordValidation = PasswordValidation.Valid
)
restoreBackupPasswordState.clearText()
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreSucceeded)
}

is RestoreBackupResult.Failure -> {
mapBackupRestoreFailure(result.failure)
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupRestoreFailed)
}
}
}

private suspend fun updateCreationProgress(progress: Float) = withContext(dispatcher.main()) {
state = state.copy(backupCreationProgress = BackupCreationProgress.InProgress(progress))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.home.settings.backup

sealed interface MPBackupSettings {
data object Disabled : MPBackupSettings

Check warning on line 21 in app/src/main/kotlin/com/wire/android/ui/home/settings/backup/MPBackupSettings.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/settings/backup/MPBackupSettings.kt#L21

Added line #L21 was not covered by tests
data object Enabled : MPBackupSettings
}
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ In group conversations, the group admin can overwrite this setting.</string>
<string name="backup_label_conversation_restored">Conversations have been restored</string>
<string name="backup_label_loading_conversations">Loading conversations</string>
<string name="backup_dialog_restore_backup_title">Restore from backup </string>
<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>
<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>
<string name="backup_dialog_choose_backup_file_option">Choose Backup File</string>
<string name="backup_label_enter_password">Enter password</string>
<string name="backup_dialog_restore_backup_password_message">This backup is password protected.</string>
Expand Down
Loading
Loading