Skip to content

Commit

Permalink
fix: bypassing app lock after timeout (#2324)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Oct 13, 2023
1 parent 88adc9e commit 04c06e3
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.wire.android.BuildConfig
Expand All @@ -53,7 +52,6 @@ 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 APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode")
private val APP_LOCK_TIMESTAMP = longPreferencesKey("app_lock_timestamp")
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 userDoubleTapToastStatusKey(userId: String): Preferences.Key<Boolean> =
Expand Down Expand Up @@ -138,6 +136,7 @@ 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<String?> =
context.dataStore.data.map {
Expand All @@ -150,6 +149,12 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
}
}

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

suspend fun clearAppLockPasscode() {
context.dataStore.edit {
it.remove(APP_LOCK_PASSCODE)
Expand All @@ -167,11 +172,4 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
}
}
}

fun getAppLockTimestampFlow(): Flow<Long?> =
context.dataStore.data.map { it[APP_LOCK_TIMESTAMP] }

suspend fun setAppLockTimestamp(timestamp: Long) {
context.dataStore.edit { it[APP_LOCK_TIMESTAMP] = timestamp }
}
}
5 changes: 0 additions & 5 deletions app/src/main/kotlin/com/wire/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import android.media.MediaPlayer
import androidx.core.app.NotificationManagerCompat
import com.wire.android.BuildConfig
import com.wire.android.mapper.MessageResourceProvider
import com.wire.android.ui.home.appLock.CurrentTimestampProvider
import com.wire.android.util.dispatchers.DefaultDispatcherProvider
import com.wire.android.util.dispatchers.DispatcherProvider
import dagger.Module
Expand Down Expand Up @@ -80,8 +79,4 @@ object AppModule {
)
}
}

