Skip to content

Commit d423ca0

Browse files
committed
feat: implement overlay-based lock screen with biometric support
- Replace PasswordOverlayActivity with LockScreenOverlayManager for displaying lock screen overlays using Compose - Add TransparentBiometricActivity to handle biometric authentication in a transparent activity - Update accessibility service config to use FLAG_RETRIEVE_INTERACTIVE_WINDOWS - Refactor AppLockAccessibilityService to use the new overlay manager and improve event handling - Minor fix in ActivityManager for RunningServiceInfo initialization
1 parent d971af8 commit d423ca0

5 files changed

Lines changed: 302 additions & 27 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package dev.pranav.applock.features.lockscreen.ui
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.graphics.PixelFormat
7+
import android.view.KeyEvent
8+
import android.view.WindowManager
9+
import androidx.compose.ui.platform.ComposeView
10+
import androidx.lifecycle.*
11+
import androidx.savedstate.SavedStateRegistry
12+
import androidx.savedstate.SavedStateRegistryController
13+
import androidx.savedstate.SavedStateRegistryOwner
14+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
15+
import dev.pranav.applock.core.utils.appLockRepository
16+
import dev.pranav.applock.data.repository.PreferencesRepository
17+
import dev.pranav.applock.ui.theme.AppLockTheme
18+
19+
@SuppressLint("ViewConstructor")
20+
class LockScreenOverlayManager(private val context: Context):
21+
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
22+
23+
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
24+
private var composeView: ComposeView? = null
25+
26+
// Lifecycle setup
27+
private val lifecycleRegistry = LifecycleRegistry(this)
28+
private val savedStateRegistryController = SavedStateRegistryController.create(this)
29+
private val store = ViewModelStore()
30+
private var isStateRestored = false
31+
32+
override val lifecycle: Lifecycle get() = lifecycleRegistry
33+
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
34+
override val viewModelStore: ViewModelStore get() = store
35+
36+
fun showOverlay(
37+
lockedPackageName: String,
38+
triggeringPackageName: String,
39+
onUnlock: () -> Unit
40+
) {
41+
if (composeView != null) return
42+
43+
if (!isStateRestored) {
44+
savedStateRegistryController.performRestore(null)
45+
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
46+
isStateRestored = true
47+
}
48+
49+
composeView = ComposeView(context).apply {
50+
setViewTreeLifecycleOwner(this@LockScreenOverlayManager)
51+
setViewTreeSavedStateRegistryOwner(this@LockScreenOverlayManager)
52+
setViewTreeViewModelStoreOwner(this@LockScreenOverlayManager)
53+
54+
setContent {
55+
AppLockTheme {
56+
val appLockRepository = context.appLockRepository()
57+
val appName = try {
58+
val pm = context.packageManager
59+
pm.getApplicationLabel(pm.getApplicationInfo(lockedPackageName, 0))
60+
.toString()
61+
} catch (_: Exception) {
62+
"App"
63+
}
64+
65+
val onPinAttemptCallback = { pin: String ->
66+
val isValid = appLockRepository.validatePassword(pin)
67+
if (isValid) {
68+
onUnlock()
69+
removeOverlay()
70+
}
71+
isValid
72+
}
73+
74+
val onPatternAttemptCallback = { pattern: String ->
75+
val isValid = appLockRepository.validatePattern(pattern)
76+
if (isValid) {
77+
onUnlock()
78+
removeOverlay()
79+
}
80+
isValid
81+
}
82+
83+
val lockType = appLockRepository.getLockType()
84+
85+
if (lockType == PreferencesRepository.LOCK_TYPE_PATTERN) {
86+
PatternLockScreen(
87+
fromMainActivity = false,
88+
lockedAppName = appName,
89+
triggeringPackageName = triggeringPackageName,
90+
onPatternAttempt = onPatternAttemptCallback
91+
)
92+
} else {
93+
PasswordOverlayScreen(
94+
showBiometricButton = appLockRepository.isBiometricAuthEnabled(),
95+
fromMainActivity = false,
96+
lockedAppName = appName,
97+
triggeringPackageName = triggeringPackageName,
98+
onAuthSuccess = {
99+
onUnlock()
100+
removeOverlay()
101+
},
102+
onBiometricAuth = {
103+
val intent = Intent(
104+
context,
105+
TransparentBiometricActivity::class.java
106+
).apply {
107+
flags =
108+
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION
109+
putExtra("locked_package", lockedPackageName)
110+
}
111+
context.startActivity(intent)
112+
},
113+
onPinAttempt = onPinAttemptCallback
114+
)
115+
}
116+
}
117+
}
118+
}
119+
120+
// Window Layout Parameters
121+
val params = WindowManager.LayoutParams(
122+
WindowManager.LayoutParams.MATCH_PARENT,
123+
WindowManager.LayoutParams.MATCH_PARENT,
124+
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
125+
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
126+
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
127+
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
128+
WindowManager.LayoutParams.FLAG_SECURE,
129+
PixelFormat.TRANSLUCENT
130+
).apply {
131+
// Respect brightness setting
132+
if (context.appLockRepository().shouldUseMaxBrightness()) {
133+
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
134+
}
135+
}
136+
137+
composeView?.isFocusableInTouchMode = true
138+
composeView?.requestFocus()
139+
140+
// Block Back Button
141+
composeView?.setOnKeyListener { _, keyCode, event ->
142+
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
143+
val intent = Intent(Intent.ACTION_MAIN).apply {
144+
addCategory(Intent.CATEGORY_HOME)
145+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
146+
}
147+
context.startActivity(intent)
148+
return@setOnKeyListener true
149+
}
150+
false
151+
}
152+
153+
try {
154+
windowManager.addView(composeView, params)
155+
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
156+
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
157+
} catch (e: Exception) {
158+
e.printStackTrace()
159+
}
160+
}
161+
162+
fun removeOverlay() {
163+
composeView?.let {
164+
try {
165+
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
166+
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
167+
windowManager.removeView(it)
168+
} catch (e: Exception) {
169+
e.printStackTrace()
170+
}
171+
composeView = null
172+
}
173+
}
174+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package dev.pranav.applock.features.lockscreen.ui
2+
3+
import android.os.Bundle
4+
import android.util.Log
5+
import androidx.biometric.BiometricManager
6+
import androidx.biometric.BiometricPrompt
7+
import androidx.core.content.ContextCompat
8+
import androidx.fragment.app.FragmentActivity
9+
import dev.pranav.applock.R
10+
import dev.pranav.applock.services.AppLockManager
11+
12+
class TransparentBiometricActivity: FragmentActivity() {
13+
private val TAG = "TransparentBiometric"
14+
private var lockedPackageName: String? = null
15+
16+
override fun onCreate(savedInstanceState: Bundle?) {
17+
super.onCreate(savedInstanceState)
18+
lockedPackageName = intent.getStringExtra("locked_package")
19+
20+
AppLockManager.reportBiometricAuthStarted()
21+
22+
val executor = ContextCompat.getMainExecutor(this)
23+
val biometricPrompt = BiometricPrompt(
24+
this, executor,
25+
object: BiometricPrompt.AuthenticationCallback() {
26+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
27+
super.onAuthenticationError(errorCode, errString)
28+
AppLockManager.reportBiometricAuthFinished()
29+
finish() // Close transparent activity if failed/canceled
30+
}
31+
32+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
33+
super.onAuthenticationSucceeded(result)
34+
AppLockManager.reportBiometricAuthFinished()
35+
lockedPackageName?.let {
36+
AppLockManager.temporarilyUnlockAppWithBiometrics(it)
37+
}
38+
39+
// The Accessibility service will detect the unlock state
40+
// and close the Service View automatically
41+
finish()
42+
}
43+
})
44+
45+
val appNameForPrompt = getString(R.string.this_app)
46+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
47+
.setTitle(getString(R.string.unlock_app_title, appNameForPrompt))
48+
.setSubtitle(getString(R.string.confirm_biometric_subtitle))
49+
.setNegativeButtonText(getString(R.string.use_pin_button))
50+
.setAllowedAuthenticators(
51+
BiometricManager.Authenticators.BIOMETRIC_WEAK or
52+
BiometricManager.Authenticators.BIOMETRIC_STRONG
53+
)
54+
.setConfirmationRequired(false)
55+
.build()
56+
57+
try {
58+
biometricPrompt.authenticate(promptInfo)
59+
} catch (e: Exception) {
60+
Log.e(TAG, "Biometric failed to start", e)
61+
finish()
62+
}
63+
}
64+
65+
override fun onPause() {
66+
super.onPause()
67+
if (isFinishing) {
68+
AppLockManager.reportBiometricAuthFinished()
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)