mirror of
https://github.com/whyorean/AuroraStore.git
synced 2026-06-11 01:06:05 -04:00
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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -46,4 +46,5 @@ sealed class Destination {
|
||||
data object UIPreference : Destination()
|
||||
data object UpdatesPreference : Destination()
|
||||
data object SourceFilters : Destination()
|
||||
data object SecurityPreference : Destination()
|
||||
}
|
||||
|
||||
@@ -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<Screen.UIPreference> { UIPreferenceScreen() }
|
||||
entry<Screen.UpdatesPreference> { UpdatesPreferenceScreen(onNavigateTo = ::navigate) }
|
||||
entry<Screen.SourceFilters> { SourceFiltersScreen() }
|
||||
entry<Screen.SecurityPreference> { SecurityPreferenceScreen() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 = {})
|
||||
}
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
64
app/src/main/java/com/aurora/store/data/AppLockManager.kt
Normal file
64
app/src/main/java/com/aurora/store/data/AppLockManager.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<KeyguardManager>()?.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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
24
app/src/main/res/drawable/ic_lock.xml
Normal file
24
app/src/main/res/drawable/ic_lock.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ Copyright (C) 2026 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M240,880q-33,0 -56.5,-23.5T160,800v-400q0,-33 23.5,-56.5T240,320h40v-80q0,-83 58.5,-141.5T480,40q83,0 141.5,58.5T680,240v80h40q33,0 56.5,23.5T800,400v400q0,33 -23.5,56.5T720,880L240,880ZM240,800h480v-400L240,400v400ZM536.5,656.5Q560,633 560,600t-23.5,-56.5Q513,520 480,520t-56.5,23.5Q400,567 400,600t23.5,56.5Q447,680 480,680t56.5,-23.5ZM360,320h240v-80q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85v80ZM240,800v-400,400Z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
@@ -179,6 +179,16 @@
|
||||
<string name="installer_root_unavailable">No root access. Grant it or change the installer.</string>
|
||||
<string name="installer_shizuku_unavailable">Shizuku is not installed or set up properly.</string>
|
||||
<string name="installer_fallback_session">Chosen installer is unavailable, using the default installer instead.</string>
|
||||
|
||||
<!-- Security -->
|
||||
<string name="title_security">Security</string>
|
||||
<string name="app_lock_title">App lock</string>
|
||||
<string name="app_lock_summary">Require biometric or screen-lock authentication to open Aurora Store.</string>
|
||||
<string name="app_lock_no_credential">Set up a screen lock (PIN, pattern, password or biometric) on your device first.</string>
|
||||
<string name="app_lock_locked_message">Authentication is required to access Aurora Store</string>
|
||||
<string name="app_lock_unlock">Unlock</string>
|
||||
<string name="app_lock_prompt_title">Unlock Aurora Store</string>
|
||||
<string name="app_lock_prompt_subtitle">Enter phone screen lock pattern, PIN, password or fingerprint</string>
|
||||
<string name="installer_service_available">"Aurora Services is available and ready to install."</string>
|
||||
<string name="installer_service_unavailable">Install Aurora Services 1.0.9 or above, or change the installer.</string>
|
||||
<string name="installer_service_misconfigured">Set up Aurora Services and grant all permissions first.</string>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user