Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.io.Serializable
enum class Folder : Serializable {
NOTES,
DELETED,
HIDDEN,
ARCHIVED;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ fun Folder.movedToResId(): Int {
Folder.DELETED -> R.plurals.deleted_selected_notes
Folder.ARCHIVED -> R.plurals.archived_selected_notes
Folder.NOTES -> R.plurals.restored_selected_notes
Folder.HIDDEN -> R.plurals.hidden_selected_notes
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,13 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
add(2, R.id.Archived, CATEGORY_SYSTEM + 2, R.string.archived)
.setCheckable(true)
.setIcon(R.drawable.archive)
add(3, R.id.Reminders, CATEGORY_SYSTEM + 3, R.string.reminders)
add(2, R.id.Hidden, CATEGORY_SYSTEM + 3, R.string.hiddens)
.setCheckable(true)
.setIcon(R.drawable.hidden)
add(3, R.id.Reminders, CATEGORY_SYSTEM + 4, R.string.reminders)
.setCheckable(true)
.setIcon(R.drawable.notifications)
add(3, R.id.Settings, CATEGORY_SYSTEM + 4, R.string.settings)
add(4, R.id.Settings, CATEGORY_SYSTEM + 5, R.string.settings)
.setCheckable(true)
.setIcon(R.drawable.settings)
}
Expand Down Expand Up @@ -556,10 +559,12 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
.find { menuItem -> menuItem.title == it }
?.let { menuItem -> menuItem.isChecked = true }
}

R.id.Unlabeled -> {
baseModel.currentLabel = CURRENT_LABEL_NONE
binding.NavigationView.setCheckedItem(destination.id)
}

else -> {
baseModel.currentLabel = CURRENT_LABEL_EMPTY
binding.NavigationView.setCheckedItem(destination.id)
Expand Down Expand Up @@ -646,6 +651,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
baseModel.duplicateSelectedBaseNotes()
}
menu.add(R.string.archive, R.drawable.archive) { moveNotes(Folder.ARCHIVED) }
menu.add(R.string.hidden, R.drawable.hidden) { moveNotes(Folder.HIDDEN) }
menu.addChangeColor()
val share = menu.addShare()
menu.addExportMenu()
Expand Down Expand Up @@ -688,6 +694,22 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
val share = menu.add(R.string.share, R.drawable.share) { share() }
model.actionMode.count.observeCount(this@MainActivity, share)
}

Folder.HIDDEN -> {
menu.add(
R.string.unhidden,
R.drawable.unhidden,
MenuItem.SHOW_AS_ACTION_ALWAYS,
) {
moveNotes(Folder.NOTES)
}
menu.addDelete(MenuItem.SHOW_AS_ACTION_ALWAYS)
menu.addLabels(MenuItem.SHOW_AS_ACTION_ALWAYS)
menu.addExportMenu()
menu.addChangeColor()
val share = menu.add(R.string.share, R.drawable.share) { share() }
model.actionMode.count.observeCount(this@MainActivity, share)
}
}
}

Expand Down Expand Up @@ -861,6 +883,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
}
true
}

else -> super.onOptionsItemSelected(item)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.philkes.notallyx.presentation.activity.main.fragment

import android.os.Bundle
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.lifecycle.LiveData
import com.philkes.notallyx.R
import com.philkes.notallyx.data.model.Folder
import com.philkes.notallyx.data.model.Item
import com.philkes.notallyx.utils.security.showBiometricOrPinPromptHidden

open class HiddenFragment : NotallyFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.folder.value = Folder.HIDDEN
}

override fun getBackground() = R.drawable.label_off

override fun getObservable(): LiveData<List<Item>> {
return model.hiddenNotes!!
Comment on lines +21 to +22
Copy link
Copy Markdown
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.

}

override fun onStart() {
super.onStart()
hide()
showBiometricOrPinPromptHidden(this, R.string.hidden_lock_title, onSuccess = { show() }) {}
}
Comment on lines +25 to +29
Copy link
Copy Markdown
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.


protected fun show() {
binding!!.root.visibility = VISIBLE
}

protected fun hide() {
binding!!.root.visibility = INVISIBLE
Comment on lines +31 to +36
Copy link
Copy Markdown
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.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SearchFragment : NotallyFragment() {
Folder.NOTES -> R.id.Notes
Folder.DELETED -> R.id.Deleted
Folder.ARCHIVED -> R.id.Archived
Folder.HIDDEN -> R.id.Hidden
Copy link
Copy Markdown
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.

}

binding?.ChipGroup?.apply {
Expand All @@ -40,6 +41,7 @@ class SearchFragment : NotallyFragment() {
R.id.Notes -> model.folder.value = Folder.NOTES
R.id.Deleted -> model.folder.value = Folder.DELETED
R.id.Archived -> model.folder.value = Folder.ARCHIVED
R.id.Hidden -> model.folder.value = Folder.HIDDEN
}
}
check(checked)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,11 @@ class NoteActionHandler(
ExportBottomSheet(activity.colorInt, ::export)
.show(activity.supportFragmentManager, ExportBottomSheet.TAG)
}

EditAction.SHARE -> share()
EditAction.DELETE -> delete()
EditAction.ARCHIVE -> archive()
EditAction.HIDDEN -> hidden()
EditAction.TOGGLE_VIEW_MODE -> toggleViewMode()
EditAction.CONVERT -> convertTo()
EditAction.DELETE_FOREVER -> deleteForever()
Expand Down Expand Up @@ -337,6 +339,14 @@ class NoteActionHandler(
}
}

private fun hidden() {
if (notallyModel.folder == Folder.HIDDEN) {
restore()
} else {
moveNote(Folder.HIDDEN)
}
}

private fun moveNote(toFolder: Folder) {
val resultIntent =
Intent().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
var baseNotes: Content? = Content(MutableLiveData(), ::transform)
var deletedNotes: Content? = Content(MutableLiveData(), ::transform)
var archivedNotes: Content? = Content(MutableLiveData(), ::transform)

var hiddenNotes: Content? = Content(MutableLiveData(), ::transform)
var reminderNotes: Content? = Content(MutableLiveData(), ::transform)

val folder = NotNullLiveData(Folder.NOTES)
Expand Down Expand Up @@ -191,6 +193,12 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
archivedNotes!!.setObserver(baseNoteDao.getFrom(Folder.ARCHIVED))
}

if (hiddenNotes == null) {
hiddenNotes = Content(baseNoteDao.getFrom(Folder.HIDDEN), ::transform)
} else {
hiddenNotes!!.setObserver(baseNoteDao.getFrom(Folder.HIDDEN))
}

if (reminderNotes == null) {
reminderNotes = Content(baseNoteDao.getAllBaseNotesWithReminders(), ::transform)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ fun ListItemSort.callback(adapterChecked: CheckedListItemAdapter) =
when (this) {
ListItemSort.AUTO_SORT_BY_CHECKED_TIMESTAMP ->
ListItemCheckedTimestampSortCallback(adapterChecked)

else -> ListItemParentSortCallback(adapterChecked)
}

Expand Down Expand Up @@ -436,6 +437,7 @@ enum class EditAction(override val textResId: Int, val drawableResId: Int) : Sta
SHARE(R.string.share, R.drawable.share),
DELETE(R.string.delete, R.drawable.delete),
ARCHIVE(R.string.archive, R.drawable.archive),
HIDDEN(R.string.hidden, R.drawable.hidden),
TOGGLE_VIEW_MODE(R.string.edit, R.drawable.visibility),
CONVERT(R.string.convert_to_list_note, R.drawable.convert_to_text),
DELETE_FOREVER(R.string.delete_forever, R.drawable.delete),
Expand All @@ -452,23 +454,30 @@ enum class EditAction(override val textResId: Int, val drawableResId: Int) : Sta
PIN -> if (pinned) R.drawable.unpin else R.drawable.pin
ARCHIVE ->
if (folder == Folder.ARCHIVED) R.drawable.unarchive else R.drawable.archive

RESTORE ->
if (folder == Folder.ARCHIVED) R.drawable.unarchive else R.drawable.restore

HIDDEN -> if (folder == Folder.HIDDEN) R.drawable.unhidden else R.drawable.hidden
TOGGLE_VIEW_MODE ->
if (viewMode == NoteViewMode.READ_ONLY) R.drawable.edit
else R.drawable.visibility

else -> drawableResId
}
val title =
when (this) {
PIN -> if (pinned) R.string.unpin else R.string.pin
ARCHIVE -> if (folder == Folder.ARCHIVED) R.string.unarchive else R.string.archive
HIDDEN -> if (folder == Folder.HIDDEN) R.string.unhidden else R.string.hidden
RESTORE -> if (folder == Folder.ARCHIVED) R.string.unarchive else R.string.restore
TOGGLE_VIEW_MODE ->
if (viewMode == NoteViewMode.READ_ONLY) R.string.edit else R.string.read_only

CONVERT ->
if (type == Type.LIST) R.string.convert_to_text_note
else R.string.convert_to_list_note

else -> textResId
}
return title to icon
Expand Down
87 changes: 84 additions & 3 deletions app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.philkes.notallyx.R
import com.philkes.notallyx.utils.canAuthenticateWithBiometrics
import javax.crypto.Cipher

