Skip to content

Commit

Permalink
feat: Show team applock change dialog (WPB-4476) (#2388)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohassine authored Nov 8, 2023
1 parent 162eded commit 1e614ca
Show file tree
Hide file tree
Showing 21 changed files with 384 additions and 86 deletions.
109 changes: 87 additions & 22 deletions app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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

Expand All @@ -47,15 +48,20 @@ 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 APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode")
private val APP_THEME_OPTION = stringPreferencesKey("app_theme_option")
private val TEAM_APP_LOCK_PASSCODE = stringPreferencesKey("team_app_lock_passcode")
val APP_THEME_OPTION = stringPreferencesKey("app_theme_option")
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCES_NAME)
private fun userMigrationStatusKey(userId: String): Preferences.Key<Int> = intPreferencesKey("user_migration_status_$userId")
private fun userMigrationStatusKey(userId: String): Preferences.Key<Int> =
intPreferencesKey("user_migration_status_$userId")

private fun userDoubleTapToastStatusKey(userId: String): Preferences.Key<Boolean> =
booleanPreferencesKey("$SHOW_CALLING_DOUBLE_TAP_TOAST$userId")

Expand All @@ -67,24 +73,31 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
context.dataStore.edit { it.clear() }
}

private fun getBooleanPreference(key: Preferences.Key<Boolean>, defaultValue: Boolean): Flow<Boolean> =
fun getBooleanPreference(key: Preferences.Key<Boolean>, defaultValue: Boolean): Flow<Boolean> =
context.dataStore.data.map { it[key] ?: defaultValue }

private fun getStringPreference(key: Preferences.Key<String>, defaultValue: String): Flow<String> =
private fun getStringPreference(
key: Preferences.Key<String>,
defaultValue: String
): Flow<String> =
context.dataStore.data.map { it[key] ?: defaultValue }

fun isMigrationCompletedFlow(): Flow<Boolean> = getBooleanPreference(MIGRATION_COMPLETED, false)

suspend fun isMigrationCompleted(): Boolean = isMigrationCompletedFlow().firstOrNull() ?: false

fun isLoggingEnabled(): Flow<Boolean> = getBooleanPreference(IS_LOGGING_ENABLED, BuildConfig.LOGGING_ENABLED)
fun isLoggingEnabled(): Flow<Boolean> =
getBooleanPreference(IS_LOGGING_ENABLED, BuildConfig.LOGGING_ENABLED)

suspend fun setLoggingEnabled(enabled: Boolean) {
context.dataStore.edit { it[IS_LOGGING_ENABLED] = enabled }
}

fun isEncryptedProteusStorageEnabled(): Flow<Boolean> =
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 }
Expand All @@ -110,7 +123,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 -> {
Expand All @@ -125,7 +141,13 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
* meaning that the user does not need to be migrated.
*/
fun getUserMigrationStatus(userId: String): Flow<UserMigrationStatus?> =
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 }
Expand All @@ -141,47 +163,90 @@ 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
/**
* returns a flow with decoded passcode
*/
@Suppress("TooGenericExceptionCaught")
fun getAppLockPasscodeFlow(): Flow<String?> =
context.dataStore.data.map {
it[APP_LOCK_PASSCODE]?.let {
fun getAppLockPasscodeFlow(): Flow<String?> {
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, it)
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
/**
* returns a flow only informing whether the passcode is set, without the need to decode it
*/
fun isAppLockPasscodeSetFlow(): Flow<Boolean> =
context.dataStore.data.map {
it.contains(APP_LOCK_PASSCODE)
}

fun isAppLockPasscodeSet(): Boolean = runBlocking {
context.dataStore.data.map {
it.contains(APP_LOCK_PASSCODE)
}.first()
}

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)
}
}

@Suppress("TooGenericExceptionCaught")
suspend fun setAppLockPasscode(passcode: String) {
private suspend fun setAppLockPasscode(
passcode: String,
key: Preferences.Key<String>
) {
context.dataStore.edit {
try {
val encrypted = EncryptionManager.encrypt(APP_LOCK_PASSCODE.name, passcode)
it[APP_LOCK_PASSCODE] = encrypted
val encrypted =
EncryptionManager.encrypt(key.name, passcode)
it[key] = encrypted
} catch (e: Exception) {
it.remove(APP_LOCK_PASSCODE)
it.remove(key)
}
}
}

suspend fun setTeamAppLock(
passcode: String,
key: Preferences.Key<String> = TEAM_APP_LOCK_PASSCODE
) {
setAppLockPasscode(passcode, key)
}

suspend fun setUserAppLock(
passcode: String,
key: Preferences.Key<String> = APP_LOCK_PASSCODE
) {
setAppLockPasscode(passcode, key)
}

suspend fun setThemeOption(option: ThemeOption) {
context.dataStore.edit { it[APP_THEME_OPTION] = option.toString() }
}

fun selectedThemeOptionFlow(): Flow<ThemeOption> = getStringPreference(APP_THEME_OPTION, ThemeOption.SYSTEM.toString())
.map { ThemeOption.valueOf(it) }
fun selectedThemeOptionFlow(): Flow<ThemeOption> =
getStringPreference(APP_THEME_OPTION, ThemeOption.SYSTEM.toString())
.map { ThemeOption.valueOf(it) }
}
5 changes: 5 additions & 0 deletions app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ class UseCaseModule {
fun provideMarkGuestLinkFeatureFlagAsNotChangedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
coreLogic.getSessionScope(currentAccount).markGuestLinkFeatureFlagAsNotChanged

@ViewModelScoped
@Provides
fun provideMarkTeamAppLockStatusAsNotifiedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) =
coreLogic.getSessionScope(currentAccount).markTeamAppLockStatusAsNotified

@ViewModelScoped
@Provides
fun provideGetOtherUserSecurityClassificationLabelUseCase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
19 changes: 14 additions & 5 deletions app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.wire.android.navigation.rememberNavigator
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
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

Expand All @@ -46,20 +47,24 @@ class AppLockActivity : AppCompatActivity() {
LocalActivity provides this
) {
WireTheme {
val canAuthenticateWithBiometrics = BiometricManager
.from(this)
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)

val navigator = rememberNavigator(this@AppLockActivity::finish)

val startDestination =
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
} else {
appLogger.i("appLock: requesting app Unlock with passcode")
EnterLockCodeScreenDestination
}
}

NavigationGraph(
navigator = navigator,
Expand All @@ -69,4 +74,8 @@ class AppLockActivity : AppCompatActivity() {
}
}
}

companion object {
const val SET_TEAM_APP_LOCK = "set_team_app_lock"
}
}
29 changes: 29 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,35 @@ 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) {
SelfDeletingMessagesDialog(
areSelfDeletingMessagesEnabled = areSelfDeletedMessagesEnabled,
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ fun GuestRoomLinkFeatureFlagDialog(
)
}

@Composable
fun TeamAppLockFeatureFlagDialog(
isTeamAppLockEnabled: Boolean,
onConfirm: () -> 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 = {},
optionButton1Properties = WireDialogButtonProperties(
onClick = onConfirm,
text = stringResource(id = R.string.label_ok),
type = WireDialogButtonType.Primary,
)
)
}

@Composable
fun UpdateAppDialog(shouldShow: Boolean, onUpdateClick: () -> Unit) {
if (shouldShow) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ fun WireDialog(
)
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun WireDialog(
title: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +45,7 @@ class LockCodeTimeManager @Inject constructor(
@ApplicationScope private val appCoroutineScope: CoroutineScope,
currentScreenManager: CurrentScreenManager,
observeAppLockConfigUseCase: ObserveAppLockConfigUseCase,
globalDataStore: GlobalDataStore
) {

private val isLockedFlow = MutableStateFlow(false)
Expand All @@ -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
}
}
Expand All @@ -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 -> {
Expand Down
Loading

0 comments on commit 1e614ca

Please sign in to comment.