@@ -9,8 +9,12 @@ import androidx.compose.animation.ExitTransition
99import androidx.compose.foundation.isSystemInDarkTheme
1010import androidx.compose.material3.MaterialTheme
1111import androidx.compose.material3.Surface
12+ import androidx.compose.runtime.LaunchedEffect
1213import androidx.compose.runtime.collectAsState
1314import androidx.compose.runtime.getValue
15+ import androidx.compose.runtime.mutableStateOf
16+ import androidx.compose.runtime.remember
17+ import androidx.compose.runtime.setValue
1418import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
1519import androidx.navigation.NavType
1620import androidx.navigation.compose.NavHost
@@ -39,119 +43,150 @@ import kotlinx.coroutines.launch
3943class MainActivity : FragmentActivity () {
4044
4145 private val settingsViewModel: SettingsViewModel by viewModels()
42- private var hasShownBiometric = false
4346
4447 override fun onCreate (savedInstanceState : Bundle ? ) {
4548 val splashScreen = installSplashScreen()
4649 super .onCreate(savedInstanceState)
47- splashScreen.setKeepOnScreenCondition {
48- ! settingsViewModel.isLoaded.value
49-
50- }
51-
52-
50+ splashScreen.setKeepOnScreenCondition { ! settingsViewModel.isLoaded.value }
5351
5452 setContent {
5553 val currentSettings by settingsViewModel.settings.collectAsState()
54+ val isAppUnlocked by settingsViewModel.isAppUnlocked.collectAsState()
55+ val isLoaded by settingsViewModel.isLoaded.collectAsState()
56+
57+ var showContent by remember { mutableStateOf(false ) }
58+
59+ LaunchedEffect (isAppUnlocked, currentSettings.biometricLock, isLoaded) {
60+ if (! isLoaded) return @LaunchedEffect // wait for real settings to load
61+ when {
62+ ! currentSettings.biometricLock || isAppUnlocked -> showContent = true
63+ else -> {
64+ showBiometricPrompt(
65+ onSuccess = {
66+ settingsViewModel.setAppUnlocked(true )
67+ showContent = true
68+ },
69+ onError = { errorCode, errString ->
70+ handleBiometricError(errorCode, errString) {
71+ showContent = true
72+ }
73+ }
74+ )
75+ }
76+ }
77+ }
5678
5779 OpenNotesTheme (settings = currentSettings) {
5880 Surface (color = MaterialTheme .colorScheme.background) {
59- val navController = rememberNavController()
60-
61- NavHost (
62- navController = navController,
63- startDestination = Screen .NotesScreen .route,
64- enterTransition = { EnterTransition .None },
65- exitTransition = { ExitTransition .None },
66- popEnterTransition = { EnterTransition .None },
67- popExitTransition = { ExitTransition .None }
68- ) {
69- composable(route = Screen .NotesScreen .route) {
70- NotesScreen (navController = navController)
71- }
72- composable(
73- route = Screen .AddEditNoteScreen .route +
74- " ?noteId={noteId}¬eColor={noteColor}" ,
75- arguments = listOf (
76- navArgument(" noteId" ) {
77- type = NavType .IntType
78- defaultValue = - 1
79- },
80- navArgument(" noteColor" ) {
81- type = NavType .IntType
82- defaultValue = - 1
81+ if (showContent) {
82+ val navController = rememberNavController()
83+ NavHost (
84+ navController = navController,
85+ startDestination = Screen .NotesScreen .route,
86+ enterTransition = { EnterTransition .None },
87+ exitTransition = { ExitTransition .None },
88+ popEnterTransition = { EnterTransition .None },
89+ popExitTransition = { ExitTransition .None }
90+ ) {
91+ composable(route = Screen .NotesScreen .route) {
92+ NotesScreen (navController = navController)
93+ }
94+ composable(
95+ route = Screen .AddEditNoteScreen .route +
96+ " ?noteId={noteId}¬eColor={noteColor}" ,
97+ arguments = listOf (
98+ navArgument(" noteId" ) {
99+ type = NavType .IntType
100+ defaultValue = - 1
101+ },
102+ navArgument(" noteColor" ) {
103+ type = NavType .IntType
104+ defaultValue = - 1
105+ }
106+ )
107+ ) { backStackEntry ->
108+ val color = backStackEntry.arguments?.getInt(" noteColor" )
109+ ?.takeIf { it != - 1 }
110+ val isDarkTheme = when (currentSettings.themeMode) {
111+ ThemeMode .SYSTEM -> isSystemInDarkTheme()
112+ ThemeMode .LIGHT -> false
113+ ThemeMode .DARK -> true
83114 }
84- )
85- ) { backStackEntry ->
86- val color = backStackEntry.arguments?.getInt(" noteColor" )
87- ?.takeIf { it != - 1 }
88- val isDarkTheme = when (currentSettings.themeMode) {
89- ThemeMode .SYSTEM ->
90- isSystemInDarkTheme()
91- ThemeMode .LIGHT -> false
92- ThemeMode .DARK -> true
115+ val resolvedColor = color ? : if (isDarkTheme) {
116+ NoteColorPalette .Dark .first().toArgb()
117+ } else {
118+ NoteColorPalette .Light .first().toArgb()
119+ }
120+ AddEditNoteScreen (
121+ navController = navController,
122+ noteColor = resolvedColor,
123+ isDarkTheme = isDarkTheme
124+ )
93125 }
94-
95- val resolvedColor = color ? : if (isDarkTheme) {
96- NoteColorPalette .Dark .first().toArgb()
97- } else {
98- NoteColorPalette .Light .first().toArgb()
126+ composable(route = Screen .SettingsScreen .route) {
127+ SettingsScreen (
128+ navController = navController,
129+ viewModel = settingsViewModel
130+ )
131+ }
132+ composable(route = Screen .AboutScreen .route) {
133+ AboutScreen (navController = navController)
99134 }
100-
101- AddEditNoteScreen (
102- navController = navController,
103- noteColor = resolvedColor,
104- isDarkTheme = isDarkTheme
105- )
106- }
107- composable(route = Screen .SettingsScreen .route) {
108- SettingsScreen (
109- navController = navController,
110- viewModel = settingsViewModel
111- )
112- }
113- composable(route = Screen .AboutScreen .route) {
114- AboutScreen (
115- navController = navController
116- )
117135 }
118-
136+ } else {
137+ // Blank surface shown until auth completes
138+ Surface (color = MaterialTheme .colorScheme.background) {}
119139 }
120140 }
121141 }
122142 }
123-
124-
125- }
126-
127- override fun onWindowFocusChanged (hasFocus : Boolean ) {
128- super .onWindowFocusChanged(hasFocus)
129- if (hasFocus && ! hasShownBiometric) {
130- hasShownBiometric = true
131- handleBiometricLock()
132- }
133143 }
134144
135- private fun handleBiometricLock () {
136- lifecycleScope.launch {
137- settingsViewModel.isLoaded.filter { it }.first()
138- if (! settingsViewModel.settings.value.biometricLock) return @launch
139-
140- val promptInfo = BiometricPrompt .PromptInfo .Builder ()
145+ private fun showBiometricPrompt (
146+ onSuccess : () -> Unit ,
147+ onError : (Int , CharSequence ) -> Unit
148+ ) {
149+ BiometricPrompt (
150+ this ,
151+ ContextCompat .getMainExecutor(this ),
152+ object : BiometricPrompt .AuthenticationCallback () {
153+ override fun onAuthenticationSucceeded (result : BiometricPrompt .AuthenticationResult ) {
154+ onSuccess()
155+ }
156+ override fun onAuthenticationError (errorCode : Int , errString : CharSequence ) {
157+ onError(errorCode, errString)
158+ }
159+ override fun onAuthenticationFailed () = Unit
160+ }
161+ ).authenticate(
162+ BiometricPrompt .PromptInfo .Builder ()
141163 .setTitle(" Unlock OpenNotes" )
142164 .setSubtitle(" Confirm your fingerprint to access your notes" )
143165 .setNegativeButtonText(" Cancel" )
144166 .build()
167+ )
168+ }
145169
146- BiometricPrompt (
147- this @MainActivity,
148- ContextCompat .getMainExecutor(this @MainActivity),
149- object : BiometricPrompt .AuthenticationCallback () {
150- override fun onAuthenticationSucceeded (result : BiometricPrompt .AuthenticationResult ) = Unit
151- override fun onAuthenticationError (errorCode : Int , errString : CharSequence ) = finish()
152- override fun onAuthenticationFailed () = Unit
153- }
154- ).authenticate(promptInfo)
170+ private fun handleBiometricError (
171+ errorCode : Int ,
172+ errString : CharSequence ,
173+ onComplete : () -> Unit
174+ ) {
175+ when (errorCode) {
176+ BiometricPrompt .ERROR_USER_CANCELED ,
177+ BiometricPrompt .ERROR_NEGATIVE_BUTTON ,
178+ BiometricPrompt .ERROR_CANCELED -> finish()
179+
180+ BiometricPrompt .ERROR_NO_BIOMETRICS ,
181+ BiometricPrompt .ERROR_HW_NOT_PRESENT ,
182+ BiometricPrompt .ERROR_HW_UNAVAILABLE -> {
183+ settingsViewModel.setAppUnlocked(true )
184+ // disable biometric lock since hardware is unavailable
185+ settingsViewModel.onBiometricLockToggleRequest(false )
186+ onComplete()
187+ }
188+
189+ else -> finish()
155190 }
156191 }
157192}
0 commit comments