Extract and unit test MultipleTapToUnlock
This commit is contained in:
parent
78e82eac28
commit
11cbc2c293
6 changed files with 95 additions and 8 deletions
|
|
@ -66,6 +66,7 @@ dependencies {
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
|
implementation(projects.libraries.uiUtils)
|
||||||
implementation(projects.libraries.fullscreenintent.api)
|
implementation(projects.libraries.fullscreenintent.api)
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
implementation(projects.features.lockscreen.api)
|
implementation(projects.features.lockscreen.api)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||||
|
|
@ -49,6 +50,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PreferencesRootState {
|
override fun present(): PreferencesRootState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val matrixUser = matrixClient.userProfile.collectAsState()
|
val matrixUser = matrixClient.userProfile.collectAsState()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Force a refresh of the profile
|
// Force a refresh of the profile
|
||||||
|
|
@ -103,7 +105,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
fun handleEvent(event: PreferencesRootEvents) {
|
fun handleEvent(event: PreferencesRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PreferencesRootEvents.OnVersionInfoClick -> {
|
is PreferencesRootEvents.OnVersionInfoClick -> {
|
||||||
showDeveloperSettingsProvider.unlockDeveloperSettings()
|
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ package io.element.android.features.preferences.impl.utils
|
||||||
|
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
import io.element.android.libraries.ui.utils.MultipleTapToUnlock
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -19,18 +21,15 @@ class ShowDeveloperSettingsProvider @Inject constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val DEVELOPER_SETTINGS_COUNTER = 7
|
const val DEVELOPER_SETTINGS_COUNTER = 7
|
||||||
}
|
}
|
||||||
private var counter = DEVELOPER_SETTINGS_COUNTER
|
|
||||||
|
private val multipleTapToUnlock = MultipleTapToUnlock(DEVELOPER_SETTINGS_COUNTER)
|
||||||
private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE
|
private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE
|
||||||
|
|
||||||
private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild)
|
private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild)
|
||||||
val showDeveloperSettings: StateFlow<Boolean> = _showDeveloperSettings
|
val showDeveloperSettings: StateFlow<Boolean> = _showDeveloperSettings
|
||||||
|
|
||||||
fun unlockDeveloperSettings() {
|
fun unlockDeveloperSettings(scope: CoroutineScope) {
|
||||||
if (counter == 0) {
|
if (multipleTapToUnlock.unlock(scope)) {
|
||||||
return
|
|
||||||
}
|
|
||||||
counter--
|
|
||||||
if (counter == 0) {
|
|
||||||
_showDeveloperSettings.value = true
|
_showDeveloperSettings.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.ui.utils
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user has tapped [numberOfTapToUnlock] times in a short amount of time.
|
||||||
|
* The counter is reset after 2 seconds of inactivity.
|
||||||
|
*
|
||||||
|
* @param numberOfTapToUnlock The number of taps required to unlock.
|
||||||
|
*/
|
||||||
|
class MultipleTapToUnlock(
|
||||||
|
private val numberOfTapToUnlock: Int = 7,
|
||||||
|
) {
|
||||||
|
private var counter = numberOfTapToUnlock
|
||||||
|
private var currentJob: Job? = null
|
||||||
|
|
||||||
|
fun unlock(scope: CoroutineScope): Boolean {
|
||||||
|
counter--
|
||||||
|
currentJob?.cancel()
|
||||||
|
return if (counter > 0) {
|
||||||
|
currentJob = scope.launch {
|
||||||
|
delay(2.seconds)
|
||||||
|
// Reset counter if user is not fast enough
|
||||||
|
counter = numberOfTapToUnlock
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.libraries.ui.utils
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class MultipleTapToUnlockTest {
|
||||||
|
@Test
|
||||||
|
fun `test multiple tap should unlock`() = runTest {
|
||||||
|
val sut = MultipleTapToUnlock(3)
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||||
|
// All next call returns true
|
||||||
|
advanceTimeBy(3.seconds)
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun `test waiting should reset counter`() = runTest {
|
||||||
|
val sut = MultipleTapToUnlock(3)
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
advanceTimeBy(3.seconds)
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isFalse()
|
||||||
|
assertThat(sut.unlock(backgroundScope)).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue