Skip to content

Commit

Permalink
fix: app lock can be changes when enforced and it is cleared when not… (
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamadJaara committed Nov 21, 2023
1 parent 1e14fbe commit 97a8a94
Show file tree
Hide file tree
Showing 17 changed files with 131 additions and 135 deletions.
28 changes: 3 additions & 25 deletions app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
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")
private val APP_LOCK_PASSCODE = stringPreferencesKey("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> =
Expand Down Expand Up @@ -169,11 +167,10 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
*/
@Suppress("TooGenericExceptionCaught")
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 ->
it[APP_LOCK_PASSCODE]?.let { passcode ->
try {
EncryptionManager.decrypt(preference.name, passcode)
EncryptionManager.decrypt(APP_LOCK_PASSCODE.name, passcode)
} catch (e: Exception) {
null
}
Expand All @@ -195,24 +192,12 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
}.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")
private suspend fun setAppLockPasscode(
passcode: String,
Expand All @@ -229,13 +214,6 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
}
}

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
Expand Down
7 changes: 7 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 @@ -35,6 +35,7 @@ import com.wire.kalium.logic.feature.connection.BlockUserUseCase
import com.wire.kalium.logic.feature.connection.UnblockUserUseCase
import com.wire.kalium.logic.feature.conversation.ObserveOtherUserSecurityClassificationLabelUseCase
import com.wire.kalium.logic.feature.conversation.ObserveSecurityClassificationLabelUseCase
import com.wire.kalium.logic.feature.featureConfig.IsAppLockEditableUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveTeamSettingsSelfDeletingStatusUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase
Expand Down Expand Up @@ -427,4 +428,10 @@ class UseCaseModule {
@KaliumCoreLogic coreLogic: CoreLogic,
@CurrentAccount currentAccount: UserId
): ObserveScreenshotCensoringConfigUseCase = coreLogic.getSessionScope(currentAccount).observeScreenshotCensoringConfig

@ViewModelScoped
@Provides
fun provideIsAppLockEditableUseCase(
@KaliumCoreLogic coreLogic: CoreLogic
): IsAppLockEditableUseCase = coreLogic.getGlobalScope().isAppLockEditableUseCase
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ class KaliumConfigsModule {
wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE,
isWebSocketEnabledByDefault = isWebsocketEnabledByDefault(context),
certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG,
teamAppLock = BuildConfig.TEAM_APP_LOCK,
teamAppLockTimeout = BuildConfig.TEAM_APP_LOCK_TIMEOUT
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ 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
Expand All @@ -30,6 +29,7 @@ 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(
Expand All @@ -41,7 +41,7 @@ class ObserveAppLockConfigUseCase @Inject constructor(
operator fun invoke(): Flow<AppLockConfig> = channelFlow {
when (val currentSession = currentSession()) {
is CurrentSessionResult.Failure -> {
send(AppLockConfig.Disabled(DEFAULT_TIMEOUT))
send(AppLockConfig.Disabled(DEFAULT_APP_LOCK_TIMEOUT))
}

is CurrentSessionResult.Success -> {
Expand All @@ -51,14 +51,18 @@ class ObserveAppLockConfigUseCase @Inject constructor(

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(isAppLockedByTeam.timeout))
) { teamAppLockConfig, isAppLockConfigured ->
when {
isAppLockConfigured -> {
emit(AppLockConfig.Enabled(teamAppLockConfig?.timeout ?: DEFAULT_APP_LOCK_TIMEOUT))
}

teamAppLockConfig != null && teamAppLockConfig.isEnforced -> {
emit(AppLockConfig.EnforcedByTeam(teamAppLockConfig.timeout))
}

else -> {
emit(AppLockConfig.Disabled(teamAppLockConfig?.timeout ?: DEFAULT_APP_LOCK_TIMEOUT))
}
}
}.collectLatest {
Expand All @@ -67,9 +71,13 @@ class ObserveAppLockConfigUseCase @Inject constructor(
}
}
}

companion object {
val DEFAULT_APP_LOCK_TIMEOUT = 60.seconds
}
}

sealed class AppLockConfig(open val timeout: Duration = DEFAULT_TIMEOUT) {
sealed class AppLockConfig(open val timeout: Duration) {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class PersistentWebSocketService : Service() {
@Inject
lateinit var notificationManager: WireNotificationManager

// TODO: remove since it is not used
@Inject
@CurrentSessionFlowService
lateinit var currentSessionFlow: CurrentSessionFlowUseCase
Expand Down
1 change: 0 additions & 1 deletion app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ class WireActivity : AppCompatActivity() {
} else {
with(featureFlagNotificationViewModel) {
markTeamAppLockStatusAsNot()
clearTeamAppLockPasscode()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,31 @@ import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton

/**
* AppLockManager provides a mechanism to determine if the app should be locked based on configuration and screen state.
*
* - [isLockedFlow] observes conditions and returns:
* - false if app lock is disabled.
* - true after a background delay if app lock is enabled.
* - false if brought back to the foreground before the delay.
*/

@Singleton
class LockCodeTimeManager @Inject constructor(
@ApplicationScope private val appCoroutineScope: CoroutineScope,
currentScreenManager: CurrentScreenManager,
observeAppLockConfigUseCase: ObserveAppLockConfigUseCase,
globalDataStore: GlobalDataStore
globalDataStore: GlobalDataStore,
) {

private val isLockedFlow = MutableStateFlow(false)
private lateinit var isLockedFlow: MutableStateFlow<Boolean>

init {
// first, set initial value - if app lock is enabled then app needs to be locked right away
runBlocking {
observeAppLockConfigUseCase().firstOrNull()?.let { appLockConfig ->
// 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
}
}
val initialValue = globalDataStore.isAppLockPasscodeSetFlow().firstOrNull() ?: false
isLockedFlow = MutableStateFlow(initialValue)
}
@Suppress("MagicNumber")

// next, listen for app lock config and app visibility changes to determine if app should be locked
appCoroutineScope.launch {
combine(
Expand All @@ -72,29 +74,27 @@ class LockCodeTimeManager @Inject constructor(
)
.distinctUntilChanged()
.flatMapLatest { (appLockConfig, isInForeground) ->
when {
appLockConfig is AppLockConfig.Disabled -> flowOf(false)
when {
appLockConfig is AppLockConfig.Disabled -> flowOf(false)

!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 should be locked if passcode is set")

!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 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)
if (appLockConfig is AppLockConfig.Enabled) {
emit(true)
}
}
}

else -> {
appLogger.i("$TAG no change to lock state, isInForeground: $isInForeground, isLocked: ${isLockedFlow.value}")
emptyFlow()
else -> {
appLogger.i("$TAG no change to lock state, isInForeground: $isInForeground, isLocked: ${isLockedFlow.value}")
emptyFlow()
}
}
}.collectLatest {
isLockedFlow.value = it
}
}.collectLatest {
isLockedFlow.value = it
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
package com.wire.android.ui.home.appLock.set

import androidx.compose.ui.text.input.TextFieldValue
import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl.Companion.DEFAULT_TIMEOUT
import com.wire.android.feature.ObserveAppLockConfigUseCase

import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import kotlin.time.Duration

data class SetLockCodeViewState(
val continueEnabled: Boolean = false,
val password: TextFieldValue = TextFieldValue(),
val passwordValidation: ValidatePasswordResult = ValidatePasswordResult.Invalid(),
val timeout: Duration = DEFAULT_TIMEOUT,
val isAppLockByUser: Boolean = true,
val timeout: Duration = ObserveAppLockConfigUseCase.DEFAULT_APP_LOCK_TIMEOUT,
val done: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
Expand Down Expand Up @@ -53,8 +52,7 @@ class SetLockScreenViewModel @Inject constructor(
observeAppLockConfigUseCase()
.collectLatest {
state = state.copy(
timeout = it.timeout,
isAppLockByUser = it !is AppLockConfig.EnforcedByTeam
timeout = it.timeout
)
}
}
Expand Down Expand Up @@ -82,11 +80,9 @@ class SetLockScreenViewModel @Inject constructor(
viewModelScope.launch {
withContext(dispatchers.io()) {
with(globalDataStore) {
if (state.isAppLockByUser) {
setUserAppLock(state.password.text.sha256())
} else {
setTeamAppLock(state.password.text.sha256())
}
setUserAppLock(state.password.text.sha256())

// TODO: call only when needed
markTeamAppLockStatusAsNotified()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.feature.AppLockConfig
import com.wire.android.appLogger
import com.wire.android.model.Clickable
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.HomeNavGraph
Expand Down Expand Up @@ -112,21 +112,29 @@ fun SettingsScreenContent(
add(SettingsItem.AppSettings)
}
add(SettingsItem.NetworkSettings)

add(SettingsItem.AppLock(
when (settingsState.appLockConfig) {
is AppLockConfig.Disabled -> SwitchState.Enabled(
value = false,
isOnOffVisible = true,
onCheckedChange = onAppLockSwitchChanged
)
is AppLockConfig.Enabled -> SwitchState.Enabled(
value = true,
isOnOffVisible = true
) {
turnAppLockOffDialogState.show(Unit)
when (settingsState.isAppLockEditable) {
true -> {
appLogger.d("AppLockConfig isAooLockEditable: ${settingsState.isAppLockEditable}")

appLogger.d("AppLockConfig isAppLockEnabled: ${settingsState.isAppLockEnabled}")
SwitchState.Enabled(
value = settingsState.isAppLockEnabled,
isOnOffVisible = true
) {
turnAppLockOffDialogState.show(Unit)
}
}
is AppLockConfig.EnforcedByTeam -> {
SwitchState.TextOnly(true)

false -> {
appLogger.d("AppLockConfig isAooLockEditable: ${settingsState.isAppLockEditable}")

appLogger.d("AppLockConfig isAppLockEnabled: ${settingsState.isAppLockEnabled}")
SwitchState.Disabled(
value = settingsState.isAppLockEnabled,
isOnOffVisible = true,
)
}
}
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
*/
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(DEFAULT_TIMEOUT),
val isAppLockEditable: Boolean = false,
val isAppLockEnabled: Boolean = false
)

0 comments on commit 97a8a94

Please sign in to comment.