Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create and share obfuscated user database [WPB-14826] #3818

Merged
merged 8 commits into from
Jan 29, 2025
Merged
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
Loading