feat: Add EnableAppLockTile to SettingsScreen

This commit is contained in:
Myzel394 2023-12-23 20:18:46 +01:00
parent a73fc6c48f
commit 025a8a3209
No known key found for this signature in database
GPG Key ID: 50098FCA22080F0F
6 changed files with 124 additions and 14 deletions

View File

@ -22,6 +22,8 @@ data class AppSettings(
val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings.getDefaultInstance(), val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings.getDefaultInstance(),
val videoRecorderSettings: VideoRecorderSettings = VideoRecorderSettings.getDefaultInstance(), val videoRecorderSettings: VideoRecorderSettings = VideoRecorderSettings.getDefaultInstance(),
val appLockSettings: AppLockSettings? = null,
val hasSeenOnboarding: Boolean = false, val hasSeenOnboarding: Boolean = false,
val showAdvancedSettings: Boolean = false, val showAdvancedSettings: Boolean = false,
val theme: Theme = Theme.SYSTEM, val theme: Theme = Theme.SYSTEM,
@ -97,6 +99,14 @@ data class AppSettings(
return copy(saveFolder = saveFolder) 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 { enum class Theme {
SYSTEM, SYSTEM,
LIGHT, LIGHT,
@ -553,8 +563,7 @@ data class NotificationSettings(
@Serializable @Serializable
class AppLockSettings { class AppLockSettings {
// If the object is present, biometric authentication is enabled. companion object {
// To disable biometric authentication, set the instance to null. fun getDefaultInstance() = AppLockSettings()
val isEnabled }
get() = true
} }

View File

@ -15,13 +15,13 @@ class AppLockHelper {
} }
companion object { companion object {
fun isSupported(context: Context): SupportType { fun getSupportType(context: Context): SupportType {
val biometricManager = BiometricManager.from(context) val biometricManager = BiometricManager.from(context)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) { return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS -> return SupportType.AVAILABLE BiometricManager.BIOMETRIC_SUCCESS -> SupportType.AVAILABLE
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return SupportType.NONE_ENROLLED BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> SupportType.NONE_ENROLLED
else -> return SupportType.UNAVAILABLE else -> SupportType.UNAVAILABLE
} }
} }
@ -29,23 +29,23 @@ class AppLockHelper {
context: Context, context: Context,
title: String, title: String,
subtitle: String subtitle: String
): CompletableDeferred<Unit> { ): CompletableDeferred<Boolean> {
val deferred = CompletableDeferred<Unit>() val deferred = CompletableDeferred<Boolean>()
val mainExecutor = ContextCompat.getMainExecutor(context) val mainExecutor = ContextCompat.getMainExecutor(context)
val biometricPrompt = BiometricPrompt( val biometricPrompt = BiometricPrompt(
context as FragmentActivity, context as FragmentActivity,
object : BiometricPrompt.AuthenticationCallback() { object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
deferred.completeExceptionally(Exception(errString.toString())) deferred.complete(false)
} }
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
deferred.complete(Unit) deferred.complete(true)
} }
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
deferred.completeExceptionally(Exception("Authentication failed")) deferred.complete(false)
} }
} }
) )

View File

@ -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
)
}
}
}
)
}
)
}

View File

@ -21,6 +21,7 @@ fun SettingsTile(
firstModifier: Modifier = Modifier, firstModifier: Modifier = Modifier,
title: String, title: String,
description: String? = null, description: String? = null,
tertiaryLine: (@Composable () -> Unit) = {},
leading: @Composable () -> Unit = {}, leading: @Composable () -> Unit = {},
trailing: @Composable () -> Unit = {}, trailing: @Composable () -> Unit = {},
extra: (@Composable () -> Unit)? = null, extra: (@Composable () -> Unit)? = null,
@ -49,6 +50,7 @@ fun SettingsTile(
text = description, text = description,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
tertiaryLine()
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
trailing() trailing()

View File

@ -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.AudioRecorderSamplingRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.SaveFolderTile 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.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.Tiles.VideoRecorderBitrateTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
@ -138,6 +139,7 @@ fun SettingsScreen(
InAppLanguagePicker() InAppLanguagePicker()
DeleteRecordingsImmediatelyTile(settings = settings) DeleteRecordingsImmediatelyTile(settings = settings)
CustomNotificationTile(navController = navController, settings = settings) CustomNotificationTile(navController = navController, settings = settings)
EnableAppLockTile(settings = settings)
GlobalSwitch( GlobalSwitch(
label = stringResource(R.string.ui_settings_advancedSettings_label), label = stringResource(R.string.ui_settings_advancedSettings_label),
checked = settings.showAdvancedSettings, checked = settings.showAdvancedSettings,

View File

@ -162,4 +162,9 @@
<string name="ui_videoRecorder_info_starting">Video Recorder is starting...</string> <string name="ui_videoRecorder_info_starting">Video Recorder is starting...</string>
<string name="ui_locked_title">Alibi is locked</string> <string name="ui_locked_title">Alibi is locked</string>
<string name="ui_locked_unlocked">Unlock</string> <string name="ui_locked_unlocked">Unlock</string>
<string name="ui_settings_option_enableAppLock_title">Enable App Lock</string>
<string name="ui_settings_option_enableAppLock_description">Require your biometric info or your password to open Alibi</string>
<string name="ui_settings_option_enableAppLock_enrollmentRequired">Please enroll a password or a biometric unlock method first</string>
<string name="identityVerificationRequired_title">Verification required</string>
<string name="identityVerificationRequired_subtitle">You need to verify your identity to continue</string>
</resources> </resources>