Skip to content

Add preference to auto remove deleted notes#846

Merged
Crustack merged 2 commits intomainfrom
feat/393
Feb 20, 2026
Merged

Add preference to auto remove deleted notes#846
Crustack merged 2 commits intomainfrom
feat/393

Conversation

@Crustack
Copy link
Copy Markdown
Owner

@Crustack Crustack commented Feb 20, 2026

Closes #393

Summary by CodeRabbit

  • New Features

    • Added configurable auto-remove for permanently deleted notes (0–365 days) with settings control, hint/tooltip, and automatic background scheduling/cancellation.
  • Documentation

    • Updated translation coverage table to use a consistent 331-denominator across languages.
  • Tests

    • Added automated tests to verify auto-remove cleanup behavior and edge cases.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 20, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds an auto-empty bin feature: new preference and UI, WorkManager worker with scheduling/cancellation, DAO/database changes to record deletion timestamps and test DB support, background cleanup logic to remove old deleted notes and attachments, translations/strings update, and Robolectric tests.

Changes

Cohort / File(s) Summary
Translations & Strings
TRANSLATIONS.md, app/src/main/res/values/strings.xml
Standardized translations denominator to 331 and updated per-language counts; added auto_remove_deleted_notes and auto_remove_deleted_notes_hint strings.
Application & WorkManager helpers
app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt
Added TAG and AUTO_REMOVE_DELETED_NOTES constants; observe autoRemoveDeletedNotesAfterDays and schedule/cancel periodic worker via new WorkManager extension functions.
Background Worker & Cleanup logic
app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt
New AutoRemoveDeletedNotesWorker CoroutineWorker and ContextWrapper.removeOldDeletedNotes() that reads preference, computes cutoff, queries DB for old deleted notes, deletes rows and attachments, and returns worker Result.
Database & DAO
app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt, app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt
Added in-memory test DB support (getTestDatabase) and test-run routing; extended BaseNoteDao with deleteAll(), batched media getters (getImages/getFiles/getAudios), getDeletedNoteIdsOlderThan(before), and move(ids, folder, timestamp).
Preferences & ViewModel
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt, app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
Introduced autoRemoveDeletedNotesAfterDays IntPreference and included it in reload(); moving to Folder.DELETED now records deletion timestamp (passes System.currentTimeMillis()).
Settings UI & Bindings
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/..., app/src/main/res/layout/fragment_settings.xml
Inserted AutoEmptyBin seekbar include in settings layout; added tooltip support and setupAutoEmptyBin() in seekbar binding; SettingsFragment saves preference and schedules/cancels worker accordingly.
Tests
app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt
New Robolectric tests verifying removal of old deleted notes and preservation of recently deleted notes; test DB setup/teardown and helpers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SettingsUI as Settings UI
    participant Prefs as Preferences
    participant App as NotallyXApplication
    participant WorkMgr as WorkManager
    participant Worker as AutoRemoveWorker
    participant DB as Database
    participant Files as FileSystem

    User->>SettingsUI: Adjust "Auto remove days" slider
    SettingsUI->>Prefs: Save autoRemoveDeletedNotesAfterDays
    Prefs->>App: Preference observer emits change
    App->>WorkMgr: scheduleAutoRemoveOldDeletedNotes() if days > 0
    App->>WorkMgr: cancelAutoRemoveOldDeletedNotes() if days == 0

    Note over WorkMgr: Periodic trigger (daily)
    WorkMgr->>Worker: doWork()
    Worker->>Prefs: Read autoRemoveDeletedNotesAfterDays
    Worker->>DB: getDeletedNoteIdsOlderThan(cutoff)
    DB-->>Worker: oldDeletedNoteIds
    Worker->>DB: getImages/getFiles/getAudios(ids)
    DB-->>Worker: attachment lists
    Worker->>DB: delete notes by ids
    Worker->>Files: deleteAttachments(attachment paths)
    Files-->>Worker: deletion results
    Worker-->>WorkMgr: Result.success()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hoppity-hop, I tidy the bin,

A slider set, the countdown begins,
A worker wakes to clear the old,
Notes and crumbs no longer hold,
The rabbit winks — the app is trim.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a preference to auto remove deleted notes, which matches the primary objective of implementing an auto-empty-bin feature.
Linked Issues check ✅ Passed All coding requirements from issue #393 are met: a new preference autoRemoveDeletedNotesAfterDays has been added, WorkManager scheduling implemented via AutoRemoveDeletedNotesWorker, UI controls added in SettingsFragment, and database cleanup logic implemented.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the auto-remove deleted notes feature. The TRANSLATIONS.md update is unrelated but minor; all code changes focus on preference management, WorkManager scheduling, and note deletion logic.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/393

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 (7)
app/src/main/res/values/strings.xml (1)

35-36: Consider replacing literal "X" with a format argument or clearer phrasing in the hint.

The hint uses a literal "X" ("permanently removed after X days") rather than a format argument like %d. If the hint is displayed alongside the seekbar value, this is a cosmetic issue, but it could be confusing if the hint is shown in isolation (e.g., as a tooltip). Consider rephrasing to "...removed after the configured number of days" or using a string format argument.

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

In `@app/src/main/res/values/strings.xml` around lines 35 - 36, The hint string
auto_remove_deleted_notes_hint uses a literal "X"; replace it with either a
format placeholder (e.g., "%d" or "%s") and update the code that sets the hint
to call getString(R.string.auto_remove_deleted_notes_hint, days) so the actual
seekbar value is inserted, or reword the string to a clearer standalone phrase
such as "By enabling this, Notes that were deleted will be permanently removed
after the configured number of days" to avoid needing a runtime substitution;
update the string resource and any code that reads it (references to
auto_remove_deleted_notes_hint) accordingly.
app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt (1)

117-128: Missing cleanup of test database singleton between test classes.

testInstance is never reset to null after db.close() in AutoRemoveDeletedNotesDbTest.tearDown(). This means if test classes execute sequentially in the same JVM process, a subsequent call to getFreshDatabase() could receive a closed database instance from a previous test class, causing test failures.

While AutoRemoveDeletedNotesDbTest clears data between individual test methods via deleteAll() in its @After hook, the singleton is not reset after the class-level teardown. This is particularly problematic if the test framework or build environment reuses static state across test class boundaries.

Additionally, testInstance lacks the @Volatile annotation present on the instance field (line 58). For consistency and correctness in the double-checked locking pattern, it should be annotated.

Recommended fix:

Add a reset of testInstance in AutoRemoveDeletedNotesDbTest.tearDown() or implement a dedicated cleanup method called from test teardown:

`@JvmStatic`
`@AfterClass`
fun tearDown() {
    db.close()
    NotallyDatabase.resetTestInstance()  // or set testInstance = null directly if accessible
}

And add @Volatile to the testInstance field.

🤖 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/NotallyDatabase.kt` around lines
117 - 128, The test singleton testInstance in NotallyDatabase is never reset and
also lacks `@Volatile`, causing closed DB reuse across test classes; update the
NotallyDatabase companion by annotating testInstance with `@Volatile` and add a
public static resetTestInstance() (or resetTestDatabase()) method that closes
and/or sets testInstance = null, and then call that reset from
AutoRemoveDeletedNotesDbTest.tearDown() (or its `@AfterClass` teardown) after
db.close(); ensure getTestDatabase still lazy-initializes testInstance via the
synchronized block (the function name getTestDatabase remains unchanged).
app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt (2)

19-21: Verify that the ContextWrapper cast is safe for all Android contexts.

context.applicationContext is cast to ContextWrapper on line 20. While Application extends ContextWrapper, if the application context were ever wrapped differently (e.g., by instrumentation), this would throw a ClassCastException. Since this is a WorkManager worker, in practice the application context should always be an Application instance — but consider casting to Application directly for clarity (consistent with line 24 which already casts to Application).

🤖 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/AutoRemoveDeletedNotesWorker.kt`
around lines 19 - 21, The cast of context.applicationContext to ContextWrapper
inside AutoRemoveDeletedNotesWorker.doWork is unsafe; change it to cast to
Application (or check instance and safely unwrap) before calling
removeOldDeletedNotes so it works with any application context variant—locate
the doWork() method in AutoRemoveDeletedNotesWorker and replace the
(context.applicationContext as ContextWrapper) cast with a safe Application cast
or conditional instance check that then calls removeOldDeletedNotes().

23-59: removeOldDeletedNotes is a member extension function — not reusable outside the worker.

This function is defined as suspend fun ContextWrapper.removeOldDeletedNotes() inside the class, making it accessible only within AutoRemoveDeletedNotesWorker. The test file imports this function from com.philkes.notallyx.utils.backup, but the function does not actually exist in the backup module — the import is incorrect and should reference the worker instead. Consider extracting the core logic into a shared top-level extension to enable reusability across modules.

🤖 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/AutoRemoveDeletedNotesWorker.kt`
around lines 23 - 59, The removeOldDeletedNotes extension is currently defined
as a member extension inside AutoRemoveDeletedNotesWorker (suspend fun
ContextWrapper.removeOldDeletedNotes), which prevents reuse from other
modules/tests; extract the core logic into a top-level suspend extension (e.g.,
suspend fun ContextWrapper.removeOldDeletedNotes or suspend fun
Context.removeOldDeletedNotes) in a shared utils file (the same package the
tests expect, e.g., com.philkes.notallyx.utils.backup) and have
AutoRemoveDeletedNotesWorker call that top-level function; update any incorrect
test imports to reference the new top-level function and keep usages of
NotallyXPreferences, NotallyDatabase, Converters, and deleteAttachments
unchanged within the extracted function.
app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt (1)

35-56: Database lifecycle inconsistency: @Before recreates db stored in companion object, but only @AfterClass closes it.

setUp() creates a fresh NotallyDatabase on every test and assigns it to the companion field, but tearDown() (annotated @AfterClass) only runs once and closes the last-assigned instance. Earlier database instances are never closed. Either:

  • Move db/dao out of companion and use @After to close, or
  • Initialize db once in a @BeforeClass/@JvmStatic method and only call dao.deleteAll() in @Before/@After for isolation.
Example: one-time init with `@BeforeClass`
     companion object {
         private lateinit var db: NotallyDatabase
         private lateinit var dao: BaseNoteDao

+        `@JvmStatic`
+        `@BeforeClass`
+        fun initDb() {
+            val context = ApplicationProvider.getApplicationContext<ContextWrapper>()
+            db = NotallyDatabase.getFreshDatabase(context, false)
+            dao = db.getBaseNoteDao()
+        }
+
         `@JvmStatic`
         `@AfterClass`
         fun tearDown() {
             db.close()
         }
     }
 
     `@Before`
     fun setUp() {
-        val context = ApplicationProvider.getApplicationContext<ContextWrapper>()
-        db = NotallyDatabase.getFreshDatabase(context, false)
-        dao = db.getBaseNoteDao()
+        runBlocking { dao.deleteAll() }
     }
 
     `@After`
     fun cleanup() {
         runBlocking { dao.deleteAll() }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt`
around lines 35 - 56, The test currently recreates NotallyDatabase in setUp()
and stores it in the companion object field db but only closes it once in the
`@JvmStatic` `@AfterClass` tearDown(), leaking earlier instances; fix by making the
lifecycle consistent: either move db and dao out of the companion so
setUp()/cleanup() manage a per-test instance (create db via
NotallyDatabase.getFreshDatabase(...) in setUp, call db.close() in `@After`
cleanup after running runBlocking { dao.deleteAll() }) or change to one-time
init by adding a `@JvmStatic` `@BeforeClass` method to create db =
NotallyDatabase.getFreshDatabase(...) and dao = db.getBaseNoteDao(), keep
per-test isolation via runBlocking { dao.deleteAll() } in `@Before/`@After, and
leave `@JvmStatic` `@AfterClass` tearDown() to call db.close(); update references to
setUp, cleanup, tearDown, db, dao, NotallyDatabase.getFreshDatabase, and
BaseNoteDao.deleteAll accordingly.
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt (1)

467-472: Nit: prefer it inside the let block.

Line 470 re-references tooltipResId inside let { ... } where the shadowed it parameter is already the non-null value. Using it is more idiomatic Kotlin.

-            tooltipResId?.let { tooltipText = context.getString(tooltipResId) }
+            tooltipResId?.let { tooltipText = context.getString(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/activity/main/fragment/settings/PreferenceBindingExtensions.kt`
around lines 467 - 472, The code in Title.apply re-checks tooltipResId inside
the let block instead of using the non-null shadowed parameter; update the
lambda in Title.apply so that tooltipResId?.let { tooltipText =
context.getString(it) } uses the provided it value (referencing tooltipResId,
tooltipText, context.getString, and Title.apply) to be more idiomatic Kotlin.
app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt (1)

261-276: The context parameter is unused for scheduling — only used in the error-logging catch block.

The ContextWrapper parameter exists solely to call context.log(...) on line 274 inside the catch. Consider whether passing a logging lambda or using Log.e (like line 262) would be simpler and avoid the extra parameter.

🤖 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
261 - 276, The scheduleAutoRemoveOldDeletedNotes function currently takes a
ContextWrapper only to call context.log in the catch; remove the ContextWrapper
parameter and replace the call to context.log(TAG, "...", throwable = e) with a
direct logging call (e.g., Log.e(TAG, "Scheduling auto removal of old deleted
notes failed", e)). Specifically, update the
WorkManager.scheduleAutoRemoveOldDeletedNotes signature to remove context, leave
enqueueUniquePeriodicWork usage unchanged, and change the catch block to call
Log.e(TAG, ..., e) (or accept an optional logging lambda parameter with a
default that delegates to Log.e if you prefer injectable logging).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt`:
- Around line 343-358: The code in SettingsFragment inside the AutoEmptyBin
callback unsafely casts requireContext() to ContextWrapper when calling
WorkManager.scheduleAutoRemoveOldDeletedNotes, which can throw
ClassCastException; change the call to pass requireActivity() (or use a safe
cast like (requireContext() as? ContextWrapper) ?: return) so
scheduleAutoRemoveOldDeletedNotes receives a guaranteed ContextWrapper, and keep
cancelAutoRemoveOldDeletedNotes as-is; update the invocation in the
binding.AutoEmptyBin.setupAutoEmptyBin lambda where model.savePreference and
WorkManager calls occur.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt`:
- Around line 118-126: The reload() method is missing a call to update the
autoRemoveDeletedNotesAfterDays IntPreference, so observers (e.g., in
NotallyXApplication and SettingsFragment) don't see imported/reset values; open
the reload() function in NotallyXPreferences and add a line calling
autoRemoveDeletedNotesAfterDays.reload() following the same pattern used for
periodicBackups.reload(), so the LiveData is refreshed immediately after
import/reset.

In
`@app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt`:
- Around line 32-35: The Log.d call in AutoRemoveDeletedNotesWorker uses an
interpolated string with an unclosed parenthesis around "(since:
${Date(before).toText()"; fix the Log.d message by adding the missing closing
parenthesis inside the string (e.g. "... (since: ${Date(before).toText()})") so
the parentheses are balanced; ensure the call to Log.d(TAG, ...) remains
syntactically correct around Date(before).toText().
- Around line 37-58: The database opened by NotallyDatabase.getFreshDatabase in
AutoRemoveDeletedNotesWorker (inside doWork) is never closed; wrap the try/catch
in a try/finally (or use Kotlin's use/auto-close pattern) and call
database.close() in the finally block to ensure the Room instance is always
closed after using baseNoteDao.getDeletedNoteIdsOlderThan,
getImages/getFiles/getAudios, delete, and deleteAttachments; reference the
NotallyDatabase.getFreshDatabase call and the local variable database to locate
where to insert the close.

---

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt`:
- Around line 117-128: The test singleton testInstance in NotallyDatabase is
never reset and also lacks `@Volatile`, causing closed DB reuse across test
classes; update the NotallyDatabase companion by annotating testInstance with
`@Volatile` and add a public static resetTestInstance() (or resetTestDatabase())
method that closes and/or sets testInstance = null, and then call that reset
from AutoRemoveDeletedNotesDbTest.tearDown() (or its `@AfterClass` teardown) after
db.close(); ensure getTestDatabase still lazy-initializes testInstance via the
synchronized block (the function name getTestDatabase remains unchanged).

In `@app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt`:
- Around line 261-276: The scheduleAutoRemoveOldDeletedNotes function currently
takes a ContextWrapper only to call context.log in the catch; remove the
ContextWrapper parameter and replace the call to context.log(TAG, "...",
throwable = e) with a direct logging call (e.g., Log.e(TAG, "Scheduling auto
removal of old deleted notes failed", e)). Specifically, update the
WorkManager.scheduleAutoRemoveOldDeletedNotes signature to remove context, leave
enqueueUniquePeriodicWork usage unchanged, and change the catch block to call
Log.e(TAG, ..., e) (or accept an optional logging lambda parameter with a
default that delegates to Log.e if you prefer injectable logging).

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt`:
- Around line 467-472: The code in Title.apply re-checks tooltipResId inside the
let block instead of using the non-null shadowed parameter; update the lambda in
Title.apply so that tooltipResId?.let { tooltipText = context.getString(it) }
uses the provided it value (referencing tooltipResId, tooltipText,
context.getString, and Title.apply) to be more idiomatic Kotlin.

In
`@app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt`:
- Around line 19-21: The cast of context.applicationContext to ContextWrapper
inside AutoRemoveDeletedNotesWorker.doWork is unsafe; change it to cast to
Application (or check instance and safely unwrap) before calling
removeOldDeletedNotes so it works with any application context variant—locate
the doWork() method in AutoRemoveDeletedNotesWorker and replace the
(context.applicationContext as ContextWrapper) cast with a safe Application cast
or conditional instance check that then calls removeOldDeletedNotes().
- Around line 23-59: The removeOldDeletedNotes extension is currently defined as
a member extension inside AutoRemoveDeletedNotesWorker (suspend fun
ContextWrapper.removeOldDeletedNotes), which prevents reuse from other
modules/tests; extract the core logic into a top-level suspend extension (e.g.,
suspend fun ContextWrapper.removeOldDeletedNotes or suspend fun
Context.removeOldDeletedNotes) in a shared utils file (the same package the
tests expect, e.g., com.philkes.notallyx.utils.backup) and have
AutoRemoveDeletedNotesWorker call that top-level function; update any incorrect
test imports to reference the new top-level function and keep usages of
NotallyXPreferences, NotallyDatabase, Converters, and deleteAttachments
unchanged within the extracted function.

In `@app/src/main/res/values/strings.xml`:
- Around line 35-36: The hint string auto_remove_deleted_notes_hint uses a
literal "X"; replace it with either a format placeholder (e.g., "%d" or "%s")
and update the code that sets the hint to call
getString(R.string.auto_remove_deleted_notes_hint, days) so the actual seekbar
value is inserted, or reword the string to a clearer standalone phrase such as
"By enabling this, Notes that were deleted will be permanently removed after the
configured number of days" to avoid needing a runtime substitution; update the
string resource and any code that reads it (references to
auto_remove_deleted_notes_hint) accordingly.

In
`@app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt`:
- Around line 35-56: The test currently recreates NotallyDatabase in setUp() and
stores it in the companion object field db but only closes it once in the
`@JvmStatic` `@AfterClass` tearDown(), leaking earlier instances; fix by making the
lifecycle consistent: either move db and dao out of the companion so
setUp()/cleanup() manage a per-test instance (create db via
NotallyDatabase.getFreshDatabase(...) in setUp, call db.close() in `@After`
cleanup after running runBlocking { dao.deleteAll() }) or change to one-time
init by adding a `@JvmStatic` `@BeforeClass` method to create db =
NotallyDatabase.getFreshDatabase(...) and dao = db.getBaseNoteDao(), keep
per-test isolation via runBlocking { dao.deleteAll() } in `@Before/`@After, and
leave `@JvmStatic` `@AfterClass` tearDown() to call db.close(); update references to
setUp, cleanup, tearDown, db, dao, NotallyDatabase.getFreshDatabase, and
BaseNoteDao.deleteAll accordingly.

Comment on lines +167 to +168
@Query("SELECT id FROM BaseNote WHERE folder = 'DELETED' AND modifiedTimestamp < :before")
suspend fun getDeletedNoteIdsOlderThan(before: Long): LongArray
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

🧩 Analysis chain

🏁 Script executed:

rg -n "modifiedTimestamp|getDeletedNoteIdsOlderThan|removeOldDeletedNotes|AutoRemoveWorker" --type=kotlin -C 5

Repository: Crustack/NotallyX

Length of output: 50373


🏁 Script executed:

rg -n "\.move\(" --type=kotlin -B 2 -A 2 | grep -A 4 -B 4 "DELETED\|folder = Folder\|Folder.DELETED" | head -100

Repository: Crustack/NotallyX

Length of output: 3023


🏁 Script executed:

rg -n "Migration7|migration|DELETED.*modifiedTimestamp|backfill" --type=kotlin -A 5 | head -80

Repository: Crustack/NotallyX

Length of output: 8400


🏁 Script executed:

rg -n "autoRemoveDeletedNotes|AutoRemoveDeletedNotes" --type=kotlin -B 3 -A 3 | grep -v test | head -100

Repository: Crustack/NotallyX

Length of output: 8814


🏁 Script executed:

rg -n "checkUpdateAutoRemoveOldDeletedNotes" --type=kotlin -A 20

Repository: Crustack/NotallyX

Length of output: 4405


Pre-existing deleted notes may be removed prematurely when the auto-remove feature is first enabled.

The implementation correctly updates modifiedTimestamp to the current time when notes are moved to DELETED going forward (BaseNoteModel.kt line 635). However, notes already in the DELETED folder before this feature was deployed have modifiedTimestamp set to their last edit time (via Migration6's default). When a user enables "auto remove after 30 days", the worker immediately runs getDeletedNoteIdsOlderThan(), which filters on modifiedTimestamp < before. This means notes deleted recently but last edited months ago will be permanently removed on the next worker execution, with no safeguard or one-time migration.

Consider either:

  1. A one-time data migration that sets modifiedTimestamp to System.currentTimeMillis() for all notes currently in the DELETED folder when the feature is first enabled.
  2. Adding a dedicated deletedTimestamp column to clearly separate edit time from deletion time.
  3. At minimum, documenting this as a known limitation so users understand the behavior.

Comment on lines +343 to +358
autoRemoveDeletedNotesAfterDays.observe(viewLifecycleOwner) { value ->
binding.AutoEmptyBin.setupAutoEmptyBin(
autoRemoveDeletedNotesAfterDays,
requireContext(),
) { newValue ->
model.savePreference(autoRemoveDeletedNotesAfterDays, newValue)
val workManager = WorkManager.getInstance(requireContext())
if (newValue > 0) {
workManager.scheduleAutoRemoveOldDeletedNotes(
requireContext() as ContextWrapper
)
} else {
workManager.cancelAutoRemoveOldDeletedNotes()
}
}
}
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

Unsafe cast requireContext() as ContextWrapper on line 352.

requireContext() returns a Context, which is typically the host Activity (a ContextWrapper subclass), but the unchecked cast will throw ClassCastException if the context is ever not a ContextWrapper. Consider using the existing requireActivity() (which is guaranteed to be an Activity extends ContextWrapper) or a safe cast.

Proposed fix
                 if (newValue > 0) {
                     workManager.scheduleAutoRemoveOldDeletedNotes(
-                        requireContext() as ContextWrapper
+                        requireActivity()
                     )
                 } else {
📝 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
autoRemoveDeletedNotesAfterDays.observe(viewLifecycleOwner) { value ->
binding.AutoEmptyBin.setupAutoEmptyBin(
autoRemoveDeletedNotesAfterDays,
requireContext(),
) { newValue ->
model.savePreference(autoRemoveDeletedNotesAfterDays, newValue)
val workManager = WorkManager.getInstance(requireContext())
if (newValue > 0) {
workManager.scheduleAutoRemoveOldDeletedNotes(
requireContext() as ContextWrapper
)
} else {
workManager.cancelAutoRemoveOldDeletedNotes()
}
}
}
autoRemoveDeletedNotesAfterDays.observe(viewLifecycleOwner) { value ->
binding.AutoEmptyBin.setupAutoEmptyBin(
autoRemoveDeletedNotesAfterDays,
requireContext(),
) { newValue ->
model.savePreference(autoRemoveDeletedNotesAfterDays, newValue)
val workManager = WorkManager.getInstance(requireContext())
if (newValue > 0) {
workManager.scheduleAutoRemoveOldDeletedNotes(
requireActivity()
)
} else {
workManager.cancelAutoRemoveOldDeletedNotes()
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt`
around lines 343 - 358, The code in SettingsFragment inside the AutoEmptyBin
callback unsafely casts requireContext() to ContextWrapper when calling
WorkManager.scheduleAutoRemoveOldDeletedNotes, which can throw
ClassCastException; change the call to pass requireActivity() (or use a safe
cast like (requireContext() as? ContextWrapper) ?: return) so
scheduleAutoRemoveOldDeletedNotes receives a guaranteed ContextWrapper, and keep
cancelAutoRemoveOldDeletedNotes as-is; update the invocation in the
binding.AutoEmptyBin.setupAutoEmptyBin lambda where model.savePreference and
WorkManager calls occur.

Comment on lines +32 to +35
Log.d(
TAG,
"Removing notes that have been deleted for $days days or more (since: ${Date(before).toText()}",
)
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

Missing closing parenthesis in log message.

Line 34: the interpolated string opens (since: but never closes it.

Fix
-            "Removing notes that have been deleted for $days days or more (since: ${Date(before).toText()}",
+            "Removing notes that have been deleted for $days days or more (since: ${Date(before).toText()})",
🤖 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/AutoRemoveDeletedNotesWorker.kt`
around lines 32 - 35, The Log.d call in AutoRemoveDeletedNotesWorker uses an
interpolated string with an unclosed parenthesis around "(since:
${Date(before).toText()"; fix the Log.d message by adding the missing closing
parenthesis inside the string (e.g. "... (since: ${Date(before).toText()})") so
the parentheses are balanced; ensure the call to Log.d(TAG, ...) remains
syntactically correct around Date(before).toText().

Comment on lines +37 to +58
val database = NotallyDatabase.getFreshDatabase(this, preferences.dataInPublicFolder.value)
val baseNoteDao = database.getBaseNoteDao()

return try {
val ids = baseNoteDao.getDeletedNoteIdsOlderThan(before)
if (ids.isNotEmpty()) {
val imageStrings = baseNoteDao.getImages(ids)
val fileStrings = baseNoteDao.getFiles(ids)
val audioStrings = baseNoteDao.getAudios(ids)

val images = imageStrings.flatMap { json -> Converters.jsonToFiles(json) }
val files = fileStrings.flatMap { json -> Converters.jsonToFiles(json) }
val audios = audioStrings.flatMap { json -> Converters.jsonToAudios(json) }

baseNoteDao.delete(ids)
deleteAttachments(images + files + audios, ids)
}
Result.success()
} catch (e: Exception) {
log(TAG, msg = "Auto remove deleted notes after $days days failed", throwable = e)
Result.failure()
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the getFreshDatabase implementation
rg "fun getFreshDatabase" -A 10

Repository: Crustack/NotallyX

Length of output: 1183


🏁 Script executed:

#!/bin/bash
# Find NotallyDatabase class definition
rg "class NotallyDatabase" -A 20

Repository: Crustack/NotallyX

Length of output: 1975


🏁 Script executed:

#!/bin/bash
# Check for other usages of getFreshDatabase
rg "getFreshDatabase" -B 2 -A 5

Repository: Crustack/NotallyX

Length of output: 13807


🏁 Script executed:

#!/bin/bash
# Check if database is being closed anywhere else
rg "database\.close\(\)" -B 2 -A 2

Repository: Crustack/NotallyX

Length of output: 1468


🏁 Script executed:

#!/bin/bash
# Find createInstance implementation
rg "fun createInstance" -A 20

Repository: Crustack/NotallyX

Length of output: 2221


🏁 Script executed:

#!/bin/bash
# Check the full context of createInstance calls - see if there's a singleton
rg "private var.*database|companion object" -A 5 app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt

Repository: Crustack/NotallyX

Length of output: 219


🏁 Script executed:

#!/bin/bash
# Check the BaseNoteModel usages to see the full pattern
rg "getFreshDatabase" -B 2 -A 15 app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Repository: Crustack/NotallyX

Length of output: 1847


🏁 Script executed:

#!/bin/bash
# Look for all notallyDatabase usages in BaseNoteModel to see the full pattern including closing
rg "notallyDatabase" -B 5 -A 20 app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Repository: Crustack/NotallyX

Length of output: 2764


🏁 Script executed:

#!/bin/bash
# Check if the test also closes the database
cat -n app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt

Repository: Crustack/NotallyX

Length of output: 4792


🏁 Script executed:

#!/bin/bash
# Check if AutoRemoveDeletedNotesWorker's database is ever closed anywhere
rg "AutoRemoveDeletedNotesWorker" -A 100 app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt

Repository: Crustack/NotallyX

Length of output: 2086


Add database.close() in finally block to prevent resource leak.

getFreshDatabase() creates a new Room database instance via Room.databaseBuilder() that must be explicitly closed. Currently, the database opened in doWork() is never closed, causing a connection leak on each daily execution.

Suggested fix
         val database = NotallyDatabase.getFreshDatabase(this, preferences.dataInPublicFolder.value)
         val baseNoteDao = database.getBaseNoteDao()
 
-        return try {
+        return try {
             val ids = baseNoteDao.getDeletedNoteIdsOlderThan(before)
             if (ids.isNotEmpty()) {
                 val imageStrings = baseNoteDao.getImages(ids)
                 val fileStrings = baseNoteDao.getFiles(ids)
                 val audioStrings = baseNoteDao.getAudios(ids)
 
                 val images = imageStrings.flatMap { json -> Converters.jsonToFiles(json) }
                 val files = fileStrings.flatMap { json -> Converters.jsonToFiles(json) }
                 val audios = audioStrings.flatMap { json -> Converters.jsonToAudios(json) }
 
                 baseNoteDao.delete(ids)
                 deleteAttachments(images + files + audios, ids)
             }
             Result.success()
         } catch (e: Exception) {
             log(TAG, msg = "Auto remove deleted notes after $days days failed", throwable = e)
             Result.failure()
+        } finally {
+            database.close()
+        }
📝 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
val database = NotallyDatabase.getFreshDatabase(this, preferences.dataInPublicFolder.value)
val baseNoteDao = database.getBaseNoteDao()
return try {
val ids = baseNoteDao.getDeletedNoteIdsOlderThan(before)
if (ids.isNotEmpty()) {
val imageStrings = baseNoteDao.getImages(ids)
val fileStrings = baseNoteDao.getFiles(ids)
val audioStrings = baseNoteDao.getAudios(ids)
val images = imageStrings.flatMap { json -> Converters.jsonToFiles(json) }
val files = fileStrings.flatMap { json -> Converters.jsonToFiles(json) }
val audios = audioStrings.flatMap { json -> Converters.jsonToAudios(json) }
baseNoteDao.delete(ids)
deleteAttachments(images + files + audios, ids)
}
Result.success()
} catch (e: Exception) {
log(TAG, msg = "Auto remove deleted notes after $days days failed", throwable = e)
Result.failure()
}
val database = NotallyDatabase.getFreshDatabase(this, preferences.dataInPublicFolder.value)
val baseNoteDao = database.getBaseNoteDao()
return try {
val ids = baseNoteDao.getDeletedNoteIdsOlderThan(before)
if (ids.isNotEmpty()) {
val imageStrings = baseNoteDao.getImages(ids)
val fileStrings = baseNoteDao.getFiles(ids)
val audioStrings = baseNoteDao.getAudios(ids)
val images = imageStrings.flatMap { json -> Converters.jsonToFiles(json) }
val files = fileStrings.flatMap { json -> Converters.jsonToFiles(json) }
val audios = audioStrings.flatMap { json -> Converters.jsonToAudios(json) }
baseNoteDao.delete(ids)
deleteAttachments(images + files + audios, ids)
}
Result.success()
} catch (e: Exception) {
log(TAG, msg = "Auto remove deleted notes after $days days failed", throwable = e)
Result.failure()
} finally {
database.close()
}
🤖 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/AutoRemoveDeletedNotesWorker.kt`
around lines 37 - 58, The database opened by NotallyDatabase.getFreshDatabase in
AutoRemoveDeletedNotesWorker (inside doWork) is never closed; wrap the try/catch
in a try/finally (or use Kotlin's use/auto-close pattern) and call
database.close() in the finally block to ensure the Room instance is always
closed after using baseNoteDao.getDeletedNoteIdsOlderThan,
getImages/getFiles/getAudios, delete, and deleteAttachments; reference the
NotallyDatabase.getFreshDatabase call and the local variable database to locate
where to insert the close.

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: 4

🧹 Nitpick comments (3)
app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt (1)

263-265: Prefer the idiomatic Kotlin PeriodicWorkRequestBuilder<T> over PeriodicWorkRequest.Builder(Class, ...).

The PeriodicWorkRequestBuilder inline helper was moved from work-runtime-ktx into work-runtime, making it the standard Kotlin API. The class-based Builder is the Java-style approach.

♻️ Proposed refactor
-    val request =
-        PeriodicWorkRequest.Builder(AutoRemoveDeletedNotesWorker::class.java, 1, TimeUnit.DAYS)
-            .build()
+    val request =
+        PeriodicWorkRequestBuilder<AutoRemoveDeletedNotesWorker>(1, TimeUnit.DAYS)
+            .build()
🤖 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
263 - 265, Replace the Java-style PeriodicWorkRequest.Builder usage with the
Kotlin idiomatic PeriodicWorkRequestBuilder for AutoRemoveDeletedNotesWorker:
locate the PeriodicWorkRequest.Builder(...) construction that creates "request"
for AutoRemoveDeletedNotesWorker and swap it to use
PeriodicWorkRequestBuilder<AutoRemoveDeletedNotesWorker>(1, TimeUnit.DAYS)
(ensure the import for PeriodicWorkRequestBuilder is present) so the code uses
the Kotlin inline helper instead of the class-based Builder.
app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt (1)

469-471: Use it inside let body instead of re-referencing the nullable parameter.

Using it is the idiomatic Kotlin pattern here and makes the smart-cast explicit.

♻️ Proposed refactor
-            tooltipResId?.let { tooltipText = context.getString(tooltipResId) }
+            tooltipResId?.let { tooltipText = context.getString(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/activity/main/fragment/settings/PreferenceBindingExtensions.kt`
around lines 469 - 471, The nullable `tooltipResId` is being re-referenced
inside its let block instead of using the lambda parameter; change the body of
the let to use the implicit `it` (e.g., replace usage of `tooltipResId` inside
the block with `it`) so the line becomes `tooltipResId?.let { tooltipText =
context.getString(it) }`, updating the code around `tooltipResId`,
`tooltipText`, and `context.getString` in PreferenceBindingExtensions.kt.
app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt (1)

110-110: deleteAll() deletes the entire BaseNote table and is only needed by tests — annotate or restrict it.

This query has no WHERE clause and is used only in test teardown. Exposing it on the production BaseNoteDao interface without any guard increases the risk of accidental use.

♻️ Proposed: add `@VisibleForTesting`
+    `@VisibleForTesting`
     `@Query`("DELETE FROM BaseNote") suspend fun deleteAll()
🤖 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` at line 110,
The deleteAll() method on BaseNoteDao performs an unguarded DELETE of the entire
table and is test-only; annotate the method to prevent accidental production use
by adding testing-only annotations (e.g., `@VisibleForTesting` and/or
`@RestrictTo`(RestrictTo.Scope.TESTS)) to the deleteAll() function in BaseNoteDao,
or alternatively move the method into a test-only DAO interface used only in
tests; ensure the annotation(s) are applied to the suspend fun deleteAll()
declaration so it’s clearly marked/limited to tests.
🤖 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/NotallyDatabase.kt`:
- Around line 117-128: testInstance is not thread-safe and remains pointing at a
closed DB; mark testInstance as `@Volatile` and ensure it is cleared when the test
DB is closed so subsequent tests get a fresh instance. Specifically, add
`@Volatile` to the testInstance declaration and update the teardown/close path
used by getTestDatabase/getFreshDatabase (or override the DB close listener) to
set testInstance = null immediately after calling close() so double-checked
locking remains valid and closed DB references are not reused.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt`:
- Around line 542-546: The label formatter in setupAutoEmptyBin uses a hardcoded
plural string and yields "1 days"; add a plurals resource named "days" (with
singular and other forms) and change the setLabelFormatter lambda to use
context.resources.getQuantityString(R.plurals.days, sliderValue.toInt(),
sliderValue.toInt()) for non-zero values so the label shows "1 day" vs "N days";
keep the zero branch returning context.getString(R.string.disabled).

In
`@app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt`:
- Around line 23-59: removeOldDeletedNotes is declared as a member extension
inside AutoRemoveDeletedNotesWorker which makes it inaccessible to external
callers/tests; move it out of the class body and declare it as a top-level
extension function on ContextWrapper (same signature: suspend fun
ContextWrapper.removeOldDeletedNotes(): Result) in the utilities file the tests
reference (or the same file but outside the class) so it can be imported as
com.philkes.notallyx.utils.backup.removeOldDeletedNotes; ensure all references
(including tests) import the top-level symbol and remove any dependency on
AutoRemoveDeletedNotesWorker instance state.

In
`@app/src/test/kotlin/com/philkes/notallyx/utils/AutoRemoveDeletedNotesDbTest.kt`:
- Line 18: Remove the invalid import
com.philkes.notallyx.utils.backup.removeOldDeletedNotes from the test; that
function is a member extension defined inside AutoRemoveDeletedNotesWorker in
package com.philkes.notallyx.utils and cannot be imported from the backup
package. Delete the import line and either call the extension via an
AutoRemoveDeletedNotesWorker instance or adjust the test to use the publicly
accessible API in AutoRemoveDeletedNotesWorker / com.philkes.notallyx.utils
instead.

---

Duplicate comments:
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt`:
- Around line 167-168: Pre-existing notes in DELETED can be erroneously purged
because their modifiedTimestamp reflects last edit time; add a one-time
migration or feature-activation step that updates modifiedTimestamp = current
system time for all notes where folder = 'DELETED' when the auto-remove feature
is first enabled; implement this by calling a DAO update (or new DAO method) to
set modifiedTimestamp = now for BaseNote rows with folder = 'DELETED' before any
calls to getDeletedNoteIdsOlderThan(...), and ensure the move(ids, folder,
timestamp) logic remains unchanged for future deletions so newly moved notes
keep correct deletion timestamps.

In
`@app/src/main/java/com/philkes/notallyx/utils/AutoRemoveDeletedNotesWorker.kt`:
- Around line 32-35: The log message in AutoRemoveDeletedNotesWorker (Log.d
called with TAG) has an unclosed parenthesis in the interpolated string for
"(since: ${Date(before).toText()"; fix the string in the Log.d call by adding
the missing closing parenthesis so it becomes "(since:
${Date(before).toText()})" (ensure the rest of the string punctuation remains
correct).
- Around line 37-58: The Room database opened via
NotallyDatabase.getFreshDatabase(...) inside the worker
(AutoRemoveDeletedNotesWorker.doWork / the shown block) is never closed; wrap
the existing try/catch in a try/catch/finally (or add a finally) and call
database.close() in the finally to ensure the DB is closed on success or
exception, keeping the rest of the logic (ids retrieval, delete,
deleteAttachments) unchanged and referencing the same database variable.

---

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt`:
- Line 110: The deleteAll() method on BaseNoteDao performs an unguarded DELETE
of the entire table and is test-only; annotate the method to prevent accidental
production use by adding testing-only annotations (e.g., `@VisibleForTesting`
and/or `@RestrictTo`(RestrictTo.Scope.TESTS)) to the deleteAll() function in
BaseNoteDao, or alternatively move the method into a test-only DAO interface
used only in tests; ensure the annotation(s) are applied to the suspend fun
deleteAll() declaration so it’s clearly marked/limited to tests.

In `@app/src/main/java/com/philkes/notallyx/NotallyXApplication.kt`:
- Around line 263-265: Replace the Java-style PeriodicWorkRequest.Builder usage
with the Kotlin idiomatic PeriodicWorkRequestBuilder for
AutoRemoveDeletedNotesWorker: locate the PeriodicWorkRequest.Builder(...)
construction that creates "request" for AutoRemoveDeletedNotesWorker and swap it
to use PeriodicWorkRequestBuilder<AutoRemoveDeletedNotesWorker>(1,
TimeUnit.DAYS) (ensure the import for PeriodicWorkRequestBuilder is present) so
the code uses the Kotlin inline helper instead of the class-based Builder.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt`:
- Around line 469-471: The nullable `tooltipResId` is being re-referenced inside
its let block instead of using the lambda parameter; change the body of the let
to use the implicit `it` (e.g., replace usage of `tooltipResId` inside the block
with `it`) so the line becomes `tooltipResId?.let { tooltipText =
context.getString(it) }`, updating the code around `tooltipResId`,
`tooltipText`, and `context.getString` in PreferenceBindingExtensions.kt.

Comment on lines +117 to +128
private var testInstance: NotallyDatabase? = null

private fun getTestDatabase(context: ContextWrapper): NotallyDatabase {
return testInstance
?: synchronized(this) {
testInstance =
Room.inMemoryDatabaseBuilder(context, NotallyDatabase::class.java)
.allowMainThreadQueries()
.build()
return testInstance!!
}
}
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

testInstance lacks @Volatile and is never reset after close(), risking stale/closed DB for subsequent test classes.

Two issues:

  1. Unlike the production instance field, testInstance is not @Volatile, breaking the double-checked locking invariant.
  2. After a test class's @AfterClass closes the database, testInstance still holds the closed reference. Any subsequent test class calling getFreshDatabase(isTestRunner=true) will receive the already-closed NotallyDatabase.
🛡️ Proposed fix
-        private var testInstance: NotallyDatabase? = null
+        `@Volatile` private var testInstance: NotallyDatabase? = null

         private fun getTestDatabase(context: ContextWrapper): NotallyDatabase {
             return testInstance
                 ?: synchronized(this) {
+                    // Re-check in case a previously closed instance was cleared
+                    if (testInstance?.isOpen == false) testInstance = null
                     testInstance =
                         Room.inMemoryDatabaseBuilder(context, NotallyDatabase::class.java)
                             .allowMainThreadQueries()
                             .build()
                     return testInstance!!
                 }
         }
🤖 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/NotallyDatabase.kt` around lines
117 - 128, testInstance is not thread-safe and remains pointing at a closed DB;
mark testInstance as `@Volatile` and ensure it is cleared when the test DB is
closed so subsequent tests get a fresh instance. Specifically, add `@Volatile` to
the testInstance declaration and update the teardown/close path used by
getTestDatabase/getFreshDatabase (or override the DB close listener) to set
testInstance = null immediately after calling close() so double-checked locking
remains valid and closed DB references are not reused.

Comment on lines +542 to +546
setLabelFormatter { sliderValue ->
if (sliderValue == 0f) {
context.getString(R.string.disabled)
} else "${sliderValue.toInt()} ${context.getString(R.string.days)}"
}
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

setupAutoEmptyBin label produces "1 days" — use plural string resource.

"${sliderValue.toInt()} ${context.getString(R.string.days)}" always appends the plural form, producing "1 days" when the slider is at 1. Use getQuantityString with a plurals resource to correctly render "1 day" vs "N days".

🌐 Proposed fix

In res/values/strings.xml, replace/add a plural:

<plurals name="days">
    <item quantity="one">%d day</item>
    <item quantity="other">%d days</item>
</plurals>

Then in setupAutoEmptyBin:

-        } else "${sliderValue.toInt()} ${context.getString(R.string.days)}"
+        } else context.resources.getQuantityString(R.plurals.days, sliderValue.toInt(), sliderValue.toInt())
📝 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
setLabelFormatter { sliderValue ->
if (sliderValue == 0f) {
context.getString(R.string.disabled)
} else "${sliderValue.toInt()} ${context.getString(R.string.days)}"
}
setLabelFormatter { sliderValue ->
if (sliderValue == 0f) {
context.getString(R.string.disabled)
} else context.resources.getQuantityString(R.plurals.days, sliderValue.toInt(), sliderValue.toInt())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/PreferenceBindingExtensions.kt`
around lines 542 - 546, The label formatter in setupAutoEmptyBin uses a
hardcoded plural string and yields "1 days"; add a plurals resource named "days"
(with singular and other forms) and change the setLabelFormatter lambda to use
context.resources.getQuantityString(R.plurals.days, sliderValue.toInt(),
sliderValue.toInt()) for non-zero values so the label shows "1 day" vs "N days";
keep the zero branch returning context.getString(R.string.disabled).

@Crustack Crustack force-pushed the feat/393 branch 2 times, most recently from 99f7af8 to 98b8fc0 Compare February 20, 2026 16:39
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.

Auto delete deleted notes

1 participant