feat(applock): Add app lock with biometric/device credential authentication#10539
Open
thoomi wants to merge 5 commits into
Open
feat(applock): Add app lock with biometric/device credential authentication#10539thoomi wants to merge 5 commits into
thoomi wants to merge 5 commits into
Conversation
8e8f727 to
b8b004d
Compare
fc121d6 to
911805d
Compare
Member
|
@thoomi Sorry for the delay. We’re currently quite busy, and since this isn’t on our immediate roadmap and involves AI-assisted written code, it requires a more thorough review—something we’re unfortunately not able to complete in a timely manner. |
There was a problem hiding this comment.
Pull request overview
This PR introduces an App Lock feature to protect app access using biometrics and/or device credentials, implemented as new feature/applock API + implementation modules and wired into the app via lifecycle callbacks and Settings navigation.
Changes:
- Added new
feature/applock/api(public contracts) andfeature/applock/impl(coordinator, biometric auth, overlays, settings UI) modules and included them in the build. - Integrated app lock gating into app startup (
BaseApplicationActivityLifecycleCallbacks) and added a “Security” entry in General Settings. - Added substantial unit/Robolectric/Compose UI test coverage for the coordinator state machine, overlays, settings ViewModel, and lifecycle callback integration.
Reviewed changes
Copilot reviewed 52 out of 52 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle.kts | Includes the new feature:applock:api and feature:applock:impl modules. |
| legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt | Adds an AppLockCoordinator binding for legacy test DI. |
| legacy/ui/legacy/src/main/res/xml/general_settings.xml | Adds a “Security” preference entry point. |
| legacy/ui/legacy/src/main/res/values/strings.xml | Adds Security string for the new settings entry. |
| legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsFragment.kt | Wires the “Security” preference click to app lock settings navigation. |
| legacy/ui/legacy/build.gradle.kts | Adds dependency on feature.applock.api for navigation contract injection. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsViewModelTest.kt | Tests settings MVI/ViewModel behavior. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/ui/DefaultAppLockGateTest.kt | Robolectric tests for overlay/auth triggering and multi-activity behavior. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/ui/AppLockUnavailableOverlayTest.kt | Compose UI tests for the “unavailable” overlay. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/ui/AppLockFailedOverlayTest.kt | Compose UI tests for the “failed” overlay. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/domain/MapErrorCodeTest.kt | Tests mapping BiometricPrompt error codes to domain errors. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/domain/FakeAuthenticator.kt | Test fake authenticator implementation. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/domain/FakeAppLockCoordinator.kt | Test fake coordinator for UI-layer tests. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/domain/DefaultAppLockCoordinatorTest.kt | Comprehensive coordinator state machine tests. |
| feature/applock/impl/src/test/kotlin/net/thunderbird/feature/applock/impl/data/AppLockConfigStoreTest.kt | Tests SharedPreferences-backed config storage. |
| feature/applock/impl/src/main/res/values/strings.xml | Adds strings for prompts, errors, overlays, and settings UI. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/DefaultAppLockSettingsNavigation.kt | Implements settings navigation contract. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsViewModel.kt | Settings screen ViewModel (enable/auth-first, timeout changes). |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsScreen.kt | Compose screen wiring effects (auth request) + lifecycle resume refresh. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsContract.kt | Defines MVI contract for settings UI. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsContent.kt | Compose UI for enable + timeout selection. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/settings/AppLockSettingsActivity.kt | Hosts the settings screen in a dedicated Activity. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/DefaultAppLockGate.kt | Gate implementation: overlays + auth triggering tied to activity lifecycle. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/ui/AppLockOverlayContent.kt | Compose overlays for failed/unavailable states. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/FeatureAppLockModule.kt | Koin module wiring for coordinator, authenticator, gate factory, settings VM/navigation. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/DefaultBiometricAvailabilityChecker.kt | Availability checking via BiometricManager + mapping to unavailable reasons. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/DefaultAppLockLifecycleHandler.kt | Process lifecycle + screen-off receiver integration. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/DefaultAppLockCoordinator.kt | Coordinator state machine and auth orchestration (pull model). |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/BiometricAuthenticatorFactory.kt | Creates BiometricPrompt-backed authenticator with localized strings. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/BiometricAuthenticator.kt | BiometricPrompt authenticator + error mapping + allowed authenticators mask. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/AppLockLifecycleHandler.kt | Abstraction for lifecycle/screen-off registration (testability). |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/AppLockConfigRepository.kt | Abstraction for config persistence. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/domain/AppLockAvailability.kt | Abstraction for auth availability/reason checks. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/data/AppLockPreferences.kt | SharedPreferences keys for app lock config. |
| feature/applock/impl/src/main/kotlin/net/thunderbird/feature/applock/impl/data/AppLockConfigStore.kt | SharedPreferences-backed config store implementation. |
| feature/applock/impl/src/main/AndroidManifest.xml | Declares the non-exported settings activity. |
| feature/applock/impl/build.gradle.kts | Adds module setup + dependencies + Robolectric Android resources for tests. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockState.kt | Defines app lock state model + unavailable reasons + isUnlocked() helper. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockResult.kt | Defines AppLockResult alias. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockError.kt | Defines domain-level authentication errors. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockCoordinator.kt | Public coordinator contract and threading notes. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockConfig.kt | Public config model + defaults. |
| feature/applock/api/src/commonMain/kotlin/net/thunderbird/feature/applock/api/AppLockAuthenticator.kt | Public authenticator abstraction. |
| feature/applock/api/src/androidMain/kotlin/net/thunderbird/feature/applock/api/AppLockSettingsNavigation.kt | Android navigation contract to open settings. |
| feature/applock/api/src/androidMain/kotlin/net/thunderbird/feature/applock/api/AppLockGate.kt | Android gate contract (lifecycle observer + factory). |
| feature/applock/api/src/androidMain/kotlin/net/thunderbird/feature/applock/api/AppLockAuthenticatorFactory.kt | Android factory contract for activity-bound authenticators. |
| feature/applock/api/build.gradle.kts | Declares the KMP API module and dependencies. |
| app-common/src/test/kotlin/net/thunderbird/app/common/feature/applock/AppLockActivityLifecycleCallbacksTest.kt | Tests lifecycle callback integration (gate created only for FragmentActivity, null-safe). |
| app-common/src/main/kotlin/net/thunderbird/app/common/feature/applock/AppLockActivityLifecycleCallbacks.kt | Registers a per-activity gate via ActivityLifecycleCallbacks. |
| app-common/src/main/kotlin/net/thunderbird/app/common/feature/AppCommonFeatureModule.kt | Includes the app lock DI module in app-common’s feature module. |
| app-common/src/main/kotlin/net/thunderbird/app/common/BaseApplication.kt | Registers AppLockActivityLifecycleCallbacks at application startup. |
| app-common/build.gradle.kts | Adds applock api/impl deps and Android resources for Robolectric tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+217
to
+232
| private fun resolveWindowBackgroundColor(): Int { | ||
| val typedValue = TypedValue() | ||
|
|
||
| val isWindowBackgroundColor = | ||
| activity.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true) && | ||
| typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && | ||
| typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT | ||
|
|
||
| val isColorBackground = !isWindowBackgroundColor && | ||
| activity.theme.resolveAttribute(android.R.attr.colorBackground, typedValue, true) | ||
|
|
||
| return when { | ||
| isWindowBackgroundColor || isColorBackground -> typedValue.data | ||
| else -> Color.BLACK | ||
| } | ||
| } |
Comment on lines
+5
to
+16
| kotlin { | ||
| androidLibrary { | ||
| namespace = "net.thunderbird.feature.applock.api" | ||
| withHostTest {} | ||
| } | ||
| sourceSets { | ||
| commonMain.dependencies { | ||
| api(projects.core.outcome) | ||
| api(libs.kotlinx.coroutines.core) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an App Lock feature that allows users to protect access to Thunderbird using
biometric authentication (fingerprint, face) or device credentials (PIN/pattern/password).
This is a highly requested community feature:
Changes
New modules
feature/applock/api- Public contracts (AppLockCoordinator, AppLockGate, state types)feature/applock/impl- Implementation (coordinator, biometric auth, UI overlays, settings)Architecture
Key design decisions
ensureUnlocked()rather than coordinator pushing UIViewoverlay (not Compose) for synchronous rendering in task switcherIntegration
app-commonintegration viaActivityLifecycleCallbacksScreenshots & Videos
Settings UI
Unlock Flow
applock_happy_path_flow.mp4
Biometric prompt shown when opening the app
Videos
Enabling app lock (requires authentication first)
applock_settings_enable_flow.mp4
PIN fallback authentication
applock_settings_pin_fallback_flow.mp4
Unavailable: navigate to device settings to enroll
applock_unavailable_to_device_settings_flow.mp4
Full flow: unavailable → enroll biometrics → return to app
applock_unavailable_to_device_settings_biometric_back_to_app_flow.mp4
Testing
Manual testing checklist
Review
This PR is structured as 5 focused commits that can be reviewed individually. If the overall diff is too large, I'm happy to split this into smaller, incremental PRs — let me know what works best. I just wanted to put it into a one big PR to see how a full implementation would look like.
I also see this PR as a spark for the discussion on the topic. Lets discuss if we want to integrate such a feature at all, since many valid concerns have been raised about it in the past. 😊
Future work
The following enhancements are planned as follow-ups:
Disclaimer
Large parts of the PR have been written and reviewed by Coding Agents. I am more like an Android and Kotlin novice but i am of course very eager to learn more about it :)