First Commit
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.formatter
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import io.element.android.libraries.androidutils.filesize.AndroidFileSizeFormatter
|
||||
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
|
||||
import io.element.android.libraries.ui.utils.version.LocalSdkIntVersionProvider
|
||||
|
||||
@Composable
|
||||
fun rememberFileSizeFormatter(): FileSizeFormatter {
|
||||
val context = LocalContext.current
|
||||
val sdkIntProvider = LocalSdkIntVersionProvider.current
|
||||
return remember {
|
||||
AndroidFileSizeFormatter(context, sdkIntProvider)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-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.time
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Format a duration as minutes:seconds.
|
||||
*
|
||||
* For example,
|
||||
* - 0 seconds will be formatted as "0:00".
|
||||
* - 65 seconds will be formatted as "1:05".
|
||||
* - 2 hours will be formatted as "120:00".
|
||||
* - negative 10 seconds will be formatted as "-0:10".
|
||||
*
|
||||
* @return the formatted duration.
|
||||
*/
|
||||
fun Duration.formatShort(): String {
|
||||
// Format as minutes:seconds
|
||||
val seconds = (absoluteValue.inWholeSeconds % 60)
|
||||
.toString()
|
||||
.padStart(2, '0')
|
||||
|
||||
val sign = isNegative().let { if (it) "-" else "" }
|
||||
|
||||
return "$sign${absoluteValue.inWholeMinutes}:$seconds"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.time
|
||||
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun isTalkbackActive(): Boolean {
|
||||
val context = LocalContext.current
|
||||
val accessibilityManager = remember { context.getSystemService(AccessibilityManager::class.java) }
|
||||
var isTouchExplorationEnabled by remember { mutableStateOf(accessibilityManager.isTouchExplorationEnabled) }
|
||||
DisposableEffect(Unit) {
|
||||
val listener = AccessibilityManager.TouchExplorationStateChangeListener { enabled ->
|
||||
isTouchExplorationEnabled = enabled
|
||||
}
|
||||
accessibilityManager.addTouchExplorationStateChangeListener(listener)
|
||||
onDispose {
|
||||
accessibilityManager.removeTouchExplorationStateChangeListener(listener)
|
||||
}
|
||||
}
|
||||
return isTouchExplorationEnabled
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.time
|
||||
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
|
||||
/**
|
||||
* Extension property to get the digit character from a KeyEvent.
|
||||
* This handles both regular digit keys and numpad keys.
|
||||
*/
|
||||
val KeyEvent.digit: Char? get() {
|
||||
val char = nativeKeyEvent.unicodeChar.toChar()
|
||||
return when {
|
||||
Character.isDigit(char) -> char
|
||||
key == Key.NumPad0 -> '0'
|
||||
key == Key.NumPad1 -> '1'
|
||||
key == Key.NumPad2 -> '2'
|
||||
key == Key.NumPad3 -> '3'
|
||||
key == Key.NumPad4 -> '4'
|
||||
key == Key.NumPad5 -> '5'
|
||||
key == Key.NumPad6 -> '6'
|
||||
key == Key.NumPad7 -> '7'
|
||||
key == Key.NumPad8 -> '8'
|
||||
key == Key.NumPad9 -> '9'
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.version
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.impl.sdk.DefaultBuildVersionSdkIntProvider
|
||||
|
||||
val LocalSdkIntVersionProvider = staticCompositionLocalOf<BuildVersionSdkIntProvider> { DefaultBuildVersionSdkIntProvider() }
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-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.time
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(value = Parameterized::class)
|
||||
class DurationFormatTest(
|
||||
private val seconds: Double,
|
||||
private val output: String,
|
||||
) {
|
||||
companion object {
|
||||
@Parameterized.Parameters(name = "{index}: format({0})={1}")
|
||||
@JvmStatic
|
||||
fun data(): Iterable<Array<Any>> {
|
||||
return arrayListOf(
|
||||
arrayOf<Any>(0, "0:00"),
|
||||
arrayOf<Any>(1, "0:01"),
|
||||
arrayOf<Any>(10, "0:10"),
|
||||
arrayOf<Any>(59.9, "0:59"),
|
||||
arrayOf<Any>(60, "1:00"),
|
||||
arrayOf<Any>(61, "1:01"),
|
||||
arrayOf<Any>(60 * 60, "60:00"),
|
||||
arrayOf<Any>(-60, "-1:00"),
|
||||
arrayOf<Any>(-1, "-0:01"),
|
||||
).toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatShort() {
|
||||
assertEquals(output, seconds.seconds.formatShort())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user