Skip to content

Commit

Permalink
feat: create and share obfuscated user database [WPB-14826] (#3818)
Browse files Browse the repository at this point in the history
Co-authored-by: Oussama Hassine <[email protected]>
Co-authored-by: Jakub Zerko <[email protected]>
  • Loading branch information
3 people authored Jan 29, 2025
1 parent f4566e5 commit c1ff13b
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 20 deletions.
15 changes: 0 additions & 15 deletions app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -326,21 +326,6 @@ class UseCaseModule {
): ObserveSecurityClassificationLabelUseCase =
coreLogic.getSessionScope(currentAccount).observeSecurityClassificationLabel

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

@ViewModelScoped
@Provides
fun provideVerifyBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
coreLogic.getSessionScope(currentAccount).backup.verify

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

@ViewModelScoped
@Provides
fun provideUpdateApiVersionsScheduler(@KaliumCoreLogic coreLogic: CoreLogic) =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.di.accountScoped

import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.backup.BackupScope
import com.wire.kalium.util.DelicateKaliumApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped

@Module
@InstallIn(ViewModelComponent::class)
class BackupModule {

@ViewModelScoped
@Provides
fun provideBackupScope(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): BackupScope =
coreLogic.getSessionScope(currentAccount).backup

@ViewModelScoped
@Provides
fun provideCreateBackupUseCase(backupScope: BackupScope) =
backupScope.create

@ViewModelScoped
@Provides
fun provideVerifyBackupUseCase(backupScope: BackupScope) =
backupScope.verify

@ViewModelScoped
@Provides
fun provideRestoreBackupUseCase(backupScope: BackupScope) =
backupScope.restore

@OptIn(DelicateKaliumApi::class)
@ViewModelScoped
@Provides
fun provideOnboardingBackupUseCase(backupScope: BackupScope) =
backupScope.createUnEncryptedCopy
}
67 changes: 63 additions & 4 deletions app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@

package com.wire.android.ui.debug

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
Expand All @@ -30,6 +32,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
Expand All @@ -40,6 +43,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.di.hiltViewModelScoped
import com.wire.android.model.Clickable
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
Expand All @@ -49,8 +54,12 @@ import com.wire.android.ui.common.scaffold.WireScaffold
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.destinations.MigrationScreenDestination
import com.wire.android.util.AppNameUtil
import com.wire.android.ui.home.conversationslist.common.FolderHeader
import com.wire.android.ui.home.settings.SettingsItem
import com.wire.android.ui.home.settings.backup.BackupAndRestoreDialog
import com.wire.android.ui.home.settings.backup.rememberBackUpAndRestoreStateHolder
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.AppNameUtil
import com.wire.android.util.getMimeType
import com.wire.android.util.getUrisOfFilesInDirectory
import com.wire.android.util.multipleFileSharingIntent
Expand All @@ -60,7 +69,10 @@ import java.io.File
@RootNavGraph
@WireDestination
@Composable
fun DebugScreen(navigator: Navigator, userDebugViewModel: UserDebugViewModel = hiltViewModel()) {
fun DebugScreen(
navigator: Navigator,
userDebugViewModel: UserDebugViewModel = hiltViewModel(),
) {
UserDebugContent(
onNavigationPressed = navigator::navigateBack,
onManualMigrationPressed = {
Expand All @@ -74,7 +86,7 @@ fun DebugScreen(navigator: Navigator, userDebugViewModel: UserDebugViewModel = h
state = userDebugViewModel.state,
onLoggingEnabledChange = userDebugViewModel::setLoggingEnabledState,
onDeleteLogs = userDebugViewModel::deleteLogs,
onDatabaseLoggerEnabledChanged = userDebugViewModel::setDatabaseLoggerEnabledState
onDatabaseLoggerEnabledChanged = userDebugViewModel::setDatabaseLoggerEnabledState,
)
}

Expand Down Expand Up @@ -121,6 +133,53 @@ internal fun UserDebugContent(
onCopyText = debugContentState::copyToClipboard,
onManualMigrationPressed = onManualMigrationPressed
)
DangerOptions()
}
}
}
}