@Singleton
@Provides
fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class ObserveAppLockConfigUseCase @Inject constructor(
) {

operator fun invoke(): Flow<AppLockConfig> =
globalDataStore.getAppLockPasscodeFlow().map { // TODO: include checking if any logged account does not enforce app-lock
globalDataStore.isAppLockPasscodeSetFlow().map { // TODO: include checking if any logged account does not enforce app-lock
when {
it.isNullOrEmpty() -> AppLockConfig.Disabled
else -> AppLockConfig.Enabled
it -> AppLockConfig.Enabled
else -> AppLockConfig.Disabled
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class WireActivity : AppCompatActivity() {
@Composable
private fun handleAppLock() {
LaunchedEffect(Unit) {
lockCodeTimeManager.shouldLock()
lockCodeTimeManager.isLocked()
.filter { it }
.collectLatest {
navigationCommands.emit(NavigationCommand(EnterLockCodeScreenDestination))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class EnterLockScreenViewModel @Inject constructor(
private val validatePassword: ValidatePasswordUseCase,
private val globalDataStore: GlobalDataStore,
private val dispatchers: DispatcherProvider,
private val lockCodeTimeManager: LockCodeTimeManager,
) : ViewModel() {

var state: EnterLockCodeViewState by mutableStateOf(EnterLockCodeViewState())
Expand Down Expand Up @@ -71,6 +72,7 @@ class EnterLockScreenViewModel @Inject constructor(
val storedPasscode = withContext(dispatchers.io()) { globalDataStore.getAppLockPasscodeFlow().firstOrNull() }
withContext(dispatchers.main()) {
state = if (storedPasscode == state.password.text.sha256()) {
lockCodeTimeManager.appUnlocked()
state.copy(done = true)
} else {
state.copy(error = EnterLockCodeError.InvalidValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@
*/
package com.wire.android.ui.home.appLock

import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.ApplicationScope
import com.wire.android.feature.AppLockConfig
import com.wire.android.feature.ObserveAppLockConfigUseCase
import com.wire.android.util.CurrentScreenManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -39,45 +42,46 @@ class LockCodeTimeManager @Inject constructor(
@ApplicationScope private val appCoroutineScope: CoroutineScope,
currentScreenManager: CurrentScreenManager,
observeAppLockConfigUseCase: ObserveAppLockConfigUseCase,
globalDataStore: GlobalDataStore,
currentTimestamp: CurrentTimestampProvider,
) {

@Suppress("MagicNumber")
private val lockCodeRequiredFlow = globalDataStore.getAppLockTimestampFlow().take(1)
.flatMapLatest { lastAppLockTimestamp ->
private val isLockedFlow = MutableStateFlow(false)

init {
// 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) {
isLockedFlow.value = true
}
}
}
@Suppress("MagicNumber")
// next, listen for app lock config and app visibility changes to determine if app should be locked
appCoroutineScope.launch {
combine(
currentScreenManager.isAppVisibleFlow()
.scan(AppVisibilityTimestampData(lastAppLockTimestamp ?: -1, false)) { previousData, currentlyVisible ->
if (previousData.isAppVisible != currentlyVisible) {
val timestamp = if (!currentlyVisible) { // app moved to background
currentTimestamp().also {
globalDataStore.setAppLockTimestamp(it)
}
} else previousData.timestamp
AppVisibilityTimestampData(
timestamp = timestamp,
isAppVisible = currentlyVisible
)
} else previousData
},
observeAppLockConfigUseCase()
) { appVisibilityTimestampData, appLockConfig ->
appVisibilityTimestampData.isAppVisible
&& appLockConfig !is AppLockConfig.Disabled
&& appVisibilityTimestampData.timestamp >= 0
&& (currentTimestamp() - appVisibilityTimestampData.timestamp) > (appLockConfig.timeoutInSeconds * 1000)
observeAppLockConfigUseCase(),
currentScreenManager.isAppVisibleFlow(),
::Pair
).flatMapLatest { (appLockConfig, isInForeground) ->
when {
appLockConfig is AppLockConfig.Disabled -> flowOf(false)

!isInForeground && !isLockedFlow.value -> flow {
delay(appLockConfig.timeoutInSeconds * 1000L)
emit(true)
}

else -> emptyFlow()
}
}.collectLatest {
isLockedFlow.value = it
}
.distinctUntilChanged()
}
.shareIn(scope = appCoroutineScope, started = SharingStarted.Eagerly, replay = 1)
}

fun shouldLock(): Flow<Boolean> = lockCodeRequiredFlow
fun appUnlocked() {
isLockedFlow.value = false
}

private data class AppVisibilityTimestampData(
val timestamp: Long,
val isAppVisible: Boolean
)
fun isLocked(): Flow<Boolean> = isLockedFlow
}

typealias CurrentTimestampProvider = () -> Long
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ObserveAppLockConfigUseCaseTest {
@Test
fun givenPasscodeIsSet_whenObservingAppLockConfig_thenReturnEnabled() = runTest {
val (_, useCase) = Arrangement()
.withAppLockPasscode("1234")
.withAppLockPasscodeSet(true)
.arrange()

val result = useCase.invoke().firstOrNull()
Expand All @@ -43,7 +43,7 @@ class ObserveAppLockConfigUseCaseTest {
@Test
fun givenPasscodeIsNotSet_whenObservingAppLockConfig_thenReturnDisabled() = runTest {
val (_, useCase) = Arrangement()
.withAppLockPasscode(null)
.withAppLockPasscodeSet(false)
.arrange()

val result = useCase.invoke().firstOrNull()
Expand All @@ -60,8 +60,8 @@ class ObserveAppLockConfigUseCaseTest {
MockKAnnotations.init(this, relaxUnitFun = true)
}

fun withAppLockPasscode(passcode: String?) = apply {
every { globalDataStore.getAppLockPasscodeFlow() } returns flowOf(passcode)
fun withAppLockPasscodeSet(value: Boolean) = apply {
every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(value)
}

fun arrange() = this to useCase
Expand Down
Loading

0 comments on commit 04c06e3

Please sign in to comment.