fun Activity.showBiometricOrPinPrompt(
Expand Down Expand Up @@ -56,6 +57,86 @@ fun Fragment.showBiometricOrPinPrompt(
)
}

fun showBiometricOrPinPromptHidden(
fragment: Fragment,
titleResId: Int,
onSuccess: () -> Unit,
onFailure: (errorCode: Int?) -> Unit,
) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
if (
BiometricManager.BIOMETRIC_SUCCESS !=
BiometricManager.from(fragment.requireContext())
.canAuthenticate(
BiometricManager.Authenticators.DEVICE_CREDENTIAL or
BiometricManager.Authenticators.BIOMETRIC_STRONG
)
) {
onSuccess.invoke()
return
}
val promptInfo =
BiometricPrompt.PromptInfo.Builder()
.apply {
setTitle(fragment.getString(titleResId))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
} else {
setNegativeButtonText(fragment.getString(R.string.cancel))
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)
}
}
.build()
val authCallback =
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke()
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke(null)
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
}
val prompt =
BiometricPrompt(
fragment,
ContextCompat.getMainExecutor(fragment.requireContext()),
authCallback,
)

prompt.authenticate(promptInfo)
}

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()
}
}
Comment on lines +125 to +137
Copy link
Copy Markdown
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).

}

private fun showBiometricOrPinPrompt(
isForDecrypt: Boolean,
context: FragmentActivity,
Expand Down Expand Up @@ -127,7 +208,7 @@ private fun showBiometricOrPinPrompt(

private fun promptPinAuthentication(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
activityResultLauncher: ActivityResultLauncher<Intent>?,
titleResId: Int,
onFailure: (errorCode: Int?) -> Unit,
) {
Expand All @@ -141,7 +222,7 @@ private fun promptPinAuthentication(
null,
)
if (intent != null) {
activityResultLauncher.launch(intent)
activityResultLauncher?.launch(intent)
} else {
onFailure.invoke(null)
}
Expand All @@ -157,7 +238,7 @@ private fun promptPinAuthentication(
null,
)
if (intent != null) {
activityResultLauncher.launch(intent)
activityResultLauncher?.launch(intent)
} else {
onFailure.invoke(null)
}
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/res/drawable/hidden.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">

<path
android:fillColor="@android:color/white"
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,562Q862,548 842,539Q822,530 800,520L800,320L447,320L367,240L160,240L160,720L280,720Q280,738 280,760Q280,782 280,800L160,800Z"/>

<path
android:strokeColor="@android:color/white"
android:strokeWidth="40"
android:strokeLineCap="round"
android:pathData="M480,720 Q640,860 800,720 M530,800 L505,850 M640,820 L640,880 M750,800 L775,850"/>
</vector>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/unhidden.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,562Q862,548 842,539Q822,530 800,520L800,320Q800,320 800,320Q800,320 800,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L280,720Q280,738 280,760Q280,782 280,800L160,800ZM640,920Q549,920 472,872Q395,824 360,740Q395,656 472,608Q549,560 640,560Q731,560 808,608Q885,656 920,740Q885,824 808,872Q731,920 640,920ZM747.5,814Q798,788 830,740Q798,692 747.5,666Q697,640 640,640Q583,640 532.5,666Q482,692 450,740Q482,788 532.5,814Q583,840 640,840Q697,840 747.5,814ZM597.5,782.5Q580,765 580,740Q580,715 597.5,697.5Q615,680 640,680Q665,680 682.5,697.5Q700,715 700,740Q700,765 682.5,782.5Q665,800 640,800Q615,800 597.5,782.5ZM160,720L160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Q160,320 160,320Q160,320 160,320L160,517Q160,499 160,489.5Q160,480 160,480Q160,480 160,544Q160,608 160,720Z"/>
</vector>
Loading