Skip to content

Added a feature to hide notes#947

Open
danmasber wants to merge 4 commits intoCrustack:mainfrom
danmasber:feature/hidden
Open

Added a feature to hide notes#947
danmasber wants to merge 4 commits intoCrustack:mainfrom
danmasber:feature/hidden

Conversation

@danmasber
Copy link

@danmasber danmasber commented Mar 22, 2026

If the screen lock is enabled, they ca…n only be accessed via the unlock method.

Summary by CodeRabbit

  • New Features
    • Added a secure Hidden notes folder that requires biometric or PIN authentication to view.
    • Move notes to/from Hidden via new menu and action-mode options (including Unhide).
    • Hidden appears in navigation, search filters, and contextual actions for easy access.
    • New icons and UI strings (including Spanish translations) for Hidden/unhidden states.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds a "Hidden" folder: new Folder.HIDDEN, HiddenFragment guarded by biometric/PIN prompt, ViewModel observable for hidden notes, UI/menu/action updates to move/toggle hidden state, LockUtils support for hidden flow, plus drawable, navigation, and string resources.

Changes

Cohort / File(s) Summary
Enum & Action Constants
app/src/main/java/com/philkes/notallyx/data/model/Folder.kt, app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt
Added Folder.HIDDEN and EditAction.HIDDEN plus title/icon handling for hidden/unhidden.
ViewModel / Data
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
Added hiddenNotes: Content? and initialization/observer wiring to fetch notes from baseNoteDao.getFrom(Folder.HIDDEN).
Fragments & Navigation
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt, app/src/main/res/navigation/navigation.xml
Added HiddenFragment (starts hidden, shows after auth) and navigation destination @+id/Hidden.
Activity & Action Mode
app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
Inserted "Hidden" drawer item, adjusted destination handling, and extended contextual/action-mode menus to move notes to/from Folder.HIDDEN.
Search / UI Extensions
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt, app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt
Included Folder.HIDDEN in chip selection logic and updated Folder.movedToResId() mapping for hidden plural.
Note Actions
app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt
Added handling for EditAction.HIDDEN and a private hidden() method to toggle note into/out of Folder.HIDDEN.
Security Utilities
app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt
Added showBiometricOrPinPromptHidden(...), made promptPinAuthentication launcher nullable, and adjusted fallback/auth flows for hidden access.
Drawables
app/src/main/res/drawable/hidden.xml, app/src/main/res/drawable/unhidden.xml
Added vector drawables for hidden/unhidden icons.
Strings / Localization
app/src/main/res/values/strings.xml, app/src/main/res/values-es/strings.xml
Added hiddens, hidden, unhidden, hidden_lock_title, hidden_selected_notes plural and Spanish equivalents plus related strings.

Sequence Diagram

sequenceDiagram
    participant HiddenFragment as HiddenFragment
    participant MainActivity as MainActivity
    participant LockUtils as LockUtils
    participant Biometric as BiometricPrompt
    participant DAO as BaseNoteDao
    participant UI as FragmentUI

    HiddenFragment->>UI: onStart() -> hide()
    HiddenFragment->>LockUtils: showBiometricOrPinPromptHidden(title, onSuccess, onFailure)
    LockUtils->>Biometric: show biometric prompt (API>=M) / fallback to PIN prompt
    alt Auth success
        Biometric-->>LockUtils: success
        LockUtils->>HiddenFragment: onSuccess()
        HiddenFragment->>DAO: request hiddenNotes (getFrom Folder.HIDDEN)
        DAO-->>HiddenFragment: returns notes
        HiddenFragment->>UI: show() -> display notes
    else Auth failure
        Biometric-->>LockUtils: failure
        LockUtils->>HiddenFragment: onFailure(errorCode?)
        HiddenFragment->>UI: remain hidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I tuck a note in moonlit fur so snug,
