From a68d1b276dae262dbdbc513ff8ded0ad90c51187 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 30 May 2026 15:25:27 +0530 Subject: [PATCH] Add optional app lock behind biometric or device credential Implements a full app lock (issue #1313) gating Aurora Store behind the device biometric or, where unavailable, the screen-lock credential (PIN/pattern/password) so it also works on TVs and older phones. - AppLockManager: process-scoped, in-memory lock state so a cold start is always locked, with a short background grace timeout to avoid re-prompting during the install dialog or the biometric sheet itself. - AppLockAuthenticator: BiometricPrompt wrapper using BIOMETRIC_STRONG | DEVICE_CREDENTIAL on API 30+ and setDeviceCredentialAllowed on older releases, plus an enrollment check. - ComposeActivity (now a FragmentActivity) gates content via a three-state machine (AUTHENTICATING/LOCKED/UNLOCKED); the authenticating state shows a blank surface behind the prompt so dismissing it never flashes the lock card. Re-locks on return past the timeout and sets FLAG_SECURE while locked. - Toggle lives on a dedicated Security preference screen, reached from Settings like the other preference entries. --- app/build.gradle.kts | 1 + .../java/com/aurora/store/ComposeActivity.kt | 105 ++++++++++++++++-- .../store/compose/navigation/Destination.kt | 1 + .../store/compose/navigation/NavDisplay.kt | 3 + .../aurora/store/compose/navigation/Screen.kt | 3 + .../store/compose/ui/lock/AppLockScreen.kt | 69 ++++++++++++ .../compose/ui/preferences/SettingsScreen.kt | 12 ++ .../security/SecurityPreferenceScreen.kt | 87 +++++++++++++++ .../com/aurora/store/data/AppLockManager.kt | 64 +++++++++++ .../aurora/store/util/AppLockAuthenticator.kt | 93 ++++++++++++++++ .../java/com/aurora/store/util/Preferences.kt | 3 + app/src/main/res/drawable/ic_lock.xml | 24 ++++ app/src/main/res/values/strings.xml | 10 ++ gradle/libs.versions.toml | 2 + 14 files changed, 469 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/lock/AppLockScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt create mode 100644 app/src/main/java/com/aurora/store/data/AppLockManager.kt create mode 100644 app/src/main/java/com/aurora/store/util/AppLockAuthenticator.kt create mode 100644 app/src/main/res/drawable/ic_lock.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c4e52a7a3..96926eac4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -196,6 +196,7 @@ dependencies { // AndroidX implementation(libs.androidx.core.ktx) implementation(libs.androidx.browser) + implementation(libs.androidx.biometric) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.navigation3) implementation(libs.androidx.preference.ktx) diff --git a/app/src/main/java/com/aurora/store/ComposeActivity.kt b/app/src/main/java/com/aurora/store/ComposeActivity.kt index ffa594b24..b98e6b6af 100644 --- a/app/src/main/java/com/aurora/store/ComposeActivity.kt +++ b/app/src/main/java/com/aurora/store/ComposeActivity.kt @@ -8,33 +8,51 @@ package com.aurora.store import android.content.Intent import android.os.Bundle -import androidx.activity.ComponentActivity +import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.core.content.IntentCompat +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.extensions.getPackageName +import com.aurora.store.R import com.aurora.store.compose.composition.LocalNetworkStatus import com.aurora.store.compose.composition.LocalUI import com.aurora.store.compose.composition.UI import com.aurora.store.compose.navigation.NavDisplay import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.theme.AuroraTheme +import com.aurora.store.compose.ui.lock.AppLockScreen +import com.aurora.store.data.AppLockManager import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.data.receiver.MigrationReceiver +import com.aurora.store.util.AppLockAuthenticator import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class ComposeActivity : ComponentActivity() { +class ComposeActivity : FragmentActivity() { @Inject lateinit var networkProvider: NetworkProvider + @Inject lateinit var appLockManager: AppLockManager + override fun onCreate(savedInstanceState: Bundle?) { MigrationReceiver.runMigrationsIfRequired(this) enableEdgeToEdge() @@ -53,17 +71,86 @@ class ComposeActivity : ComponentActivity() { val networkStatus by networkProvider.status.collectAsStateWithLifecycle( initialValue = NetworkStatus.AVAILABLE ) - CompositionLocalProvider( - LocalUI provides localUI, - LocalNetworkStatus provides networkStatus - ) { - AuroraTheme { - NavDisplay(startDestination = startDestination) + AuroraTheme { + var lockState by remember { + mutableStateOf( + if (appLockManager.shouldLock(this@ComposeActivity)) { + LockState.AUTHENTICATING + } else { + LockState.UNLOCKED + } + ) + } + + // Keep FLAG_SECURE on until unlocked; auto-prompt while authenticating + LaunchedEffect(lockState) { + when (lockState) { + LockState.AUTHENTICATING -> { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + promptUnlock( + onSuccess = { lockState = LockState.UNLOCKED }, + onError = { lockState = LockState.LOCKED } + ) + } + + LockState.LOCKED -> window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + LockState.UNLOCKED -> + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + // Re-lock when returning from the background past the grace timeout + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_STOP -> appLockManager.onBackgrounded() + Lifecycle.Event.ON_START -> + if (lockState == LockState.UNLOCKED && + appLockManager.shouldLock(this@ComposeActivity) + ) { + lockState = LockState.AUTHENTICATING + } + + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + when (lockState) { + LockState.UNLOCKED -> CompositionLocalProvider( + LocalUI provides localUI, + LocalNetworkStatus provides networkStatus + ) { + NavDisplay(startDestination = startDestination) + } + + // Plain surface behind the prompt so dismissing it doesn't flash the lock card + LockState.AUTHENTICATING -> Surface(modifier = Modifier.fillMaxSize()) {} + + LockState.LOCKED -> AppLockScreen( + onUnlock = { lockState = LockState.AUTHENTICATING } + ) } } } } + private fun promptUnlock(onSuccess: () -> Unit, onError: () -> Unit) { + AppLockAuthenticator.authenticate( + activity = this, + title = getString(R.string.app_lock_prompt_title), + subtitle = getString(R.string.app_lock_prompt_subtitle), + onSuccess = { + appLockManager.markUnlocked() + onSuccess() + }, + onError = { onError() } + ) + } + private fun resolveStartDestination(): Screen { // Parcel-based navigation (e.g. from NotificationUtil) IntentCompat.getParcelableExtra(intent, Screen.PARCEL_KEY, Screen::class.java) @@ -91,4 +178,6 @@ class ComposeActivity : ComponentActivity() { !Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding else -> Screen.Splash() } + + private enum class LockState { AUTHENTICATING, LOCKED, UNLOCKED } } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt index 8f8bc2b4f..7a3d48a53 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt @@ -46,4 +46,5 @@ sealed class Destination { data object UIPreference : Destination() data object UpdatesPreference : Destination() data object SourceFilters : Destination() + data object SecurityPreference : Destination() } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 47ee6896f..b2223f827 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -53,6 +53,7 @@ import com.aurora.store.compose.ui.preferences.UIPreferenceScreen import com.aurora.store.compose.ui.preferences.installation.InstallationPreferenceScreen import com.aurora.store.compose.ui.preferences.installation.InstallerScreen import com.aurora.store.compose.ui.preferences.network.NetworkPreferenceScreen +import com.aurora.store.compose.ui.preferences.security.SecurityPreferenceScreen import com.aurora.store.compose.ui.preferences.updates.SourceFiltersScreen import com.aurora.store.compose.ui.preferences.updates.UpdatesPreferenceScreen import com.aurora.store.compose.ui.search.SearchScreen @@ -162,6 +163,7 @@ fun NavDisplay(startDestination: NavKey) { Destination.UIPreference -> backstack.add(Screen.UIPreference) Destination.UpdatesPreference -> backstack.add(Screen.UpdatesPreference) Destination.SourceFilters -> backstack.add(Screen.SourceFilters) + Destination.SecurityPreference -> backstack.add(Screen.SecurityPreference) } } @@ -284,6 +286,7 @@ fun NavDisplay(startDestination: NavKey) { entry { UIPreferenceScreen() } entry { UpdatesPreferenceScreen(onNavigateTo = ::navigate) } entry { SourceFiltersScreen() } + entry { SecurityPreferenceScreen() } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index 1ce32189a..87e14c11f 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -95,6 +95,9 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object SourceFilters : Screen() + @Serializable + data object SecurityPreference : Screen() + @Serializable data class Splash(val packageName: String? = null) : Screen() diff --git a/app/src/main/java/com/aurora/store/compose/ui/lock/AppLockScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/lock/AppLockScreen.kt new file mode 100644 index 000000000..41ed48802 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/lock/AppLockScreen.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.lock + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.compose.ui.unit.dp +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +/** + * Full-screen lock placeholder shown in place of the app content while the app is locked. + * The actual authentication prompt is driven by the hosting activity; [onUnlock] re-triggers + * it, e.g. after the user dismissed the prompt. + */ +@Composable +fun AppLockScreen(onUnlock: () -> Unit) { + Scaffold { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(R.drawable.ic_lock), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .padding(bottom = 16.dp) + ) + Text( + text = stringResource(R.string.app_lock_locked_message), + modifier = Modifier.padding(bottom = 24.dp), + textAlign = TextAlign.Center + ) + Button(onClick = onUnlock) { + Text(stringResource(R.string.app_lock_unlock)) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun AppLockScreenPreview() { + AppLockScreen(onUnlock = {}) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt index a623e2a77..cde261b01 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt @@ -112,6 +112,18 @@ private fun ScreenContent(onNavigateTo: (Destination) -> Unit = {}) { headlineContent = { Text(stringResource(R.string.title_updates)) } ) } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.SecurityPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_lock), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_security)) } + ) + } } } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt new file mode 100644 index 000000000..134fff43a --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.security + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.extensions.toast +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.util.AppLockAuthenticator +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_APP_LOCK_ENABLED +import com.aurora.store.util.save + +@Composable +fun SecurityPreferenceScreen() { + ScreenContent() +} + +@Composable +private fun ScreenContent() { + val context = LocalContext.current + var appLockEnabled by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_APP_LOCK_ENABLED, false)) + } + + fun setAppLock(enabled: Boolean) { + // Refuse to enable the lock when the device has no biometric or screen-lock to fall back on + if (enabled && !AppLockAuthenticator.canAuthenticate(context)) { + context.toast(R.string.app_lock_no_credential) + return + } + appLockEnabled = enabled + context.save(PREFERENCE_APP_LOCK_ENABLED, enabled) + } + + Scaffold( + topBar = { TopAppBar(title = stringResource(R.string.title_security)) } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { setAppLock(!appLockEnabled) }, + headlineContent = { Text(stringResource(R.string.app_lock_title)) }, + supportingContent = { Text(stringResource(R.string.app_lock_summary)) }, + trailingContent = { + Switch( + checked = appLockEnabled, + onCheckedChange = { setAppLock(it) } + ) + } + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun SecurityPreferenceScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/data/AppLockManager.kt b/app/src/main/java/com/aurora/store/data/AppLockManager.kt new file mode 100644 index 000000000..d8253f908 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/AppLockManager.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.data + +import android.content.Context +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_APP_LOCK_ENABLED +import com.aurora.store.util.Preferences.PREFERENCE_APP_LOCK_TIMEOUT +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tracks the app-lock state at process scope. The unlocked flag lives only in memory, so a + * fresh process (cold start or process death) always starts locked. Re-locking after the + * app is backgrounded is governed by a short grace [timeout] to avoid re-prompting during + * transient stops such as the system install dialog or the biometric sheet itself. + */ +@Singleton +class AppLockManager @Inject constructor() { + + companion object { + // Grace period before a backgrounded app re-locks, in seconds + private const val DEFAULT_TIMEOUT_SECONDS = 30 + } + + private var unlocked = false + private var backgroundedAt = 0L + + /** + * Whether the app should currently present the lock screen, i.e. the feature is enabled + * and the session is not (still) unlocked within the grace [timeout]. + */ + fun shouldLock(context: Context): Boolean { + if (!Preferences.getBoolean(context, PREFERENCE_APP_LOCK_ENABLED, false)) return false + if (!unlocked) return true + + if (backgroundedAt == 0L) return false + val elapsed = System.currentTimeMillis() - backgroundedAt + return elapsed >= timeoutMillis(context) + } + + /** Marks the current session as authenticated. */ + fun markUnlocked() { + unlocked = true + backgroundedAt = 0L + } + + /** Records when the app went to the background so the grace timeout can be measured. */ + fun onBackgrounded() { + if (unlocked) backgroundedAt = System.currentTimeMillis() + } + + private fun timeoutMillis(context: Context): Long { + val seconds = Preferences.getInteger( + context, + PREFERENCE_APP_LOCK_TIMEOUT, + DEFAULT_TIMEOUT_SECONDS + ) + return seconds * 1000L + } +} diff --git a/app/src/main/java/com/aurora/store/util/AppLockAuthenticator.kt b/app/src/main/java/com/aurora/store/util/AppLockAuthenticator.kt new file mode 100644 index 000000000..4901db64a --- /dev/null +++ b/app/src/main/java/com/aurora/store/util/AppLockAuthenticator.kt @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.util + +import android.app.KeyguardManager +import android.content.Context +import android.os.Build +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.fragment.app.FragmentActivity + +/** + * Thin wrapper over [BiometricPrompt] that authenticates with the device biometric *or* the + * device credential (PIN / pattern / password). Devices without a fingerprint sensor — TVs, + * older phones — therefore fall back to the lockscreen credential automatically. + * + * Combining a biometric with [DEVICE_CREDENTIAL] is only supported by the prompt on API 30+; + * on older releases the deprecated `setDeviceCredentialAllowed` provides the same behaviour. + */ +object AppLockAuthenticator { + + private val supportsCombinedAuthenticators = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + + /** + * Whether the device can satisfy an app-lock challenge. True when a biometric or device + * credential is enrolled, or — for the pre-API-30 fallback path — the device simply has a + * secure lockscreen set. + */ + fun canAuthenticate(context: Context): Boolean { + val authenticators = if (supportsCombinedAuthenticators) { + BIOMETRIC_STRONG or DEVICE_CREDENTIAL + } else { + BIOMETRIC_STRONG + } + if (BiometricManager.from(context).canAuthenticate(authenticators) == + BiometricManager.BIOMETRIC_SUCCESS + ) { + return true + } + + // Pre-API-30 path: no enrolled biometric, but a PIN/pattern/password works as fallback + return context.getSystemService()?.isDeviceSecure == true + } + + /** + * Shows the authentication prompt. [onSuccess] fires on a successful unlock; [onError] + * fires on a non-recoverable error or user cancellation (the prompt itself handles + * transient failures like a wrong fingerprint). + */ + fun authenticate( + activity: FragmentActivity, + title: String, + subtitle: String, + onSuccess: () -> Unit, + onError: (CharSequence) -> Unit + ) { + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = + onSuccess() + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = + onError(errString) + } + + val prompt = BiometricPrompt( + activity, + ContextCompat.getMainExecutor(activity), + callback + ) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .apply { + if (supportsCombinedAuthenticators) { + setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + } else { + @Suppress("DEPRECATION") + setDeviceCredentialAllowed(true) + } + } + .build() + + prompt.authenticate(promptInfo) + } +} diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 58b2c13e8..9c7b621d0 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -76,6 +76,9 @@ object Preferences { const val PREFERENCE_SELF_UPDATE_ENABLED = "PREFERENCE_SELF_UPDATE_ENABLED" + const val PREFERENCE_APP_LOCK_ENABLED = "PREFERENCE_APP_LOCK_ENABLED" + const val PREFERENCE_APP_LOCK_TIMEOUT = "PREFERENCE_APP_LOCK_TIMEOUT" + private var prefs: SharedPreferences? = null fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) { diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 000000000..9a0688bf9 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a0fc49e5..8712221e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -179,6 +179,16 @@ No root access. Grant it or change the installer. Shizuku is not installed or set up properly. Chosen installer is unavailable, using the default installer instead. + + + Security + App lock + Require biometric or screen-lock authentication to open Aurora Store. + Set up a screen lock (PIN, pattern, password or biometric) on your device first. + Authentication is required to access Aurora Store + Unlock + Unlock Aurora Store + Enter phone screen lock pattern, PIN, password or fingerprint "Aurora Services is available and ready to install." Install Aurora Services 1.0.9 or above, or change the installer. Set up Aurora Services and grant all permissions first. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 010365dd1..e191bf108 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ agCoreservice = "13.3.1.300" androidGradlePlugin = "9.2.1" androidx-hilt = "1.3.0" androidx-junit = "1.3.0" +biometric = "1.1.0" browser = "1.10.0" coil = "3.4.0" composeBom = "2026.05.01" @@ -48,6 +49,7 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp androidx-adaptive-core = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" } androidx-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" } androidx-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" } +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }