Skip to content

Add pin to status bar note action#962

Merged
Crustack merged 12 commits intomainfrom
feat/880
Apr 4, 2026
Merged

Add pin to status bar note action#962
Crustack merged 12 commits intomainfrom
feat/880

Conversation

@Crustack
Copy link
Copy Markdown
Owner

@Crustack Crustack commented Apr 4, 2026

Closes #880
Closes #914

  • Adds pin note to status bar action to overview actions and in note-view actions
  • Pinned to status bar notes always show a pinned notification
  • For Android >= 14 an app cant disallow swiping a notification away, so when the user swipes away a pinned notification it will immediately be re-notified
notallyx_issues_880.webm

Summary by CodeRabbit

  • New Features

    • Pin notes to the device status bar as persistent notifications; restore pinned notes on boot; bulk pin/unpin from action mode.
  • Improvements

    • Reminder notifications redesigned for richer content, reliable updates/cancels, and undo flows; moves/deletes preserve or cancel reminders and pinned state; editor/actions reflect pin-to-status state and permission handling.
  • Localization

    • Reminder confirmation string key added/renamed across locales.
  • UI

    • New pin/unpin icons and updated notification visuals.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds "pin to status bar": database schema v10 with new BaseNote.isPinnedToStatus and migration; DAO, model, ViewModel, importer, and UI changes to support pinning; new PinnedNotificationManager and reminder/notification refactors; permission flows and action-mode extraction; resource and manifest adjustments.

Changes

Cohort / File(s) Summary
Database & Schema
app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json, app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt
Adds schema v10 and migration 9→10: adds non-null isPinnedToStatus INT DEFAULT 0 to BaseNote.
Model & DAO
app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt, app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
BaseNote now has isPinnedToStatus; equals/hash and constructor usages updated; added equalContents(); DAO: queries for pinned/reminder-or-pinned and update methods for isPinnedToStatus.
Startup & Restore
app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt
Restores pinned notifications on app start by querying pinned notes and notifying via PinnedNotificationManager.
Pinned Notifications & Helpers
app/src/main/java/com/philkes/notallyx/utils/PinnedNotificationManager.kt, app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt
New manager for posting/cancelling pinned-note notifications; helpers to build per-note and group summary notifications and delete intents.
Receiver & Alarm utils
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt, app/src/main/java/com/philkes/notallyx/utils/AlarmExtensions.kt
ReminderReceiver gains actions (unpin, delete, dismissed, update); reminder scheduling/cancellation refactored to handle pin + reminders; notification tagging/grouping updated.
Activities & Permission Flow
app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt, app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt, app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
Adds PIN_TO_STATUS action, permission request flow, Activity extensions for handling rejection and refreshing pinned notifications; MainActivity restores pins on create; EditActivity handles permission result.
ViewModels & App Logic
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt, app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
NotallyModel tracks isPinnedToStatus and broadcasts update-intent on content change; BaseNoteModel adds pin-to-status-bar operations and adapts move/delete flows to new cancel/schedule APIs and suspend semantics.
Action Mode / UI Refactor
app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt, app/src/main/java/com/philkes/notallyx/presentation/view/note/action/MoreNoteBottomSheet.kt, app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt
Extracts action-mode logic to ModelFolderObserver; bottom sheet and EditAction extended to include isPinnedToStatus; added PIN_TO_STATUS enum and icons.
Move/Delete/Import & Backup
app/src/main/java/com/philkes/notallyx/data/dao/DaoExtensions.kt, app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt, app/src/main/java/com/philkes/notallyx/data/imports/*, app/src/main/java/com/philkes/notallyx/utils/backup/XmlParserExtensions.kt
New Context.moveBaseNotes handles folder moves and pin/reminder scheduling; importers and backup parsing populate isPinnedToStatus; import restore uses combined reminders-or-pinned APIs.
Intent Helpers
app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt
Simplified getOpenNoteIntent signature; pending-intent callers updated.
Resources & Localization
app/src/main/res/drawable/*pin*.xml, app/src/main/res/drawable/pinboard*.xml, app/src/main/res/values*/strings.xml
Adds pin/unpin vector icons and pinboard assets; adds strings (pin_to_status_bar, unpin_from_status_bar, to_pin_note_status_bar); renames delete_reminderdelete_reminder_question across locales and adjusts default string.
Manifest
app/src/main/AndroidManifest.xml
Sets ReminderReceiver android:exported="false".
Misc & Call-site updates
assorted files (MainActivity, RemindersActivity, importers, serializers)
Updated call sites to include new BaseNote field; minor dialog/string adjustments and refactors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Activity as EditActivity/MainActivity
    participant Handler as NoteActionHandler
    participant Perm as PermissionManager
    participant DAO as BaseNoteDao
    participant PinMgr as PinnedNotificationManager
    participant Notif as NotificationManager

    User->>Activity: Tap "Pin to status bar"
    Activity->>Handler: pinToStatus()
    Handler->>Perm: request notification permission
    Perm-->>Handler: granted / denied
    alt granted
        Handler->>DAO: updatePinnedToStatus(id, true)
        Handler->>PinMgr: notify(context, note)
        PinMgr->>Notif: notify(NOTIFICATION_TAG, note.id)
    else denied
        Handler->>Activity: handleRejection(msgResId)
    end
    alt unpin
        Handler->>DAO: updatePinnedToStatus(id, false)
        Handler->>PinMgr: cancel(context, note.id)
        PinMgr->>Notif: cancel(NOTIFICATION_TAG, note.id)
    end
Loading
sequenceDiagram
    participant System
    participant Receiver as ReminderReceiver
    participant DAO as BaseNoteDao
    participant Alarm as AlarmManager
    participant PinMgr as PinnedNotificationManager
    participant Notif as NotificationManager

    System->>Receiver: ACTION_DELETE_NOTE / ACTION_UNPIN_NOTE / ACTION_UPDATE_NOTIFICATIONS
    Receiver->>DAO: fetch/update or move note
    alt delete
        Receiver->>Alarm: cancel alarms for note.reminders
        Receiver->>PinMgr: cancel(context, note.id)
    else unpin
        Receiver->>DAO: updatePinnedToStatus(note.id, false)
        Receiver->>PinMgr: cancel(context, note.id)
    else update
        Receiver->>DAO: get note
        Receiver->>Notif: repost reminder notifications for note (createNotification)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, v7.9.0

Poem

🐰📌 I found a note to pin on high,
Hop-scotch to the status sky.
Tiny bell and gentle tone,
The rabbit keeps your thought its own.
Hop—your reminder stays in sway!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add pin to status bar note action' clearly and directly summarizes the main feature added: a new action allowing users to pin notes to the status bar, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR comprehensively implements requirements from both linked issues: Issue #880 requires pin-to-status-bar functionality with persistent notifications (✓ implemented via isPinnedToStatus field, PinnedNotificationManager, and status bar notification UI), and Issue #914 requires delete actions in reminder notifications (✓ added via ReminderReceiver broadcast handlers for ACTION_DELETE_NOTE and ACTION_UNPIN_NOTE).
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: the database schema upgrade, new DAO methods, notification management, and UI updates are all necessary to implement pin-to-status-bar functionality and delete actions. String key renames and minor permission handling updates are supportive infrastructure, not out-of-scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/880

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
Copy Markdown
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: 5

🧹 Nitpick comments (8)
app/src/main/java/com/philkes/notallyx/utils/PinnedNotificationManager.kt (1)

62-73: Consider edge case: Summary notification cleanup on pre-M devices.

The summary notification is only canceled on Android M+ where activeNotifications is available. On older devices, stale summary notifications may persist after all pinned notes are unpinned.

💡 Potential improvement for pre-M devices

Since Android M (API 23) is the minimum practical target for most apps today, this is likely acceptable. However, if supporting older devices, consider tracking pinned note count separately:

 fun cancel(context: Context, noteId: Long) {
     val manager = context.getSystemService<NotificationManager>()!!
     manager.cancel(NOTIFICATION_TAG, noteId.toInt())
     if (
         Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
             manager.activeNotifications.none {
                 it.tag == NOTIFICATION_TAG && it.id != noteId.toInt()
             }
     ) {
         manager.cancel(SUMMARY_ID)
+    } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+        // Pre-M: Always cancel summary; it will be recreated if needed
+        manager.cancel(SUMMARY_ID)
     }
 }
🤖 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/PinnedNotificationManager.kt`
around lines 62 - 73, The cancel(...) method only removes SUMMARY_ID when
Build.VERSION.SDK_INT >= M using NotificationManager.activeNotifications,
leaving stale summary notifications on pre-M devices; update the logic to
maintain a persisted pinned-note counter (e.g., in SharedPreferences) that is
incremented when creating/posting a pinned notification and decremented in
cancel(noteId), and on decrement to zero call manager.cancel(SUMMARY_ID). Modify
the functions that post pinned notifications (where NOTIFICATION_TAG and
SUMMARY_ID are used) to update this counter so cancel(...) can rely on it for
pre-M devices when activeNotifications is unavailable.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt (2)

302-311: Redundant filter: getAllPinnedToStatusNotes() already filters by isPinnedToStatus = 1.

The .filter { it.isPinnedToStatus } on line 306 is redundant since the query on line 304 already returns only notes where isPinnedToStatus = 1.

♻️ Remove redundant filter
     private suspend fun restorePinnedNotifications(context: Context) {
         val baseNoteDao = getDatabase(context).getBaseNoteDao()
         val allNotes = baseNoteDao.getAllPinnedToStatusNotes()
         allNotes
-            .filter { it.isPinnedToStatus }
             .forEach { note ->
                 Log.d(TAG, "restorePinnedNotifications: Pinning noteID: ${note.id} to status bar")
                 PinnedNotificationManager.notify(context, note)
             }
     }
🤖 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/note/reminders/ReminderReceiver.kt`
around lines 302 - 311, The restorePinnedNotifications function applies an
unnecessary runtime filter after fetching pinned notes; remove the redundant
.filter { it.isPinnedToStatus } call in restorePinnedNotifications since
getAllPinnedToStatusNotes() already returns only notes with isPinnedToStatus =
1, and then iterate directly over allNotes and call
PinnedNotificationManager.notify(context, note) as before.

153-156: Remove commented-out code.

Dead code comments should be removed. If this logic is needed for future reference, it should be tracked in a separate issue or commit history.

♻️ Remove commented code
                 manager.activeNotifications.ofNote(noteId).forEach { notification ->
                     val reminderId = notification.id
                     Log.d(TAG, "Updating notification for noteId: $noteId reminderId: $reminderId")
                     notify(
                         context,
                         noteId,
                         reminderId.toLong(),
                         schedule = false,
                         isOnlyUpdate = true,
                     )
                 }
-            //            val mostRecentReminder = note.reminders.findLastNotified() ?: return
-            //            if (mostRecentReminder.isNotificationVisible) {
-            //                notify(context, note.id, mostRecentReminder.id, schedule = false)
-            //            }
         }
     }
🤖 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/note/reminders/ReminderReceiver.kt`
around lines 153 - 156, Remove the dead commented-out reminder logic in
ReminderReceiver: delete the three commented lines that reference
mostRecentReminder, note.reminders.findLastNotified(), and the notify(...) call
so the class/function is not cluttered with commented code; if the behavior must
be preserved for future reference, open an issue or rely on VCS history rather
than leaving commented code in ReminderReceiver.
app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt (1)

336-338: Consider batching label updates for better performance.

Currently, each note's labels are updated individually in a loop. For a large selection, this could result in many database operations.

♻️ Suggested batch update approach
// Consider collecting updates and using a batch DAO method:
val updates = baseNotes.zip(updatedBaseNotesLabels)
    .map { (baseNote, labels) -> baseNote.id to labels }
baseModel.updateBaseNotesLabels(updates) // batch operation
🤖 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/ModelFolderObserver.kt`
around lines 336 - 338, The loop calls baseModel.updateBaseNoteLabels for each
note causing many DB ops; instead collect pairs of note id and labels from
baseNotes.zip(updatedBaseNotesLabels) and call a new batch method (e.g.,
baseModel.updateBaseNotesLabels(updates: List<Pair<Id, Labels>>) to perform one
batched update; add the batch method to the DAO/repository and use it from
ModelFolderObserver (replace the forEach block referencing
baseModel.updateBaseNoteLabels) to reduce round-trips and improve performance.
app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt (1)

163-166: Minor SQL style inconsistency: use = instead of ==.

The query uses isPinnedToStatus == 1 while getAllPinnedToStatusNotes uses isPinnedToStatus = 1. Both work in SQLite, but using = is the standard SQL equality operator and maintains consistency.

♻️ Suggested fix
     `@Query`(
-        "SELECT * FROM BaseNote WHERE folder = 'NOTES' AND ((reminders IS NOT NULL AND reminders != '[]') OR isPinnedToStatus == 1)"
+        "SELECT * FROM BaseNote WHERE folder = 'NOTES' AND ((reminders IS NOT NULL AND reminders != '[]') OR isPinnedToStatus = 1)"
     )
     suspend fun getAllWithRemindersOrPinned(): List<BaseNote>
🤖 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/data/dao/BaseNoteDao.kt` around lines
163 - 166, The SQL query in getAllWithRemindersOrPinned uses nonstandard
equality operator `==`; update the WHERE clause to use `=` for consistency
(change `isPinnedToStatus == 1` to `isPinnedToStatus = 1`) so it matches the
style used in getAllPinnedToStatusNotes and standard SQL conventions.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt (1)

517-535: Redundant permission check in refreshStatusBarPin.

When note.isPinnedToStatus is true, the permission was already granted during the pinToStatus() call that set this flag. The additional checkNotificationPermission call on lines 526-531 is redundant and adds unnecessary complexity.

♻️ Simplified implementation
 fun Activity.refreshStatusBarPin(note: BaseNote) {
-    val refresh = {
-        if (note.isPinnedToStatus) {
-            PinnedNotificationManager.notify(this, note)
-        } else {
-            PinnedNotificationManager.cancel(this, note.id)
-        }
-    }
     if (note.isPinnedToStatus) {
-        checkNotificationPermission(
-            REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS,
-            alsoCheckAlarmPermission = false,
-        ) {
-            refresh()
-        }
+        PinnedNotificationManager.notify(this, note)
     } else {
-        refresh()
+        PinnedNotificationManager.cancel(this, note.id)
     }
 }
🤖 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/note/NoteActionHandler.kt`
around lines 517 - 535, The refreshStatusBarPin function performs an unnecessary
checkNotificationPermission when note.isPinnedToStatus is true even though
permission was already handled in pinToStatus; remove the conditional permission
branch and always invoke the refresh lambda directly for both pinned and
unpinned cases (i.e., eliminate the checkNotificationPermission call inside
refreshStatusBarPin), leaving only the refresh logic that calls
PinnedNotificationManager.notify(this, note) or
PinnedNotificationManager.cancel(this, note.id).
app/src/main/java/com/philkes/notallyx/utils/AlarmExtensions.kt (1)

51-58: cancelPinAndReminders unconditionally cancels pinned notification regardless of isPinnedToStatus state.

The function calls PinnedNotificationManager.cancel(this, noteId) without checking if the note is actually pinned. While this is a no-op for non-existent notifications, it's semantically inconsistent and could mask bugs. Consider adding a conditional check or documenting this as intentional "best-effort cleanup" behavior.

♻️ Suggested conditional cancellation
 fun Context.cancelPinAndReminders(noteId: Long, reminders: List<Reminder>) {
     reminders.forEach { reminder -> cancelReminder(noteId, reminder.id) }
+    // Always attempt to cancel pinned notification for cleanup purposes
     PinnedNotificationManager.cancel(this, noteId)
 }
+
+// Alternative: Add overload that respects isPinnedToStatus
+fun Context.cancelPinAndReminders(note: BaseNote) {
+    note.reminders.forEach { reminder -> cancelReminder(note.id, reminder.id) }
+    if (note.isPinnedToStatus) {
+        PinnedNotificationManager.cancel(this, note.id)
+    }
+}
🤖 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/AlarmExtensions.kt` around lines
51 - 58, The cancelPinAndReminders functions unconditionally call
PinnedNotificationManager.cancel(this, noteId); update them to only cancel
pinned notifications by checking the note's isPinnedToStatus before calling
PinnedNotificationManager.cancel (e.g., in fun
Context.cancelPinAndReminders(noteId: Long, reminders: List<Reminder>) check the
associated note or add an isPinned flag parameter, and in fun
Context.cancelPinAndReminders(notes: List<BaseNote>) use note.isPinnedToStatus
to decide per-note cancellation), ensuring reminders are still cancelled
unconditionally; alternatively, if intentional, add a short comment documenting
the best-effort behavior.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt (1)

26-38: Duplicate code: contentText and bigText are computed identically.

Lines 26-31 and 33-38 contain the exact same logic. Extract to a single variable.

♻️ Remove duplication
-    val contentText =
+    val bodyText =
         if (note.type == com.philkes.notallyx.data.model.Type.LIST) {
             note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body }
         } else {
             note.body
         }
 
-    val bigText =
-        if (note.type == com.philkes.notallyx.data.model.Type.LIST) {
-            note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body }
-        } else {
-            note.body
-        }
     val notification =
         NotificationCompat.Builder(this, channelId)
             .apply {
                 setSubText(subtext)
                 setSmallIcon(iconResId)
                 setCategory(NotificationCompat.CATEGORY_REMINDER)
                 setContentTitle(note.title.ifEmpty { getString(R.string.note) })
-                setContentText(contentText)
-                setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
+                setContentText(bodyText)
+                setStyle(NotificationCompat.BigTextStyle().bigText(bodyText))
🤖 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/note/reminders/RemindersUtils.kt`
around lines 26 - 38, The code computes identical values for contentText and
bigText; extract the shared logic into a single local val (e.g., formattedText
or formattedBody) that checks note.type ==
com.philkes.notallyx.data.model.Type.LIST and builds the joinToString for
note.items or uses note.body, then assign contentText = formattedText and
bigText = formattedText (replace the duplicated blocks in RemindersUtils.kt).
Ensure you reference the same note variable and preserve the "✅"/"🔳" and
newline behavior.
🤖 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/ModelFolderObserver.kt`:
- Around line 229-247: The loading flag is cleared in moveNotes' finally block
before the async DB work started by baseModel.moveBaseNotes(...) completes;
change the flow so baseModel.actionMode.loading.value is reset only when the
background operation actually finishes: either make
baseModel.moveBaseNotes(folderTo) a suspend function and call it from a
coroutine (e.g., lifecycleScope.launch { try { ... await moveBaseNotes(...) }
finally { loading.value = false } }), or have moveBaseNotes return a
Job/Deferred and call .invokeOnCompletion / await that Job before resetting
baseModel.actionMode.loading.value; reference moveNotes,
baseModel.moveBaseNotes(folderTo), baseModel.moveBaseNotes(ids, folderFrom) and
baseModel.actionMode.loading.value when implementing the fix.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt`:
- Line 64: Duplicate pinned notification restoration occurs because both
NotallyXApplication.onCreate() and ReminderReceiver (BOOT_COMPLETED handler)
call restorePinnedNotifications(context); remove the redundant call from
ReminderReceiver (the BOOT_COMPLETED branch) so only
NotallyXApplication.onCreate() invokes restorePinnedNotifications(context),
ensuring pinned notifications are restored once on boot.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt`:
- Around line 301-308: The broadcast to ReminderReceiver via Intent(app,
ReminderReceiver::class.java) is safe as an explicit intent, but the manifest
currently exposes the receiver; update the ReminderReceiver AndroidManifest
entry to set android:exported="false" (instead of "true") so external apps
cannot receive or invoke ACTION_UPDATE_NOTIFICATIONS/EXTRA_NOTE_ID broadcasts;
locate the ReminderReceiver declaration in the manifest and change the exported
attribute accordingly.

In `@app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt`:
- Around line 448-465: Update getOpenNoteIntent to include
Intent.FLAG_ACTIVITY_NEW_TASK in the addFlags call so intents launched from
non-Activity contexts (e.g., notifications) will start correctly; modify the
addFlags invocation in getOpenNoteIntent (used by getOpenNotePendingIntent and
callers like PinnedNotificationManager.notify and
RemindersUtils.createNotification) to add FLAG_ACTIVITY_NEW_TASK alongside
FLAG_ACTIVITY_SINGLE_TOP and FLAG_ACTIVITY_CLEAR_TOP.

In `@app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt`:
- Around line 221-223: The ZIP import drops the status-bar pin because the
backup DAO query (used by getAllWithRemindersOrPinned) does not select the
isPinnedToStatus column and toBaseNote constructs imported BaseNote with the
default false; update the import/backup query in BaseNoteDao (the query backing
getAllWithRemindersOrPinned) to include the is_pinned_to_status/isPinnedToStatus
column, and update toBaseNote (or the import mapping code) to map that column
into the BaseNote.isPinnedToStatus field so that cancelPinAndReminders(...) and
pinAndScheduleReminders(...) operate on the correct pinned state after import.

---

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt`:
- Around line 163-166: The SQL query in getAllWithRemindersOrPinned uses
nonstandard equality operator `==`; update the WHERE clause to use `=` for
consistency (change `isPinnedToStatus == 1` to `isPinnedToStatus = 1`) so it
matches the style used in getAllPinnedToStatusNotes and standard SQL
conventions.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt`:
- Around line 336-338: The loop calls baseModel.updateBaseNoteLabels for each
note causing many DB ops; instead collect pairs of note id and labels from
baseNotes.zip(updatedBaseNotesLabels) and call a new batch method (e.g.,
baseModel.updateBaseNotesLabels(updates: List<Pair<Id, Labels>>) to perform one
batched update; add the batch method to the DAO/repository and use it from
ModelFolderObserver (replace the forEach block referencing
baseModel.updateBaseNoteLabels) to reduce round-trips and improve performance.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt`:
- Around line 517-535: The refreshStatusBarPin function performs an unnecessary
checkNotificationPermission when note.isPinnedToStatus is true even though
permission was already handled in pinToStatus; remove the conditional permission
branch and always invoke the refresh lambda directly for both pinned and
unpinned cases (i.e., eliminate the checkNotificationPermission call inside
refreshStatusBarPin), leaving only the refresh logic that calls
PinnedNotificationManager.notify(this, note) or
PinnedNotificationManager.cancel(this, note.id).

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt`:
- Around line 302-311: The restorePinnedNotifications function applies an
unnecessary runtime filter after fetching pinned notes; remove the redundant
.filter { it.isPinnedToStatus } call in restorePinnedNotifications since
getAllPinnedToStatusNotes() already returns only notes with isPinnedToStatus =
1, and then iterate directly over allNotes and call
PinnedNotificationManager.notify(context, note) as before.
- Around line 153-156: Remove the dead commented-out reminder logic in
ReminderReceiver: delete the three commented lines that reference
mostRecentReminder, note.reminders.findLastNotified(), and the notify(...) call
so the class/function is not cluttered with commented code; if the behavior must
be preserved for future reference, open an issue or rely on VCS history rather
than leaving commented code in ReminderReceiver.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt`:
- Around line 26-38: The code computes identical values for contentText and
bigText; extract the shared logic into a single local val (e.g., formattedText
or formattedBody) that checks note.type ==
com.philkes.notallyx.data.model.Type.LIST and builds the joinToString for
note.items or uses note.body, then assign contentText = formattedText and
bigText = formattedText (replace the duplicated blocks in RemindersUtils.kt).
Ensure you reference the same note variable and preserve the "✅"/"🔳" and
newline behavior.

In `@app/src/main/java/com/philkes/notallyx/utils/AlarmExtensions.kt`:
- Around line 51-58: The cancelPinAndReminders functions unconditionally call
PinnedNotificationManager.cancel(this, noteId); update them to only cancel
pinned notifications by checking the note's isPinnedToStatus before calling
PinnedNotificationManager.cancel (e.g., in fun
Context.cancelPinAndReminders(noteId: Long, reminders: List<Reminder>) check the
associated note or add an isPinned flag parameter, and in fun
Context.cancelPinAndReminders(notes: List<BaseNote>) use note.isPinnedToStatus
to decide per-note cancellation), ensuring reminders are still cancelled
unconditionally; alternatively, if intentional, add a short comment documenting
the best-effort behavior.

In `@app/src/main/java/com/philkes/notallyx/utils/PinnedNotificationManager.kt`:
- Around line 62-73: The cancel(...) method only removes SUMMARY_ID when
Build.VERSION.SDK_INT >= M using NotificationManager.activeNotifications,
leaving stale summary notifications on pre-M devices; update the logic to
maintain a persisted pinned-note counter (e.g., in SharedPreferences) that is
incremented when creating/posting a pinned notification and decremented in
cancel(noteId), and on decrement to zero call manager.cancel(SUMMARY_ID). Modify
the functions that post pinned notifications (where NOTIFICATION_TAG and
SUMMARY_ID are used) to update this counter so cancel(...) can rely on it for
pre-M devices when activeNotifications is unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf409de0-9ecd-4d5d-ade1-e81c7e88481e

📥 Commits

Reviewing files that changed from the base of the PR and between 5365b07 and 81c75f2.

⛔ Files ignored due to path filters (1)
  • app/translations.xlsx is excluded by !**/*.xlsx
📒 Files selected for processing (38)
  • app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json
  • app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt
  • app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt
  • app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
  • app/src/main/java/com/philkes/notallyx/data/dao/DaoExtensions.kt
  • app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/note/action/MoreNoteBottomSheet.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt
  • app/src/main/java/com/philkes/notallyx/utils/AlarmExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/PinnedNotificationManager.kt
  • app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt
  • app/src/main/res/drawable/notification_pin.xml
  • app/src/main/res/drawable/notification_unpin.xml
  • app/src/main/res/drawable/pinboard.xml
  • app/src/main/res/drawable/pinboard_filled.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml

rescheduleAlarms(context)
}
restoreRemindersNotifications(context)
restorePinnedNotifications(context)
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

Duplicate pinned notification restoration on boot.

Both NotallyXApplication.onCreate() (see context snippet 1) and ReminderReceiver.BOOT_COMPLETED call restorePinnedNotifications(). Since Application.onCreate() runs before BOOT_COMPLETED broadcast delivery, pinned notifications will be posted twice on device boot.

🔧 Suggested fix: Remove from one location

Remove the call from ReminderReceiver since NotallyXApplication already handles it:

                     intent.action == Intent.ACTION_BOOT_COMPLETED -> {
                         if (canScheduleExactAlarms) {
                             rescheduleAlarms(context)
                         }
                         restoreRemindersNotifications(context)
-                        restorePinnedNotifications(context)
                     }

Or alternatively, remove from NotallyXApplication.onCreate() and keep only the boot receiver handling.

Also applies to: 302-311

🤖 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/note/reminders/ReminderReceiver.kt`
at line 64, Duplicate pinned notification restoration occurs because both
NotallyXApplication.onCreate() and ReminderReceiver (BOOT_COMPLETED handler)
call restorePinnedNotifications(context); remove the redundant call from
ReminderReceiver (the BOOT_COMPLETED branch) so only
NotallyXApplication.onCreate() invokes restorePinnedNotifications(context),
ensuring pinned notifications are restored once on boot.

Copy link
Copy Markdown
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: 2

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/data/model/ModelExtensions.kt (1)

126-156: ⚠️ Potential issue | 🟠 Major

Serialize isPinnedToStatus in toJson() too.

toBaseNote() now reads the field, but BaseNote.toJson() above still never writes it. Any toJson() round-trip will silently clear status-bar pins.

Suggested fix
 fun BaseNote.toJson(): String {
     val jsonObject =
         JSONObject()
             .put("type", type.name)
             .put("color", color)
             .put("title", title)
             .put("pinned", pinned)
+            .put("isPinnedToStatus", isPinnedToStatus)
             .put("timestamp", timestamp)
             .put("modifiedTimestamp", modifiedTimestamp)
             .put("labels", JSONArray(labels))
🤖 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/data/model/ModelExtensions.kt` around
lines 126 - 156, The toBaseNote() deserializer reads the isPinnedToStatus field
(pinnedToStatusBar) but BaseNote.toJson() never writes it, so round-trips lose
the status-bar pin; update BaseNote.toJson() (or the serializer used for
BaseNote) to emit the boolean property using the exact key "isPinnedToStatus"
from the pinnedToStatusBar field (or BaseNote.isPinnedToStatus) so serialization
and deserialization are symmetric.
♻️ Duplicate comments (2)
app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt (1)

323-331: ⚠️ Potential issue | 🔴 Critical

ZIP import will now fail before the first note is mapped.

This code calls getColumnIndexOrThrow("isPinnedToStatus"), but the cursor built in importZip() still does not project that column. That turns the old “flag gets dropped” bug into an import-time crash. Add isPinnedToStatus to the columns array and fall back to false when the source backup predates the column.

Run this read-only check to verify the missing projection:

#!/bin/bash
set -euo pipefail

echo "# BaseNote projection used during ZIP import"
sed -n '124,146p' app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt

echo
echo "# isPinnedToStatus lookup in cursor mapper"
sed -n '323,332p' app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt
🤖 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/backup/ImportExtensions.kt`
around lines 323 - 331, The ZIP import crashes because
ImportExtensions.importZip() does not include "isPinnedToStatus" in the columns
projection but the cursor mapping code uses
getColumnIndexOrThrow("isPinnedToStatus"); update the projection (the columns
array used by importZip()) to include "isPinnedToStatus" and change the mapper
to use getColumnIndex("isPinnedToStatus") with a fallback of false (keep the
existing when/0/1 mapping) so older backups that predate the column will not
crash.
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

657-660: ⚠️ Potential issue | 🟠 Major

Give callers a real completion callback.

This launches and returns immediately, then invokes callable on Dispatchers.IO only after a successful move. ModelFolderObserver.moveNotes() uses that callback to clear loading.value, so failures leave loading stuck and the success path pushes a LiveData/UI update off-main.

Suggested fix
-    fun moveBaseNotes(ids: LongArray, folder: Folder, callable: (() -> Unit)? = null) {
-        viewModelScope.launch(Dispatchers.IO) {
-            app.moveBaseNotes(baseNoteDao, ids, folder)
-            callable?.invoke()
-        }
-    }
+    fun moveBaseNotes(ids: LongArray, folder: Folder, callable: (() -> Unit)? = null) =
+        viewModelScope.launch {
+            try {
+                withContext(Dispatchers.IO) {
+                    app.moveBaseNotes(baseNoteDao, ids, folder)
+                }
+            } finally {
+                callable?.invoke()
+            }
+        }

Run this read-only check to confirm the callback dispatch and the type behind loading:

#!/bin/bash
set -euo pipefail

echo "# moveBaseNotes callback dispatch"
sed -n '650,662p' app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

echo
echo "# ActionMode definition"
fd 'ActionMode.kt' app/src/main/java | xargs -r sed -n '1,220p'

echo
echo "# NotNullLiveData implementation"
fd 'NotNullLiveData.kt' app/src/main/java | xargs -r sed -n '1,220p'
🤖 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/viewmodel/BaseNoteModel.kt`
around lines 657 - 660, The moveBaseNotes implementation currently launches on
Dispatchers.IO and only invokes callable on the IO thread after a successful
move; change it to always invoke callable on the main thread regardless of
success or failure. Surround the app.moveBaseNotes(baseNoteDao, ids, folder)
call with try/catch/finally inside viewModelScope.launch(Dispatchers.IO), and in
the finally block switch to Dispatchers.Main (e.g.,
withContext(Dispatchers.Main)) to call callable?.invoke() so callers (like
ModelFolderObserver.moveNotes) get a real completion callback on the UI thread
even when the move fails; keep exception handling (log or rethrow) as
appropriate.
🤖 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/ModelFolderObserver.kt`:
- Around line 258-269: The undo currently reinserts notes but their attachment
files were already deleted by
baseModel.deleteSelectedBaseNotes()/BaseNoteModel.deleteBaseNotes(...); change
the flow so files are not permanently removed until the Snackbar undo window
passes: either (1) modify BaseNoteModel.deleteBaseNotes to only mark rows
deleted and return the list of attachment file paths (or accept a deferDeletion
flag) and then perform physical file deletion in the Snackbar dismiss callback
when no undo occurred, or (2) capture the attachment file paths before calling
baseModel.deleteSelectedBaseNotes() and, if the user taps Undo (setAction ->
baseModel.saveNotes(removedNotes)), restore those files from a temporary staging
area (or skip deleting them). Update ModelFolderObserver (the positive button
handler and the Snackbar setAction/dismiss logic) to coordinate this deferred
deletion behavior and use the unique symbols
baseModel.deleteSelectedBaseNotes(), BaseNoteModel.deleteBaseNotes(...), and
baseModel.saveNotes(removedNotes) to locate and implement the changes.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 631-632: The loop that calls activity.refreshStatusBarPin for
every note returned by baseNoteDao.getByIds(ids) should only re-notify notes
that are currently in Folder.NOTES; change the logic around
baseNoteDao.getByIds(ids) / updatedNotes.forEach {
activity.refreshStatusBarPin(it) } to filter updatedNotes to only those whose
folder equals Folder.NOTES (or use the note property/enum that indicates folder
membership) before calling activity.refreshStatusBarPin, so archived or
non-NOTES pins aren’t surfaced briefly and then removed by the existing
DaoExtensions behavior.

---

Outside diff comments:
In `@app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt`:
- Around line 126-156: The toBaseNote() deserializer reads the isPinnedToStatus
field (pinnedToStatusBar) but BaseNote.toJson() never writes it, so round-trips
lose the status-bar pin; update BaseNote.toJson() (or the serializer used for
BaseNote) to emit the boolean property using the exact key "isPinnedToStatus"
from the pinnedToStatusBar field (or BaseNote.isPinnedToStatus) so serialization
and deserialization are symmetric.

---

Duplicate comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 657-660: The moveBaseNotes implementation currently launches on
Dispatchers.IO and only invokes callable on the IO thread after a successful
move; change it to always invoke callable on the main thread regardless of
success or failure. Surround the app.moveBaseNotes(baseNoteDao, ids, folder)
call with try/catch/finally inside viewModelScope.launch(Dispatchers.IO), and in
the finally block switch to Dispatchers.Main (e.g.,
withContext(Dispatchers.Main)) to call callable?.invoke() so callers (like
ModelFolderObserver.moveNotes) get a real completion callback on the UI thread
even when the move fails; keep exception handling (log or rethrow) as
appropriate.

In `@app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt`:
- Around line 323-331: The ZIP import crashes because
ImportExtensions.importZip() does not include "isPinnedToStatus" in the columns
projection but the cursor mapping code uses
getColumnIndexOrThrow("isPinnedToStatus"); update the projection (the columns
array used by importZip()) to include "isPinnedToStatus" and change the mapper
to use getColumnIndex("isPinnedToStatus") with a fallback of false (keep the
existing when/0/1 mapping) so older backups that predate the column will not
crash.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f9c55cb-3a28-4725-a18c-92d9eafc5860

📥 Commits

Reviewing files that changed from the base of the PR and between 81c75f2 and 27ae89c.

📒 Files selected for processing (11)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/philkes/notallyx/data/imports/evernote/EvernoteImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/imports/txt/PlainTextImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt
  • app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
  • app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/backup/XmlParserExtensions.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt
  • app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt

Comment on lines +258 to +269
.setPositiveButton(R.string.delete) { _, _ ->
val removedNotes = baseModel.actionMode.selectedNotes.values.toList()
baseModel.deleteSelectedBaseNotes()
Snackbar.make(
activity.findViewById(R.id.DrawerLayout),
activity.getQuantityString(
R.plurals.deleted_selected_notes,
removedNotes.size,
),
Snackbar.LENGTH_SHORT,
)
.apply { setAction(R.string.undo) { baseModel.saveNotes(removedNotes) } }
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

Undo here restores rows, not the deleted files.

baseModel.deleteSelectedBaseNotes() goes through BaseNoteModel.deleteBaseNotes(...), which deletes attachment files before the snackbar appears. The undo action only re-inserts removedNotes, so notes with images/files/audio come back pointing at files that no longer exist.

🤖 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/ModelFolderObserver.kt`
around lines 258 - 269, The undo currently reinserts notes but their attachment
files were already deleted by
baseModel.deleteSelectedBaseNotes()/BaseNoteModel.deleteBaseNotes(...); change
the flow so files are not permanently removed until the Snackbar undo window
passes: either (1) modify BaseNoteModel.deleteBaseNotes to only mark rows
deleted and return the list of attachment file paths (or accept a deferDeletion
flag) and then perform physical file deletion in the Snackbar dismiss callback
when no undo occurred, or (2) capture the attachment file paths before calling
baseModel.deleteSelectedBaseNotes() and, if the user taps Undo (setAction ->
baseModel.saveNotes(removedNotes)), restore those files from a temporary staging
area (or skip deleting them). Update ModelFolderObserver (the positive button
handler and the Snackbar setAction/dismiss logic) to coordinate this deferred
deletion behavior and use the unique symbols
baseModel.deleteSelectedBaseNotes(), BaseNoteModel.deleteBaseNotes(...), and
baseModel.saveNotes(removedNotes) to locate and implement the changes.

Copy link
Copy Markdown
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: 3

♻️ Duplicate comments (1)
app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt (1)

276-296: ⚠️ Potential issue | 🔴 Critical

Delete attachments only when Undo was not used.

DISMISS_EVENT_ACTION is the Undo case. Deleting files under that branch means Undo still loses attachments, while timeout/swipe/consecutive dismissals now leak them on disk.

Suggested fix
                                     override fun onDismissed(
                                         transientBottomBar: Snackbar?,
                                         event: Int,
                                     ) {
-                                        if (event == DISMISS_EVENT_ACTION) {
+                                        if (event != DISMISS_EVENT_ACTION) {
                                             val attachments = ArrayList<Attachment>()
                                             deletedNotes.forEach { note ->
                                                 attachments.addAll(note.images)
                                                 attachments.addAll(note.files)
                                                 attachments.addAll(note.audios)
🤖 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/ModelFolderObserver.kt`
around lines 276 - 296, The current onDismissed handler deletes attachments when
event == DISMISS_EVENT_ACTION (the Undo case); change the logic so attachments
are deleted only when the dismiss event is NOT DISMISS_EVENT_ACTION. In the
Snackbar.Callback.onDismissed override (referencing DISMISS_EVENT_ACTION,
deletedNotes, baseModel.progress, DeleteProgress, and
activity.deleteAttachments), invert the condition (or use !=) so the block that
collects attachments and calls activity.deleteAttachments(...) runs only when
event != DISMISS_EVENT_ACTION, ensuring Undo preserves attachments and only
timeout/swipe/other dismissals trigger deletion.
🤖 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/ModelFolderObserver.kt`:
- Line 275: The Snackbar Undo currently only calls saveNotes(removedNotes) which
restores rows but not reminders/pinned notifications; create a restore helper
(e.g., restoreDeletedNotes(removedNotes) or reuse an existing restore flow) that
reinserts the notes (saveNotes) and then reactivates them by calling
pinAndScheduleReminders(removedNotes) (the inverse of app.cancelPinAndReminders
used in BaseNoteModel.deleteBaseNotes()), and change the
setAction(R.string.undo) lambda to call that restore helper instead of directly
calling saveNotes(removedNotes).

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 656-660: The moveBaseNotes function currently launches a coroutine
that calls app.moveBaseNotes(...) and only invokes callable on success, so
exceptions inside the coroutine skip the callback and leave loading stuck; wrap
the call in a try/finally (or try/catch then finally) inside the launched
coroutine so that callable?.invoke() is executed in the finally block regardless
of success or failure, referencing moveBaseNotes, app.moveBaseNotes,
viewModelScope.launch and callable?.invoke to locate the change; preserve
Dispatchers.IO and ensure any caught exception is optionally logged/handled
before rethrowing if needed.
- Around line 625-632: pinBaseNotesToStatusBar currently runs DB and
UI/permission work on Dispatchers.IO and captures an Activity directly; change
it to collect ids and create a WeakReference(activity) first, then run
baseNoteDao.updatePinnedToStatus(ids, pinnedToStatusBar) and val updatedNotes =
baseNoteDao.getByIds(ids) on Dispatchers.IO, and afterwards switch to
Dispatchers.Main (withContext or a separate launch on Main) to get the Activity
from the WeakReference and only if non-null call activity.refreshStatusBarPin
for each note; keep references to actionMode.selectedIds,
actionMode.close(true), baseNoteDao.updatePinnedToStatus, baseNoteDao.getByIds
and activity.refreshStatusBarPin to locate the code to modify.

---

Duplicate comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt`:
- Around line 276-296: The current onDismissed handler deletes attachments when
event == DISMISS_EVENT_ACTION (the Undo case); change the logic so attachments
are deleted only when the dismiss event is NOT DISMISS_EVENT_ACTION. In the
Snackbar.Callback.onDismissed override (referencing DISMISS_EVENT_ACTION,
deletedNotes, baseModel.progress, DeleteProgress, and
activity.deleteAttachments), invert the condition (or use !=) so the block that
collects attachments and calls activity.deleteAttachments(...) runs only when
event != DISMISS_EVENT_ACTION, ensuring Undo preserves attachments and only
timeout/swipe/other dismissals trigger deletion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a73c84ca-33f4-4525-ba14-cea28ccd3538

📥 Commits

Reviewing files that changed from the base of the PR and between 27ae89c and 2dd1b04.

📒 Files selected for processing (2)
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Copy link
Copy Markdown
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

🤖 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/viewmodel/BaseNoteModel.kt`:
- Around line 628-635: The code uses with(Dispatchers.IO) inside
viewModelScope.launch which does not change coroutine context, so
baseNoteDao.updatePinnedToStatus and baseNoteDao.getByIds still run on the Main
dispatcher; replace with(Dispatchers.IO) by calling withContext(Dispatchers.IO)
to perform those DB calls on the IO dispatcher, then keep the rest
(updatedNotes.forEach { activity.refreshStatusBarPin(it) }) on the original
coroutine context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c0d6e50-06a8-49d8-a042-0e5f2ac91637

📥 Commits

Reviewing files that changed from the base of the PR and between 2dd1b04 and 31016a7.

📒 Files selected for processing (1)
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

@Crustack
Copy link
Copy Markdown
Owner Author

Crustack commented Apr 4, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
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

♻️ Duplicate comments (1)
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

629-634: ⚠️ Potential issue | 🟠 Major

Only post new status-bar notifications for notes still in Folder.NOTES.

This loop is fine for unpinning because refreshStatusBarPin() cancels when isPinnedToStatus is false, but for pinning it will also notify archived/deleted notes. That conflicts with app/src/main/java/com/philkes/notallyx/data/dao/DaoExtensions.kt:8-21, which treats status-bar pins as active only in Folder.NOTES.

Minimal fix
-            updatedNotes.forEach { activity.refreshStatusBarPin(it) }
+            updatedNotes.forEach { note ->
+                if (!note.isPinnedToStatus || note.folder == Folder.NOTES) {
+                    activity.refreshStatusBarPin(note)
+                }
+            }
🤖 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/viewmodel/BaseNoteModel.kt`
around lines 629 - 634, After updating pins and fetching updatedNotes in
BaseNoteModel (the block calling baseNoteDao.updatePinnedToStatus(ids,
pinnedToStatusBar) and baseNoteDao.getByIds(ids)), only call
activity.refreshStatusBarPin(...) for notes that are actually in Folder.NOTES;
filter updatedNotes by note.folder == Folder.NOTES (or the equivalent folder
property) before iterating so archived/deleted notes do not produce status-bar
notifications when pinned.
🧹 Nitpick comments (1)
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

660-664: Consider refactoring to make dispatcher switching explicit.

While the current implementation is technically safe—postValue() is designed to be called from any thread and safely posts updates to the main thread—the suggested refactor improves code clarity by making the dispatcher boundary explicit:

Suggested refactor
-        viewModelScope.launch(Dispatchers.IO) {
-            try {
-                app.moveBaseNotes(baseNoteDao, ids, folder)
-            } finally {
-                callable?.invoke()
-            }
-        }
+        viewModelScope.launch {
+            try {
+                withContext(Dispatchers.IO) {
+                    app.moveBaseNotes(baseNoteDao, ids, folder)
+                }
+            } finally {
+                callable?.invoke()
+            }
+        }

This ensures the callback always runs on the default dispatcher (Main), making the threading behavior explicit rather than relying on postValue()'s thread-safety semantics.

🤖 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/viewmodel/BaseNoteModel.kt`
around lines 660 - 664, The code launches a coroutine on Dispatchers.IO to call
app.moveBaseNotes(...) and then invokes callable?.invoke() in the finally block;
make the dispatcher boundary explicit by ensuring heavy work runs on
Dispatchers.IO (viewModelScope.launch(Dispatchers.IO) calling app.moveBaseNotes)
but switch to Dispatchers.Main before invoking the UI callback so
callable?.invoke() runs on Main. Locate the coroutine using
viewModelScope.launch(Dispatchers.IO) and the finally block with
callable?.invoke(), wrap the callback invocation in a
withContext(Dispatchers.Main) (or equivalent dispatcher switch) so the threading
intent is explicit and clear.
🤖 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/viewmodel/BaseNoteModel.kt`:
- Around line 692-699: deleteBaseNotes currently fetches notes, closes action
mode and cancels pins/reminders but never removes attached files from disk
before hard-deleting rows, which leaves orphaned files; update deleteBaseNotes
to call app.deleteAttachments(notes) (same call used in
deleteAllTrashedBaseNotes) after fetching the notes and before invoking
baseNoteDao.delete(ids) so attachments are removed on the permanent-delete path
while preserving the existing actionMode.close(false) and
app.cancelPinAndReminders(notes) calls.

---

Duplicate comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 629-634: After updating pins and fetching updatedNotes in
BaseNoteModel (the block calling baseNoteDao.updatePinnedToStatus(ids,
pinnedToStatusBar) and baseNoteDao.getByIds(ids)), only call
activity.refreshStatusBarPin(...) for notes that are actually in Folder.NOTES;
filter updatedNotes by note.folder == Folder.NOTES (or the equivalent folder
property) before iterating so archived/deleted notes do not produce status-bar
notifications when pinned.

---

Nitpick comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 660-664: The code launches a coroutine on Dispatchers.IO to call
app.moveBaseNotes(...) and then invokes callable?.invoke() in the finally block;
make the dispatcher boundary explicit by ensuring heavy work runs on
Dispatchers.IO (viewModelScope.launch(Dispatchers.IO) calling app.moveBaseNotes)
but switch to Dispatchers.Main before invoking the UI callback so
callable?.invoke() runs on Main. Locate the coroutine using
viewModelScope.launch(Dispatchers.IO) and the finally block with
callable?.invoke(), wrap the callback invocation in a
withContext(Dispatchers.Main) (or equivalent dispatcher switch) so the threading
intent is explicit and clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c64bc99d-4705-4903-bd43-16bd16b96eef

📥 Commits

Reviewing files that changed from the base of the PR and between 31016a7 and 1cf1e5b.

📒 Files selected for processing (1)
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Copy link
Copy Markdown
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: 5

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt (1)

126-157: ⚠️ Potential issue | 🟠 Major

isPinnedToStatus is parsed but not serialized in toJson().

The toBaseNote() function correctly parses isPinnedToStatus from JSON (line 126), but the corresponding toJson() function (lines 90-114) does not serialize this field. This creates an asymmetry that will cause the pin-to-status-bar state to be lost when notes are exported and re-imported via backup.

🐛 Proposed fix to add serialization in `toJson()`

Add the following line in the toJson() function after the existing field serializations (around line 112):

     jsonObject.put("reminders", Converters.remindersToJSONArray(reminders))
     jsonObject.put("viewMode", viewMode.name)
+    jsonObject.put("isPinnedToStatus", isPinnedToStatus)
     return jsonObject.toString(2)
 }
🤖 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/data/model/ModelExtensions.kt` around
lines 126 - 157, toBaseNote() reads the isPinnedToStatus flag into
BaseNote.pinnedToStatusBar but toJson() never writes it back; update the
toJson() implementation to serialize BaseNote.pinnedToStatusBar (the
isPinnedToStatus JSON key) so the pin-to-status-bar state round-trips. Locate
the toJson() function for BaseNote/notes and add a boolean entry for
"isPinnedToStatus" using the BaseNote.pinnedToStatusBar property (ensure the
JSON key matches the one parsed by toBaseNote()).
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

678-690: ⚠️ Potential issue | 🟠 Major

deleteAll() leaves orphaned attachment files on disk.

When clearing all data, deleteAll() calls deleteBaseNotes(ids) which only removes database rows and cancels reminders—it does not delete attachment files. In contrast, deleteAllTrashedBaseNotes() properly fetches all image, file, and audio attachments and calls app.deleteAttachments() before deleting notes. This inconsistency causes orphaned files to remain in storage after the "clear all data" operation completes.

🤖 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/viewmodel/BaseNoteModel.kt`
around lines 678 - 690, deleteAll() currently deletes DB rows and cancels
reminders but does not remove attachment files, causing orphaned files; update
deleteAll() to mirror deleteAllTrashedBaseNotes() by loading all attachments for
the notes before deleting and calling app.deleteAttachments(...) for
image/file/audio attachments (or the same attachment-collection helper used in
deleteAllTrashedBaseNotes()) on the IO dispatcher, then proceed to cancel
reminders and call deleteBaseNotes(ids); reference functions/variables:
deleteAll(), deleteBaseNotes(ids), deleteAllTrashedBaseNotes(),
app.deleteAttachments(...), baseNoteDao.getAllIds(),
baseNoteDao.getAllReminders() to locate where to add the
attachment-fetch-and-delete step.
♻️ Duplicate comments (4)
app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt (1)

125-146: ⚠️ Potential issue | 🔴 Critical

Include isPinnedToStatus in the ZIP import cursor.

Lines 125-144 don't project isPinnedToStatus, but Line 323 now calls getColumnIndexOrThrow("isPinnedToStatus"). That makes ZIP import fail as soon as it reads the first note row. If legacy backups are still supported, the parser should also use getColumnIndex() and fall back to false when the column is absent.

Minimal fix for the current crash
                 val columns =
                     arrayOf(
                         "id",
                         "type",
                         "folder",
                         "color",
                         "title",
                         "pinned",
                         "timestamp",
                         "modifiedTimestamp",
                         "labels",
                         "SUBSTR(body, 1, ${MAX_BODY_CHAR_LENGTH}) AS body",
                         "spans",
                         "items",
                         "images",
                         "files",
                         "audios",
                         "reminders",
                         "viewMode",
+                        "isPinnedToStatus",
                     )

Also applies to: 323-331

🤖 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/backup/ImportExtensions.kt`
around lines 125 - 146, The ZIP import cursor projection is missing
"isPinnedToStatus" causing getColumnIndexOrThrow at the note-parsing code (where
getColumnIndexOrThrow("isPinnedToStatus") is called) to crash; update the
columns array used in the database.query for BaseNote to include
"isPinnedToStatus" and change the parsing logic that currently calls
getColumnIndexOrThrow("isPinnedToStatus") to instead use
getColumnIndex("isPinnedToStatus") with a fallback to false when the column
index is -1 (to support legacy backups). Ensure you update both the projection
near the columns/ database.query and the parsing block that reads the
isPinnedToStatus value so missing-column cases default to false.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt (1)

64-64: ⚠️ Potential issue | 🟠 Major

Duplicate pinned notification restoration on boot.

Both NotallyXApplication.onCreate() and ReminderReceiver.BOOT_COMPLETED call restorePinnedNotifications(). Since Application.onCreate() runs before the BOOT_COMPLETED broadcast is delivered, pinned notifications will be posted twice on device boot.

🔧 Suggested fix: Remove from ReminderReceiver
                     intent.action == Intent.ACTION_BOOT_COMPLETED -> {
                         if (canScheduleExactAlarms) {
                             rescheduleAlarms(context)
                         }
                         restoreRemindersNotifications(context)
-                        restorePinnedNotifications(context)
                     }
🤖 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/note/reminders/ReminderReceiver.kt`
at line 64, The restorePinnedNotifications(context) call is duplicated:
NotallyXApplication.onCreate() already restores pinned notifications before the
BOOT_COMPLETED broadcast arrives, so remove the duplicate call from
ReminderReceiver's BOOT_COMPLETED handling. Edit ReminderReceiver.onReceive (the
branch handling Intent.ACTION_BOOT_COMPLETED / BOOT_COMPLETED) to delete the
restorePinnedNotifications(context) invocation while keeping any other
boot-specific logic intact so notifications are posted only once.
app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt (1)

274-275: ⚠️ Potential issue | 🟠 Major

Undo does not restore reminders and status-bar pins.

deleteSelectedBaseNotes() cancels reminders and pins via app.cancelPinAndReminders(notes), but the undo action only re-inserts rows with saveNotes(removedNotes). After undo, scheduled reminders and pinned notifications are lost.

🔧 Proposed fix
-                            setAction(R.string.undo) { baseModel.saveNotes(removedNotes) }
+                            setAction(R.string.undo) {
+                                baseModel.saveNotes(removedNotes)
+                                activity.pinAndScheduleReminders(removedNotes)
+                            }

You'll need to import pinAndScheduleReminders from com.philkes.notallyx.utils.

🤖 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/ModelFolderObserver.kt`
around lines 274 - 275, The undo action currently only calls
baseModel.saveNotes(removedNotes) so reminders and pinned notifications
cancelled by app.cancelPinAndReminders(notes) in deleteSelectedBaseNotes() are
not restored; update the undo lambda (the setAction in ModelFolderObserver) to,
after saving the notes with baseModel.saveNotes(removedNotes), call
com.philkes.notallyx.utils.pinAndScheduleReminders(removedNotes) to re-pin
notifications and re-schedule reminders, and add the import for
pinAndScheduleReminders at the top of the file.
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

625-636: ⚠️ Potential issue | 🟠 Major

Activity reference captured in coroutine may crash on configuration change.

The activity parameter is captured directly in the viewModelScope coroutine. If a configuration change (e.g., rotation) occurs before the coroutine completes, the Activity is destroyed while the ViewModel and its coroutine survive, leaving a stale reference. Calling refreshStatusBarPin on the destroyed Activity will crash.

🛡️ Proposed fix using WeakReference
+import java.lang.ref.WeakReference
+
     fun pinBaseNotesToStatusBar(activity: Activity, pinnedToStatusBar: Boolean) {
         val ids = actionMode.selectedIds.toLongArray()
+        val activityRef = WeakReference(activity)
         actionMode.close(true)
         viewModelScope.launch {
             val updatedNotes =
                 withContext(Dispatchers.IO) {
                     baseNoteDao.updatePinnedToStatus(ids, pinnedToStatusBar)
                     baseNoteDao.getByIds(ids)
                 }
-            updatedNotes.forEach { activity.refreshStatusBarPin(it) }
+            activityRef.get()?.let { act ->
+                updatedNotes.forEach { act.refreshStatusBarPin(it) }
+            }
         }
     }
🤖 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/viewmodel/BaseNoteModel.kt`
around lines 625 - 636, The pinBaseNotesToStatusBar function captures the
Activity directly into the viewModelScope coroutine which can hold a stale
reference across configuration changes; change it to capture a
WeakReference(activity) before launching the coroutine (leave actionMode and ids
handling unchanged), and inside the coroutine obtain activityRef.get() and only
call refreshStatusBarPin(it) when the retrieved Activity is non-null and not
finishing/destroyed; this removes the strong Activity reference and prevents
calling refreshStatusBarPin on a destroyed Activity while keeping the existing
calls to baseNoteDao.updatePinnedToStatus and baseNoteDao.getByIds intact.
🧹 Nitpick comments (6)
app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt (1)

170-174: Optional cleanup: remove redundant pinned-state check in restore loop.

getAllPinnedToStatusNotes() already implies filtering, so the if (note.isPinnedToStatus) guard at Line 172 appears unnecessary.

🤖 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/NotallyXApplication.kt` around lines
170 - 174, The restore loop calls getAllPinnedToStatusNotes() which already
returns only pinned notes, so remove the redundant if (note.isPinnedToStatus)
guard and directly call
PinnedNotificationManager.notify(this@NotallyXApplication, note) inside the
forEach; update the loop in NotallyXApplication.kt (the block iterating over
getAllPinnedToStatusNotes()) to eliminate the unnecessary conditional and keep
the notification invocation.
app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt (1)

214-218: Consider making updatePinnedToStatus suspend for consistency.

The updatePinnedToStatus methods are not suspend functions, while similar methods like updatePinned (line 212) are. While the current callers handle this correctly by running on Dispatchers.IO, marking these as suspend would:

  1. Maintain consistency with other update methods
  2. Ensure Room handles threading automatically
  3. Prevent potential misuse from main thread
♻️ Suggested change
-    `@Query`("UPDATE BaseNote SET isPinnedToStatus = :isPinnedToStatus WHERE id = :id")
-    fun updatePinnedToStatus(id: Long, isPinnedToStatus: Boolean)
+    `@Query`("UPDATE BaseNote SET isPinnedToStatus = :isPinnedToStatus WHERE id = :id")
+    suspend fun updatePinnedToStatus(id: Long, isPinnedToStatus: Boolean)

-    `@Query`("UPDATE BaseNote SET isPinnedToStatus = :isPinnedToStatus WHERE id IN (:ids)")
-    fun updatePinnedToStatus(ids: LongArray, isPinnedToStatus: Boolean)
+    `@Query`("UPDATE BaseNote SET isPinnedToStatus = :isPinnedToStatus WHERE id IN (:ids)")
+    suspend fun updatePinnedToStatus(ids: LongArray, isPinnedToStatus: Boolean)
🤖 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/data/dao/BaseNoteDao.kt` around lines
214 - 218, The two DAO methods updatePinnedToStatus in BaseNoteDao should be
marked suspend for consistency and to let Room manage threading; update both
signatures (the single-id overload and the LongArray overload) to "suspend fun
updatePinnedToStatus(...)" keeping the same parameters and return type (Unit),
and then run a quick grep of callers to ensure they don't rely on them being
non-suspending (update callers to call from a coroutine or suspend context if
needed).
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt (2)

153-156: Remove commented-out code.

This commented code block appears to be leftover from development. If this logic is no longer needed, please remove it to keep the codebase clean.

🧹 Suggested fix
-            //            val mostRecentReminder = note.reminders.findLastNotified() ?: return
-            //            if (mostRecentReminder.isNotificationVisible) {
-            //                notify(context, note.id, mostRecentReminder.id, schedule = false)
-            //            }
🤖 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/note/reminders/ReminderReceiver.kt`
around lines 153 - 156, In ReminderReceiver.kt remove the leftover commented-out
block that includes calls to findLastNotified() and notify(...) (the three
commented lines referencing mostRecentReminder and isNotificationVisible) to
clean up dead code; edit the ReminderReceiver class to delete those commented
lines so only active logic remains and run a build to ensure no referenced
symbols (mostRecentReminder, findLastNotified, isNotificationVisible) are needed
anywhere else.

302-311: Redundant filter: getAllPinnedToStatusNotes() already returns only pinned notes.

The query at line 126-128 in BaseNoteDao.kt includes WHERE isPinnedToStatus = 1, so the .filter { it.isPinnedToStatus } at line 306 is unnecessary.

♻️ Suggested fix
     private suspend fun restorePinnedNotifications(context: Context) {
         val baseNoteDao = getDatabase(context).getBaseNoteDao()
         val allNotes = baseNoteDao.getAllPinnedToStatusNotes()
         allNotes
-            .filter { it.isPinnedToStatus }
             .forEach { note ->
                 Log.d(TAG, "restorePinnedNotifications: Pinning noteID: ${note.id} to status bar")
                 PinnedNotificationManager.notify(context, note)
             }
     }
🤖 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/note/reminders/ReminderReceiver.kt`
around lines 302 - 311, The restorePinnedNotifications function redundantly
filters results already constrained by BaseNoteDao.getAllPinnedToStatusNotes();
remove the .filter { it.isPinnedToStatus } call and iterate directly over
allNotes (e.g., allNotes.forEach { note -> ... }) so restorePinnedNotifications
uses the DAO result without re-checking isPinnedToStatus.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt (1)

517-535: Consider simplifying refreshStatusBarPin permission check.

The function checks notification permission when isPinnedToStatus is true, but if a note is already pinned, permission should have been granted during the initial pin action. The permission check here is defensive (handles permission revocation after pinning), but the structure could be simplified:

♻️ Simplified alternative
 fun Activity.refreshStatusBarPin(note: BaseNote) {
-    val refresh = {
-        if (note.isPinnedToStatus) {
-            PinnedNotificationManager.notify(this, note)
-        } else {
-            PinnedNotificationManager.cancel(this, note.id)
-        }
-    }
     if (note.isPinnedToStatus) {
-        checkNotificationPermission(
-            REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS,
-            alsoCheckAlarmPermission = false,
-        ) {
-            refresh()
-        }
+        // Permission already verified during pin action; notify silently fails if revoked
+        PinnedNotificationManager.notify(this, note)
     } else {
-        refresh()
+        PinnedNotificationManager.cancel(this, note.id)
     }
 }

The current defensive approach is valid if you want to re-prompt for permission when it was revoked after initial grant.

🤖 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/note/NoteActionHandler.kt`
around lines 517 - 535, The refreshStatusBarPin function currently defines a
refresh lambda and then branches to either call checkNotificationPermission(...)
{ refresh() } when note.isPinnedToStatus or refresh() otherwise; simplify by
removing the refresh lambda and inlining the actions: when note.isPinnedToStatus
call checkNotificationPermission(REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS,
alsoCheckAlarmPermission = false) { PinnedNotificationManager.notify(this, note)
} and otherwise directly call PinnedNotificationManager.cancel(this, note.id).
Keep the same permission request symbol
(REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS) and methods
(checkNotificationPermission, PinnedNotificationManager.notify,
PinnedNotificationManager.cancel) to locate the change.
app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt (1)

26-38: Remove duplicate text computation.

contentText and bigText are computed with identical logic. Extract to a single variable.

♻️ Proposed fix
-    val contentText =
+    val noteText =
         if (note.type == com.philkes.notallyx.data.model.Type.LIST) {
             note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body }
         } else {
             note.body
         }
 
-    val bigText =
-        if (note.type == com.philkes.notallyx.data.model.Type.LIST) {
-            note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body }
-        } else {
-            note.body
-        }
     val notification =
         NotificationCompat.Builder(this, channelId)
             .apply {
                 setSubText(subtext)
                 setSmallIcon(iconResId)
                 setCategory(NotificationCompat.CATEGORY_REMINDER)
                 setContentTitle(note.title.ifEmpty { getString(R.string.note) })
-                setContentText(contentText)
-                setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
+                setContentText(noteText)
+                setStyle(NotificationCompat.BigTextStyle().bigText(noteText))
🤖 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/note/reminders/RemindersUtils.kt`
around lines 26 - 38, The logic computing the same string for contentText and
bigText is duplicated; compute it once (e.g., val computedText or noteText) by
checking note.type == com.philkes.notallyx.data.model.Type.LIST and using
note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body } or
note.body, then assign contentText = computedText and bigText = computedText so
both variables reuse the single computed value (update references to contentText
and bigText in RemindersUtils.kt accordingly).
🤖 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/data/model/BaseNote.kt`:
- Around line 64-69: The equalContents(other: BaseNote?) method in BaseNote
fails to compare the note type, so notes that only differ by type (Type.NOTE vs
Type.LIST) are treated as equal and cause incorrect notification intents; update
equalContents to include a comparison of the type property (e.g., add type ==
other.type) alongside title, body, spans and items so type changes are detected
and notifications get rebuilt appropriately.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt`:
- Around line 276-299: The dismissal branch is inverted: inside
Snackbar.Callback.onDismissed you currently delete attachments when event ==
DISMISS_EVENT_ACTION (which means the user tapped "Undo"); change the condition
so attachments are deleted only when the dismissal was NOT the action/undo.
Update the onDismissed check (in the anonymous Snackbar.Callback implementation)
to run the attachment collection and call activity.deleteAttachments(...) only
when event != DISMISS_EVENT_ACTION (i.e., when the user did not undo), leaving
the rest of the logic (deletedNotes, attachments list, baseModel.progress =
DeleteProgress(...)) intact.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt`:
- Around line 83-102: Change createDeleteReminderIntent to return a non-nullable
PendingIntent and guard against the case where both reminderId and
requestCodePostfix are null: validate at the start of
Context.createDeleteReminderIntent that if reminderId == null then
requestCodePostfix is not null (throw IllegalArgumentException or similar with a
clear message), then build the request-code string using either reminderId or
requestCodePostfix and call PendingIntent.getBroadcast as before; update the
function signature to return PendingIntent (not PendingIntent?) and reference
EXTRA_NOTE_ID, EXTRA_REMINDER_ID and ReminderReceiver::class.java unchanged.

In `@app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt`:
- Around line 462-468: The PendingIntent returned by getOpenNotePendingIntent
currently uses a fixed requestCode (0), causing different note notifications to
collapse into the same PendingIntent; update getOpenNotePendingIntent to
generate a unique requestCode per note (e.g., derive from noteId and noteType —
combine noteId.hashCode() and noteType.ordinal or type.hashCode()) and pass that
as the second argument to PendingIntent.getActivity so each note's PendingIntent
is distinct and notifications won't reuse the same intent.

In `@app/src/main/res/drawable/notification_unpin.xml`:
- Around line 10-21: The vector path in notification_unpin.xml has a missing
segment in the android:pathData causing bad rendering; update the pathData in
notification_unpin.xml (the first <path> entry) to include the missing segment
`200L280,200L280,514L234,560` between the coordinates after `387,` and before
`560L497,` or simply regenerate the entire icon from the original vector/source
asset (matching notification_pin.xml's corresponding path) so the geometry is
correct and the icon renders properly.

---

Outside diff comments:
In `@app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt`:
- Around line 126-157: toBaseNote() reads the isPinnedToStatus flag into
BaseNote.pinnedToStatusBar but toJson() never writes it back; update the
toJson() implementation to serialize BaseNote.pinnedToStatusBar (the
isPinnedToStatus JSON key) so the pin-to-status-bar state round-trips. Locate
the toJson() function for BaseNote/notes and add a boolean entry for
"isPinnedToStatus" using the BaseNote.pinnedToStatusBar property (ensure the
JSON key matches the one parsed by toBaseNote()).

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 678-690: deleteAll() currently deletes DB rows and cancels
reminders but does not remove attachment files, causing orphaned files; update
deleteAll() to mirror deleteAllTrashedBaseNotes() by loading all attachments for
the notes before deleting and calling app.deleteAttachments(...) for
image/file/audio attachments (or the same attachment-collection helper used in
deleteAllTrashedBaseNotes()) on the IO dispatcher, then proceed to cancel
reminders and call deleteBaseNotes(ids); reference functions/variables:
deleteAll(), deleteBaseNotes(ids), deleteAllTrashedBaseNotes(),
app.deleteAttachments(...), baseNoteDao.getAllIds(),
baseNoteDao.getAllReminders() to locate where to add the
attachment-fetch-and-delete step.

---

Duplicate comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt`:
- Around line 274-275: The undo action currently only calls
baseModel.saveNotes(removedNotes) so reminders and pinned notifications
cancelled by app.cancelPinAndReminders(notes) in deleteSelectedBaseNotes() are
not restored; update the undo lambda (the setAction in ModelFolderObserver) to,
after saving the notes with baseModel.saveNotes(removedNotes), call
com.philkes.notallyx.utils.pinAndScheduleReminders(removedNotes) to re-pin
notifications and re-schedule reminders, and add the import for
pinAndScheduleReminders at the top of the file.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt`:
- Line 64: The restorePinnedNotifications(context) call is duplicated:
NotallyXApplication.onCreate() already restores pinned notifications before the
BOOT_COMPLETED broadcast arrives, so remove the duplicate call from
ReminderReceiver's BOOT_COMPLETED handling. Edit ReminderReceiver.onReceive (the
branch handling Intent.ACTION_BOOT_COMPLETED / BOOT_COMPLETED) to delete the
restorePinnedNotifications(context) invocation while keeping any other
boot-specific logic intact so notifications are posted only once.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 625-636: The pinBaseNotesToStatusBar function captures the
Activity directly into the viewModelScope coroutine which can hold a stale
reference across configuration changes; change it to capture a
WeakReference(activity) before launching the coroutine (leave actionMode and ids
handling unchanged), and inside the coroutine obtain activityRef.get() and only
call refreshStatusBarPin(it) when the retrieved Activity is non-null and not
finishing/destroyed; this removes the strong Activity reference and prevents
calling refreshStatusBarPin on a destroyed Activity while keeping the existing
calls to baseNoteDao.updatePinnedToStatus and baseNoteDao.getByIds intact.

In `@app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt`:
- Around line 125-146: The ZIP import cursor projection is missing
"isPinnedToStatus" causing getColumnIndexOrThrow at the note-parsing code (where
getColumnIndexOrThrow("isPinnedToStatus") is called) to crash; update the
columns array used in the database.query for BaseNote to include
"isPinnedToStatus" and change the parsing logic that currently calls
getColumnIndexOrThrow("isPinnedToStatus") to instead use
getColumnIndex("isPinnedToStatus") with a fallback to false when the column
index is -1 (to support legacy backups). Ensure you update both the projection
near the columns/ database.query and the parsing block that reads the
isPinnedToStatus value so missing-column cases default to false.

---

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt`:
- Around line 214-218: The two DAO methods updatePinnedToStatus in BaseNoteDao
should be marked suspend for consistency and to let Room manage threading;
update both signatures (the single-id overload and the LongArray overload) to
"suspend fun updatePinnedToStatus(...)" keeping the same parameters and return
type (Unit), and then run a quick grep of callers to ensure they don't rely on
them being non-suspending (update callers to call from a coroutine or suspend
context if needed).

In `@app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt`:
- Around line 170-174: The restore loop calls getAllPinnedToStatusNotes() which
already returns only pinned notes, so remove the redundant if
(note.isPinnedToStatus) guard and directly call
PinnedNotificationManager.notify(this@NotallyXApplication, note) inside the
forEach; update the loop in NotallyXApplication.kt (the block iterating over
getAllPinnedToStatusNotes()) to eliminate the unnecessary conditional and keep
the notification invocation.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt`:
- Around line 517-535: The refreshStatusBarPin function currently defines a
refresh lambda and then branches to either call checkNotificationPermission(...)
{ refresh() } when note.isPinnedToStatus or refresh() otherwise; simplify by
removing the refresh lambda and inlining the actions: when note.isPinnedToStatus
call checkNotificationPermission(REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS,
alsoCheckAlarmPermission = false) { PinnedNotificationManager.notify(this, note)
} and otherwise directly call PinnedNotificationManager.cancel(this, note.id).
Keep the same permission request symbol
(REQUEST_NOTIFICATION_PERMISSION_PIN_TO_STATUS) and methods
(checkNotificationPermission, PinnedNotificationManager.notify,
PinnedNotificationManager.cancel) to locate the change.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt`:
- Around line 153-156: In ReminderReceiver.kt remove the leftover commented-out
block that includes calls to findLastNotified() and notify(...) (the three
commented lines referencing mostRecentReminder and isNotificationVisible) to
clean up dead code; edit the ReminderReceiver class to delete those commented
lines so only active logic remains and run a build to ensure no referenced
symbols (mostRecentReminder, findLastNotified, isNotificationVisible) are needed
anywhere else.
- Around line 302-311: The restorePinnedNotifications function redundantly
filters results already constrained by BaseNoteDao.getAllPinnedToStatusNotes();
remove the .filter { it.isPinnedToStatus } call and iterate directly over
allNotes (e.g., allNotes.forEach { note -> ... }) so restorePinnedNotifications
uses the DAO result without re-checking isPinnedToStatus.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt`:
- Around line 26-38: The logic computing the same string for contentText and
bigText is duplicated; compute it once (e.g., val computedText or noteText) by
checking note.type == com.philkes.notallyx.data.model.Type.LIST and using
note.items.joinToString("\n") { (if (it.checked) "✅ " else "🔳 ") + it.body } or
note.body, then assign contentText = computedText and bigText = computedText so
both variables reuse the single computed value (update references to contentText
and bigText in RemindersUtils.kt accordingly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e05e0a13-b001-4993-8da1-91c13b99a0dd

📥 Commits

Reviewing files that changed from the base of the PR and between 6dcea19 and fc79645.

⛔ Files ignored due to path filters (1)
  • app/translations.xlsx is excluded by !**/*.xlsx
📒 Files selected for processing (44)
  • app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt
  • app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt
  • app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
  • app/src/main/java/com/philkes/notallyx/data/dao/DaoExtensions.kt
  • app/src/main/java/com/philkes/notallyx/data/imports/evernote/EvernoteImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/imports/google/GoogleKeepImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/imports/txt/PlainTextImporter.kt
  • app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt
  • app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/main/ModelFolderObserver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/NoteActionHandler.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/ReminderReceiver.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersActivity.kt
  • app/src/main/java/com/philkes/notallyx/presentation/activity/note/reminders/RemindersUtils.kt
  • app/src/main/java/com/philkes/notallyx/presentation/view/note/action/MoreNoteBottomSheet.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/NotallyModel.kt
  • app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/Preference.kt
  • app/src/main/java/com/philkes/notallyx/utils/AlarmExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/PinnedNotificationManager.kt
  • app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt
  • app/src/main/java/com/philkes/notallyx/utils/backup/XmlParserExtensions.kt
  • app/src/main/res/drawable/notification_pin.xml
  • app/src/main/res/drawable/notification_unpin.xml
  • app/src/main/res/drawable/pinboard.xml
  • app/src/main/res/drawable/pinboard_filled.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml

Comment on lines +83 to +102
fun Context.createDeleteReminderIntent(
noteId: Long,
reminderId: Long?,
requestCodePostfix: String? = null,
): PendingIntent? {
val deleteIntent =
Intent(this, ReminderReceiver::class.java).apply {
action =
if (reminderId != null) ReminderReceiver.ACTION_NOTIFICATION_DISMISSED
else ReminderReceiver.ACTION_PINNED_NOTIFICATION_DISMISSED
putExtra(EXTRA_NOTE_ID, noteId)
reminderId?.let { putExtra(EXTRA_REMINDER_ID, it) }
}
return PendingIntent.getBroadcast(
this,
"$noteId-${reminderId ?: requestCodePostfix!!}".hashCode(),
deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
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 | 🟡 Minor

Return type should be non-nullable; add guard against null inputs.

The function always returns a PendingIntent from getBroadcast, yet the return type is PendingIntent?. More critically, if both reminderId and requestCodePostfix are null, line 98 will throw NullPointerException on the !! operator.

🛡️ Proposed fix
 fun Context.createDeleteReminderIntent(
     noteId: Long,
     reminderId: Long?,
     requestCodePostfix: String? = null,
-): PendingIntent? {
+): PendingIntent {
+    require(reminderId != null || requestCodePostfix != null) {
+        "Either reminderId or requestCodePostfix must be non-null"
+    }
     val deleteIntent =
         Intent(this, ReminderReceiver::class.java).apply {
📝 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
fun Context.createDeleteReminderIntent(
noteId: Long,
reminderId: Long?,
requestCodePostfix: String? = null,
): PendingIntent? {
val deleteIntent =
Intent(this, ReminderReceiver::class.java).apply {
action =
if (reminderId != null) ReminderReceiver.ACTION_NOTIFICATION_DISMISSED
else ReminderReceiver.ACTION_PINNED_NOTIFICATION_DISMISSED
putExtra(EXTRA_NOTE_ID, noteId)
reminderId?.let { putExtra(EXTRA_REMINDER_ID, it) }
}
return PendingIntent.getBroadcast(
this,
"$noteId-${reminderId ?: requestCodePostfix!!}".hashCode(),
deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
fun Context.createDeleteReminderIntent(
noteId: Long,
reminderId: Long?,
requestCodePostfix: String? = null,
): PendingIntent {
require(reminderId != null || requestCodePostfix != null) {
"Either reminderId or requestCodePostfix must be non-null"
}
val deleteIntent =
Intent(this, ReminderReceiver::class.java).apply {
action =
if (reminderId != null) ReminderReceiver.ACTION_NOTIFICATION_DISMISSED
else ReminderReceiver.ACTION_PINNED_NOTIFICATION_DISMISSED
putExtra(EXTRA_NOTE_ID, noteId)
reminderId?.let { putExtra(EXTRA_REMINDER_ID, it) }
}
return PendingIntent.getBroadcast(
this,
"$noteId-${reminderId ?: requestCodePostfix!!}".hashCode(),
deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
🤖 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/note/reminders/RemindersUtils.kt`
around lines 83 - 102, Change createDeleteReminderIntent to return a
non-nullable PendingIntent and guard against the case where both reminderId and
requestCodePostfix are null: validate at the start of
Context.createDeleteReminderIntent that if reminderId == null then
requestCodePostfix is not null (throw IllegalArgumentException or similar with a
clear message), then build the request-code string using either reminderId or
requestCodePostfix and call PendingIntent.getBroadcast as before; update the
function signature to return PendingIntent (not PendingIntent?) and reference
EXTRA_NOTE_ID, EXTRA_REMINDER_ID and ReminderReceiver::class.java unchanged.

Comment on lines +10 to +21
android:pathData=
"M360,
920L320,880L320,
640L120,640L120,
560L200,480L200,
200L160,200L160,
120L445,120Q427,
137 412.5,157Q398, 177 387,
560L497,560Q521,
574 546.5,583.5Q572,
593 600,597L600,
640L400,640L400,880L360,920Z" />
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import xml.etree.ElementTree as ET

ANDROID_NS = "{http://schemas.android.com/apk/res/android}"
files = [
    "app/src/main/res/drawable/notification_pin.xml",
    "app/src/main/res/drawable/notification_unpin.xml",
]

def first_path_data(path):
    root = ET.parse(path).getroot()
    first_path = root.findall("path")[0]
    return " ".join(first_path.attrib[f"{ANDROID_NS}pathData"].split())

pin = first_path_data(files[0])
unpin = first_path_data(files[1])

print("PIN   :", pin)
print("UNPIN :", unpin)
print("\nPIN contains segment '200L280,200L280,514L234,560':", "200L280,200L280,514L234,560" in pin)
print("UNPIN contains segment '200L280,200L280,514L234,560':", "200L280,200L280,514L234,560" in unpin)
PY

Repository: Crustack/NotallyX

Length of output: 613


Regenerate icon from source asset—the pathData is missing a segment.

The first pathData in notification_unpin.xml is missing the segment 200L280,200L280,514L234,560 that exists in notification_pin.xml. The unpin path jumps directly from 387, to 560L497, instead of following the complete geometry. This will cause the icon to render incorrectly. Unwrap the pathData or regenerate it from the vector source.

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

In `@app/src/main/res/drawable/notification_unpin.xml` around lines 10 - 21, The
vector path in notification_unpin.xml has a missing segment in the
android:pathData causing bad rendering; update the pathData in
notification_unpin.xml (the first <path> entry) to include the missing segment
`200L280,200L280,514L234,560` between the coordinates after `387,` and before
`560L497,` or simply regenerate the entire icon from the original vector/source
asset (matching notification_pin.xml's corresponding path) so the geometry is
correct and the icon renders properly.

@Crustack Crustack merged commit b7f395f into main Apr 4, 2026
1 check passed
@Crustack Crustack deleted the feat/880 branch April 4, 2026 15:10
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.

Add delete reminder and delete note to reminder notification Pin to Status Bar

1 participant