A tiny lock, a secret, folded rug.
A whisker scan, a thumb, the latch unbinds,
I hop, I peek, then guard the hidden finds. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature added across the changeset: a comprehensive hidden notes functionality with encryption, biometric/PIN protection, and UI integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt`:
- Around line 25-29: The fragment currently hides its root in onStart() then
calls showBiometricOrPinPromptHidden(this, ..., onSuccess = { show() }) with an
empty failure lambda, which leaves the UI blank on cancel/error; update the
failure lambda (and the equivalent launcher-null path in LockUtils functions) to
restore a visible state or navigate away: e.g., call show() to reveal a
retryable locked UI or call
requireActivity().onBackPressedDispatcher.onBackPressed() / navigateUp to leave
the fragment, and optionally display a toast or locked-state view; locate
handlers in HiddenFragment.onStart, the hide()/show() calls, and the
biometric/pin launch code in LockUtils to implement the change.
- Around line 21-22: In getObservable() of HiddenFragment avoid force-unwrapping
model.hiddenNotes; instead return a safe non-null LiveData by checking
model.hiddenNotes and returning a fallback LiveData when it's null (e.g. a new
MutableLiveData<List<Item>>(emptyList())) so a cold open won't crash; reference
model.hiddenNotes and getObservable()/HiddenFragment to locate the change and
ensure BaseNoteModel.hiddenNotes initialization race is handled by providing the
safe fallback.
- Around line 31-36: The show()/hide() methods dereference binding with
binding!! which can crash if showBiometricOrPinPromptHidden() completes after
onDestroyView(); change both to safely access the view (e.g., replace binding!!
with binding? and set visibility only when non-null: binding?.root?.visibility =
VISIBLE / INVISIBLE) or guard with binding?.root?.let { it.visibility = ... }
and optionally check fragment state (isAdded or viewLifecycleOwner.lifecycle)
before touching the view to avoid post-destroy access.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt`:
- Line 35: The fragment references R.id.Hidden (used in
SearchFragment.check(R.id.Hidden) / folder selection) but the ChipGroup in
fragment_notes.xml lacks a Chip with id "@+id/Hidden"; add a new Chip with
android:id="@+id/Hidden" (use the same style as the other filter chips, e.g.,
style="@style/FilterChip", wrap_content sizes, and text="@string/hidden") inside
the existing ChipGroup alongside Notes, Deleted, and Archived so the
check(R.id.Hidden) call can find and select the chip and enable filtering by the
Hidden folder.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt`:
- Around line 563-566: The Hidden menu case in MainActivity sets
baseModel.currentLabel to the Unlabeled sentinel CURRENT_LABEL_NONE, making
Hidden indistinguishable from Unlabeled; change the assignment in the
R.id.Hidden branch (the block that also calls
binding.NavigationView.setCheckedItem(destination.id)) to use a distinct
sentinel such as CURRENT_LABEL_EMPTY (or create a dedicated
CURRENT_LABEL_HIDDEN) instead of CURRENT_LABEL_NONE so menu rebuilds and Search
setup can differentiate Hidden from Unlabeled.

In `@app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt`:
- Around line 124-128: The PIN fallback path calls promptPinAuthentication with
a null activityResultLauncher so the PIN confirmation intent is launched but its
result is never delivered and onSuccess is never invoked; fix by either (A)
requiring a non-null ActivityResultLauncher: change promptPinAuthentication
signature to accept a non-null ActivityResultLauncher and update the caller here
to pass HiddenFragment’s launcher so the result callback can invoke onSuccess,
or (B) for API 21-22 implement the legacy flow: call
fragment.startActivityForResult for the KEYGUARD intent and handle the result in
the fragment’s onActivityResult to call onSuccess on success, or (C) if
intended, explicitly short-circuit and call onSuccess when the device is
unsecured (matching the biometric unsecured branch) — choose one approach and
update promptPinAuthentication, its callers, and HiddenFragment result handling
accordingly.

In `@app/src/main/res/values/strings.xml`:
- Around line 389-390: The strings.xml entry for <string name="unhidden"> is
incorrectly set to the same value as <string name="hidden">, causing toggle
actions in MainActivity.kt and Preference.kt to both display "Hidden"; update
the <string name="unhidden"> resource to a distinct English label (e.g.,
"Unhidden" or "Show") so the hide vs unhide actions display correctly, then
rebuild to verify MainActivity.kt and Preference.kt use the corrected resource.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2864abd7-2575-4759-b24f-07c92a9bcbbc

📥 Commits

Reviewing files that changed from the base of the PR and between 6c931aa and ee4250f.

📒 Files selected for processing (14)
  • app/src/main/java/com/philkes/notallyx/data/model/Folder.kt
  • app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt
  • app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt
  • app/src/main/res/drawable/hidden.xml
  • app/src/main/res/drawable/unhidden.xml
  • app/src/main/res/navigation/navigation.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values/strings.xml

Comment on lines +21 to +22
override fun getObservable(): LiveData<List<Item>> {
return model.hiddenNotes!!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't force-unwrap hiddenNotes here.

This override is reached from the super.onViewCreated() call on Line 15, but BaseNoteModel.hiddenNotes is still initialized later in app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt:196-200. A cold open of Hidden can therefore crash on model.hiddenNotes!!.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt`
around lines 21 - 22, In getObservable() of HiddenFragment avoid
force-unwrapping model.hiddenNotes; instead return a safe non-null LiveData by
checking model.hiddenNotes and returning a fallback LiveData when it's null
(e.g. a new MutableLiveData<List<Item>>(emptyList())) so a cold open won't
crash; reference model.hiddenNotes and getObservable()/HiddenFragment to locate
the change and ensure BaseNoteModel.hiddenNotes initialization race is handled
by providing the safe fallback.

