First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
@@ -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
}
}
}
@@ -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)
}
}
@@ -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"
}
@@ -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
}
@@ -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
}
}
@@ -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() }
@@ -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()
}
}
@@ -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())
}
}