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),