Comment on lines +25 to +29
override fun onStart() {
super.onStart()
hide()
showBiometricOrPinPromptHidden(this, R.string.hidden_lock_title, onSuccess = { show() }) {}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle failed or cancelled auth before leaving the view blank.

Line 27 hides the root, but the failure lambda is a no-op. Any cancel/error path leaves this fragment on an empty screen, and the launcher-null path in app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt:200-240 has the same outcome. Navigate away or show a retryable locked state when auth fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt`
around lines 25 - 29, The fragment currently hides its root in onStart() then
calls showBiometricOrPinPromptHidden(this, ..., onSuccess = { show() }) with an
empty failure lambda, which leaves the UI blank on cancel/error; update the
failure lambda (and the equivalent launcher-null path in LockUtils functions) to
restore a visible state or navigate away: e.g., call show() to reveal a
retryable locked UI or call
requireActivity().onBackPressedDispatcher.onBackPressed() / navigateUp to leave
the fragment, and optionally display a toast or locked-state view; locate
handlers in HiddenFragment.onStart, the hide()/show() calls, and the
biometric/pin launch code in LockUtils to implement the change.

Comment on lines +31 to +36
protected fun show() {
binding!!.root.visibility = VISIBLE
}

protected fun hide() {
binding!!.root.visibility = INVISIBLE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid binding!! in the biometric callback path.

showBiometricOrPinPromptHidden() resolves asynchronously, so show() can run after onDestroyView() on rotation or back navigation. binding!! will then crash the fragment; nullable view access is safer here.

Suggested fix
     protected fun show() {
-        binding!!.root.visibility = VISIBLE
+        binding?.root?.visibility = VISIBLE
     }

     protected fun hide() {
-        binding!!.root.visibility = INVISIBLE
+        binding?.root?.visibility = INVISIBLE
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/HiddenFragment.kt`
around lines 31 - 36, The show()/hide() methods dereference binding with
binding!! which can crash if showBiometricOrPinPromptHidden() completes after
onDestroyView(); change both to safely access the view (e.g., replace binding!!
with binding? and set visibility only when non-null: binding?.root?.visibility =
VISIBLE / INVISIBLE) or guard with binding?.root?.let { it.visibility = ... }
and optionally check fragment state (isAdded or viewLifecycleOwner.lifecycle)
before touching the view to avoid post-destroy access.

Folder.NOTES -> R.id.Notes
Folder.DELETED -> R.id.Deleted
Folder.ARCHIVED -> R.id.Archived
Folder.HIDDEN -> R.id.Hidden
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Hidden chip is missing from fragment_notes.xml
rg -n "android:id.*Hidden" app/src/main/res/layout/

Repository: Crustack/NotallyX

Length of output: 262


🏁 Script executed:

# Find SearchFragment.kt and check lines around 35
fd -e "SearchFragment.kt" app/src/main/java/ --exec cat -n {} \; | head -70

Repository: Crustack/NotallyX

Length of output: 438


🏁 Script executed:

# Find and inspect fragment_notes.xml
fd -e "fragment_notes.xml" app/src/main/res/layout/ --exec cat -n {} \;

Repository: Crustack/NotallyX

Length of output: 462


🏁 Script executed:

# Check if there's any definition of Hidden chip or Folder enum
rg -n "Folder\.HIDDEN|@\+id/Hidden" app/src/main/java/ --type kt

Repository: Crustack/NotallyX

Length of output: 87


🏁 Script executed:

# Find SearchFragment.kt with correct fd syntax
fd . app/src/main/java -name "SearchFragment.kt" | head -1 | xargs -I {} cat -n {}

Repository: Crustack/NotallyX

Length of output: 291


🏁 Script executed:

# Find fragment_notes.xml with correct fd syntax
fd . app/src/main/res/layout -name "fragment_notes.xml" | head -1 | xargs -I {} cat -n {}

Repository: Crustack/NotallyX

Length of output: 291


🏁 Script executed:

# Check for Folder.HIDDEN and Hidden chip references
rg -n "Folder\.HIDDEN|@\+id/Hidden" app/src/main/java/

Repository: Crustack/NotallyX

Length of output: 1911


🏁 Script executed:

# Find and read fragment_notes.xml - fix fd syntax
fd SearchFragment.kt app/src/main/java/ --exec head -50 {} \;

Repository: Crustack/NotallyX

Length of output: 2119


🏁 Script executed:

# Find fragment_notes.xml with correct syntax
find app/src/main/res/layout -name "fragment_notes.xml" -exec cat -n {} \;

Repository: Crustack/NotallyX

Length of output: 6797


🏁 Script executed:

# Also check for any chip definitions with Hidden in their id
rg -n "android:id.*Hidden|<com.google.android.material.chip.Chip" app/src/main/res/layout/

Repository: Crustack/NotallyX

Length of output: 1983


Missing "Hidden" chip in layout causes broken folder selection.

The code references R.id.Hidden, but fragment_notes.xml (lines 44-63) only defines three chips: Notes, Deleted, and Archived. There is no chip with android:id="@+id/Hidden".

This causes:

  1. check(R.id.Hidden) on line 47 silently fails - no chip gets selected when navigating from Hidden folder
  2. Users cannot filter search results by Hidden folder since the chip doesn't exist

Add a Hidden chip to the ChipGroup in fragment_notes.xml:

Proposed fix for fragment_notes.xml
<com.google.android.material.chip.Chip
    android:id="@+id/Hidden"
    style="@style/FilterChip"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hidden" />

Add this chip to the ChipGroup alongside Notes, Deleted, and Archived.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/SearchFragment.kt`
at line 35, The fragment references R.id.Hidden (used in
SearchFragment.check(R.id.Hidden) / folder selection) but the ChipGroup in
fragment_notes.xml lacks a Chip with id "@+id/Hidden"; add a new Chip with
android:id="@+id/Hidden" (use the same style as the other filter chips, e.g.,
style="@style/FilterChip", wrap_content sizes, and text="@string/hidden") inside
the existing ChipGroup alongside Notes, Deleted, and Archived so the
check(R.id.Hidden) call can find and select the chip and enable filtering by the
Hidden folder.

Comment on lines 563 to 566
R.id.Hidden -> {
baseModel.currentLabel = CURRENT_LABEL_NONE
binding.NavigationView.setCheckedItem(destination.id)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't reuse the Unlabeled sentinel for Hidden.

CURRENT_LABEL_NONE already means the Unlabeled destination on Lines 307-310 and is forwarded into Search on Lines 871-879. Setting it for R.id.Hidden makes Hidden indistinguishable from Unlabeled during menu rebuilds and search setup. Use CURRENT_LABEL_EMPTY or a dedicated hidden sentinel here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt`
around lines 563 - 566, The Hidden menu case in MainActivity sets
baseModel.currentLabel to the Unlabeled sentinel CURRENT_LABEL_NONE, making
Hidden indistinguishable from Unlabeled; change the assignment in the
R.id.Hidden branch (the block that also calls
binding.NavigationView.setCheckedItem(destination.id)) to use a distinct
sentinel such as CURRENT_LABEL_EMPTY (or create a dedicated
CURRENT_LABEL_HIDDEN) instead of CURRENT_LABEL_NONE so menu rebuilds and Search
setup can differentiate Hidden from Unlabeled.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt (1)

209-248: ⚠️ Potential issue | 🟠 Major

Nullable launcher silently fails to authenticate.

When activityResultLauncher is null and the device is secured (lines 224-228, 240-244), the intent is not launched but onFailure is also not called—the function returns silently. This enables the security bypass in showBiometricOrPinPromptHidden where onSuccess is called immediately after.

Consider calling onFailure(null) when the launcher is null but intent creation succeeds, to signal that authentication couldn't be performed:

Proposed fix
             if (intent != null) {
-                activityResultLauncher?.launch(intent)
+                if (activityResultLauncher != null) {
+                    activityResultLauncher.launch(intent)
+                } else {
+                    onFailure.invoke(null)
+                }
             } else {
                 onFailure.invoke(null)
             }

Apply this pattern to both occurrences (lines 224-228 and 240-244).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt` around
lines 209 - 248, In promptPinAuthentication, handle the case where
activityResultLauncher is null by invoking onFailure(null) whenever an intent is
created but cannot be launched; update both branches (the Build.VERSION.SDK_INT
>= Build.VERSION_CODES.M path and the else path that checks isKeyguardSecure) so
that after creating the intent you call activityResultLauncher?.launch(intent)
if non-null, otherwise call onFailure(null) instead of returning silently—this
prevents showBiometricOrPinPromptHidden from treating a null launcher as a
successful authentication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt`:
- Around line 125-137: The fallback branch in LockUtils.kt must not reference
android.hardware.biometrics.BiometricManager on API 21-22 and must not call
onSuccess.invoke() before authentication completes; replace the
BIOMETRIC_SUCCESS check with a KeyguardManager.isKeyguardSecure() check for API
<23 (use
fragment.requireContext().getSystemService(KeyguardManager::class.java)) and
only proceed to authentication if the device is secure. Ensure
promptPinAuthentication is invoked with a non-null ActivityResultLauncher (or
add an ActivityResultLauncher parameter to the function) and remove the
immediate onSuccess.invoke() call; instead call onSuccess only after the
PIN/password result handler confirms successful authentication (handle result in
HiddenFragment or via the passed launcher).

---

Outside diff comments:
In `@app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt`:
- Around line 209-248: In promptPinAuthentication, handle the case where
activityResultLauncher is null by invoking onFailure(null) whenever an intent is
created but cannot be launched; update both branches (the Build.VERSION.SDK_INT
>= Build.VERSION_CODES.M path and the else path that checks isKeyguardSecure) so
that after creating the intent you call activityResultLauncher?.launch(intent)
if non-null, otherwise call onFailure(null) instead of returning silently—this
prevents showBiometricOrPinPromptHidden from treating a null launcher as a
successful authentication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53c618c4-dfd8-4a18-b854-3d29fa3d1e39

📥 Commits

Reviewing files that changed from the base of the PR and between 2371a25 and 7b4c5ac.

📒 Files selected for processing (2)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
  • app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt

Comment on lines +125 to +137
else -> {
if (
fragment.requireContext().canAuthenticateWithBiometrics() !=
android.hardware.biometrics.BiometricManager.BIOMETRIC_SUCCESS
) {
onSuccess.invoke()
return
}
// API 21-22: No biometric support, fallback to PIN/Password
promptPinAuthentication(fragment.requireContext(), null, titleResId, onFailure)
onSuccess.invoke()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Crash on API 21-22 and security bypass.

Two severe issues in this fallback branch:

  1. Line 128 - NoClassDefFoundError crash: android.hardware.biometrics.BiometricManager was added in API 29, but this else branch executes on API < 23 (21-22). Referencing this class will crash the app.

  2. Lines 134-135 - Security bypass: promptPinAuthentication is called with a null launcher (so the PIN intent is never actually launched), and then onSuccess.invoke() is called immediately—granting access without any authentication.

Proposed fix

For API 21-22, use KeyguardManager.isKeyguardSecure to check device security (it's available since API 16), and either require a non-null ActivityResultLauncher to properly handle the PIN result, or allow access if the device has no lock:

         else -> {
-            if (
-                fragment.requireContext().canAuthenticateWithBiometrics() !=
-                    android.hardware.biometrics.BiometricManager.BIOMETRIC_SUCCESS
-            ) {
+            val keyguardManager = ContextCompat.getSystemService(
+                fragment.requireContext(),
+                KeyguardManager::class.java
+            )
+            if (keyguardManager?.isKeyguardSecure != true) {
+                // Device has no lock screen - allow access
                 onSuccess.invoke()
                 return
             }
-            // API 21-22: No biometric support, fallback to PIN/Password
-            promptPinAuthentication(fragment.requireContext(), null, titleResId, onFailure)
-            onSuccess.invoke()
+            // Device is secured but we can't properly handle PIN auth without a launcher
+            // Either require an ActivityResultLauncher parameter, or deny access
+            onFailure.invoke(null)
         }

Alternatively, add an ActivityResultLauncher parameter to this function and handle the PIN authentication result properly in HiddenFragment.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else -> {
if (
fragment.requireContext().canAuthenticateWithBiometrics() !=
android.hardware.biometrics.BiometricManager.BIOMETRIC_SUCCESS
) {
onSuccess.invoke()
return
}
// API 21-22: No biometric support, fallback to PIN/Password
promptPinAuthentication(fragment.requireContext(), null, titleResId, onFailure)
onSuccess.invoke()
}
}
else -> {
val keyguardManager = ContextCompat.getSystemService(
fragment.requireContext(),
KeyguardManager::class.java
)
if (keyguardManager?.isKeyguardSecure != true) {
// Device has no lock screen - allow access
onSuccess.invoke()
return
}
// Device is secured but we can't properly handle PIN auth without a launcher
// Either require an ActivityResultLauncher parameter, or deny access
onFailure.invoke(null)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt` around
lines 125 - 137, The fallback branch in LockUtils.kt must not reference
android.hardware.biometrics.BiometricManager on API 21-22 and must not call
onSuccess.invoke() before authentication completes; replace the
BIOMETRIC_SUCCESS check with a KeyguardManager.isKeyguardSecure() check for API
<23 (use
fragment.requireContext().getSystemService(KeyguardManager::class.java)) and
only proceed to authentication if the device is secure. Ensure
promptPinAuthentication is invoked with a non-null ActivityResultLauncher (or
add an ActivityResultLauncher parameter to the function) and remove the
immediate onSuccess.invoke() call; instead call onSuccess only after the
PIN/password result handler confirms successful authentication (handle result in
HiddenFragment or via the passed launcher).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant