From 7dfa29856e42e21920762cae61c1e90eed086612 Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Thu, 21 Dec 2023 12:46:05 +0100
Subject: [PATCH 1/6] feat: Add LockedApp
---
.../java/app/myzel394/alibi/MainActivity.kt | 19 +----
.../java/app/myzel394/alibi/ui/LockedApp.kt | 80 +++++++++++++++++++
.../java/app/myzel394/alibi/ui/Navigation.kt | 15 ++++
.../alibi/ui/screens/RecorderScreen.kt | 3 +-
app/src/main/res/values/strings.xml | 2 +
5 files changed, 99 insertions(+), 20 deletions(-)
create mode 100644 app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt
diff --git a/app/src/main/java/app/myzel394/alibi/MainActivity.kt b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
index 664b96e..7222997 100644
--- a/app/src/main/java/app/myzel394/alibi/MainActivity.kt
+++ b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
@@ -14,6 +14,7 @@ import androidx.core.view.WindowCompat
import androidx.datastore.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.AppSettingsSerializer
+import app.myzel394.alibi.ui.LockedApp
import app.myzel394.alibi.ui.Navigation
import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.theme.AlibiTheme
@@ -31,24 +32,6 @@ class MainActivity : AppCompatActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
- val dataStore = LocalContext.current.dataStore
- val settings = dataStore
- .data
- .collectAsState(initial = AppSettings.getDefaultInstance())
- .value
-
- LaunchedEffect(settings.theme) {
- if (!SUPPORTS_DARK_MODE_NATIVELY) {
- val currentValue = AppCompatDelegate.getDefaultNightMode()
-
- if (settings.theme == AppSettings.Theme.LIGHT && currentValue != AppCompatDelegate.MODE_NIGHT_NO) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- } else if (settings.theme == AppSettings.Theme.DARK && currentValue != AppCompatDelegate.MODE_NIGHT_YES) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- }
- }
- }
-
AlibiTheme {
Navigation()
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt b/app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt
new file mode 100644
index 0000000..b152ab8
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt
@@ -0,0 +1,80 @@
+package app.myzel394.alibi.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Fingerprint
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+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.stringResource
+import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.R
+
+// After this amount, close the app
+const val MAX_TRIES = 5
+
+@Composable
+fun LockedApp() {
+ Scaffold { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Box {}
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Icon(
+ Icons.Default.Fingerprint,
+ contentDescription = null,
+ modifier = Modifier
+ .size(64.dp)
+ )
+ Text(
+ text = stringResource(R.string.ui_locked_title),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(BIG_PRIMARY_BUTTON_SIZE),
+ onClick = {},
+ colors = ButtonDefaults.filledTonalButtonColors(),
+ ) {
+ Icon(
+ Icons.Default.Lock,
+ contentDescription = null,
+ modifier = Modifier
+ .size(ButtonDefaults.IconSize)
+ )
+ Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
+ Text(
+ text = stringResource(R.string.ui_locked_unlocked),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
index d9a73dd..4860f10 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
@@ -3,6 +3,7 @@ package app.myzel394.alibi.ui
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
+import androidx.appcompat.app.AppCompatDelegate
import androidx.camera.core.CameraX
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@@ -24,6 +25,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.myzel394.alibi.dataStore
+import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.models.VideoRecorderModel
@@ -59,6 +61,18 @@ fun Navigation(
}
}
+ LaunchedEffect(settings.theme) {
+ if (!SUPPORTS_DARK_MODE_NATIVELY) {
+ val currentValue = AppCompatDelegate.getDefaultNightMode()
+
+ if (settings.theme == AppSettings.Theme.LIGHT && currentValue != AppCompatDelegate.MODE_NIGHT_NO) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ } else if (settings.theme == AppSettings.Theme.DARK && currentValue != AppCompatDelegate.MODE_NIGHT_YES) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ }
+ }
+ }
+
NavHost(
modifier = Modifier
.background(MaterialTheme.colorScheme.background),
@@ -84,6 +98,7 @@ fun Navigation(
navController = navController,
audioRecorder = audioRecorder,
videoRecorder = videoRecorder,
+ settings = settings,
)
}
composable(
diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
index 026d89b..cb5d969 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt
@@ -40,12 +40,11 @@ fun RecorderScreen(
navController: NavController,
audioRecorder: AudioRecorderModel,
videoRecorder: VideoRecorderModel,
+ settings: AppSettings,
) {
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
- val settings = rememberSettings()
-
RecorderEventsHandler(
settings = settings,
snackbarHostState = snackbarHostState,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 82ea4f6..eacf755 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -160,4 +160,6 @@
Recording started %s
Saving now will save until %s
Video Recorder is starting...
+ Alibi is locked
+ Unlock
\ No newline at end of file
From bf84396a86f5dc724301056c98f6bf67ecd07ef6 Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Thu, 21 Dec 2023 18:12:46 +0100
Subject: [PATCH 2/6] current stand
---
app/build.gradle | 2 +
app/src/main/AndroidManifest.xml | 2 +
.../java/app/myzel394/alibi/db/AppSettings.kt | 21 ++++++++-
.../java/app/myzel394/alibi/ui/Navigation.kt | 43 +++++++++++++++++++
4 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/app/build.gradle b/app/build.gradle
index 03d67f5..20252d1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -146,4 +146,6 @@ dependencies {
implementation "com.valentinilk.shimmer:compose-shimmer:1.2.0"
+
+ implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3d59dbe..fdab32f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,8 @@
+
+
= Build.VERSION_CODES.R) {
+ val executor = ContextCompat.getMainExecutor(context)
+ val promptInfo = BiometricPrompt.Builder(context)
+ .setTitle("Biometric login for my app")
+ .setSubtitle("Log in using your biometric credential")
+ .setAllowedAuthenticators(
+ BIOMETRIC_STRONG or DEVICE_CREDENTIAL
+ )
+ .build()
+
+ // Prompt appears when user clicks "Log in".
+ // Consider integrating with the keystore to unlock cryptographic operations,
+ // if needed by your app.
+ promptInfo.authenticate(
+ CancellationSignal(),
+ executor,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ }
+ })
+ }
+ }
+
LaunchedEffect(settings.theme) {
if (!SUPPORTS_DARK_MODE_NATIVELY) {
val currentValue = AppCompatDelegate.getDefaultNightMode()
From a73fc6c48f6dd7d34080c1722522163ba3d85a28 Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Sat, 23 Dec 2023 19:57:55 +0100
Subject: [PATCH 3/6] feat: Add biometric authentication check
---
.../java/app/myzel394/alibi/db/AppSettings.kt | 12 +---
.../myzel394/alibi/helpers/AppLockHelper.kt | 64 +++++++++++++++++++
.../java/app/myzel394/alibi/ui/Navigation.kt | 30 +--------
3 files changed, 68 insertions(+), 38 deletions(-)
create mode 100644 app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
index 4a68400..6657419 100644
--- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
+++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
@@ -553,16 +553,8 @@ data class NotificationSettings(
@Serializable
class AppLockSettings {
+ // If the object is present, biometric authentication is enabled.
+ // To disable biometric authentication, set the instance to null.
val isEnabled
get() = true
-
- companion object {
- fun getDefaultInstance(): AppLockSettings = AppLockSettings()
-
- fun isSupported(context: Context): Boolean {
- val biometricManager = BiometricManager.from(context)
- when (biometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL)) {
-
- }
- }
}
diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
new file mode 100644
index 0000000..b963a14
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
@@ -0,0 +1,64 @@
+package app.myzel394.alibi.helpers
+
+import android.content.Context
+import androidx.biometric.BiometricManager
+import androidx.core.content.ContextCompat
+import androidx.biometric.BiometricPrompt
+import androidx.fragment.app.FragmentActivity
+import kotlinx.coroutines.CompletableDeferred
+
+class AppLockHelper {
+ enum class SupportType {
+ AVAILABLE,
+ UNAVAILABLE,
+ NONE_ENROLLED,
+ }
+
+ companion object {
+ fun isSupported(context: Context): SupportType {
+ val biometricManager = BiometricManager.from(context)
+ when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
+ BiometricManager.BIOMETRIC_SUCCESS -> return SupportType.AVAILABLE
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return SupportType.NONE_ENROLLED
+
+ else -> return SupportType.UNAVAILABLE
+ }
+ }
+
+ fun authenticate(
+ context: Context,
+ title: String,
+ subtitle: String
+ ): CompletableDeferred {
+ val deferred = CompletableDeferred()
+
+ val mainExecutor = ContextCompat.getMainExecutor(context)
+ val biometricPrompt = BiometricPrompt(
+ context as FragmentActivity,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ deferred.completeExceptionally(Exception(errString.toString()))
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ deferred.complete(Unit)
+ }
+
+ override fun onAuthenticationFailed() {
+ deferred.completeExceptionally(Exception("Authentication failed"))
+ }
+ }
+ )
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle(subtitle)
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+ .build()
+
+ biometricPrompt.authenticate(promptInfo)
+
+ return deferred
+ }
+ }
+}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
index dfb3418..bc1aa2a 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
@@ -45,6 +45,7 @@ import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
import app.myzel394.alibi.ui.screens.SettingsScreen
import app.myzel394.alibi.ui.screens.WelcomeScreen
import app.myzel394.alibi.ui.utils.CameraInfo
+import app.myzel394.alibi.helpers.AppLockHelper
const val SCALE_IN = 1.25f
@@ -73,34 +74,7 @@ fun Navigation(
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- val executor = ContextCompat.getMainExecutor(context)
- val promptInfo = BiometricPrompt.Builder(context)
- .setTitle("Biometric login for my app")
- .setSubtitle("Log in using your biometric credential")
- .setAllowedAuthenticators(
- BIOMETRIC_STRONG or DEVICE_CREDENTIAL
- )
- .build()
-
- // Prompt appears when user clicks "Log in".
- // Consider integrating with the keystore to unlock cryptographic operations,
- // if needed by your app.
- promptInfo.authenticate(
- CancellationSignal(),
- executor,
- object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
- super.onAuthenticationError(errorCode, errString)
- }
-
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- super.onAuthenticationSucceeded(result)
- }
-
- override fun onAuthenticationFailed() {
- super.onAuthenticationFailed()
- }
- })
+ AppLockHelper.authenticate(context, "Title", "Subtitle")
}
}
From 025a8a3209682cfffb0e8d294088e5eea110952e Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Sat, 23 Dec 2023 20:18:46 +0100
Subject: [PATCH 4/6] feat: Add EnableAppLockTile to SettingsScreen
---
.../java/app/myzel394/alibi/db/AppSettings.kt | 17 +++-
.../myzel394/alibi/helpers/AppLockHelper.kt | 20 ++--
.../SettingsScreen/Tiles/EnableAppLockTile.kt | 92 +++++++++++++++++++
.../alibi/ui/components/atoms/SettingsTile.kt | 2 +
.../alibi/ui/screens/SettingsScreen.kt | 2 +
app/src/main/res/values/strings.xml | 5 +
6 files changed, 124 insertions(+), 14 deletions(-)
create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/EnableAppLockTile.kt
diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
index 6657419..c57ecfe 100644
--- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
+++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
@@ -22,6 +22,8 @@ data class AppSettings(
val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings.getDefaultInstance(),
val videoRecorderSettings: VideoRecorderSettings = VideoRecorderSettings.getDefaultInstance(),
+ val appLockSettings: AppLockSettings? = null,
+
val hasSeenOnboarding: Boolean = false,
val showAdvancedSettings: Boolean = false,
val theme: Theme = Theme.SYSTEM,
@@ -97,6 +99,14 @@ data class AppSettings(
return copy(saveFolder = saveFolder)
}
+ fun setAppLockSettings(appLockSettings: AppLockSettings?): AppSettings {
+ return copy(appLockSettings = appLockSettings)
+ }
+
+ // If the object is present, biometric authentication is enabled.
+ // To disable biometric authentication, set the instance to null.
+ fun isAppLockEnabled() = appLockSettings != null
+
enum class Theme {
SYSTEM,
LIGHT,
@@ -553,8 +563,7 @@ data class NotificationSettings(
@Serializable
class AppLockSettings {
- // If the object is present, biometric authentication is enabled.
- // To disable biometric authentication, set the instance to null.
- val isEnabled
- get() = true
+ companion object {
+ fun getDefaultInstance() = AppLockSettings()
+ }
}
diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
index b963a14..182172e 100644
--- a/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
+++ b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
@@ -15,13 +15,13 @@ class AppLockHelper {
}
companion object {
- fun isSupported(context: Context): SupportType {
+ fun getSupportType(context: Context): SupportType {
val biometricManager = BiometricManager.from(context)
- when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
- BiometricManager.BIOMETRIC_SUCCESS -> return SupportType.AVAILABLE
- BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return SupportType.NONE_ENROLLED
+ return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
+ BiometricManager.BIOMETRIC_SUCCESS -> SupportType.AVAILABLE
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> SupportType.NONE_ENROLLED
- else -> return SupportType.UNAVAILABLE
+ else -> SupportType.UNAVAILABLE
}
}
@@ -29,23 +29,23 @@ class AppLockHelper {
context: Context,
title: String,
subtitle: String
- ): CompletableDeferred {
- val deferred = CompletableDeferred()
+ ): CompletableDeferred {
+ val deferred = CompletableDeferred()
val mainExecutor = ContextCompat.getMainExecutor(context)
val biometricPrompt = BiometricPrompt(
context as FragmentActivity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
- deferred.completeExceptionally(Exception(errString.toString()))
+ deferred.complete(false)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- deferred.complete(Unit)
+ deferred.complete(true)
}
override fun onAuthenticationFailed() {
- deferred.completeExceptionally(Exception("Authentication failed"))
+ deferred.complete(false)
}
}
)
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/EnableAppLockTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/EnableAppLockTile.kt
new file mode 100644
index 0000000..aeddb5e
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/EnableAppLockTile.kt
@@ -0,0 +1,92 @@
+package app.myzel394.alibi.ui.components.SettingsScreen.Tiles
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.magnifier
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Fingerprint
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.myzel394.alibi.R
+import app.myzel394.alibi.dataStore
+import app.myzel394.alibi.db.AppLockSettings
+import app.myzel394.alibi.db.AppSettings
+import app.myzel394.alibi.helpers.AppLockHelper
+import app.myzel394.alibi.ui.components.atoms.SettingsTile
+import kotlinx.coroutines.launch
+
+@Composable
+fun EnableAppLockTile(
+ settings: AppSettings,
+) {
+ val scope = rememberCoroutineScope()
+
+ val context = LocalContext.current
+ val dataStore = context.dataStore
+
+ val appLockSupport = AppLockHelper.getSupportType(context)
+
+ if (appLockSupport === AppLockHelper.SupportType.UNAVAILABLE) {
+ return
+ }
+
+ SettingsTile(
+ title = stringResource(R.string.ui_settings_option_enableAppLock_title),
+ description = stringResource(R.string.ui_settings_option_enableAppLock_description),
+ tertiaryLine = {
+ if (appLockSupport === AppLockHelper.SupportType.NONE_ENROLLED) {
+ Text(
+ stringResource(R.string.ui_settings_option_enableAppLock_enrollmentRequired),
+ color = Color.Yellow,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+ },
+ leading = {
+ Icon(
+ Icons.Default.Fingerprint,
+ contentDescription = null,
+ )
+ },
+ trailing = {
+ val title = stringResource(R.string.identityVerificationRequired_title)
+ val subtitle = stringResource(R.string.identityVerificationRequired_subtitle)
+
+ Switch(
+ checked = settings.isAppLockEnabled(),
+ enabled = appLockSupport === AppLockHelper.SupportType.AVAILABLE,
+ onCheckedChange = {
+ scope.launch {
+ val authenticationSuccessful = AppLockHelper.authenticate(
+ context,
+ title = title,
+ subtitle = subtitle,
+ ).await()
+
+ if (!authenticationSuccessful) {
+ return@launch
+ }
+
+ dataStore.updateData {
+ it.setAppLockSettings(
+ if (it.appLockSettings == null)
+ AppLockSettings.getDefaultInstance()
+ else
+ null
+ )
+ }
+ }
+ }
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/SettingsTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/SettingsTile.kt
index 729995f..2f255fe 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/SettingsTile.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/SettingsTile.kt
@@ -21,6 +21,7 @@ fun SettingsTile(
firstModifier: Modifier = Modifier,
title: String,
description: String? = null,
+ tertiaryLine: (@Composable () -> Unit) = {},
leading: @Composable () -> Unit = {},
trailing: @Composable () -> Unit = {},
extra: (@Composable () -> Unit)? = null,
@@ -49,6 +50,7 @@ fun SettingsTile(
text = description,
style = MaterialTheme.typography.bodySmall,
)
+ tertiaryLine()
}
Spacer(modifier = Modifier.width(16.dp))
trailing()
diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
index 471ee2f..217641d 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
@@ -52,6 +52,7 @@ import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderOutput
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderSamplingRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.SaveFolderTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderShowAllMicrophonesTile
+import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.EnableAppLockTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderBitrateTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
@@ -138,6 +139,7 @@ fun SettingsScreen(
InAppLanguagePicker()
DeleteRecordingsImmediatelyTile(settings = settings)
CustomNotificationTile(navController = navController, settings = settings)
+ EnableAppLockTile(settings = settings)
GlobalSwitch(
label = stringResource(R.string.ui_settings_advancedSettings_label),
checked = settings.showAdvancedSettings,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eacf755..ced0bc1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -162,4 +162,9 @@
Video Recorder is starting...
Alibi is locked
Unlock
+ Enable App Lock
+ Require your biometric info or your password to open Alibi
+ Please enroll a password or a biometric unlock method first
+ Verification required
+ You need to verify your identity to continue
\ No newline at end of file
From 6661d457ea9d38bffb31b85f1ec891e978494b75 Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Sat, 23 Dec 2023 20:41:36 +0100
Subject: [PATCH 5/6] feat: Add AsLockedApp wrapper to require id verification
if enabled
---
.../java/app/myzel394/alibi/MainActivity.kt | 6 +-
.../myzel394/alibi/helpers/AppLockHelper.kt | 12 +++
.../alibi/ui/{LockedApp.kt => AsLockedApp.kt} | 77 ++++++++++++++++++-
.../java/app/myzel394/alibi/ui/Navigation.kt | 6 --
4 files changed, 90 insertions(+), 11 deletions(-)
rename app/src/main/java/app/myzel394/alibi/ui/{LockedApp.kt => AsLockedApp.kt} (58%)
diff --git a/app/src/main/java/app/myzel394/alibi/MainActivity.kt b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
index 7222997..45e5c82 100644
--- a/app/src/main/java/app/myzel394/alibi/MainActivity.kt
+++ b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
@@ -14,7 +14,7 @@ import androidx.core.view.WindowCompat
import androidx.datastore.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.AppSettingsSerializer
-import app.myzel394.alibi.ui.LockedApp
+import app.myzel394.alibi.ui.AsLockedApp
import app.myzel394.alibi.ui.Navigation
import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.theme.AlibiTheme
@@ -33,7 +33,9 @@ class MainActivity : AppCompatActivity() {
setContent {
AlibiTheme {
- Navigation()
+ AsLockedApp {
+ Navigation()
+ }
}
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
index 182172e..5f10137 100644
--- a/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
+++ b/app/src/main/java/app/myzel394/alibi/helpers/AppLockHelper.kt
@@ -1,11 +1,13 @@
package app.myzel394.alibi.helpers
+import android.app.Activity
import android.content.Context
import androidx.biometric.BiometricManager
import androidx.core.content.ContextCompat
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.CompletableDeferred
+import kotlin.system.exitProcess
class AppLockHelper {
enum class SupportType {
@@ -60,5 +62,15 @@ class AppLockHelper {
return deferred
}
+
+ fun closeApp(context: Context) {
+ (context as? Activity)?.let {
+ it.finishAndRemoveTask()
+ it.finishAffinity()
+ it.finish()
+ }
+
+ exitProcess(0)
+ }
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt b/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
similarity index 58%
rename from app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt
rename to app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
index b152ab8..48ec3fc 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/LockedApp.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
@@ -20,17 +20,88 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
+import app.myzel394.alibi.dataStore
+import app.myzel394.alibi.helpers.AppLockHelper
+import kotlinx.coroutines.launch
// After this amount, close the app
-const val MAX_TRIES = 5
+const val MAX_TRIES = 10
@Composable
-fun LockedApp() {
+fun AsLockedApp(
+ content: (@Composable () -> Unit),
+) {
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ val settings = context
+ .dataStore
+ .data
+ .collectAsState(initial = null)
+ .value ?: return
+
+ // -1 = Unlocked, any other value = locked
+ var tries by remember { mutableIntStateOf(0) }
+
+ LaunchedEffect(settings.isAppLockEnabled()) {
+ if (!settings.isAppLockEnabled()) {
+ tries = -1
+ }
+ }
+
+ if (tries == -1) {
+ return content()
+ }
+
+ val title = stringResource(R.string.identityVerificationRequired_title)
+ val subtitle = stringResource(R.string.identityVerificationRequired_subtitle)
+
+ fun openAuthentication() {
+ if (tries >= MAX_TRIES) {
+ AppLockHelper.closeApp(context)
+ return
+ }
+
+ scope.launch {
+ val successful = AppLockHelper.authenticate(
+ context,
+ title,
+ subtitle,
+ ).await()
+
+ if (successful) {
+ tries = -1
+ return@launch
+ }
+
+ tries++
+
+ if (tries >= MAX_TRIES) {
+ AppLockHelper.closeApp(context)
+ }
+ }
+ }
+
+ LaunchedEffect(settings.isAppLockEnabled()) {
+ if (settings.isAppLockEnabled()) {
+ openAuthentication()
+ }
+ }
+
Scaffold { paddingValues ->
Column(
modifier = Modifier
@@ -60,7 +131,7 @@ fun LockedApp() {
modifier = Modifier
.fillMaxWidth()
.height(BIG_PRIMARY_BUTTON_SIZE),
- onClick = {},
+ onClick = ::openAuthentication,
colors = ButtonDefaults.filledTonalButtonColors(),
) {
Icon(
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
index bc1aa2a..a352502 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
@@ -72,12 +72,6 @@ fun Navigation(
}
}
- LaunchedEffect(Unit) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- AppLockHelper.authenticate(context, "Title", "Subtitle")
- }
- }
-
LaunchedEffect(settings.theme) {
if (!SUPPORTS_DARK_MODE_NATIVELY) {
val currentValue = AppCompatDelegate.getDefaultNightMode()
From 2d22a65506be4963c502428e602032b9220d7e3a Mon Sep 17 00:00:00 2001
From: Myzel394 <50424412+Myzel394@users.noreply.github.com>
Date: Mon, 25 Dec 2023 11:54:32 +0100
Subject: [PATCH 6/6] feat: Improve AsLockedApp
---
.../java/app/myzel394/alibi/MainActivity.kt | 28 ++++++++++------
.../java/app/myzel394/alibi/ui/AsLockedApp.kt | 3 +-
.../myzel394/alibi/ui/LockedAppHandlers.kt | 32 +++++++++++++++++++
.../java/app/myzel394/alibi/ui/Navigation.kt | 12 -------
4 files changed, 52 insertions(+), 23 deletions(-)
create mode 100644 app/src/main/java/app/myzel394/alibi/ui/LockedAppHandlers.kt
diff --git a/app/src/main/java/app/myzel394/alibi/MainActivity.kt b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
index 45e5c82..79b3966 100644
--- a/app/src/main/java/app/myzel394/alibi/MainActivity.kt
+++ b/app/src/main/java/app/myzel394/alibi/MainActivity.kt
@@ -2,21 +2,19 @@ package app.myzel394.alibi
import android.content.Context
import android.os.Bundle
-import android.view.MotionEvent
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.datastore.dataStore
-import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.AppSettingsSerializer
import app.myzel394.alibi.ui.AsLockedApp
+import app.myzel394.alibi.ui.LockedAppHandlers
import app.myzel394.alibi.ui.Navigation
-import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.theme.AlibiTheme
const val SETTINGS_FILE = "settings.json"
@@ -33,8 +31,18 @@ class MainActivity : AppCompatActivity() {
setContent {
AlibiTheme {
- AsLockedApp {
- Navigation()
+ LockedAppHandlers()
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ MaterialTheme.colorScheme.background
+ )
+ ) {
+ AsLockedApp {
+ Navigation()
+ }
}
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt b/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
index 48ec3fc..634f7a3 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/AsLockedApp.kt
@@ -39,8 +39,9 @@ import app.myzel394.alibi.helpers.AppLockHelper
import kotlinx.coroutines.launch
// After this amount, close the app
-const val MAX_TRIES = 10
+const val MAX_TRIES = 5
+// Makes sure the app needs to be unlocked first, if app lock is enabled
@Composable
fun AsLockedApp(
content: (@Composable () -> Unit),
diff --git a/app/src/main/java/app/myzel394/alibi/ui/LockedAppHandlers.kt b/app/src/main/java/app/myzel394/alibi/ui/LockedAppHandlers.kt
new file mode 100644
index 0000000..5db781c
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/LockedAppHandlers.kt
@@ -0,0 +1,32 @@
+package app.myzel394.alibi.ui
+
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.LocalContext
+import app.myzel394.alibi.dataStore
+import app.myzel394.alibi.db.AppSettings
+
+// Handlers that can safely be run when the app is locked (biometric authentication required)
+@Composable
+fun LockedAppHandlers() {
+ val context = LocalContext.current
+ val settings = context
+ .dataStore
+ .data
+ .collectAsState(initial = null)
+ .value ?: return
+
+ LaunchedEffect(settings.theme) {
+ if (!SUPPORTS_DARK_MODE_NATIVELY) {
+ val currentValue = AppCompatDelegate.getDefaultNightMode()
+
+ if (settings.theme == AppSettings.Theme.LIGHT && currentValue != AppCompatDelegate.MODE_NIGHT_NO) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ } else if (settings.theme == AppSettings.Theme.DARK && currentValue != AppCompatDelegate.MODE_NIGHT_YES) {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
index a352502..78a30e0 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt
@@ -72,18 +72,6 @@ fun Navigation(
}
}
- LaunchedEffect(settings.theme) {
- if (!SUPPORTS_DARK_MODE_NATIVELY) {
- val currentValue = AppCompatDelegate.getDefaultNightMode()
-
- if (settings.theme == AppSettings.Theme.LIGHT && currentValue != AppCompatDelegate.MODE_NIGHT_NO) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- } else if (settings.theme == AppSettings.Theme.DARK && currentValue != AppCompatDelegate.MODE_NIGHT_YES) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- }
- }
- }
-
NavHost(
modifier = Modifier
.background(MaterialTheme.colorScheme.background),