@Composable
fun DangerOptions(
modifier: Modifier = Modifier,
exportObfuscatedCopyViewModel: ExportObfuscatedCopyViewModel =
hiltViewModelScoped<ExportObfuscatedCopyViewModelImpl, ExportObfuscatedCopyViewModel, ExportObfuscatedCopyArgs>(
ExportObfuscatedCopyArgs
),
) {

Column(modifier = modifier) {
FolderHeader("Danger Zone DO NOT TOUCH")
@SuppressLint("ComposeViewModelInjection")
if (BuildConfig.PRIVATE_BUILD) {
val backupAndRestoreStateHolder = rememberBackUpAndRestoreStateHolder()

SettingsItem(
text = "Create Obfuscated Database Copy",
onRowPressed = Clickable(enabled = true, onClick = backupAndRestoreStateHolder::showBackupDialog),
modifier = Modifier.background(Color.Red)
)
when (backupAndRestoreStateHolder.dialogState) {
BackupAndRestoreDialog.CreateBackup -> {
CreateObfuscatedCopyFlow(
backUpAndRestoreState = exportObfuscatedCopyViewModel.state,
backupPasswordTextState = exportObfuscatedCopyViewModel.createBackupPasswordState,
onCreateBackup = exportObfuscatedCopyViewModel::createObfuscatedCopy,
onSaveBackup = exportObfuscatedCopyViewModel::saveCopy,
onShareBackup = exportObfuscatedCopyViewModel::shareCopy,
onCancelCreateBackup = {
backupAndRestoreStateHolder.dismissDialog()
exportObfuscatedCopyViewModel.cancelBackupCreation()
},
onPermissionPermanentlyDenied = {}
)
}

BackupAndRestoreDialog.None -> {
/*no-op*/
}

BackupAndRestoreDialog.RestoreBackup -> TODO("Restore backup not implemented")
}
}
}
Expand Down Expand Up @@ -184,6 +243,6 @@ internal fun PreviewUserDebugContent() = WireTheme {
onManualMigrationPressed = {},
onLoggingEnabledChange = {},
onDeleteLogs = {},
onDatabaseLoggerEnabledChanged = {}
onDatabaseLoggerEnabledChanged = {},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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.debug

import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
import com.wire.android.di.ScopedArgs
import com.wire.android.di.ViewModelScopedPreview
import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl
import com.wire.android.feature.analytics.model.AnalyticsEvent
import com.wire.android.ui.home.settings.backup.BackupAndRestoreState
import com.wire.android.ui.home.settings.backup.BackupCreationProgress
import com.wire.android.util.FileManager
import com.wire.android.util.dispatchers.DefaultDispatcherProvider
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.feature.backup.CreateBackupResult
import com.wire.kalium.logic.feature.backup.CreateObfuscatedCopyUseCase
import com.wire.kalium.util.DelicateKaliumApi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import javax.inject.Inject

@ViewModelScopedPreview
interface ExportObfuscatedCopyViewModel {

val state: BackupAndRestoreState get() = BackupAndRestoreState.INITIAL_STATE

val createBackupPasswordState: TextFieldState
get() = TextFieldState()

fun createObfuscatedCopy() {}
fun shareCopy() {}
fun saveCopy(uri: Uri) {}
fun cancelBackupCreation() {}
}

@HiltViewModel
class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject constructor(
private val createUnencryptedCopy: CreateObfuscatedCopyUseCase,
private val dispatcher: DispatcherProvider = DefaultDispatcherProvider(),
private val fileManager: FileManager,
) : ViewModel(), ExportObfuscatedCopyViewModel {

override var state by mutableStateOf(BackupAndRestoreState.INITIAL_STATE)

override val createBackupPasswordState: TextFieldState = TextFieldState()

@VisibleForTesting
internal var latestCreatedBackup: BackupAndRestoreState.CreatedBackup? = null

@OptIn(DelicateKaliumApi::class)
override fun createObfuscatedCopy() {
viewModelScope.launch {

when (val result = createUnencryptedCopy(null)) {
is CreateBackupResult.Success -> {
state = state.copy(backupCreationProgress = BackupCreationProgress.Finished(result.backupFileName))
latestCreatedBackup = BackupAndRestoreState.CreatedBackup(
result.backupFilePath,
result.backupFileName,
result.backupFileSize,
false
)
}

is CreateBackupResult.Failure -> {
state = state.copy(backupCreationProgress = BackupCreationProgress.Failed)
appLogger.e("Failed to create backup: ${result.coreFailure}")
AnonymousAnalyticsManagerImpl.sendEvent(event = AnalyticsEvent.BackupExportFailed)
}
}
}
}

override fun shareCopy() {
viewModelScope.launch {
latestCreatedBackup?.let { backupData ->
withContext(dispatcher.io()) {
fileManager.shareWithExternalApp(backupData.path, backupData.assetName) {}
}
}
state = state.copy(
backupCreationProgress = BackupCreationProgress.InProgress(),
)
}
}

override fun saveCopy(uri: Uri) {
viewModelScope.launch {
latestCreatedBackup?.let { backupData ->
fileManager.copyToUri(backupData.path, uri, dispatcher)
}
state = state.copy(
backupCreationProgress = BackupCreationProgress.InProgress(),
)
}
}

override fun cancelBackupCreation() {
viewModelScope.launch(dispatcher.main()) {
createBackupPasswordState.clearText()
}
}
}

@Serializable
object ExportObfuscatedCopyArgs : ScopedArgs {
override val key = "ExportObfuscatedCopyArgsKey"
}
Loading

0 comments on commit c1ff13b

Please sign in to comment.