From 37434b6ae38acaec449130e77c51883ea3be3b81 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 19 Oct 2023 13:04:27 +0200 Subject: [PATCH 01/25] fix: app crash when user fails to unlock app with fingerprint --- .../android/biomitric/BiometricPromptUtils.kt | 13 ++-- .../com/wire/android/ui/WireActivity.kt | 65 ++++++++++--------- .../appLock/AppUnlockWithBiometricsScreen.kt | 2 +- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt b/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt index 3ea1dbce96e..871cd3717ec 100644 --- a/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt +++ b/app/src/main/kotlin/com/wire/android/biomitric/BiometricPromptUtils.kt @@ -43,7 +43,7 @@ object BiometricPromptUtils { override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) { super.onAuthenticationError(errorCode, errorString) appLogger.i("$TAG errorCode is $errorCode and errorString is: $errorString") - if (errorCode == ERROR_NEGATIVE_BUTTON) { + if (errorCode == ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_LOCKOUT) { onRequestPasscode() } else { onCancel() @@ -78,16 +78,15 @@ fun AppCompatActivity.showBiometricPrompt( onCancel: () -> Unit, onRequestPasscode: () -> Unit ) { - appLogger.i("$TAG showing biometrics dialog...") - val canAuthenticate = BiometricManager.from(this) .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { + appLogger.i("$TAG showing biometrics dialog...") val biometricPrompt = BiometricPromptUtils.createBiometricPrompt( - this, - onSuccess, - onCancel, - onRequestPasscode, + activity = this, + onSuccess = onSuccess, + onCancel = onCancel, + onRequestPasscode = onRequestPasscode ) val promptInfo = BiometricPromptUtils.createPromptInfo(this) biometricPrompt.authenticate(promptInfo) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 26fe01a0ab5..c67d6098d1a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -30,6 +30,7 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.SnackbarHostState @@ -40,7 +41,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf @@ -48,7 +48,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController import com.ramcosta.composedestinations.spec.Route @@ -92,12 +95,9 @@ import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.ui.updateScreenSettings import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.launch import javax.inject.Inject @@ -168,7 +168,6 @@ class WireActivity : AppCompatActivity() { Column(modifier = Modifier.statusBarsPadding()) { ReportDrawnWhen { isLoaded } val navigator = rememberNavigator(this@WireActivity::finish) - val scope = rememberCoroutineScope() CommonTopAppBar( connectivityUIState = commonTopAppBarViewModel.connectivityState, onReturnToCallClick = { establishedCall -> @@ -182,7 +181,7 @@ class WireActivity : AppCompatActivity() { // This setup needs to be done after the navigation graph is created, because building the graph takes some time, // and if any NavigationCommand is executed before the graph is fully built, it will cause a NullPointerException. - setUpNavigation(navigator.navController, onComplete, scope) + setUpNavigation(navigator.navController, onComplete) isLoaded = true handleScreenshotCensoring() handleAppLock() @@ -197,17 +196,20 @@ class WireActivity : AppCompatActivity() { private fun setUpNavigation( navController: NavHostController, onComplete: () -> Unit, - scope: CoroutineScope ) { val currentKeyboardController by rememberUpdatedState(LocalSoftwareKeyboardController.current) val currentNavController by rememberUpdatedState(navController) - LaunchedEffect(scope) { - navigationCommands - .onSubscription { onComplete() } - .onEach { command -> - currentKeyboardController?.hide() - currentNavController.navigateToItem(command) - }.launchIn(scope) + LaunchedEffect(Unit) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + navigationCommands + .onSubscription { onComplete() } + .collectLatest { + currentKeyboardController?.hide() + currentNavController.navigateToItem(it) + } + } + } } DisposableEffect(navController) { @@ -239,23 +241,26 @@ class WireActivity : AppCompatActivity() { @Composable private fun handleAppLock() { LaunchedEffect(Unit) { - lockCodeTimeManager.isLocked() - .filter { it } - .collectLatest { - val canAuthenticateWithBiometrics = BiometricManager - .from(this@WireActivity) - .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) - - if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { - navigationCommands.emit( - NavigationCommand(AppUnlockWithBiometricsScreenDestination) - ) - } else { - navigationCommands.emit( - NavigationCommand(EnterLockCodeScreenDestination) - ) + lifecycleScope.launch { + // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle + lockCodeTimeManager.isLocked() + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .filter { it } + .collectLatest { + val canAuthenticateWithBiometrics = BiometricManager + .from(this@WireActivity) + .canAuthenticate(BIOMETRIC_STRONG) + if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { + navigationCommands.emit( + NavigationCommand(AppUnlockWithBiometricsScreenDestination) + ) + } else { + navigationCommands.emit( + NavigationCommand(EnterLockCodeScreenDestination) + ) + } } - } + } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt index 8a18796e45a..6e598841d99 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt @@ -80,7 +80,7 @@ fun AppUnlockWithBiometricsScreen( navigator.navigate( NavigationCommand( EnterLockCodeScreenDestination(), - BackStackMode.CLEAR_WHOLE + BackStackMode.REMOVE_CURRENT ) ) } From 8f3e6ebb32b770b9d453910136f4b184698e03d6 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 23 Oct 2023 15:53:21 +0200 Subject: [PATCH 02/25] feat(appLock): adjust appLock to be handled correctly with other features --- app/src/main/AndroidManifest.xml | 8 +++ .../android/biomitric/BiometricPromptUtils.kt | 18 +++-- .../MessageNotificationManager.kt | 18 +++-- .../notification/NotificationActions.kt | 26 ++++--- .../com/wire/android/ui/AppLockActivity.kt | 62 ++++++++++++++++ .../com/wire/android/ui/WireActivity.kt | 44 +++++------- .../ui/common/scaffold/WireScaffold.kt | 28 ++++---- .../appLock/AppUnlockWithBiometricsScreen.kt | 71 ++++++++++++------- .../ui/home/appLock/EnterLockCodeScreen.kt | 26 +++++-- .../ui/home/appLock/LockCodeTimeManager.kt | 4 +- app/src/main/res/values/strings.xml | 1 + .../home/appLock/LockCodeTimeManagerTest.kt | 12 ++-- kalium | 2 +- 13 files changed, 218 insertions(+), 102 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 897d7058766..04b6bf8394f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -71,6 +71,14 @@ android:theme="@style/AppTheme.SplashScreen" tools:replace="android:allowBackup,android:supportsRtl"> + + Unit, onCancel: () -> Unit, - onRequestPasscode: () -> Unit + onRequestPasscode: () -> Unit, + onTooManyFailedAttempts: () -> Unit ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(activity) @@ -43,10 +45,10 @@ object BiometricPromptUtils { override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) { super.onAuthenticationError(errorCode, errorString) appLogger.i("$TAG errorCode is $errorCode and errorString is: $errorString") - if (errorCode == ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_LOCKOUT) { - onRequestPasscode() - } else { - onCancel() + when (errorCode) { + ERROR_NEGATIVE_BUTTON -> onRequestPasscode() + ERROR_LOCKOUT -> onTooManyFailedAttempts() + else -> onCancel() } } @@ -76,7 +78,8 @@ object BiometricPromptUtils { fun AppCompatActivity.showBiometricPrompt( onSuccess: () -> Unit, onCancel: () -> Unit, - onRequestPasscode: () -> Unit + onRequestPasscode: () -> Unit, + onTooManyFailedAttempts: () -> Unit ) { val canAuthenticate = BiometricManager.from(this) .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) @@ -86,7 +89,8 @@ fun AppCompatActivity.showBiometricPrompt( activity = this, onSuccess = onSuccess, onCancel = onCancel, - onRequestPasscode = onRequestPasscode + onRequestPasscode = onRequestPasscode, + onTooManyFailedAttempts = onTooManyFailedAttempts ) val promptInfo = BiometricPromptUtils.createPromptInfo(this) biometricPrompt.authenticate(promptInfo) diff --git a/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt index 923e7b58a36..aab9c2f55ce 100644 --- a/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt @@ -37,6 +37,7 @@ import androidx.core.text.toSpannable import com.wire.android.R import com.wire.android.appLogger import com.wire.android.notification.NotificationConstants.getConversationNotificationId +import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.android.util.toBitmap import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID @@ -51,7 +52,8 @@ class MessageNotificationManager @Inject constructor( private val context: Context, private val notificationManagerCompat: NotificationManagerCompat, - private val notificationManager: NotificationManager + private val notificationManager: NotificationManager, + private val lockCodeTimeManager: LockCodeTimeManager ) { fun handleNotification(newNotifications: List, userId: QualifiedID, userName: String) { @@ -201,8 +203,9 @@ class MessageNotificationManager } is NotificationMessage.Comment -> { + val isAppLocked = lockCodeTimeManager.isAppLocked() setContentIntent(messagePendingIntent(context, conversation.id, userIdString)) - addAction(getActionReply(context, conversation.id, userIdString)) + addAction(getActionReply(context, conversation.id, userIdString, isAppLocked)) } is NotificationMessage.Knock -> { @@ -211,13 +214,15 @@ class MessageNotificationManager } is NotificationMessage.Text -> { + val isAppLocked = lockCodeTimeManager.isAppLocked() setContentIntent(messagePendingIntent(context, conversation.id, userIdString)) - addAction(getActionReply(context, conversation.id, userIdString)) + addAction(getActionReply(context, conversation.id, userIdString, isAppLocked)) } is NotificationMessage.ObfuscatedMessage -> { + val isAppLocked = lockCodeTimeManager.isAppLocked() setContentIntent(messagePendingIntent(context, conversation.id, userIdString)) - addAction(getActionReply(context, conversation.id, userIdString)) + addAction(getActionReply(context, conversation.id, userIdString, isAppLocked)) } is NotificationMessage.ObfuscatedKnock -> { @@ -226,8 +231,9 @@ class MessageNotificationManager } null -> { + val isAppLocked = lockCodeTimeManager.isAppLocked() setContentIntent(messagePendingIntent(context, conversation.id, userIdString)) - addAction(getActionReply(context, conversation.id, userIdString)) + addAction(getActionReply(context, conversation.id, userIdString, isAppLocked)) } } } @@ -470,7 +476,7 @@ class MessageNotificationManager val notification = setUpNotificationBuilder(context, userId).apply { setContentIntent(messagePendingIntent(context, conversationId, userIdString)) - addAction(getActionReply(context, conversationId, userIdString)) + addAction(getActionReply(context, conversationId, userIdString, false)) setWhen(System.currentTimeMillis()) diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt index 01e30c3845f..d118304ad08 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt @@ -30,15 +30,25 @@ import com.wire.android.R fun getActionFromOldOne(oldAction: Notification.Action) = NotificationCompat.Action.Builder(null, oldAction.title, oldAction.actionIntent).build() -fun getActionReply(context: Context, conversationId: String, userId: String?): NotificationCompat.Action { - val resultPendingIntent = replyMessagePendingIntent(context, conversationId, userId) +fun getActionReply( + context: Context, + conversationId: String, + userId: String?, + isAppLocked: Boolean +): NotificationCompat.Action { + return if (isAppLocked) { + val resultPendingIntent = messagePendingIntent(context, conversationId, userId) + NotificationCompat.Action.Builder(null, context.getString(R.string.notification_action_reply), resultPendingIntent) + .build() + } else { + val resultPendingIntent = replyMessagePendingIntent(context, conversationId, userId) + val remoteInput = RemoteInput.Builder(NotificationConstants.KEY_TEXT_REPLY).build() - val remoteInput = RemoteInput.Builder(NotificationConstants.KEY_TEXT_REPLY).build() - - return NotificationCompat.Action.Builder(null, context.getString(R.string.notification_action_reply), resultPendingIntent) - .addRemoteInput(remoteInput) - .setAllowGeneratedReplies(true) - .build() + NotificationCompat.Action.Builder(null, context.getString(R.string.notification_action_reply), resultPendingIntent) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .build() + } } fun getOpenIncomingCallAction(context: Context, conversationId: String, userId: String) = getAction( diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt new file mode 100644 index 00000000000..50dbd4aa1ee --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -0,0 +1,62 @@ +/* + * Wire + * Copyright (C) 2023 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 + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.core.view.WindowCompat +import com.wire.android.appLogger +import com.wire.android.navigation.NavigationGraph +import com.wire.android.navigation.rememberNavigator +import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination +import com.wire.android.ui.destinations.EnterLockCodeScreenDestination +import com.wire.android.ui.theme.WireTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AppLockActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + WireTheme { + val canAuthenticateWithBiometrics = BiometricManager + .from(this) + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + + val navigator = rememberNavigator(this@AppLockActivity::finish) + + val startDestination = + if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { + appLogger.i("appLock: requesting app Unlock with biometrics") + AppUnlockWithBiometricsScreenDestination + } else { + appLogger.i("appLock: requesting app Unlock with passcode") + EnterLockCodeScreenDestination + } + + NavigationGraph( + navigator = navigator, + startDestination = startDestination + ) + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 0cf7940a54f..fbeeda6f134 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -29,8 +29,6 @@ import androidx.activity.compose.ReportDrawnWhen import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.SnackbarHostState @@ -70,9 +68,7 @@ import com.wire.android.ui.calling.ProximitySensorManager import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel -import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination import com.wire.android.ui.destinations.ConversationScreenDestination -import com.wire.android.ui.destinations.EnterLockCodeScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.IncomingCallScreenDestination @@ -98,7 +94,7 @@ import com.wire.android.util.ui.updateScreenSettings import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.launch import javax.inject.Inject @@ -186,7 +182,6 @@ class WireActivity : AppCompatActivity() { setUpNavigation(navigator.navController, onComplete) isLoaded = true handleScreenshotCensoring() - handleAppLock(navigator::navigate) handleDialogs(navigator::navigate) } } @@ -240,28 +235,6 @@ class WireActivity : AppCompatActivity() { } } - @Composable - private fun handleAppLock(navigate: (NavigationCommand) -> Unit) { - LaunchedEffect(Unit) { - lifecycleScope.launch { - // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle - lockCodeTimeManager.isLocked() - .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) - .filter { it } - .collectLatest { - val canAuthenticateWithBiometrics = BiometricManager - .from(this@WireActivity) - .canAuthenticate(BIOMETRIC_STRONG) - if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { - navigate(NavigationCommand(AppUnlockWithBiometricsScreenDestination, BackStackMode.UPDATE_EXISTED)) - } else { - navigate(NavigationCommand(EnterLockCodeScreenDestination, BackStackMode.UPDATE_EXISTED)) - } - } - } - } - } - @Composable private fun handleDialogs(navigate: (NavigationCommand) -> Unit) { featureFlagNotificationViewModel.loadInitialSync() @@ -345,6 +318,21 @@ class WireActivity : AppCompatActivity() { override fun onResume() { super.onResume() + + lifecycleScope.launch { + lockCodeTimeManager.observeAppLock() + // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .first().let { + if (it) { + startActivity( + Intent(this@WireActivity, AppLockActivity::class.java) + ) + } + } + + } + proximitySensorManager.registerListener() } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt b/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt index 5577006d118..51726c348c0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/scaffold/WireScaffold.kt @@ -54,6 +54,7 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState @Composable fun WireScaffold( modifier: Modifier = Modifier, + snackbarHost: @Composable () -> Unit = { WireScaffoldSnackbarHost() }, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, @@ -68,18 +69,7 @@ fun WireScaffold( .systemBarsPadding(), topBar = topBar, bottomBar = bottomBar, - snackbarHost = { - SnackbarHost( - hostState = LocalSnackbarHostState.current, - snackbar = { data -> - SwipeableSnackbar( - hostState = LocalSnackbarHostState.current, - data = data, - onDismiss = { data.dismiss() } - ) - } - ) - }, + snackbarHost = snackbarHost, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, containerColor = containerColor, @@ -88,3 +78,17 @@ fun WireScaffold( content = content ) } + +@Composable +private fun WireScaffoldSnackbarHost() { + SnackbarHost( + hostState = LocalSnackbarHostState.current, + snackbar = { data -> + SwipeableSnackbar( + hostState = LocalSnackbarHostState.current, + data = data, + onDismiss = { data.dismiss() } + ) + } + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt index 6e598841d99..b17f05c2d8a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/AppUnlockWithBiometricsScreen.kt @@ -17,7 +17,7 @@ */ package com.wire.android.ui.home.appLock -import androidx.activity.compose.BackHandler +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -37,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R +import com.wire.android.appLogger import com.wire.android.biomitric.showBiometricPrompt import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -50,8 +51,50 @@ import com.wire.android.ui.destinations.EnterLockCodeScreenDestination @Composable fun AppUnlockWithBiometricsScreen( appUnlockWithBiometricsViewModel: AppUnlockWithBiometricsViewModel = hiltViewModel(), - navigator: Navigator, + navigator: Navigator ) { + AppUnLockBackground() + + val context = LocalContext.current + val tooManyAttemptsMessage = stringResource( + id = R.string.biometrics_app_unlock_too_many_attempts + ) + + LaunchedEffect(Unit) { + (context as AppCompatActivity).showBiometricPrompt( + onSuccess = { + appLogger.i("appLock: app Unlocked with biometrics") + appUnlockWithBiometricsViewModel.onAppUnlocked() + navigator.navigateBack() + }, + onCancel = { + appLogger.i("appLock: biometrics unlock canceled") + context.finishAffinity() + }, + onTooManyFailedAttempts = { + Toast.makeText(context, tooManyAttemptsMessage, Toast.LENGTH_SHORT).show() + navigator.navigate( + NavigationCommand( + destination = EnterLockCodeScreenDestination, + backStackMode = BackStackMode.REMOVE_CURRENT + ) + ) + }, + onRequestPasscode = { + appLogger.i("appLock: requesting passcode from biometrics unlock") + navigator.navigate( + NavigationCommand( + destination = EnterLockCodeScreenDestination, + backStackMode = BackStackMode.NONE + ) + ) + } + ) + } +} + +@Composable +private fun AppUnLockBackground() { Box( modifier = Modifier .fillMaxSize() @@ -65,29 +108,5 @@ fun AppUnlockWithBiometricsScreen( tint = MaterialTheme.colorScheme.onBackground, contentDescription = stringResource(id = R.string.content_description_welcome_wire_logo) ) - - val activity = LocalContext.current - LaunchedEffect(Unit) { - (activity as AppCompatActivity).showBiometricPrompt( - onSuccess = { - appUnlockWithBiometricsViewModel.onAppUnlocked() - navigator.navigateBack() - }, - onCancel = { - navigator.finish() - }, - onRequestPasscode = { - navigator.navigate( - NavigationCommand( - EnterLockCodeScreenDestination(), - BackStackMode.REMOVE_CURRENT - ) - ) - } - ) - } - } - BackHandler { - navigator.finish() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeScreen.kt index 0307c960c4d..1f0c4338314 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockCodeScreen.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.appLock import androidx.activity.compose.BackHandler +import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.ScrollState import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -39,6 +40,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -49,6 +51,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.utils.destination import com.wire.android.R import com.wire.android.navigation.Navigator import com.wire.android.navigation.rememberNavigator @@ -58,6 +61,7 @@ import com.wire.android.ui.common.rememberBottomBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.textfield.WirePasswordTextField import com.wire.android.ui.common.textfield.WireTextFieldState +import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions @@ -77,8 +81,7 @@ fun EnterLockCodeScreen( state = viewModel.state, scrollState = rememberScrollState(), onPasswordChanged = viewModel::onPasswordChanged, - onContinue = viewModel::onContinue, - onBackPress = { navigator.finish() } + onContinue = viewModel::onContinue ) } @@ -89,19 +92,26 @@ fun EnterLockCodeScreenContent( state: EnterLockCodeViewState, scrollState: ScrollState, onPasswordChanged: (TextFieldValue) -> Unit, - onBackPress: () -> Unit, onContinue: () -> Unit ) { + val context = LocalContext.current LaunchedEffect(state.done) { if (state.done) { navigator.navigateBack() } } + BackHandler { - onBackPress() + if (navigator.navController.previousBackStackEntry?.destination() is AppUnlockWithBiometricsScreenDestination) { + navigator.navigateBack() + } else { + (context as AppCompatActivity).finishAffinity() + } } - WireScaffold { internalPadding -> + WireScaffold( + snackbarHost = {} + ) { internalPadding -> Column( modifier = Modifier .fillMaxSize() @@ -145,11 +155,14 @@ fun EnterLockCodeScreenContent( EnterLockCodeError.InvalidValue -> WireTextFieldState.Error( errorText = stringResource(R.string.settings_enter_lock_screen_wrong_passcode_label) ) + EnterLockCodeError.None -> WireTextFieldState.Default }, autofill = false, placeholderText = stringResource(R.string.settings_set_lock_screen_passcode_label), - labelText = stringResource(R.string.settings_set_lock_screen_passcode_label).uppercase(Locale.getDefault()) + labelText = stringResource(R.string.settings_set_lock_screen_passcode_label).uppercase( + Locale.getDefault() + ) ) Spacer(modifier = Modifier.weight(1f)) } @@ -202,7 +215,6 @@ fun PreviewEnterLockCodeScreen() { state = EnterLockCodeViewState(), scrollState = rememberScrollState(), onPasswordChanged = {}, - onBackPress = {}, onContinue = {} ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt index bd2ef0e668a..07b2590980b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt @@ -94,7 +94,9 @@ class LockCodeTimeManager @Inject constructor( isLockedFlow.value = false } - fun isLocked(): Flow = isLockedFlow + fun isAppLocked() : Boolean = isLockedFlow.value + + fun observeAppLock(): Flow = isLockedFlow companion object { private const val TAG = "LockCodeTimeManager" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f8bc31af4e..c1c573ff1cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1229,4 +1229,5 @@ Authenticate with biometrics To unlock Wire Use passcode + Too many attempts on Biometrics. Unlock with passcode instead. diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt index b9077898866..716627ec28f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt @@ -52,7 +52,7 @@ class LockCodeTimeManagerTest { arrangement.withIsAppVisible(true) advanceUntilIdle() // then - assertEquals(expected, manager.isLocked().first()) + assertEquals(expected, manager.observeAppLock().first()) } @Test @@ -75,7 +75,7 @@ class LockCodeTimeManagerTest { arrangement.withIsAppVisible(false) advanceTimeBy(delayAfterStop) // then - assertEquals(expected, manager.isLocked().first()) + assertEquals(expected, manager.observeAppLock().first()) } @Test @@ -104,7 +104,7 @@ class LockCodeTimeManagerTest { manager.appUnlocked() advanceUntilIdle() // when-then - manager.isLocked().test { + manager.observeAppLock().test { arrangement.withIsAppVisible(false) assertEquals(false, awaitItem()) assertEquals(true, awaitItem()) @@ -124,7 +124,7 @@ class LockCodeTimeManagerTest { advanceTimeBy(startDelay) arrangement.withIsAppVisible(true) // then - assertEquals(expected, manager.isLocked().first()) + assertEquals(expected, manager.observeAppLock().first()) } @Test @@ -155,7 +155,7 @@ class LockCodeTimeManagerTest { advanceTimeBy(AppLockConfig.Enabled.timeoutInMillis() - 100L) arrangement.withIsAppVisible(true) // then - assertEquals(true, manager.isLocked().first()) + assertEquals(true, manager.observeAppLock().first()) } @Test @@ -170,7 +170,7 @@ class LockCodeTimeManagerTest { manager.appUnlocked() advanceUntilIdle() // then - assertEquals(false, manager.isLocked().first()) + assertEquals(false, manager.observeAppLock().first()) } class Arrangement(dispatcher: TestDispatcher) { diff --git a/kalium b/kalium index c82d8aa7757..3b1d9b2b29e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c82d8aa7757ddb2b22c6ded6120daed6e2c35a2a +Subproject commit 3b1d9b2b29e9a84a2a61ef2e4d537d14b02743f7 From 198aa81e982706234ec2d435194a0f7e10ee3234 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 23 Oct 2023 16:27:49 +0200 Subject: [PATCH 03/25] chore: detekt --- app/src/main/kotlin/com/wire/android/ui/WireActivity.kt | 1 - .../com/wire/android/ui/home/appLock/LockCodeTimeManager.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index fbeeda6f134..3fc8e0b5357 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -330,7 +330,6 @@ class WireActivity : AppCompatActivity() { ) } } - } proximitySensorManager.registerListener() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt index 07b2590980b..2a0d6bc9052 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt @@ -94,7 +94,7 @@ class LockCodeTimeManager @Inject constructor( isLockedFlow.value = false } - fun isAppLocked() : Boolean = isLockedFlow.value + fun isAppLocked(): Boolean = isLockedFlow.value fun observeAppLock(): Flow = isLockedFlow From 8fd6940eeb954f4647f7604fef8f5afc8882d77a Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 23 Oct 2023 16:28:15 +0200 Subject: [PATCH 04/25] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 3b1d9b2b29e..97ad1de4028 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3b1d9b2b29e9a84a2a61ef2e4d537d14b02743f7 +Subproject commit 97ad1de40282564091820a92523684f9ae0c986a From a1958dd785dab9b06e9e5ed82b2fb3f14c5feef0 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 27 Oct 2023 16:04:05 +0200 Subject: [PATCH 05/25] feat: observe team app lock config --- .../com/wire/android/di/CoreLogicModule.kt | 8 + .../feature/ObserveAppLockConfigUseCase.kt | 50 ++++-- .../ui/home/appLock/SetLockCodeViewState.kt | 4 +- .../ui/home/settings/SettingsScreen.kt | 17 +- .../ObserveAppLockConfigUseCaseTest.kt | 151 +++++++++++++++-- .../home/appLock/LockCodeTimeManagerTest.kt | 160 +++++++++++------- kalium | 2 +- 7 files changed, 297 insertions(+), 95 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index b67c5ad429b..2d37f9b041c 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -391,6 +391,14 @@ class UseCaseModule { fun provideMarkGuestLinkFeatureFlagAsNotChangedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = coreLogic.getSessionScope(currentAccount).markGuestLinkFeatureFlagAsNotChanged +// @ViewModelScoped +// @Provides +// fun provideAppLockTeamFeatureConfigObserver( +// @KaliumCoreLogic coreLogic: CoreLogic, +// @CurrentAccount currentAccount: UserId +// ): AppLockTeamFeatureConfigObserver = +// coreLogic.getSessionScope(currentAccount).appLockTeamFeatureConfigObserver + @ViewModelScoped @Provides fun provideGetOtherUserSecurityClassificationLabelUseCase( diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index e35117e638b..bb90fe4e7a7 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -18,33 +18,59 @@ package com.wire.android.feature import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl.Companion.DEFAULT_TIMEOUT +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combineTransform import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds @Singleton class ObserveAppLockConfigUseCase @Inject constructor( private val globalDataStore: GlobalDataStore, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val currentSession: CurrentSessionUseCase ) { - operator fun invoke(): Flow = - globalDataStore.isAppLockPasscodeSetFlow().map { // TODO: include checking if any logged account does not enforce app-lock - when { - it -> AppLockConfig.Enabled - else -> AppLockConfig.Disabled + operator fun invoke(): Flow = channelFlow { + when (val currentSession = currentSession()) { + is CurrentSessionResult.Failure -> { + send(AppLockConfig.Disabled) + } + + is CurrentSessionResult.Success -> { + val userId = currentSession.accountInfo.userId + val appLockTeamFeatureConfigFlow = + coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver + + appLockTeamFeatureConfigFlow().combineTransform( + globalDataStore.isAppLockPasscodeSetFlow() + ) { isAppLockedByTeam, isAppLocked -> + if (isAppLockedByTeam.isEnabled) { + emit(AppLockConfig.EnforcedByTeam(isAppLockedByTeam.timeout)) + } else { + if (isAppLocked) { + emit(AppLockConfig.Enabled(isAppLockedByTeam.timeout)) + } else { + emit(AppLockConfig.Disabled) + } + } + }.collectLatest { + send(it) + } } } + } } sealed class AppLockConfig(open val timeout: Duration = DEFAULT_TIMEOUT) { data object Disabled : AppLockConfig() - data object Enabled : AppLockConfig() + data class Enabled(override val timeout: Duration) : AppLockConfig(timeout) data class EnforcedByTeam(override val timeout: Duration) : AppLockConfig(timeout) - - companion object { - val DEFAULT_TIMEOUT = 60.seconds - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt index 267d748c155..dfcf8ddfc3e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt @@ -18,7 +18,7 @@ package com.wire.android.ui.home.appLock import androidx.compose.ui.text.input.TextFieldValue -import com.wire.android.feature.AppLockConfig +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl.Companion.DEFAULT_TIMEOUT import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import kotlin.time.Duration @@ -26,6 +26,6 @@ data class SetLockCodeViewState( val continueEnabled: Boolean = false, val password: TextFieldValue = TextFieldValue(), val passwordValidation: ValidatePasswordResult = ValidatePasswordResult.Invalid(), - val timeout: Duration = AppLockConfig.DEFAULT_TIMEOUT, + val timeout: Duration = DEFAULT_TIMEOUT, val done: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt index 436c2263b37..120d12a1cad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt @@ -113,9 +113,20 @@ fun SettingsScreenContent( add(SettingsItem.NetworkSettings) add(SettingsItem.AppLock( when (settingsState.appLockConfig) { - AppLockConfig.Disabled -> SwitchState.Enabled(false, true, onAppLockSwitchChanged) - AppLockConfig.Enabled -> SwitchState.Enabled(true, true) { turnAppLockOffDialogState.show(Unit) } - is AppLockConfig.EnforcedByTeam -> SwitchState.TextOnly(true) + is AppLockConfig.Disabled -> SwitchState.Enabled( + value = false, + isOnOffVisible = true, + onCheckedChange = onAppLockSwitchChanged + ) + is AppLockConfig.Enabled -> SwitchState.Enabled( + value = true, + isOnOffVisible = true + ) { + turnAppLockOffDialogState.show(Unit) + } + is AppLockConfig.EnforcedByTeam -> { + SwitchState.TextOnly(true) + } } )) }, diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index 33143b9d01f..d86944ee877 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -17,44 +17,126 @@ */ package com.wire.android.feature +import app.cash.turbine.test import com.wire.android.datastore.GlobalDataStore +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.UserSessionScope +import com.wire.kalium.logic.feature.applock.AppLockTeamConfig +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver +import com.wire.kalium.logic.feature.auth.AccountInfo +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.seconds class ObserveAppLockConfigUseCaseTest { - // TODO: include checking if any logged account does not enforce app-lock @Test - fun givenPasscodeIsSet_whenObservingAppLockConfig_thenReturnEnabled() = runTest { + fun givenNoValidSession_whenObservingAppLock_thenSendDisabledStatus() = runTest { val (_, useCase) = Arrangement() - .withAppLockPasscodeSet(true) + .withNonValidSession() .arrange() - val result = useCase.invoke().firstOrNull() + val result = useCase.invoke() - assert(result is AppLockConfig.Enabled) + result.test { + val appLockStatus = awaitItem() + + assertEquals(AppLockConfig.Disabled, appLockStatus) + awaitComplete() + } } @Test - fun givenPasscodeIsNotSet_whenObservingAppLockConfig_thenReturnDisabled() = runTest { - val (_, useCase) = Arrangement() - .withAppLockPasscodeSet(false) - .arrange() + fun givenValidSessionAndAppLockedByTeam_whenObservingAppLock_thenSendEnforcedByTeamStatus() = + runTest { + val (_, useCase) = Arrangement() + .withValidSession() + .withTeamAppLockEnabled() + .withAppLockedByCurrentUser() + .arrange() - val result = useCase.invoke().firstOrNull() + val result = useCase.invoke() + + result.test { + val appLockStatus = awaitItem() + + assertEquals(AppLockConfig.EnforcedByTeam(timeout), appLockStatus) + awaitComplete() + } + } + + @Test + fun givenValidSessionAndAppLockedByUserOnly_whenObservingAppLock_thenSendEnabledStatus() = + runTest { + val (_, useCase) = Arrangement() + .withValidSession() + .withTeamAppLockDisabled() + .withAppLockedByCurrentUser() + .arrange() + + val result = useCase.invoke() + + result.test { + val appLockStatus = awaitItem() + + assertEquals(AppLockConfig.Enabled(timeout), appLockStatus) + awaitComplete() + } + } + + @Test + fun givenValidSessionAndAppNotLockedByUserNorTeam_whenObservingAppLock_thenSendDisabledStatus() = + runTest { + val (_, useCase) = Arrangement() + .withValidSession() + .withTeamAppLockDisabled() + .withAppNonLockedByCurrentUser() + .arrange() + + val result = useCase.invoke() + + result.test { + val appLockStatus = awaitItem() + + assertEquals(AppLockConfig.Disabled, appLockStatus) + awaitComplete() + } + } - assert(result is AppLockConfig.Disabled) - } inner class Arrangement { + @MockK lateinit var globalDataStore: GlobalDataStore - val useCase by lazy { ObserveAppLockConfigUseCase(globalDataStore) } + + @MockK + lateinit var currentSession: CurrentSessionUseCase + + @MockK + lateinit var coreLogic: CoreLogic + + @MockK + lateinit var userSessionScope: UserSessionScope + + @MockK + lateinit var appLockTeamFeatureConfigObserver: AppLockTeamFeatureConfigObserver + + val useCase by lazy { + ObserveAppLockConfigUseCase( + globalDataStore = globalDataStore, + coreLogic = coreLogic, + currentSession = currentSession + ) + } init { MockKAnnotations.init(this, relaxUnitFun = true) @@ -65,5 +147,46 @@ class ObserveAppLockConfigUseCaseTest { } fun arrange() = this to useCase + + fun withNonValidSession() = apply { + coEvery { currentSession() } returns CurrentSessionResult.Failure.SessionNotFound + } + + fun withValidSession() = apply { + coEvery { currentSession() } returns CurrentSessionResult.Success(accountInfo) + } + + fun withTeamAppLockEnabled() = apply { + every { coreLogic.getSessionScope(any()) } returns userSessionScope + every { + userSessionScope.appLockTeamFeatureConfigObserver + } returns appLockTeamFeatureConfigObserver + every { + appLockTeamFeatureConfigObserver.invoke() + } returns flowOf(AppLockTeamConfig(true, timeout)) + } + + fun withTeamAppLockDisabled() = apply { + every { coreLogic.getSessionScope(any()) } returns userSessionScope + every { + userSessionScope.appLockTeamFeatureConfigObserver + } returns appLockTeamFeatureConfigObserver + every { + appLockTeamFeatureConfigObserver.invoke() + } returns flowOf(AppLockTeamConfig(false, timeout)) + } + + fun withAppLockedByCurrentUser() = apply { + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(true) + } + + fun withAppNonLockedByCurrentUser() = apply { + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(false) + } + } + + companion object { + private val accountInfo = AccountInfo.Valid(UserId("userId", "domain")) + private val timeout = 60.seconds } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt index b9077898866..c8b04581264 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt @@ -21,6 +21,7 @@ import app.cash.turbine.test import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.util.CurrentScreenManager +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl.Companion.DEFAULT_TIMEOUT import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -34,6 +35,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.seconds class LockCodeTimeManagerTest { @@ -41,50 +43,60 @@ class LockCodeTimeManagerTest { private fun AppLockConfig.timeoutInMillis(): Long = this.timeout.inWholeMilliseconds - private fun testInitialStart(appLockConfig: AppLockConfig, expected: Boolean) = runTest(dispatcher) { - // given - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(appLockConfig) - .withIsAppVisible(false) - .arrange() - advanceUntilIdle() - // when - arrangement.withIsAppVisible(true) - advanceUntilIdle() - // then - assertEquals(expected, manager.isLocked().first()) - } + private fun testInitialStart(appLockConfig: AppLockConfig, expected: Boolean) = + runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withIsAppVisible(false) + .arrange() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(true) + advanceUntilIdle() + // then + assertEquals(expected, manager.isLocked().first()) + } @Test fun givenLockEnabled_whenAppInitiallyOpened_thenLocked() = - testInitialStart(AppLockConfig.Enabled, true) + testInitialStart(AppLockConfig.Enabled(DEFAULT_TIMEOUT), true) @Test fun givenLockDisabled_whenAppInitiallyOpened_thenNotLocked() = testInitialStart(AppLockConfig.Disabled, false) - private fun testStop(appLockConfig: AppLockConfig, delayAfterStop: Long, expected: Boolean) = runTest(dispatcher) { - // given - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(appLockConfig) - .withIsAppVisible(true) - .arrange() - manager.appUnlocked() - advanceUntilIdle() - // when - arrangement.withIsAppVisible(false) - advanceTimeBy(delayAfterStop) - // then - assertEquals(expected, manager.isLocked().first()) - } + private fun testStop(appLockConfig: AppLockConfig, delayAfterStop: Long, expected: Boolean) = + runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withIsAppVisible(true) + .arrange() + manager.appUnlocked() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(false) + advanceTimeBy(delayAfterStop) + // then + assertEquals(expected, manager.isLocked().first()) + } @Test fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndWaitedMoreThanTimeout_thenLocked() = - testStop(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() + 100L, true) + testStop( + AppLockConfig.Enabled(DEFAULT_TIMEOUT), + AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() + 100L, + true + ) @Test fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndWaitedLessThanTimeout_thenNotLocked() = - testStop(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() - 100L, false) + testStop( + AppLockConfig.Enabled(DEFAULT_TIMEOUT), + AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L, + false + ) @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndWaitedMoreThanTimeout_thenNotLocked() = @@ -95,23 +107,28 @@ class LockCodeTimeManagerTest { testStop(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) @Test - fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAnd_thenAfterTimeoutShouldChangeFromNotLockedToLocked() = runTest(dispatcher) { - // given - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(AppLockConfig.Enabled) - .withIsAppVisible(true) - .arrange() - manager.appUnlocked() - advanceUntilIdle() - // when-then - manager.isLocked().test { - arrangement.withIsAppVisible(false) - assertEquals(false, awaitItem()) - assertEquals(true, awaitItem()) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAnd_thenAfterTimeoutShouldChangeFromNotLockedToLocked() = + runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.Enabled(timeout = 1000.seconds)) + .withIsAppVisible(true) + .arrange() + manager.appUnlocked() + advanceUntilIdle() + // when-then + manager.isLocked().test { + arrangement.withIsAppVisible(false) + assertEquals(false, awaitItem()) + assertEquals(true, awaitItem()) + } } - } - private fun testStopAndStart(appLockConfig: AppLockConfig, startDelay: Long, expected: Boolean) = runTest(dispatcher) { + private fun testStopAndStart( + appLockConfig: AppLockConfig, + startDelay: Long, + expected: Boolean + ) = runTest(dispatcher) { // given val (arrangement, manager) = Arrangement(dispatcher) .withAppLockConfig(appLockConfig) @@ -129,40 +146,57 @@ class LockCodeTimeManagerTest { @Test fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenNotLocked() = - testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() - 100L, false) + testStopAndStart( + AppLockConfig.Enabled(DEFAULT_TIMEOUT), + AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L, + false + ) @Test fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainAfterLockTimeout_thenLocked() = - testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() + 100L, true) + testStopAndStart( + AppLockConfig.Enabled(DEFAULT_TIMEOUT), + AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() + 100L, + true + ) @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenNotLocked() = - testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) + testStopAndStart( + AppLockConfig.Disabled, + AppLockConfig.Disabled.timeoutInMillis() - 100L, + false + ) @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainAfterLockTimeout_thenNotLocked() = - testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() + 100L, false) + testStopAndStart( + AppLockConfig.Disabled, + AppLockConfig.Disabled.timeoutInMillis() + 100L, + false + ) @Test - fun givenLockEnabledAndAppOpenedLocked_whenAppClosedAndOpenedBeforeLockTimeout_thenShouldStillBeLocked() = runTest(dispatcher) { - // given - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(AppLockConfig.Enabled) - .withIsAppVisible(false) - .arrange() - advanceUntilIdle() - // when - advanceTimeBy(AppLockConfig.Enabled.timeoutInMillis() - 100L) - arrangement.withIsAppVisible(true) - // then - assertEquals(true, manager.isLocked().first()) - } + fun givenLockEnabledAndAppOpenedLocked_whenAppClosedAndOpenedBeforeLockTimeout_thenShouldStillBeLocked() = + runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.Enabled(DEFAULT_TIMEOUT)) + .withIsAppVisible(false) + .arrange() + advanceUntilIdle() + // when + advanceTimeBy(AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L) + arrangement.withIsAppVisible(true) + // then + assertEquals(true, manager.isLocked().first()) + } @Test fun givenLockEnabledAndAppOpenedLocked_whenAppIsUnlocked_thenNotLocked() = runTest(dispatcher) { // given val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(AppLockConfig.Enabled) + .withAppLockConfig(AppLockConfig.Enabled(DEFAULT_TIMEOUT)) .withIsAppVisible(true) .arrange() advanceUntilIdle() diff --git a/kalium b/kalium index 290418d9b92..f148d31d9b9 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 290418d9b924aae4480617720fb678857cb77bf7 +Subproject commit f148d31d9b9ddc937324a9661136722274847a36 From 8c1e0a3f6afa9cddf16d1d499414197545fcb5df Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 27 Oct 2023 16:14:55 +0200 Subject: [PATCH 06/25] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 7156bddfda2..b8e837903ae 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 7156bddfda2a47c1743777d5e270ad47049d97d1 +Subproject commit b8e837903ae6d0f275f101aa41486e15d2a7a330 From 9c1e8237c72d6997575054d9c345ee4390dbb601 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 27 Oct 2023 16:38:42 +0200 Subject: [PATCH 07/25] chore: pass timeout to app lock disabled status --- .../feature/ObserveAppLockConfigUseCase.kt | 6 ++--- .../android/ui/home/settings/SettingsState.kt | 3 ++- .../ObserveAppLockConfigUseCaseTest.kt | 4 +-- .../home/appLock/LockCodeTimeManagerTest.kt | 25 +++++++++++++------ .../appLock/SetLockScreenViewModelTest.kt | 5 +++- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index bb90fe4e7a7..86962956f28 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -41,7 +41,7 @@ class ObserveAppLockConfigUseCase @Inject constructor( operator fun invoke(): Flow = channelFlow { when (val currentSession = currentSession()) { is CurrentSessionResult.Failure -> { - send(AppLockConfig.Disabled) + send(AppLockConfig.Disabled(DEFAULT_TIMEOUT)) } is CurrentSessionResult.Success -> { @@ -58,7 +58,7 @@ class ObserveAppLockConfigUseCase @Inject constructor( if (isAppLocked) { emit(AppLockConfig.Enabled(isAppLockedByTeam.timeout)) } else { - emit(AppLockConfig.Disabled) + emit(AppLockConfig.Disabled(isAppLockedByTeam.timeout)) } } }.collectLatest { @@ -70,7 +70,7 @@ class ObserveAppLockConfigUseCase @Inject constructor( } sealed class AppLockConfig(open val timeout: Duration = DEFAULT_TIMEOUT) { - data object Disabled : AppLockConfig() + data class Disabled(override val timeout: Duration) : AppLockConfig(timeout) data class Enabled(override val timeout: Duration) : AppLockConfig(timeout) data class EnforcedByTeam(override val timeout: Duration) : AppLockConfig(timeout) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsState.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsState.kt index 9a65a671a8d..8f958209621 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsState.kt @@ -18,7 +18,8 @@ package com.wire.android.ui.home.settings import com.wire.android.feature.AppLockConfig +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl.Companion.DEFAULT_TIMEOUT data class SettingsState( - val appLockConfig: AppLockConfig = AppLockConfig.Disabled, + val appLockConfig: AppLockConfig = AppLockConfig.Disabled(DEFAULT_TIMEOUT), ) diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index d86944ee877..dda358240d1 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -50,7 +50,7 @@ class ObserveAppLockConfigUseCaseTest { result.test { val appLockStatus = awaitItem() - assertEquals(AppLockConfig.Disabled, appLockStatus) + assertEquals(AppLockConfig.Disabled(timeout), appLockStatus) awaitComplete() } } @@ -107,7 +107,7 @@ class ObserveAppLockConfigUseCaseTest { result.test { val appLockStatus = awaitItem() - assertEquals(AppLockConfig.Disabled, appLockStatus) + assertEquals(AppLockConfig.Disabled(timeout), appLockStatus) awaitComplete() } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt index c8b04581264..305cf97f401 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt @@ -64,7 +64,7 @@ class LockCodeTimeManagerTest { @Test fun givenLockDisabled_whenAppInitiallyOpened_thenNotLocked() = - testInitialStart(AppLockConfig.Disabled, false) + testInitialStart(AppLockConfig.Disabled(DEFAULT_TIMEOUT), false) private fun testStop(appLockConfig: AppLockConfig, delayAfterStop: Long, expected: Boolean) = runTest(dispatcher) { @@ -100,11 +100,19 @@ class LockCodeTimeManagerTest { @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndWaitedMoreThanTimeout_thenNotLocked() = - testStop(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() + 100L, false) + testStop( + AppLockConfig.Disabled(DEFAULT_TIMEOUT), + AppLockConfig.Disabled(DEFAULT_TIMEOUT).timeoutInMillis() + 100L, + false + ) @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndWaitedLessThanTimeout_thenNotLocked() = - testStop(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) + testStop( + AppLockConfig.Disabled(DEFAULT_TIMEOUT), + AppLockConfig.Disabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L, + false + ) @Test fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAnd_thenAfterTimeoutShouldChangeFromNotLockedToLocked() = @@ -163,16 +171,16 @@ class LockCodeTimeManagerTest { @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenNotLocked() = testStopAndStart( - AppLockConfig.Disabled, - AppLockConfig.Disabled.timeoutInMillis() - 100L, + AppLockConfig.Disabled(DEFAULT_TIMEOUT), + AppLockConfig.Disabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L, false ) @Test fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainAfterLockTimeout_thenNotLocked() = testStopAndStart( - AppLockConfig.Disabled, - AppLockConfig.Disabled.timeoutInMillis() + 100L, + AppLockConfig.Disabled(DEFAULT_TIMEOUT), + AppLockConfig.Disabled(DEFAULT_TIMEOUT).timeoutInMillis() + 100L, false ) @@ -224,7 +232,8 @@ class LockCodeTimeManagerTest { } private val isAppVisibleStateFlow = MutableStateFlow(false) - private val appLockConfigStateFlow = MutableStateFlow(AppLockConfig.Disabled) + private val appLockConfigStateFlow = + MutableStateFlow(AppLockConfig.Disabled(DEFAULT_TIMEOUT)) fun arrange() = this to lockCodeTimeManager diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt index adadd656aa9..227cca15faf 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt @@ -23,6 +23,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import io.mockk.MockKAnnotations @@ -76,7 +77,9 @@ class SetLockScreenViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { globalDataStore.setAppLockPasscode(any()) } returns Unit - coEvery { observeAppLockConfigUseCase() } returns flowOf(AppLockConfig.Disabled) + coEvery { observeAppLockConfigUseCase() } returns flowOf( + AppLockConfig.Disabled(AppLockTeamFeatureConfigObserverImpl.DEFAULT_TIMEOUT) + ) } fun withValidPassword() = apply { From 640de6daa1d2e3ffd6e78cc09dda48841fc5610e Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 27 Oct 2023 16:45:20 +0200 Subject: [PATCH 08/25] chore: detekt :) --- .../com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index dda358240d1..a436d4d6c40 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -112,7 +112,6 @@ class ObserveAppLockConfigUseCaseTest { } } - inner class Arrangement { @MockK From 9012111f4cc58f81a99133461d9fa7868816d788 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 31 Oct 2023 14:10:03 +0100 Subject: [PATCH 09/25] feat: show team app lock change dialog --- .../applock/passcode/PasscodeHelper.kt | 78 +++++++++++++++++++ .../wire/android/datastore/GlobalDataStore.kt | 44 +---------- .../com/wire/android/di/CoreLogicModule.kt | 11 +-- .../wire/android/di/KaliumConfigsModule.kt | 4 +- .../feature/ObserveAppLockConfigUseCase.kt | 3 +- .../com/wire/android/ui/WireActivity.kt | 10 +++ .../wire/android/ui/WireActivityDialogs.kt | 21 +++++ .../wire/android/ui/home/FeatureFlagState.kt | 2 + .../home/appLock/EnterLockScreenViewModel.kt | 1 + .../ui/home/appLock/SetLockCodeViewState.kt | 1 + .../ui/home/appLock/SetLockScreenViewModel.kt | 18 ++++- .../ui/home/settings/SettingsViewModel.kt | 7 +- .../sync/FeatureFlagNotificationViewModel.kt | 27 ++++++- .../devices/DeviceDetailsViewModel.kt | 18 +++-- app/src/main/res/values/strings.xml | 2 + .../ObserveAppLockConfigUseCaseTest.kt | 13 ++-- .../appLock/SetLockScreenViewModelTest.kt | 1 + .../kotlin/customization/FeatureConfigs.kt | 2 + default.json | 4 +- kalium | 2 +- 20 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt diff --git a/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt b/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt new file mode 100644 index 00000000000..b6786cca667 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt @@ -0,0 +1,78 @@ +/* + * Wire + * Copyright (C) 2023 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.applock.passcode + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.wire.android.datastore.EncryptionManager +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.GlobalDataStore.Companion.dataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") +private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") + +/** + * returns a flow with decoded passcode + */ +@Suppress("TooGenericExceptionCaught") +fun GlobalDataStore.getAppLockPasscodeFlow(): Flow = + context.dataStore.data.map { + it[APP_LOCK_PASSCODE]?.let { passcode -> + try { + EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, passcode) + } catch (e: Exception) { + null + } + } + } + +/** + * returns a flow only informing whether the passcode is set, without the need to decode it + */ +fun GlobalDataStore.isAppLockPasscodeSetFlow(): Flow = + context.dataStore.data.map { + it.contains(APP_LOCK_PASSCODE) + } + +suspend fun GlobalDataStore.clearAppLockPasscode() { + context.dataStore.edit { + it.remove(APP_LOCK_PASSCODE) + } +} + +@Suppress("TooGenericExceptionCaught") +suspend fun GlobalDataStore.setAppLockPasscode(passcode: String) { + context.dataStore.edit { + try { + val encrypted = + EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode) + it[APP_LOCK_PASSCODE] = encrypted + } catch (e: Exception) { + it.remove(APP_LOCK_PASSCODE) + } + } +} + +fun GlobalDataStore.isAppLockedByUserFlow(): Flow = getBooleanPreference(IS_APP_LOCKED_BY_USER, false) + +suspend fun GlobalDataStore.setAppLockedByUser(isLocked: Boolean) { + context.dataStore.edit { it[IS_APP_LOCKED_BY_USER] = isLocked } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index 01a0f703bce..5dca661247f 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -40,7 +40,7 @@ import javax.inject.Singleton @Suppress("TooManyFunctions") @Singleton -class GlobalDataStore @Inject constructor(@ApplicationContext private val context: Context) { +class GlobalDataStore @Inject constructor(@ApplicationContext val context: Context) { companion object { private const val PREFERENCES_NAME = "global_data" @@ -51,8 +51,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex private val WELCOME_SCREEN_PRESENTED = booleanPreferencesKey("welcome_screen_presented") private val IS_LOGGING_ENABLED = booleanPreferencesKey("is_logging_enabled") private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = booleanPreferencesKey("is_encrypted_proteus_storage_enabled") - private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") - private val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME) + val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME) private fun userMigrationStatusKey(userId: String): Preferences.Key = intPreferencesKey("user_migration_status_$userId") private fun userDoubleTapToastStatusKey(userId: String): Preferences.Key = booleanPreferencesKey("$SHOW_CALLING_DOUBLE_TAP_TOAST$userId") @@ -65,7 +64,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex context.dataStore.edit { it.clear() } } - private fun getBooleanPreference(key: Preferences.Key, defaultValue: Boolean): Flow = + fun getBooleanPreference(key: Preferences.Key, defaultValue: Boolean): Flow = context.dataStore.data.map { it[key] ?: defaultValue } fun isMigrationCompletedFlow(): Flow = getBooleanPreference(MIGRATION_COMPLETED, false) @@ -135,41 +134,4 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex suspend fun getShouldShowDoubleTapToast(userId: String): Boolean = getBooleanPreference(userDoubleTapToastStatusKey(userId), true).first() - - // returns a flow with decoded passcode - @Suppress("TooGenericExceptionCaught") - fun getAppLockPasscodeFlow(): Flow = - context.dataStore.data.map { - it[APP_LOCK_PASSCODE]?.let { - try { - EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, it) - } catch (e: Exception) { - null - } - } - } - - // returns a flow only informing whether the passcode is set, without the need to decode it - fun isAppLockPasscodeSetFlow(): Flow = - context.dataStore.data.map { - it.contains(APP_LOCK_PASSCODE) - } - - suspend fun clearAppLockPasscode() { - context.dataStore.edit { - it.remove(APP_LOCK_PASSCODE) - } - } - - @Suppress("TooGenericExceptionCaught") - suspend fun setAppLockPasscode(passcode: String) { - context.dataStore.edit { - try { - val encrypted = EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode) - it[APP_LOCK_PASSCODE] = encrypted - } catch (e: Exception) { - it.remove(APP_LOCK_PASSCODE) - } - } - } } diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 2d37f9b041c..bb454292456 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -391,13 +391,10 @@ class UseCaseModule { fun provideMarkGuestLinkFeatureFlagAsNotChangedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = coreLogic.getSessionScope(currentAccount).markGuestLinkFeatureFlagAsNotChanged -// @ViewModelScoped -// @Provides -// fun provideAppLockTeamFeatureConfigObserver( -// @KaliumCoreLogic coreLogic: CoreLogic, -// @CurrentAccount currentAccount: UserId -// ): AppLockTeamFeatureConfigObserver = -// coreLogic.getSessionScope(currentAccount).appLockTeamFeatureConfigObserver + @ViewModelScoped + @Provides + fun provideMarkTeamAppLockStatusAsNotifiedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = + coreLogic.getSessionScope(currentAccount).markTeamAppLockStatusAsNotified @ViewModelScoped @Provides diff --git a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt index 0ae97d2d55b..948ded9062f 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -64,7 +64,9 @@ class KaliumConfigsModule { wipeOnDeviceRemoval = BuildConfig.WIPE_ON_DEVICE_REMOVAL, wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, isWebSocketEnabledByDefault = isWebsocketEnabledByDefault(context), - certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG + certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, + teamAppLock = BuildConfig.TEAM_APP_LOCK, + teamAppLockTimeout = BuildConfig.TEAM_APP_LOCK_TIMEOUT ) } } diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index 86962956f28..c71f5fbdd91 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -17,6 +17,7 @@ */ package com.wire.android.feature +import com.wire.android.applock.passcode.isAppLockedByUserFlow import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic @@ -50,7 +51,7 @@ class ObserveAppLockConfigUseCase @Inject constructor( coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver appLockTeamFeatureConfigFlow().combineTransform( - globalDataStore.isAppLockPasscodeSetFlow() + globalDataStore.isAppLockedByUserFlow() ) { isAppLockedByTeam, isAppLocked -> if (isAppLockedByTeam.isEnabled) { emit(AppLockConfig.EnforcedByTeam(isAppLockedByTeam.timeout)) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 0cf7940a54f..ba9203feccd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -82,6 +82,7 @@ import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination +import com.wire.android.ui.destinations.SetLockCodeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EISnoozeDialog @@ -280,6 +281,15 @@ class WireActivity : AppCompatActivity() { ) } + if (shouldShowTeamAppLockDialog) { + TeamAppLockFeatureFlagDialog( + isTeamAppLockEnabled = isTeamAppLockEnabled, + onDismiss = { + // TODO to be handled in next PR + } + ) + } + if (shouldShowSelfDeletingMessagesDialog) { SelfDeletingMessagesDialog( areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled, diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index eaf1edab2e1..196ea572273 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -127,6 +127,27 @@ fun GuestRoomLinkFeatureFlagDialog( ) } +@Composable +fun TeamAppLockFeatureFlagDialog( + isTeamAppLockEnabled: Boolean, + onDismiss: () -> Unit, +) { + val text: String = + stringResource(id = if (isTeamAppLockEnabled) R.string.team_app_lock_enabled + else R.string.team_app_lock_disabled) + + WireDialog( + title = stringResource(id = R.string.team_settings_changed), + text = text, + onDismiss = onDismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = onDismiss, + text = stringResource(id = R.string.label_ok), + type = WireDialogButtonType.Primary, + ) + ) +} + @Composable fun UpdateAppDialog(shouldShow: Boolean, onUpdateClick: () -> Unit) { if (shouldShow) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt index a3eb2ac548a..ce5d0ce6f8c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt @@ -28,6 +28,8 @@ data class FeatureFlagState( val isFileSharingEnabledState: Boolean = true, val fileSharingRestrictedState: SharingRestrictedState? = null, val shouldShowGuestRoomLinkDialog: Boolean = false, + val shouldShowTeamAppLockDialog: Boolean = false, + val isTeamAppLockEnabled: Boolean = false, val isGuestRoomLinkEnabled: Boolean = true, val shouldShowSelfDeletingMessagesDialog: Boolean = false, val enforcedTimeoutDuration: SelfDeletionDuration = SelfDeletionDuration.None, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt index 5cd7b4317f2..e7a6dc3e4ca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.applock.passcode.getAppLockPasscodeFlow import com.wire.android.datastore.GlobalDataStore import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.sha256 diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt index dfcf8ddfc3e..7d0fe122c67 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeViewState.kt @@ -27,5 +27,6 @@ data class SetLockCodeViewState( val password: TextFieldValue = TextFieldValue(), val passwordValidation: ValidatePasswordResult = ValidatePasswordResult.Invalid(), val timeout: Duration = DEFAULT_TIMEOUT, + val isAppLockByUser: Boolean = true, val done: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt index 9612aefd045..69e35663306 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt @@ -23,10 +23,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.applock.passcode.setAppLockPasscode +import com.wire.android.applock.passcode.setAppLockedByUser import com.wire.android.datastore.GlobalDataStore +import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.sha256 +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest @@ -39,7 +43,7 @@ class SetLockScreenViewModel @Inject constructor( private val validatePassword: ValidatePasswordUseCase, private val globalDataStore: GlobalDataStore, private val dispatchers: DispatcherProvider, - private val observeAppLockConfigUseCase: ObserveAppLockConfigUseCase, + private val observeAppLockConfigUseCase: ObserveAppLockConfigUseCase ) : ViewModel() { var state: SetLockCodeViewState by mutableStateOf(SetLockCodeViewState()) @@ -49,7 +53,10 @@ class SetLockScreenViewModel @Inject constructor( viewModelScope.launch { observeAppLockConfigUseCase() .collectLatest { - state = state.copy(timeout = it.timeout) + state = state.copy( + timeout = it.timeout, + isAppLockByUser = it !is AppLockConfig.EnforcedByTeam + ) } } } @@ -75,7 +82,12 @@ class SetLockScreenViewModel @Inject constructor( if (it.isValid) { viewModelScope.launch { withContext(dispatchers.io()) { - globalDataStore.setAppLockPasscode(state.password.text.sha256()) + with(globalDataStore){ + setAppLockPasscode(state.password.text.sha256()) + if (state.isAppLockByUser) { + setAppLockedByUser(true) + } + } } withContext(dispatchers.main()) { state = state.copy(done = true) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt index 85610a7c0ca..ddbd46c2108 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.wire.android.applock.passcode.clearAppLockPasscode +import com.wire.android.applock.passcode.setAppLockedByUser import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.ObserveAppLockConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -47,7 +49,10 @@ class SettingsViewModel @Inject constructor( fun disableAppLock() { viewModelScope.launch { - globalDataStore.clearAppLockPasscode() + with(globalDataStore) { + clearAppLockPasscode() + setAppLockedByUser(false) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 472cf020854..a0a54083f5a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.sync +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -34,11 +35,13 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSelfDeleteTimer import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @@ -46,7 +49,8 @@ import javax.inject.Inject @HiltViewModel class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, - private val currentSessionUseCase: CurrentSessionUseCase + private val currentSessionUseCase: CurrentSessionUseCase, + private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase ) : ViewModel() { var featureFlagState by mutableStateOf(FeatureFlagState()) @@ -84,6 +88,7 @@ class FeatureFlagNotificationViewModel @Inject constructor( observeTeamSettingsSelfDeletionStatus(userId) setGuestRoomLinkFeatureFlag(userId) setE2EIRequiredState(userId) + setTeamAppLockFeatureFlag(userId) } } } @@ -124,6 +129,22 @@ class FeatureFlagNotificationViewModel @Inject constructor( } } + private fun setTeamAppLockFeatureFlag(userId: UserId) { + viewModelScope.launch { + coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() + .distinctUntilChanged() + .collect { + Log.d("appLock", "appLockTeamFeatureConfigObserver changed $it ") + it.isStatusChanged?.let { isStatusChanged -> + featureFlagState = featureFlagState.copy( + isTeamAppLockEnabled = it.isEnabled, + shouldShowTeamAppLockDialog = isStatusChanged + ) + } + } + } + } + private fun observeTeamSettingsSelfDeletionStatus(userId: UserId) { viewModelScope.launch { coreLogic.getSessionScope(userId).observeTeamSettingsSelfDeletionStatus().collect { teamSettingsSelfDeletingStatus -> @@ -183,6 +204,10 @@ class FeatureFlagNotificationViewModel @Inject constructor( featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = false) } + fun dismissTeamAppLockDialog() { + featureFlagState = featureFlagState.copy(shouldShowTeamAppLockDialog = false) + } + fun getE2EICertificate() { // TODO do the magic featureFlagState = featureFlagState.copy(e2EIRequired = null) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index 2886561fe91..645b6be5ddc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -80,14 +80,16 @@ class DeviceDetailsViewModel @Inject constructor( } private fun getE2eiCertificate() { - val certificate = e2eiCertificate(deviceId) - state = if (certificate is GetE2EICertificateUseCaseResult.Success) { - state.copy( - isE2eiCertificateActivated = true, - e2eiCertificate = certificate.certificate - ) - } else { - state.copy(isE2eiCertificateActivated = false) + viewModelScope.launch { + val certificate = e2eiCertificate(deviceId) + state = if (certificate is GetE2EICertificateUseCaseResult.Success) { + state.copy( + isE2eiCertificateActivated = true, + e2eiCertificate = certificate.certificate + ) + } else { + state.copy(isE2eiCertificateActivated = false) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42c28663049..16107a0290c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -949,6 +949,8 @@ Turn app lock off? You will no longer need to unlock Wire with your passcode or biometric authentication. Turn Off + App lock is now mandatory. Wire will lock itself after a certain time of inactivity. To unlock the app, you need to enter a passcode or use biometric authentication. + App lock is not mandatory any more. Wire will no longer lock itself after a certain time of inactivity. Your Devices Current Device diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index a436d4d6c40..cd7af02c1a6 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -18,11 +18,12 @@ package com.wire.android.feature import app.cash.turbine.test +import com.wire.android.applock.passcode.isAppLockedByUserFlow import com.wire.android.datastore.GlobalDataStore import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.UserSessionScope -import com.wire.kalium.logic.feature.applock.AppLockTeamConfig +import com.wire.kalium.logic.configuration.AppLockTeamConfig import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver import com.wire.kalium.logic.feature.auth.AccountInfo import com.wire.kalium.logic.feature.session.CurrentSessionResult @@ -142,7 +143,7 @@ class ObserveAppLockConfigUseCaseTest { } fun withAppLockPasscodeSet(value: Boolean) = apply { - every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(value) + every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(value) } fun arrange() = this to useCase @@ -162,7 +163,7 @@ class ObserveAppLockConfigUseCaseTest { } returns appLockTeamFeatureConfigObserver every { appLockTeamFeatureConfigObserver.invoke() - } returns flowOf(AppLockTeamConfig(true, timeout)) + } returns flowOf(AppLockTeamConfig(true, timeout, false)) } fun withTeamAppLockDisabled() = apply { @@ -172,15 +173,15 @@ class ObserveAppLockConfigUseCaseTest { } returns appLockTeamFeatureConfigObserver every { appLockTeamFeatureConfigObserver.invoke() - } returns flowOf(AppLockTeamConfig(false, timeout)) + } returns flowOf(AppLockTeamConfig(false, timeout, false)) } fun withAppLockedByCurrentUser() = apply { - every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(true) + every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(true) } fun withAppNonLockedByCurrentUser() = apply { - every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(false) + every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(false) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt index 227cca15faf..79f92164888 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.appLock import androidx.compose.ui.text.input.TextFieldValue +import com.wire.android.applock.passcode.setAppLockPasscode import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index e15645d80be..36d8e6d45be 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -48,6 +48,8 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { UPDATE_APP_URL("update_app_url", ConfigType.STRING), ENABLE_BLACKLIST("enable_blacklist", ConfigType.BOOLEAN), WEBSOCKET_ENABLED_BY_DEFAULT("websocket_enabled_by_default", ConfigType.BOOLEAN), + TEAM_APP_LOCK("team_app_lock", ConfigType.BOOLEAN), + TEAM_APP_LOCK_TIMEOUT("team_app_lock_timeout", ConfigType.INT), /** * Security/Cryptography stuff diff --git a/default.json b/default.json index e8631fd4e83..17e7d8c0e02 100644 --- a/default.json +++ b/default.json @@ -107,5 +107,7 @@ "default_backend_title": "wire-production", "cert_pinning_config": {}, "is_password_protected_guest_link_enabled": false, - "url_rss_release_notes": "https://medium.com/feed/wire-news/tagged/android" + "url_rss_release_notes": "https://medium.com/feed/wire-news/tagged/android", + "team_app_lock": false, + "team_app_lock_timeout": 60 } diff --git a/kalium b/kalium index b8e837903ae..147caa1a161 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b8e837903ae6d0f275f101aa41486e15d2a7a330 +Subproject commit 147caa1a161d5e32d2bfe00b7b9c2c13f3b68b6e From 8173e85fa7b860d644e32bd6cadea98640ec4ca5 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 31 Oct 2023 14:13:21 +0100 Subject: [PATCH 10/25] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 07301bd8a12..033a8ffbc4a 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 07301bd8a120b930e057a94c1d80fb99e9edc16f +Subproject commit 033a8ffbc4a23baae24cd708eb5faa22c0da303d From acd4d3d392606b6757b9dd2211f1e1b21b8985dd Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 1 Nov 2023 16:00:40 +0100 Subject: [PATCH 11/25] feat: show set app lock screen --- .../applock/passcode/PasscodeHelper.kt | 78 --------------- .../wire/android/datastore/GlobalDataStore.kt | 89 +++++++++++++++++ .../feature/ObserveAppLockConfigUseCase.kt | 3 +- .../com/wire/android/ui/AppLockActivity.kt | 27 ++++-- .../com/wire/android/ui/WireActivity.kt | 24 ++++- .../wire/android/ui/WireActivityDialogs.kt | 6 +- .../com/wire/android/ui/common/WireDialog.kt | 1 - .../home/appLock/EnterLockScreenViewModel.kt | 1 - .../ui/home/appLock/LockCodeTimeManager.kt | 17 +++- .../ui/home/appLock/SetLockCodeScreen.kt | 14 +-- .../ui/home/appLock/SetLockScreenViewModel.kt | 11 ++- .../ui/home/settings/SettingsViewModel.kt | 7 +- .../sync/FeatureFlagNotificationViewModel.kt | 29 +++++- .../ObserveAppLockConfigUseCaseTest.kt | 7 +- .../home/appLock/LockCodeTimeManagerTest.kt | 96 +++++++++++++++---- .../appLock/SetLockScreenViewModelTest.kt | 18 +++- .../FeatureFlagNotificationViewModelTest.kt | 12 ++- 17 files changed, 292 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt diff --git a/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt b/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt deleted file mode 100644 index b6786cca667..00000000000 --- a/app/src/main/kotlin/com/wire/android/applock/passcode/PasscodeHelper.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 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.applock.passcode - -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import com.wire.android.datastore.EncryptionManager -import com.wire.android.datastore.GlobalDataStore -import com.wire.android.datastore.GlobalDataStore.Companion.dataStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") -private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") - -/** - * returns a flow with decoded passcode - */ -@Suppress("TooGenericExceptionCaught") -fun GlobalDataStore.getAppLockPasscodeFlow(): Flow = - context.dataStore.data.map { - it[APP_LOCK_PASSCODE]?.let { passcode -> - try { - EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, passcode) - } catch (e: Exception) { - null - } - } - } - -/** - * returns a flow only informing whether the passcode is set, without the need to decode it - */ -fun GlobalDataStore.isAppLockPasscodeSetFlow(): Flow = - context.dataStore.data.map { - it.contains(APP_LOCK_PASSCODE) - } - -suspend fun GlobalDataStore.clearAppLockPasscode() { - context.dataStore.edit { - it.remove(APP_LOCK_PASSCODE) - } -} - -@Suppress("TooGenericExceptionCaught") -suspend fun GlobalDataStore.setAppLockPasscode(passcode: String) { - context.dataStore.edit { - try { - val encrypted = - EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode) - it[APP_LOCK_PASSCODE] = encrypted - } catch (e: Exception) { - it.remove(APP_LOCK_PASSCODE) - } - } -} - -fun GlobalDataStore.isAppLockedByUserFlow(): Flow = getBooleanPreference(IS_APP_LOCKED_BY_USER, false) - -suspend fun GlobalDataStore.setAppLockedByUser(isLocked: Boolean) { - context.dataStore.edit { it[IS_APP_LOCKED_BY_USER] = isLocked } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index 5dca661247f..9c334bed9d9 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @@ -134,4 +135,92 @@ class GlobalDataStore @Inject constructor(@ApplicationContext val context: Conte suspend fun getShouldShowDoubleTapToast(userId: String): Boolean = getBooleanPreference(userDoubleTapToastStatusKey(userId), true).first() + + private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") + private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") + private val TEAM_APP_LOCK_PASSCODE = stringPreferencesKey("team_app_lock_passcode") + + /** + * returns a flow with decoded passcode + */ + @Suppress("TooGenericExceptionCaught") + fun getAppLockPasscodeFlow(): Flow = + context.dataStore.data.map { + it[APP_LOCK_PASSCODE]?.let { passcode -> + try { + EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, passcode) + } catch (e: Exception) { + null + } + } + } + + /** + * returns a flow only informing whether the passcode is set, without the need to decode it + */ + fun isAppLockPasscodeSetFlow(): Flow = + context.dataStore.data.map { + it.contains(APP_LOCK_PASSCODE) + } + + fun isAppLockPasscodeSet(): Boolean = runBlocking { + context.dataStore.data.map { + it.contains(APP_LOCK_PASSCODE) + }.first() + } + + fun isAppTeamPasscodeSetFlow(): Flow = + context.dataStore.data.map { + it.contains(TEAM_APP_LOCK_PASSCODE) + } + + fun isAppTeamPasscodeSet(): Boolean = runBlocking { + context.dataStore.data.map { + it.contains(TEAM_APP_LOCK_PASSCODE) + }.first() + } + + suspend fun clearAppLockPasscode() { + context.dataStore.edit { + it.remove(APP_LOCK_PASSCODE) + } + } + suspend fun clearTeamAppLockPasscode() { + context.dataStore.edit { + it.remove(TEAM_APP_LOCK_PASSCODE) + } + } + + private suspend fun setAppLockPasscode( + passcode: String, + key: Preferences.Key + ) { + context.dataStore.edit { + try { + val encrypted = + EncryptionManager.encrypt(key.name, passcode) + it[key] = encrypted + } catch (e: Exception) { + it.remove(key) + } + } + } + + suspend fun setTeamAppLock( + passcode: String, + key: Preferences.Key = TEAM_APP_LOCK_PASSCODE + ) { + setAppLockPasscode(passcode, key) + } + + suspend fun setUserAppLock( + passcode: String, + key: Preferences.Key = APP_LOCK_PASSCODE + ) { + setAppLockPasscode(passcode, key) + } + + fun isAppLockedByUserFlow(): Flow = + getBooleanPreference(IS_APP_LOCKED_BY_USER, false) + } diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index c71f5fbdd91..86962956f28 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature -import com.wire.android.applock.passcode.isAppLockedByUserFlow import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic @@ -51,7 +50,7 @@ class ObserveAppLockConfigUseCase @Inject constructor( coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver appLockTeamFeatureConfigFlow().combineTransform( - globalDataStore.isAppLockedByUserFlow() + globalDataStore.isAppLockPasscodeSetFlow() ) { isAppLockedByTeam, isAppLocked -> if (isAppLockedByTeam.isEnabled) { emit(AppLockConfig.EnforcedByTeam(isAppLockedByTeam.timeout)) diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt index 50dbd4aa1ee..7f0f1fc5ef4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -27,6 +27,7 @@ import com.wire.android.navigation.NavigationGraph import com.wire.android.navigation.rememberNavigator import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination import com.wire.android.ui.destinations.EnterLockCodeScreenDestination +import com.wire.android.ui.destinations.SetLockCodeScreenDestination import com.wire.android.ui.theme.WireTheme import dagger.hilt.android.AndroidEntryPoint @@ -37,19 +38,23 @@ class AppLockActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { WireTheme { - val canAuthenticateWithBiometrics = BiometricManager - .from(this) - .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) - val navigator = rememberNavigator(this@AppLockActivity::finish) val startDestination = - if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { - appLogger.i("appLock: requesting app Unlock with biometrics") - AppUnlockWithBiometricsScreenDestination + if (intent.getBooleanExtra(SET_TEAM_APP_LOCK, false)) { + appLogger.i("appLock: requesting set team app lock") + SetLockCodeScreenDestination } else { - appLogger.i("appLock: requesting app Unlock with passcode") - EnterLockCodeScreenDestination + val canAuthenticateWithBiometrics = BiometricManager + .from(this) + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { + appLogger.i("appLock: requesting app Unlock with biometrics") + AppUnlockWithBiometricsScreenDestination + } else { + appLogger.i("appLock: requesting app Unlock with passcode") + EnterLockCodeScreenDestination + } } NavigationGraph( @@ -59,4 +64,8 @@ class AppLockActivity : AppCompatActivity() { } } } + + companion object { + const val SET_TEAM_APP_LOCK = "set_team_app_lock" + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 432f2102133..856de8f590f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -257,8 +257,28 @@ class WireActivity : AppCompatActivity() { if (shouldShowTeamAppLockDialog) { TeamAppLockFeatureFlagDialog( isTeamAppLockEnabled = isTeamAppLockEnabled, - onDismiss = { - // TODO to be handled in next PR + onConfirm = { + featureFlagNotificationViewModel.dismissTeamAppLockDialog() + if (isTeamAppLockEnabled) { + val isUserAppLockSet = + featureFlagNotificationViewModel.isUserAppLockSet() + // No need to setup another app lock if the user already has one + if (!isUserAppLockSet) { + Intent(this@WireActivity, AppLockActivity::class.java) + .apply { + putExtra(AppLockActivity.SET_TEAM_APP_LOCK, true) + }.also { + startActivity(it) + } + } else { + featureFlagNotificationViewModel.markTeamAppLockStatusAsNot() + } + } else { + with(featureFlagNotificationViewModel) { + markTeamAppLockStatusAsNot() + clearTeamAppLockPasscode() + } + } } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index 196ea572273..1b907862343 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -130,7 +130,7 @@ fun GuestRoomLinkFeatureFlagDialog( @Composable fun TeamAppLockFeatureFlagDialog( isTeamAppLockEnabled: Boolean, - onDismiss: () -> Unit, + onConfirm: () -> Unit, ) { val text: String = stringResource(id = if (isTeamAppLockEnabled) R.string.team_app_lock_enabled @@ -139,9 +139,9 @@ fun TeamAppLockFeatureFlagDialog( WireDialog( title = stringResource(id = R.string.team_settings_changed), text = text, - onDismiss = onDismiss, + onDismiss = {}, optionButton1Properties = WireDialogButtonProperties( - onClick = onDismiss, + onClick = onConfirm, text = stringResource(id = R.string.label_ok), type = WireDialogButtonType.Primary, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt index 8e2db34c383..ea1ca3467e9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt @@ -116,7 +116,6 @@ fun WireDialog( ) } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun WireDialog( title: String, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt index e7a6dc3e4ca..5cd7b4317f2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.applock.passcode.getAppLockPasscodeFlow import com.wire.android.datastore.GlobalDataStore import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.sha256 diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt index 2a0d6bc9052..64b8deb6e79 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.appLock import com.wire.android.appLogger +import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.ApplicationScope import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase @@ -44,6 +45,7 @@ class LockCodeTimeManager @Inject constructor( @ApplicationScope private val appCoroutineScope: CoroutineScope, currentScreenManager: CurrentScreenManager, observeAppLockConfigUseCase: ObserveAppLockConfigUseCase, + globalDataStore: GlobalDataStore ) { private val isLockedFlow = MutableStateFlow(false) @@ -52,8 +54,10 @@ class LockCodeTimeManager @Inject constructor( // first, set initial value - if app lock is enabled then app needs to be locked right away runBlocking { observeAppLockConfigUseCase().firstOrNull()?.let { appLockConfig -> - if (appLockConfig !is AppLockConfig.Disabled) { - appLogger.i("$TAG app initially locked") + // app could be locked by team but user still didn't set the passcode + val isTeamAppLockSet = appLockConfig is AppLockConfig.EnforcedByTeam && + globalDataStore.isAppTeamPasscodeSet() + if (appLockConfig is AppLockConfig.Enabled || isTeamAppLockSet) { isLockedFlow.value = true } } @@ -74,8 +78,13 @@ class LockCodeTimeManager @Inject constructor( !isInForeground && !isLockedFlow.value -> flow { appLogger.i("$TAG lock is enabled and app in the background, lock count started") delay(appLockConfig.timeout.inWholeMilliseconds) - appLogger.i("$TAG lock count ended, app state is locked") - emit(true) + appLogger.i("$TAG lock count ended, app state should be locked if passcode is set") + // app could be locked by team but user still didn't set the passcode + val isTeamAppLockSet = appLockConfig is AppLockConfig.EnforcedByTeam + && globalDataStore.isAppTeamPasscodeSet() + if (appLockConfig is AppLockConfig.Enabled || isTeamAppLockSet) { + emit(true) + } } else -> { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeScreen.kt index bee7339e887..e9792bc9817 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockCodeScreen.kt @@ -104,12 +104,14 @@ fun SetLockCodeScreenContent( } } - WireScaffold(topBar = { - WireCenterAlignedTopAppBar( - onNavigationPressed = onBackPress, - elevation = dimensions().spacing0x, - title = stringResource(id = R.string.settings_set_lock_screen_title) - ) + WireScaffold( + snackbarHost = {}, + topBar = { + WireCenterAlignedTopAppBar( + onNavigationPressed = onBackPress, + elevation = dimensions().spacing0x, + title = stringResource(id = R.string.settings_set_lock_screen_title) + ) }) { internalPadding -> Column( modifier = Modifier diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt index 69e35663306..caf41bf4676 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt @@ -23,8 +23,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.applock.passcode.setAppLockPasscode -import com.wire.android.applock.passcode.setAppLockedByUser import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase @@ -43,7 +41,8 @@ class SetLockScreenViewModel @Inject constructor( private val validatePassword: ValidatePasswordUseCase, private val globalDataStore: GlobalDataStore, private val dispatchers: DispatcherProvider, - private val observeAppLockConfigUseCase: ObserveAppLockConfigUseCase + private val observeAppLockConfigUseCase: ObserveAppLockConfigUseCase, + private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase ) : ViewModel() { var state: SetLockCodeViewState by mutableStateOf(SetLockCodeViewState()) @@ -83,10 +82,12 @@ class SetLockScreenViewModel @Inject constructor( viewModelScope.launch { withContext(dispatchers.io()) { with(globalDataStore){ - setAppLockPasscode(state.password.text.sha256()) if (state.isAppLockByUser) { - setAppLockedByUser(true) + setUserAppLock(state.password.text.sha256()) + } else { + setTeamAppLock(state.password.text.sha256()) } + markTeamAppLockStatusAsNotified() } } withContext(dispatchers.main()) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt index ddbd46c2108..85610a7c0ca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt @@ -25,8 +25,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.applock.passcode.clearAppLockPasscode -import com.wire.android.applock.passcode.setAppLockedByUser import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.ObserveAppLockConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -49,10 +47,7 @@ class SettingsViewModel @Inject constructor( fun disableAppLock() { viewModelScope.launch { - with(globalDataStore) { - clearAppLockPasscode() - setAppLockedByUser(false) - } + globalDataStore.clearAppLockPasscode() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index a0a54083f5a..0e06908c976 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -20,13 +20,13 @@ package com.wire.android.ui.home.sync -import android.util.Log 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.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration @@ -41,7 +41,9 @@ import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @@ -50,7 +52,8 @@ import javax.inject.Inject class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSessionUseCase: CurrentSessionUseCase, - private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase + private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase, + private val globalDataStore: GlobalDataStore ) : ViewModel() { var featureFlagState by mutableStateOf(FeatureFlagState()) @@ -133,13 +136,20 @@ class FeatureFlagNotificationViewModel @Inject constructor( viewModelScope.launch { coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver() .distinctUntilChanged() - .collect { - Log.d("appLock", "appLockTeamFeatureConfigObserver changed $it ") + .collectLatest { it.isStatusChanged?.let { isStatusChanged -> featureFlagState = featureFlagState.copy( isTeamAppLockEnabled = it.isEnabled, shouldShowTeamAppLockDialog = isStatusChanged ) + } ?: run { + // to handle the case of first app install with team app lock enabled + if (it.isEnabled && !globalDataStore.isAppTeamPasscodeSetFlow().first()) { + featureFlagState = featureFlagState.copy( + isTeamAppLockEnabled = true, + shouldShowTeamAppLockDialog = true + ) + } } } } @@ -207,6 +217,17 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissTeamAppLockDialog() { featureFlagState = featureFlagState.copy(shouldShowTeamAppLockDialog = false) } + fun markTeamAppLockStatusAsNot() { + markTeamAppLockStatusAsNotified() + } + + fun clearTeamAppLockPasscode() { + viewModelScope.launch { + globalDataStore.clearTeamAppLockPasscode() + } + } + + fun isUserAppLockSet() = globalDataStore.isAppLockPasscodeSet() fun getE2EICertificate() { // TODO do the magic diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index cd7af02c1a6..172470a4125 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -18,7 +18,6 @@ package com.wire.android.feature import app.cash.turbine.test -import com.wire.android.applock.passcode.isAppLockedByUserFlow import com.wire.android.datastore.GlobalDataStore import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId @@ -143,7 +142,7 @@ class ObserveAppLockConfigUseCaseTest { } fun withAppLockPasscodeSet(value: Boolean) = apply { - every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(value) + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(value) } fun arrange() = this to useCase @@ -177,11 +176,11 @@ class ObserveAppLockConfigUseCaseTest { } fun withAppLockedByCurrentUser() = apply { - every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(true) + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(true) } fun withAppNonLockedByCurrentUser() = apply { - every { globalDataStore.isAppLockedByUserFlow() } returns flowOf(false) + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(false) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt index 1544544c6a9..79ee92b44f7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.appLock import app.cash.turbine.test +import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.util.CurrentScreenManager @@ -43,28 +44,48 @@ class LockCodeTimeManagerTest { private fun AppLockConfig.timeoutInMillis(): Long = this.timeout.inWholeMilliseconds - private fun testInitialStart(appLockConfig: AppLockConfig, expected: Boolean) = - runTest(dispatcher) { - // given - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(appLockConfig) - .withIsAppVisible(false) - .arrange() - advanceUntilIdle() - // when - arrangement.withIsAppVisible(true) - advanceUntilIdle() - // then - assertEquals(expected, manager.observeAppLock().first()) - } + private fun testInitialStart( + appLockConfig: AppLockConfig, + isTeamPasscodeSet: Boolean = false, + expected: Boolean + ) = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withTeamPasscodeSet(isTeamPasscodeSet) + .withIsAppVisible(false) + .arrange() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(true) + advanceUntilIdle() + // then + assertEquals(expected, manager.observeAppLock().first()) + } @Test fun givenLockEnabled_whenAppInitiallyOpened_thenLocked() = - testInitialStart(AppLockConfig.Enabled(DEFAULT_TIMEOUT), true) + testInitialStart(AppLockConfig.Enabled(DEFAULT_TIMEOUT), expected = true) @Test fun givenLockDisabled_whenAppInitiallyOpened_thenNotLocked() = - testInitialStart(AppLockConfig.Disabled(DEFAULT_TIMEOUT), false) + testInitialStart(AppLockConfig.Disabled(DEFAULT_TIMEOUT), expected = false) + + @Test + fun givenLockForcedByTeamAndPasscodeSet_whenAppInitiallyOpened_thenAppIsLocked() = + testInitialStart( + appLockConfig = AppLockConfig.EnforcedByTeam(DEFAULT_TIMEOUT), + isTeamPasscodeSet = true, + expected = true + ) + + @Test + fun givenLockForcedByTeamAndPasscodeNotSet_whenAppInitiallyOpened_thenAppIsNotLocked() = + testInitialStart( + appLockConfig = AppLockConfig.EnforcedByTeam(DEFAULT_TIMEOUT), + isTeamPasscodeSet = false, + expected = false + ) private fun testStop(appLockConfig: AppLockConfig, delayAfterStop: Long, expected: Boolean) = runTest(dispatcher) { @@ -121,6 +142,7 @@ class LockCodeTimeManagerTest { val (arrangement, manager) = Arrangement(dispatcher) .withAppLockConfig(AppLockConfig.Enabled(timeout = 1000.seconds)) .withIsAppVisible(true) + .withTeamPasscodeSet(true) .arrange() manager.appUnlocked() advanceUntilIdle() @@ -203,7 +225,7 @@ class LockCodeTimeManagerTest { @Test fun givenLockEnabledAndAppOpenedLocked_whenAppIsUnlocked_thenNotLocked() = runTest(dispatcher) { // given - val (arrangement, manager) = Arrangement(dispatcher) + val (_, manager) = Arrangement(dispatcher) .withAppLockConfig(AppLockConfig.Enabled(DEFAULT_TIMEOUT)) .withIsAppVisible(true) .arrange() @@ -215,6 +237,38 @@ class LockCodeTimeManagerTest { assertEquals(false, manager.observeAppLock().first()) } + @Test + fun givenLockEnforcedByTeamAndPasscodeSet_whenAppIsUnlocked_thenNotLocked() = + runTest(dispatcher) { + // given + val (_, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.EnforcedByTeam(DEFAULT_TIMEOUT)) + .withTeamPasscodeSet(true) + .withIsAppVisible(true) + .arrange() + advanceUntilIdle() + // when + advanceTimeBy(AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L) + // then + assertEquals(true, manager.observeAppLock().first()) + } + + @Test + fun givenLockEnforcedByTeamAndNoPasscodeSet_whenAppIsUnlocked_thenNotLocked() = + runTest(dispatcher) { + // given + val (_, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.EnforcedByTeam(DEFAULT_TIMEOUT)) + .withTeamPasscodeSet(false) + .withIsAppVisible(true) + .arrange() + advanceUntilIdle() + // when + advanceTimeBy(AppLockConfig.Enabled(DEFAULT_TIMEOUT).timeoutInMillis() - 100L) + // then + assertEquals(false, manager.observeAppLock().first()) + } + class Arrangement(dispatcher: TestDispatcher) { @MockK @@ -223,11 +277,15 @@ class LockCodeTimeManagerTest { @MockK private lateinit var observeAppLockConfigUseCase: ObserveAppLockConfigUseCase + @MockK + private lateinit var globalDataStore: GlobalDataStore + private val lockCodeTimeManager by lazy { LockCodeTimeManager( CoroutineScope(dispatcher), currentScreenManager, observeAppLockConfigUseCase, + globalDataStore ) } @@ -250,5 +308,9 @@ class LockCodeTimeManagerTest { appLockConfigStateFlow.value = value every { observeAppLockConfigUseCase() } returns appLockConfigStateFlow } + + fun withTeamPasscodeSet(value: Boolean): Arrangement = apply { + every { globalDataStore.isAppTeamPasscodeSet() } returns value + } } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt index 79f92164888..245cea7e644 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModelTest.kt @@ -18,13 +18,13 @@ package com.wire.android.ui.home.appLock import androidx.compose.ui.text.input.TextFieldValue -import com.wire.android.applock.passcode.setAppLockPasscode import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import io.mockk.MockKAnnotations @@ -68,17 +68,24 @@ class SetLockScreenViewModelTest { } private class Arrangement { + @MockK lateinit var validatePassword: ValidatePasswordUseCase + @MockK lateinit var globalDataStore: GlobalDataStore + + @MockK + private lateinit var observeAppLockConfig: ObserveAppLockConfigUseCase + @MockK - private lateinit var observeAppLockConfigUseCase: ObserveAppLockConfigUseCase + private lateinit var markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase init { MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { globalDataStore.setAppLockPasscode(any()) } returns Unit - coEvery { observeAppLockConfigUseCase() } returns flowOf( + coEvery { globalDataStore.setUserAppLock(any()) } returns Unit + coEvery { globalDataStore.setTeamAppLock(any()) } returns Unit + coEvery { observeAppLockConfig() } returns flowOf( AppLockConfig.Disabled(AppLockTeamFeatureConfigObserverImpl.DEFAULT_TIMEOUT) ) } @@ -95,7 +102,8 @@ class SetLockScreenViewModelTest { validatePassword, globalDataStore, TestDispatcherProvider(), - observeAppLockConfigUseCase + observeAppLockConfig, + markTeamAppLockStatusAsNotified ) fun arrange() = this to viewModel diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 65553bda1b9..da156cf63e6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -1,6 +1,7 @@ package com.wire.android.ui.home.sync import com.wire.android.config.CoroutineTestExtension +import com.wire.android.datastore.GlobalDataStore import com.wire.android.framework.TestUser import com.wire.android.ui.home.FeatureFlagState import com.wire.kalium.logic.CoreLogic @@ -8,6 +9,7 @@ import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.configuration.GuestRoomLinkStatus import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.AccountInfo import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase @@ -218,9 +220,17 @@ class FeatureFlagNotificationViewModelTest { @MockK lateinit var markE2EIRequiredAsNotified: MarkEnablingE2EIAsNotifiedUseCase + @MockK + lateinit var markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase + + @MockK + lateinit var globalDataStore: GlobalDataStore + val viewModel: FeatureFlagNotificationViewModel = FeatureFlagNotificationViewModel( coreLogic = coreLogic, - currentSessionUseCase = currentSession + currentSessionUseCase = currentSession, + markTeamAppLockStatusAsNotified = markTeamAppLockStatusAsNotified, + globalDataStore = globalDataStore ) init { From 1a77eda9770faded2880f98d5cf8075eff8998cd Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 09:11:48 +0100 Subject: [PATCH 12/25] chore: detekt --- .../kotlin/com/wire/android/datastore/GlobalDataStore.kt | 8 ++++---- app/src/main/kotlin/com/wire/android/ui/WireActivity.kt | 1 - .../android/ui/home/appLock/SetLockScreenViewModel.kt | 2 +- .../ui/home/sync/FeatureFlagNotificationViewModel.kt | 1 + kalium | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index a31f1e52e44..0c0b382defa 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -53,6 +53,9 @@ class GlobalDataStore @Inject constructor(@ApplicationContext val context: Conte private val WELCOME_SCREEN_PRESENTED = booleanPreferencesKey("welcome_screen_presented") private val IS_LOGGING_ENABLED = booleanPreferencesKey("is_logging_enabled") private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = booleanPreferencesKey("is_encrypted_proteus_storage_enabled") + private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") + private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") + private val TEAM_APP_LOCK_PASSCODE = stringPreferencesKey("team_app_lock_passcode") val APP_THEME_OPTION = stringPreferencesKey("app_theme_option") private val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME) private fun userMigrationStatusKey(userId: String): Preferences.Key = intPreferencesKey("user_migration_status_$userId") @@ -141,10 +144,6 @@ class GlobalDataStore @Inject constructor(@ApplicationContext val context: Conte suspend fun getShouldShowDoubleTapToast(userId: String): Boolean = getBooleanPreference(userDoubleTapToastStatusKey(userId), true).first() - private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") - private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") - private val TEAM_APP_LOCK_PASSCODE = stringPreferencesKey("team_app_lock_passcode") - /** * returns a flow with decoded passcode */ @@ -196,6 +195,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext val context: Conte } } + @Suppress("TooGenericExceptionCaught") private suspend fun setAppLockPasscode( passcode: String, key: Preferences.Key diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 01b7c38481e..7e07ea7b00f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -79,7 +79,6 @@ import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination -import com.wire.android.ui.destinations.SetLockCodeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EISnoozeDialog diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt index caf41bf4676..ee77e7aee4b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/SetLockScreenViewModel.kt @@ -81,7 +81,7 @@ class SetLockScreenViewModel @Inject constructor( if (it.isValid) { viewModelScope.launch { withContext(dispatchers.io()) { - with(globalDataStore){ + with(globalDataStore) { if (state.isAppLockByUser) { setUserAppLock(state.password.text.sha256()) } else { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 0e06908c976..e5c8a44580a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("TooManyFunctions") @HiltViewModel class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, diff --git a/kalium b/kalium index 147caa1a161..eb56d62d323 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 147caa1a161d5e32d2bfe00b7b9c2c13f3b68b6e +Subproject commit eb56d62d323f9878e31f9fe0c16f3a97a1f796c6 From feef673cc76cd5a42c90f217ede5f0d2d3aa9bb4 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 10:21:05 +0100 Subject: [PATCH 13/25] chore: unit test --- .../android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index da156cf63e6..4a6a3f1ec4b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -203,6 +203,7 @@ class FeatureFlagNotificationViewModelTest { coEvery { currentSession() } returns CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)) coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns flowOf(SyncState.Live) coEvery { coreLogic.getSessionScope(any()).observeTeamSettingsSelfDeletionStatus() } returns flowOf() + coEvery { coreLogic.getSessionScope(any()).appLockTeamFeatureConfigObserver() } returns flowOf() } @MockK From df749e7e595763ff755e8d70c2e428bf0b7dd20e Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 13:03:39 +0100 Subject: [PATCH 14/25] fix: check session before using usecase --- .../sync/FeatureFlagNotificationViewModel.kt | 86 +++++++++++-------- .../FeatureFlagNotificationViewModelTest.kt | 5 -- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index e5c8a44580a..6b15214ce32 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -53,7 +53,6 @@ import javax.inject.Inject class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSessionUseCase: CurrentSessionUseCase, - private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase, private val globalDataStore: GlobalDataStore ) : ViewModel() { @@ -80,7 +79,8 @@ class FeatureFlagNotificationViewModel @Inject constructor( when (currentSessionResult) { is CurrentSessionResult.Failure -> { appLogger.e("Failure while getting current session from FeatureFlagNotificationViewModel") - featureFlagState = featureFlagState.copy(fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER) + featureFlagState = + featureFlagState.copy(fileSharingRestrictedState = FeatureFlagState.SharingRestrictedState.NO_USER) } is CurrentSessionResult.Success -> { @@ -122,14 +122,15 @@ class FeatureFlagNotificationViewModel @Inject constructor( private fun setGuestRoomLinkFeatureFlag(userId: UserId) { viewModelScope.launch { - coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag().collect { guestRoomLinkStatus -> - guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { - featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) - } - guestRoomLinkStatus.isStatusChanged?.let { - featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) + coreLogic.getSessionScope(userId).observeGuestRoomLinkFeatureFlag() + .collect { guestRoomLinkStatus -> + guestRoomLinkStatus.isGuestRoomLinkEnabled?.let { + featureFlagState = featureFlagState.copy(isGuestRoomLinkEnabled = it) + } + guestRoomLinkStatus.isStatusChanged?.let { + featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = it) + } } - } } } @@ -152,32 +153,33 @@ class FeatureFlagNotificationViewModel @Inject constructor( ) } } - } + } } } private fun observeTeamSettingsSelfDeletionStatus(userId: UserId) { viewModelScope.launch { - coreLogic.getSessionScope(userId).observeTeamSettingsSelfDeletionStatus().collect { teamSettingsSelfDeletingStatus -> - val areSelfDeletedMessagesEnabled = - teamSettingsSelfDeletingStatus.enforcedSelfDeletionTimer !is TeamSelfDeleteTimer.Disabled - val shouldShowSelfDeletingMessagesDialog = - teamSettingsSelfDeletingStatus.hasFeatureChanged ?: false - val enforcedTimeoutDuration: SelfDeletionDuration = - with(teamSettingsSelfDeletingStatus.enforcedSelfDeletionTimer) { - when (this) { - TeamSelfDeleteTimer.Disabled, - TeamSelfDeleteTimer.Enabled -> SelfDeletionDuration.None - - is TeamSelfDeleteTimer.Enforced -> this.enforcedDuration.toSelfDeletionDuration() + coreLogic.getSessionScope(userId).observeTeamSettingsSelfDeletionStatus() + .collect { teamSettingsSelfDeletingStatus -> + val areSelfDeletedMessagesEnabled = + teamSettingsSelfDeletingStatus.enforcedSelfDeletionTimer !is TeamSelfDeleteTimer.Disabled + val shouldShowSelfDeletingMessagesDialog = + teamSettingsSelfDeletingStatus.hasFeatureChanged ?: false + val enforcedTimeoutDuration: SelfDeletionDuration = + with(teamSettingsSelfDeletingStatus.enforcedSelfDeletionTimer) { + when (this) { + TeamSelfDeleteTimer.Disabled, + TeamSelfDeleteTimer.Enabled -> SelfDeletionDuration.None + + is TeamSelfDeleteTimer.Enforced -> this.enforcedDuration.toSelfDeletionDuration() + } } - } - featureFlagState = featureFlagState.copy( - areSelfDeletedMessagesEnabled = areSelfDeletedMessagesEnabled, - shouldShowSelfDeletingMessagesDialog = shouldShowSelfDeletingMessagesDialog, - enforcedTimeoutDuration = enforcedTimeoutDuration - ) - } + featureFlagState = featureFlagState.copy( + areSelfDeletedMessagesEnabled = areSelfDeletedMessagesEnabled, + shouldShowSelfDeletingMessagesDialog = shouldShowSelfDeletingMessagesDialog, + enforcedTimeoutDuration = enforcedTimeoutDuration + ) + } } } @@ -186,8 +188,14 @@ class FeatureFlagNotificationViewModel @Inject constructor( val state = when (result) { E2EIRequiredResult.NoGracePeriod.Create -> FeatureFlagState.E2EIRequired.NoGracePeriod.Create E2EIRequiredResult.NoGracePeriod.Renew -> FeatureFlagState.E2EIRequired.NoGracePeriod.Renew - is E2EIRequiredResult.WithGracePeriod.Create -> FeatureFlagState.E2EIRequired.WithGracePeriod.Create(result.timeLeft) - is E2EIRequiredResult.WithGracePeriod.Renew -> FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(result.timeLeft) + is E2EIRequiredResult.WithGracePeriod.Create -> FeatureFlagState.E2EIRequired.WithGracePeriod.Create( + result.timeLeft + ) + + is E2EIRequiredResult.WithGracePeriod.Renew -> FeatureFlagState.E2EIRequired.WithGracePeriod.Renew( + result.timeLeft + ) + E2EIRequiredResult.NotRequired -> null } featureFlagState = featureFlagState.copy(e2EIRequired = state) @@ -197,7 +205,9 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissSelfDeletingMessagesDialog() { featureFlagState = featureFlagState.copy(shouldShowSelfDeletingMessagesDialog = false) viewModelScope.launch { - currentUserId?.let { coreLogic.getSessionScope(it).markSelfDeletingMessagesAsNotified() } + currentUserId?.let { + coreLogic.getSessionScope(it).markSelfDeletingMessagesAsNotified() + } } } @@ -210,7 +220,9 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissGuestRoomLinkDialog() { viewModelScope.launch { - currentUserId?.let { coreLogic.getSessionScope(it).markGuestLinkFeatureFlagAsNotChanged() } + currentUserId?.let { + coreLogic.getSessionScope(it).markGuestLinkFeatureFlagAsNotChanged() + } } featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = false) } @@ -218,8 +230,14 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissTeamAppLockDialog() { featureFlagState = featureFlagState.copy(shouldShowTeamAppLockDialog = false) } + fun markTeamAppLockStatusAsNot() { - markTeamAppLockStatusAsNotified() + viewModelScope.launch { + val currentSession = currentSessionUseCase() + if (currentSessionUseCase() is CurrentSessionResult.Success) { + coreLogic.getSessionScope((currentSession as CurrentSessionResult.Success).accountInfo.userId).markTeamAppLockStatusAsNotified() + } + } } fun clearTeamAppLockPasscode() { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 4a6a3f1ec4b..62fb66a6e92 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -9,7 +9,6 @@ import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.configuration.GuestRoomLinkStatus import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.AccountInfo import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase @@ -221,16 +220,12 @@ class FeatureFlagNotificationViewModelTest { @MockK lateinit var markE2EIRequiredAsNotified: MarkEnablingE2EIAsNotifiedUseCase - @MockK - lateinit var markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase - @MockK lateinit var globalDataStore: GlobalDataStore val viewModel: FeatureFlagNotificationViewModel = FeatureFlagNotificationViewModel( coreLogic = coreLogic, currentSessionUseCase = currentSession, - markTeamAppLockStatusAsNotified = markTeamAppLockStatusAsNotified, globalDataStore = globalDataStore ) From 6aab2f445a105962e38afcc891f5d13143c1bb55 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 14:33:07 +0100 Subject: [PATCH 15/25] chore: make context private --- .../main/kotlin/com/wire/android/datastore/GlobalDataStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index 0c0b382defa..c43e1f3bb6e 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -42,7 +42,7 @@ import javax.inject.Singleton @Suppress("TooManyFunctions") @Singleton -class GlobalDataStore @Inject constructor(@ApplicationContext val context: Context) { +class GlobalDataStore @Inject constructor(@ApplicationContext private val context: Context) { companion object { private const val PREFERENCES_NAME = "global_data" From 82fba4ca3f312dca4741e3ef81d5c141870919f2 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 14:56:16 +0100 Subject: [PATCH 16/25] chore: cleanup --- .../ui/home/sync/FeatureFlagNotificationViewModel.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 6b15214ce32..b0cc6dbc805 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -41,6 +41,7 @@ import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -144,14 +145,6 @@ class FeatureFlagNotificationViewModel @Inject constructor( isTeamAppLockEnabled = it.isEnabled, shouldShowTeamAppLockDialog = isStatusChanged ) - } ?: run { - // to handle the case of first app install with team app lock enabled - if (it.isEnabled && !globalDataStore.isAppTeamPasscodeSetFlow().first()) { - featureFlagState = featureFlagState.copy( - isTeamAppLockEnabled = true, - shouldShowTeamAppLockDialog = true - ) - } } } } From f706db1eb4b641f1a2188a776bd36b0a40dec67f Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 15:13:05 +0100 Subject: [PATCH 17/25] chore: detekt --- .../ui/home/sync/FeatureFlagNotificationViewModel.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index b0cc6dbc805..f6d28386188 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -35,16 +35,13 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSelfDeleteTimer import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.E2EIRequiredResult import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @@ -228,7 +225,9 @@ class FeatureFlagNotificationViewModel @Inject constructor( viewModelScope.launch { val currentSession = currentSessionUseCase() if (currentSessionUseCase() is CurrentSessionResult.Success) { - coreLogic.getSessionScope((currentSession as CurrentSessionResult.Success).accountInfo.userId).markTeamAppLockStatusAsNotified() + coreLogic.getSessionScope( + (currentSession as CurrentSessionResult.Success).accountInfo.userId + ).markTeamAppLockStatusAsNotified() } } } From 72ab4122811047bd433f7a362badb83d15e013e1 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 15:49:24 +0100 Subject: [PATCH 18/25] feat: handle app lock with team applock --- .../com/wire/android/ui/WireActivity.kt | 141 ++++++++++-------- 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 7e07ea7b00f..e9259b3d4ca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -249,20 +249,6 @@ class WireActivity : AppCompatActivity() { private fun handleDialogs(navigate: (NavigationCommand) -> Unit) { featureFlagNotificationViewModel.loadInitialSync() with(featureFlagNotificationViewModel.featureFlagState) { - if (showFileSharingDialog) { - FileRestrictionDialog( - isFileSharingEnabled = isFileSharingEnabledState, - hideDialogStatus = featureFlagNotificationViewModel::dismissFileSharingDialog - ) - } - - if (shouldShowGuestRoomLinkDialog) { - GuestRoomLinkFeatureFlagDialog( - isGuestRoomLinkEnabled = isGuestRoomLinkEnabled, - onDismiss = featureFlagNotificationViewModel::dismissGuestRoomLinkDialog - ) - } - if (shouldShowTeamAppLockDialog) { TeamAppLockFeatureFlagDialog( isTeamAppLockEnabled = isTeamAppLockEnabled, @@ -290,63 +276,88 @@ class WireActivity : AppCompatActivity() { } } ) - } + } else { + if (showFileSharingDialog) { + FileRestrictionDialog( + isFileSharingEnabled = isFileSharingEnabledState, + hideDialogStatus = featureFlagNotificationViewModel::dismissFileSharingDialog + ) + } - if (shouldShowSelfDeletingMessagesDialog) { - SelfDeletingMessagesDialog( - areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled, - enforcedTimeout = enforcedTimeoutDuration, - hideDialogStatus = featureFlagNotificationViewModel::dismissSelfDeletingMessagesDialog - ) - } + if (shouldShowGuestRoomLinkDialog) { + GuestRoomLinkFeatureFlagDialog( + isGuestRoomLinkEnabled = isGuestRoomLinkEnabled, + onDismiss = featureFlagNotificationViewModel::dismissGuestRoomLinkDialog + ) + } - e2EIRequired?.let { - E2EIRequiredDialog( - result = e2EIRequired, - getCertificate = featureFlagNotificationViewModel::getE2EICertificate, - snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog - ) - } - e2EISnoozeInfo?.let { - E2EISnoozeDialog( - timeLeft = e2EISnoozeInfo.timeLeft, - dismissDialog = featureFlagNotificationViewModel::dismissSnoozeE2EIdRequiredDialog + + if (shouldShowSelfDeletingMessagesDialog) { + SelfDeletingMessagesDialog( + areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled, + enforcedTimeout = enforcedTimeoutDuration, + hideDialogStatus = featureFlagNotificationViewModel::dismissSelfDeletingMessagesDialog + ) + } + + e2EIRequired?.let { + E2EIRequiredDialog( + result = e2EIRequired, + getCertificate = featureFlagNotificationViewModel::getE2EICertificate, + snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog + ) + } + + e2EISnoozeInfo?.let { + E2EISnoozeDialog( + timeLeft = e2EISnoozeInfo.timeLeft, + dismissDialog = featureFlagNotificationViewModel::dismissSnoozeE2EIdRequiredDialog + ) + } + + UpdateAppDialog(viewModel.globalAppState.updateAppDialog, ::updateTheApp) + JoinConversationDialog( + viewModel.globalAppState.conversationJoinedDialog, + navigate, + viewModel::onJoinConversationFlowCompleted + ) + CustomBackendDialog( + viewModel.globalAppState, + viewModel::dismissCustomBackendDialog + ) { + viewModel.customBackendDialogProceedButtonClicked { + navigate( + NavigationCommand( + WelcomeScreenDestination + ) + ) + } + } + MaxAccountDialog( + shouldShow = viewModel.globalAppState.maxAccountDialog, + onConfirm = { + viewModel.dismissMaxAccountDialog() + navigate(NavigationCommand(SelfUserProfileScreenDestination)) + }, + onDismiss = viewModel::dismissMaxAccountDialog + ) + AccountLoggedOutDialog( + viewModel.globalAppState.blockUserUI + ) { viewModel.tryToSwitchAccount(NavigationSwitchAccountActions(navigate)) } + NewClientDialog( + viewModel.globalAppState.newClientDialog, + { navigate(NavigationCommand(SelfDevicesScreenDestination)) }, + { + viewModel.switchAccount( + userId = it, + actions = NavigationSwitchAccountActions(navigate), + onComplete = { navigate(NavigationCommand(SelfDevicesScreenDestination)) }) + }, + viewModel::dismissNewClientsDialog ) } } - UpdateAppDialog(viewModel.globalAppState.updateAppDialog, ::updateTheApp) - JoinConversationDialog( - viewModel.globalAppState.conversationJoinedDialog, - navigate, - viewModel::onJoinConversationFlowCompleted - ) - CustomBackendDialog( - viewModel.globalAppState, - viewModel::dismissCustomBackendDialog - ) { viewModel.customBackendDialogProceedButtonClicked { navigate(NavigationCommand(WelcomeScreenDestination)) } } - MaxAccountDialog( - shouldShow = viewModel.globalAppState.maxAccountDialog, - onConfirm = { - viewModel.dismissMaxAccountDialog() - navigate(NavigationCommand(SelfUserProfileScreenDestination)) - }, - onDismiss = viewModel::dismissMaxAccountDialog - ) - AccountLoggedOutDialog( - viewModel.globalAppState.blockUserUI - ) { viewModel.tryToSwitchAccount(NavigationSwitchAccountActions(navigate)) } - NewClientDialog( - viewModel.globalAppState.newClientDialog, - { navigate(NavigationCommand(SelfDevicesScreenDestination)) }, - { - viewModel.switchAccount( - userId = it, - actions = NavigationSwitchAccountActions(navigate), - onComplete = { navigate(NavigationCommand(SelfDevicesScreenDestination)) }) - }, - viewModel::dismissNewClientsDialog - ) } private fun updateTheApp() { From d4ace4e7e282577ccea2aeec18996e19b2dcbb13 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 16:20:50 +0100 Subject: [PATCH 19/25] feat: cleanup --- .../com/wire/android/datastore/GlobalDataStore.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index c43e1f3bb6e..7dace77b5e1 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -148,16 +148,18 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex * returns a flow with decoded passcode */ @Suppress("TooGenericExceptionCaught") - fun getAppLockPasscodeFlow(): Flow = - context.dataStore.data.map { - it[APP_LOCK_PASSCODE]?.let { passcode -> + fun getAppLockPasscodeFlow(): Flow { + val preference = if(isAppLockPasscodeSet()) APP_LOCK_PASSCODE else TEAM_APP_LOCK_PASSCODE + return context.dataStore.data.map { + it[preference]?.let { passcode -> try { - EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, passcode) + EncryptionManager.decrypt(preference.name, passcode) } catch (e: Exception) { null } } } + } /** * returns a flow only informing whether the passcode is set, without the need to decode it @@ -173,11 +175,6 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex }.first() } - fun isAppTeamPasscodeSetFlow(): Flow = - context.dataStore.data.map { - it.contains(TEAM_APP_LOCK_PASSCODE) - } - fun isAppTeamPasscodeSet(): Boolean = runBlocking { context.dataStore.data.map { it.contains(TEAM_APP_LOCK_PASSCODE) From ebc5f0f0d7d3f8ac050bc4f76f121d97d06cc9fa Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 16:31:38 +0100 Subject: [PATCH 20/25] feat: detekt --- .../wire/android/datastore/GlobalDataStore.kt | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index 7dace77b5e1..d1008acf9c5 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -48,17 +48,21 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex private const val PREFERENCES_NAME = "global_data" // keys - private val SHOW_CALLING_DOUBLE_TAP_TOAST = booleanPreferencesKey("show_calling_double_tap_toast_") + private val SHOW_CALLING_DOUBLE_TAP_TOAST = + booleanPreferencesKey("show_calling_double_tap_toast_") private val MIGRATION_COMPLETED = booleanPreferencesKey("migration_completed") private val WELCOME_SCREEN_PRESENTED = booleanPreferencesKey("welcome_screen_presented") private val IS_LOGGING_ENABLED = booleanPreferencesKey("is_logging_enabled") - private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = booleanPreferencesKey("is_encrypted_proteus_storage_enabled") + private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = + booleanPreferencesKey("is_encrypted_proteus_storage_enabled") private val IS_APP_LOCKED_BY_USER = booleanPreferencesKey("is_app_locked_by_user") private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") private val TEAM_APP_LOCK_PASSCODE = stringPreferencesKey("team_app_lock_passcode") val APP_THEME_OPTION = stringPreferencesKey("app_theme_option") private val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME) - private fun userMigrationStatusKey(userId: String): Preferences.Key = intPreferencesKey("user_migration_status_$userId") + private fun userMigrationStatusKey(userId: String): Preferences.Key = + intPreferencesKey("user_migration_status_$userId") + private fun userDoubleTapToastStatusKey(userId: String): Preferences.Key = booleanPreferencesKey("$SHOW_CALLING_DOUBLE_TAP_TOAST$userId") @@ -73,21 +77,28 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex fun getBooleanPreference(key: Preferences.Key, defaultValue: Boolean): Flow = context.dataStore.data.map { it[key] ?: defaultValue } - private fun getStringPreference(key: Preferences.Key, defaultValue: String): Flow = + private fun getStringPreference( + key: Preferences.Key, + defaultValue: String + ): Flow = context.dataStore.data.map { it[key] ?: defaultValue } fun isMigrationCompletedFlow(): Flow = getBooleanPreference(MIGRATION_COMPLETED, false) suspend fun isMigrationCompleted(): Boolean = isMigrationCompletedFlow().firstOrNull() ?: false - fun isLoggingEnabled(): Flow = getBooleanPreference(IS_LOGGING_ENABLED, BuildConfig.LOGGING_ENABLED) + fun isLoggingEnabled(): Flow = + getBooleanPreference(IS_LOGGING_ENABLED, BuildConfig.LOGGING_ENABLED) suspend fun setLoggingEnabled(enabled: Boolean) { context.dataStore.edit { it[IS_LOGGING_ENABLED] = enabled } } fun isEncryptedProteusStorageEnabled(): Flow = - getBooleanPreference(IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED, BuildConfig.ENCRYPT_PROTEUS_STORAGE) + getBooleanPreference( + IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED, + BuildConfig.ENCRYPT_PROTEUS_STORAGE + ) suspend fun setEncryptedProteusStorageEnabled(enabled: Boolean) { context.dataStore.edit { it[IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED] = enabled } @@ -113,7 +124,10 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex when (status) { UserMigrationStatus.Completed, UserMigrationStatus.CompletedWithErrors, - UserMigrationStatus.Successfully -> setUserMigrationAppVersion(userId, BuildConfig.VERSION_CODE) + UserMigrationStatus.Successfully -> setUserMigrationAppVersion( + userId, + BuildConfig.VERSION_CODE + ) UserMigrationStatus.NoNeed, UserMigrationStatus.NotStarted -> { @@ -128,7 +142,13 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex * meaning that the user does not need to be migrated. */ fun getUserMigrationStatus(userId: String): Flow = - context.dataStore.data.map { it[userMigrationStatusKey(userId)]?.let { status -> UserMigrationStatus.fromInt(status) } } + context.dataStore.data.map { + it[userMigrationStatusKey(userId)]?.let { status -> + UserMigrationStatus.fromInt( + status + ) + } + } suspend fun setUserMigrationAppVersion(userId: String, version: Int) { context.dataStore.edit { it[userLastMigrationAppVersion(userId)] = version } @@ -149,7 +169,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex */ @Suppress("TooGenericExceptionCaught") fun getAppLockPasscodeFlow(): Flow { - val preference = if(isAppLockPasscodeSet()) APP_LOCK_PASSCODE else TEAM_APP_LOCK_PASSCODE + val preference = if (isAppLockPasscodeSet()) APP_LOCK_PASSCODE else TEAM_APP_LOCK_PASSCODE return context.dataStore.data.map { it[preference]?.let { passcode -> try { @@ -186,6 +206,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex it.remove(APP_LOCK_PASSCODE) } } + suspend fun clearTeamAppLockPasscode() { context.dataStore.edit { it.remove(TEAM_APP_LOCK_PASSCODE) @@ -229,6 +250,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex context.dataStore.edit { it[APP_THEME_OPTION] = option.toString() } } - fun selectedThemeOptionFlow(): Flow = getStringPreference(APP_THEME_OPTION, ThemeOption.SYSTEM.toString()) - .map { ThemeOption.valueOf(it) } + fun selectedThemeOptionFlow(): Flow = + getStringPreference(APP_THEME_OPTION, ThemeOption.SYSTEM.toString()) + .map { ThemeOption.valueOf(it) } } From 023b5ba9713fc39c3556865e5b0015fa716ab38e Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 16:53:22 +0100 Subject: [PATCH 21/25] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index eb56d62d323..6473f76f841 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit eb56d62d323f9878e31f9fe0c16f3a97a1f796c6 +Subproject commit 6473f76f8410b9b9b12be8bc2e091b36b137b473 From ec57ca3713980782d6b351715f38f7b6904fc824 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 2 Nov 2023 17:06:33 +0100 Subject: [PATCH 22/25] feat: detekt --- app/src/main/kotlin/com/wire/android/ui/WireActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index e9259b3d4ca..86833863873 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -291,8 +291,6 @@ class WireActivity : AppCompatActivity() { ) } - - if (shouldShowSelfDeletingMessagesDialog) { SelfDeletingMessagesDialog( areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled, From 9b76be03e5bd404c46809a19bb5943e121d240c7 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 8 Nov 2023 14:46:06 +0100 Subject: [PATCH 23/25] chore: detekt --- .../com/wire/android/ui/WireActivity.kt | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index de713e123e0..86833863873 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -291,34 +291,7 @@ class WireActivity : AppCompatActivity() { ) } - if (shouldShowTeamAppLockDialog) { - TeamAppLockFeatureFlagDialog( - isTeamAppLockEnabled = isTeamAppLockEnabled, - onConfirm = { - featureFlagNotificationViewModel.dismissTeamAppLockDialog() - if (isTeamAppLockEnabled) { - val isUserAppLockSet = - featureFlagNotificationViewModel.isUserAppLockSet() - // No need to setup another app lock if the user already has one - if (!isUserAppLockSet) { - Intent(this@WireActivity, AppLockActivity::class.java) - .apply { - putExtra(AppLockActivity.SET_TEAM_APP_LOCK, true) - }.also { - startActivity(it) - } - } else { - featureFlagNotificationViewModel.markTeamAppLockStatusAsNot() - } - } else { - with(featureFlagNotificationViewModel) { - markTeamAppLockStatusAsNot() - clearTeamAppLockPasscode() - } - } - } - ) - }if (shouldShowSelfDeletingMessagesDialog) { + if (shouldShowSelfDeletingMessagesDialog) { SelfDeletingMessagesDialog( areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled, enforcedTimeout = enforcedTimeoutDuration, From 701add10d1b5fc58d2ac775caea66f63e040a292 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 8 Nov 2023 14:54:18 +0100 Subject: [PATCH 24/25] chore: duplicate strings --- app/src/main/res/values/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 650f9759ebb..a474e5c4eb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -960,8 +960,6 @@ Turn Off App lock is now mandatory. Wire will lock itself after a certain time of inactivity. To unlock the app, you need to enter a passcode or use biometric authentication. App lock is not mandatory any more. Wire will no longer lock itself after a certain time of inactivity. - App lock is now mandatory. Wire will lock itself after a certain time of inactivity. To unlock the app, you need to enter a passcode or use biometric authentication. - App lock is not mandatory any more. Wire will no longer lock itself after a certain time of inactivity. Forgot your app lock passcode? The data stored on this device can only be accessed with your app lock passcode. If you have forgotten your passcode, you can reset this device. By resetting your device, all local data and messages for this account will be permanently deleted. From 06400fdd1eac22aafabfab210d6c957460012bb0 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 8 Nov 2023 14:56:14 +0100 Subject: [PATCH 25/25] chore: resolve conflicts --- .../com/wire/android/ui/AppLockActivity.kt | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt index 598a2c3e6df..09a9225860f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -49,36 +49,31 @@ class AppLockActivity : AppCompatActivity() { WireTheme { val navigator = rememberNavigator(this@AppLockActivity::finish) - val startDestination = - if (intent.getBooleanExtra(SET_TEAM_APP_LOCK, false)) { - appLogger.i("appLock: requesting set team app lock") - SetLockCodeScreenDestination - } else { - val canAuthenticateWithBiometrics = BiometricManager - .from(this) - .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) - if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { - appLogger.i("appLock: requesting app Unlock with biometrics") - AppUnlockWithBiometricsScreenDestination + val startDestination = + if (intent.getBooleanExtra(SET_TEAM_APP_LOCK, false)) { + appLogger.i("appLock: requesting set team app lock") + SetLockCodeScreenDestination } else { - appLogger.i("appLock: requesting app Unlock with passcode") - EnterLockCodeScreenDestination + val canAuthenticateWithBiometrics = BiometricManager + .from(this) + .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) { + appLogger.i("appLock: requesting app Unlock with biometrics") + AppUnlockWithBiometricsScreenDestination + } else { + appLogger.i("appLock: requesting app Unlock with passcode") + EnterLockCodeScreenDestination + } } - } - NavigationGraph( - navigator = navigator, - startDestination = startDestination - ) + NavigationGraph( + navigator = navigator, + startDestination = startDestination + ) + } } } } - - companion object { - const val SET_TEAM_APP_LOCK = "set_team_app_lock" - } -} - companion object { const val SET_TEAM_APP_LOCK = "set_team_app_lock" }