Skip to content

Commit 7a6988d

Browse files
committed
added lifesycle support
1 parent c1697b3 commit 7a6988d

File tree

7 files changed

+171
-0
lines changed

7 files changed

+171
-0
lines changed

PIN_REAUTH_IMPLEMENTATION.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# PIN Re-Authentication Feature
2+
3+
## Overview
4+
This implementation ensures that users must re-enter their PIN every time they return to the app after switching to another app or going to the home screen.
5+
6+
## How It Works
7+
8+
### 1. **AuthStateManager** (`auth/AuthStateManager.kt`)
9+
A singleton that manages the authentication state across the app:
10+
- Tracks whether the user is currently authenticated
11+
- Tracks whether re-authentication is required
12+
- Provides methods to update authentication state when app goes to background/foreground
13+
14+
### 2. **MainActivity Lifecycle Observer**
15+
The MainActivity now observes the app's lifecycle using `ProcessLifecycleOwner`:
16+
- **onStop()**: Called when the app goes to background → marks that re-authentication is required
17+
- **onStart()**: Called when the app returns to foreground → the app checks if authentication is needed
18+
19+
### 3. **AppNavigation Updates**
20+
The navigation component monitors the `requiresAuth` state:
21+
- When `requiresAuth` becomes true and the user is not authenticated, it automatically navigates to the Auth screen
22+
- This happens regardless of which screen the user was on
23+
24+
### 4. **AuthScreen Updates**
25+
The AuthScreen now updates the AuthStateManager when authentication succeeds:
26+
- After successful PIN entry, it marks the user as authenticated
27+
- After successful biometric authentication, it marks the user as authenticated
28+
29+
## User Experience
30+
31+
1. User opens the app and enters their PIN → authenticated
32+
2. User presses home button or switches to another app → app goes to background
33+
3. App detects background state and marks that re-authentication is required
34+
4. User returns to the app → Auth screen is automatically shown
35+
5. User must enter PIN again to continue
36+
37+
## Testing
38+
39+
To test this feature:
40+
1. Open the app and log in with your PIN
41+
2. Press the home button or switch to another app
42+
3. Return to the app
43+
4. You should see the PIN entry screen again
44+
45+
## Technical Notes
46+
47+
- Uses `ProcessLifecycleOwner` to observe app-wide lifecycle events (not just single Activity)
48+
- State is managed using Kotlin StateFlow for reactive updates
49+
- Navigation is handled automatically without requiring back stack manipulation
50+
- Works seamlessly with both PIN and biometric authentication

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ dependencies {
8989
// Lifecycle
9090
implementation libs.androidx.lifecycle.viewmodel.compose
9191
implementation libs.androidx.lifecycle.runtime.compose
92+
implementation libs.androidx.lifecycle.process
9293

9394
// DataStore for saving onboarding state
9495
implementation libs.androidx.datastore.preferences

app/src/main/java/com/secure/privacyfirst/MainActivity.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,29 @@ import androidx.compose.ui.Modifier
1313
import androidx.compose.ui.graphics.Color
1414
import androidx.compose.ui.graphics.toArgb
1515
import androidx.core.view.WindowCompat
16+
import androidx.lifecycle.DefaultLifecycleObserver
17+
import androidx.lifecycle.LifecycleOwner
18+
import androidx.lifecycle.ProcessLifecycleOwner
19+
import com.secure.privacyfirst.auth.AuthStateManager
1620
import com.secure.privacyfirst.navigation.AppNavigation
1721
import com.secure.privacyfirst.ui.theme.PrivacyFirstTheme
1822

1923
class MainActivity : AppCompatActivity() {
24+
25+
private val appLifecycleObserver = object : DefaultLifecycleObserver {
26+
override fun onStop(owner: LifecycleOwner) {
27+
super.onStop(owner)
28+
// App went to background - require re-authentication
29+
AuthStateManager.onAppBackgrounded()
30+
}
31+
32+
override fun onStart(owner: LifecycleOwner) {
33+
super.onStart(owner)
34+
// App came to foreground
35+
AuthStateManager.onAppForegrounded()
36+
}
37+
}
38+
2039
override fun onCreate(savedInstanceState: Bundle?) {
2140
super.onCreate(savedInstanceState)
2241

@@ -28,6 +47,9 @@ class MainActivity : AppCompatActivity() {
2847
// Edge-to-edge automatically handles system bar colors
2948
WindowCompat.setDecorFitsSystemWindows(window, false)
3049

50+
// Register lifecycle observer to track app foreground/background state
51+
ProcessLifecycleOwner.get().lifecycle.addObserver(appLifecycleObserver)
52+
3153
// Handle back button press using OnBackPressedDispatcher (modern approach)
3254
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
3355
override fun handleOnBackPressed() {
@@ -51,4 +73,10 @@ class MainActivity : AppCompatActivity() {
5173
}
5274
}
5375
}
76+
77+
override fun onDestroy() {
78+
super.onDestroy()
79+
// Cleanup lifecycle observer
80+
ProcessLifecycleOwner.get().lifecycle.removeObserver(appLifecycleObserver)
81+
}
5482
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.secure.privacyfirst.auth
2+
3+
import kotlinx.coroutines.flow.MutableStateFlow
4+
import kotlinx.coroutines.flow.StateFlow
5+
import kotlinx.coroutines.flow.asStateFlow
6+
7+
/**
8+
* Singleton to manage authentication state across the app
9+
* Tracks whether user needs to re-authenticate when returning from background
10+
*/
11+
object AuthStateManager {
12+
private val _isAuthenticated = MutableStateFlow(false)
13+
val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()
14+
15+
private val _requiresAuth = MutableStateFlow(false)
16+
val requiresAuth: StateFlow<Boolean> = _requiresAuth.asStateFlow()
17+
18+
/**
19+
* Mark user as authenticated (after successful PIN entry)
20+
*/
21+
fun setAuthenticated(authenticated: Boolean) {
22+
_isAuthenticated.value = authenticated
23+
if (authenticated) {
24+
_requiresAuth.value = false
25+
}
26+
}
27+
28+
/**
29+
* Mark that re-authentication is required (app went to background)
30+
*/
31+
fun setRequiresAuth(required: Boolean) {
32+
_requiresAuth.value = required
33+
}
34+
35+
/**
36+
* Called when app goes to background
37+
*/
38+
fun onAppBackgrounded() {
39+
if (_isAuthenticated.value) {
40+
_requiresAuth.value = true
41+
_isAuthenticated.value = false
42+
}
43+
}
44+
45+
/**
46+
* Called when app comes to foreground
47+
*/
48+
fun onAppForegrounded() {
49+
// requiresAuth will be true if user was authenticated before
50+
// This will trigger the auth screen to show
51+
}
52+
53+
/**
54+
* Reset all auth state (for logout or similar)
55+
*/
56+
fun reset() {
57+
_isAuthenticated.value = false
58+
_requiresAuth.value = false
59+
}
60+
}

app/src/main/java/com/secure/privacyfirst/navigation/AppNavigation.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package com.secure.privacyfirst.navigation
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.collectAsState
6+
import androidx.compose.runtime.getValue
47
import androidx.compose.ui.platform.LocalContext
58
import androidx.navigation.compose.NavHost
69
import androidx.navigation.compose.composable
710
import androidx.navigation.compose.rememberNavController
11+
import com.secure.privacyfirst.auth.AuthStateManager
812
import com.secure.privacyfirst.data.AppDatabase
913
import com.secure.privacyfirst.ui.screens.OnboardingScreen
1014
import com.secure.privacyfirst.ui.screens.SetupScreen
@@ -20,6 +24,27 @@ fun AppNavigation() {
2024
val context = LocalContext.current
2125
val database = AppDatabase.getDatabase(context)
2226

27+
// Observe authentication state
28+
val requiresAuth by AuthStateManager.requiresAuth.collectAsState()
29+
val isAuthenticated by AuthStateManager.isAuthenticated.collectAsState()
30+
31+
// When app comes back from background and requires auth, navigate to Auth screen
32+
LaunchedEffect(requiresAuth) {
33+
if (requiresAuth && !isAuthenticated) {
34+
val currentRoute = navController.currentBackStackEntry?.destination?.route
35+
// Only navigate if we're not already on auth/splash/setup/onboarding screens
36+
if (currentRoute != Screen.Auth.route &&
37+
currentRoute != Screen.Splash.route &&
38+
currentRoute != Screen.Setup.route &&
39+
currentRoute != Screen.Onboarding.route) {
40+
navController.navigate(Screen.Auth.route) {
41+
// Don't pop the back stack - just overlay auth screen
42+
launchSingleTop = true
43+
}
44+
}
45+
}
46+
}
47+
2348
NavHost(
2449
navController = navController,
2550
startDestination = Screen.Splash.route

app/src/main/java/com/secure/privacyfirst/ui/screens/AuthScreen.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.fragment.app.FragmentActivity
2727
import androidx.core.content.ContextCompat
2828
import androidx.lifecycle.viewmodel.compose.viewModel
2929
import androidx.navigation.NavHostController
30+
import com.secure.privacyfirst.auth.AuthStateManager
3031
import com.secure.privacyfirst.navigation.Screen
3132
import com.secure.privacyfirst.viewmodel.PasswordViewModel
3233
import kotlinx.coroutines.launch
@@ -75,6 +76,8 @@ fun AuthScreen(
7576
object : BiometricPrompt.AuthenticationCallback() {
7677
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
7778
Toast.makeText(context, "Authenticated", Toast.LENGTH_SHORT).show()
79+
// Mark user as authenticated
80+
AuthStateManager.setAuthenticated(true)
7881
if (navController != null) {
7982
navController.navigate(Screen.WebView.route) {
8083
popUpTo(Screen.Auth.route) { inclusive = true }
@@ -205,6 +208,8 @@ fun AuthScreen(
205208

206209
if (valid) {
207210
Toast.makeText(context, "PIN Accepted", Toast.LENGTH_SHORT).show()
211+
// Mark user as authenticated
212+
AuthStateManager.setAuthenticated(true)
208213
if (navController != null) {
209214
navController.navigate(Screen.WebView.route) {
210215
popUpTo(Screen.Auth.route) { inclusive = true }

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ datastorePreferences = "1.2.0"
55
espressoCoreVersion = "3.7.0"
66
kotlin = "2.2.21"
77
ksp = "2.3.2"
8+
lifecycleProcess = "2.10.0"
89
room = "2.8.4"
910
coreKtx = "1.17.0"
1011
junit = "4.13.2"
@@ -36,6 +37,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
3637
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
3738
androidx-espresso-core-v351 = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCoreVersion" }
3839
androidx-junit-v115 = { module = "androidx.test.ext:junit", version.ref = "androidxJunit" }
40+
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleProcess" }
3941
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
4042
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
4143
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }

0 commit comments

Comments
 (0)