Skip to content

Fix/data migrations#840

Merged
Crustack merged 4 commits into
mainfrom
fix/data-migrations
Feb 18, 2026
Merged

Fix/data migrations#840
Crustack merged 4 commits into
mainfrom
fix/data-migrations

Conversation

@Crustack
Copy link
Copy Markdown
Owner

@Crustack Crustack commented Feb 18, 2026

Fixes #837

Summary by CodeRabbit

  • Bug Fixes

    • Better recovery for notes that exceed database limits: attempts repair then removes irreparable notes to prevent crashes.
    • Prevents repeated handling of the same crash within short windows.
  • Improvements

    • View-model observation starts earlier during startup for more reliable initialization.
    • Import truncates overly long note content to protect stability.
    • Added simple time utilities and migration progress reporting during recovery.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a guarded global uncaught-exception handler in LockedActivity to detect SQLiteBlobTooBigException, launch an asynchronous split/truncate migration (splitOversizedNotes) with progress reporting and mutex/timestamp deduplication; moves ViewModel observation into startObserving(); makes migration repair/delete resilient; truncates bodies during import; adds TimeMillis helper and secondsBetween.

Changes

Cohort / File(s) Summary
Activity lifecycle & exception handler
app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt
Added initViewModel() hook; setupGlobalExceptionHandler() installs a global uncaught-exception handler that detects SQLiteBlobTooBigException, guards handling with a Mutex+timestamp, exposes migration progress LiveData, and launches splitOversizedNotes() on IO.
Main entrypoint init hook
app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt
Added empty override fun initViewModel() and ensured baseModel.startObserving() is called earlier in the migration/startup path.
ViewModel observation refactor
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt
Replaced eager init with startObserving(); initialized labels, reminders, allNotes with non-null defaults (NotNullLiveData) and pre-initialized Content wrappers; adjusted observer removal and startup flow.
Migration: oversized-note repair
app/src/main/java/com/philkes/notallyx/utils/DataSchemaMigrations.kt
Made splitOversizedNotes public; added guarded retry around truncateBodyAndFixSpans and delete-on-persistent-failure to avoid propagating SQLiteBlobTooBigException.
Import truncation
app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt
Query now projects SUBSTR(body, 1, MAX_BODY_CHAR_LENGTH) AS body (uses MAX_BODY_CHAR_LENGTH) to truncate large bodies during import and avoid cursor window errors.
Small utilities
app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt
Added typealias TimeMillis = Long and fun TimeMillis.secondsBetween(other: TimeMillis): Long (absolute seconds difference).

Sequence Diagram

sequenceDiagram
    participant LockedActivity as LockedActivity (onCreate)
    participant BaseModel as BaseNoteModel
    participant DB as Room/Database
    participant ExceptionHandler as GlobalExceptionHandler
    participant Migration as splitOversizedNotes

    LockedActivity->>LockedActivity: initViewModel()
    LockedActivity->>BaseModel: startObserving()
    BaseModel->>DB: attach LiveData observers / run queries
    DB-->>BaseModel: query throws SQLiteBlobTooBigException
    DB->>ExceptionHandler: uncaught exception propagates
    ExceptionHandler->>ExceptionHandler: check Mutex + timestamp
    ExceptionHandler->>LockedActivity: create MigrationProgress LiveData
    ExceptionHandler->>Migration: launch splitOversizedNotes() on IO
    Migration->>DB: read, truncate/repair notes (or delete if unrecoverable)
    Migration-->>LockedActivity: post progress updates
    Migration->>DB: observers re-run and succeed
    ExceptionHandler->>ExceptionHandler: release Mutex / update last timestamp
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 I found a blob too big to bite,
I split and trimmed it through the night.
With mutex, timestamp, and a hop,
I fixed the crash and made it stop.
Now your notes hop safe — all tucked in tight. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/data migrations' is vague and generic, using a broad category term without clearly conveying the specific problem being addressed. Consider a more specific title like 'Handle SQLiteBlobTooBigException and delay DB observation' to better communicate the primary fixes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All code changes directly address the linked issue #837: global exception handler catches SQLiteBlobTooBigException, splitOversizedNotes repairs/deletes oversized notes, and DB observation is delayed post-migration.
Out of Scope Changes check ✅ Passed All changes are in scope: exception handling for oversized blobs, database observation timing adjustments, and supporting utilities align with resolving the crash issue.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/data-migrations

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

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/presentation/viewmodel/BaseNoteModel.kt (1)

152-157: ⚠️ Potential issue | 🟠 Major

searchResults!! in folder.observeForever is an NPE waiting to happen.

folder is a NotNullLiveData with an existing value, so observeForever on Line 154 dispatches immediately and synchronously. At that point, searchResults is still null unless NotallyDatabase.getDatabase(app).observeForever(::init) on Line 153 also dispatched synchronously (which it does only if the database LiveData already has a value). If the database is still initializing (possible in edge cases even after migrations complete), init hasn't run, searchResults is null, and searchResults!!.fetch(...) crashes.

Swap !! for a safe call:

🛡️ Proposed fix
     folder.observeForever { newFolder ->
-        searchResults!!.fetch(keyword, newFolder, currentLabel)
+        searchResults?.fetch(keyword, newFolder, currentLabel)
     }
🤖 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 152 - 157, In startObserving(), change the unsafe non-null
assertion on searchResults inside folder.observeForever to a safe null check:
ensure that when folder.observeForever triggers (in
BaseNoteModel::startObserving), you only call searchResults.fetch(keyword,
newFolder, currentLabel) if searchResults is non-null (e.g., guard with an if
(searchResults != null) or use safe-call), so that asynchronous initialization
via NotallyDatabase.getDatabase(app).observeForever(::init) and init() can
complete without causing an NPE on searchResults.
🧹 Nitpick comments (3)
app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt (1)

115-117: LGTM — optional: consider @JvmInline value class for stronger type safety.

The arithmetic is correct (abs(Long - Long) / 1000). The typealias approach is idiomatic for lightweight semantic documentation, but it provides no compile-time protection: any Long (e.g., a byte count, duration, or plain integer) can be passed to secondsBetween without a warning.

If misuse across the codebase is a concern, a @JvmInline value class would enforce the distinction at the type level with zero runtime overhead:

♻️ Optional: value class alternative
-typealias TimeMillis = Long
+@JvmInline
+value class TimeMillis(val value: Long)

Note: adopting this would require wrapping/unwrapping at every call site that bridges with system APIs returning plain Long (e.g., System.currentTimeMillis()), so the tradeoff may not be worth it unless type safety is a priority here.

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

In `@app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt` around lines
115 - 117, Replace the typealias TimeMillis = Long with a `@JvmInline` value class
to get compile-time type safety: define `@JvmInline` value class TimeMillis(val
ms: Long) and update the extension/function secondsBetween to use the new
wrapper (e.g., fun TimeMillis.secondsBetween(other: TimeMillis): Long =
abs(this.ms - other.ms) / 1000), and then call sites that use raw Longs (e.g.,
System.currentTimeMillis()) should be wrapped/unwrapped explicitly
(TimeMillis(System.currentTimeMillis())).
app/src/main/java/com/philkes/notallyx/utils/DataSchemaMigrations.kt (1)

90-97: Delete-on-failure is a reasonable last resort, but consider broader exception handling.

The nested try-catch properly handles a persistent SQLiteBlobTooBigException by deleting the unrecoverable note. However, truncateBodyAndFixSpans (line 91) could also throw other exceptions (e.g., generic SQLException from dao.truncateBody or dao.insert). An unexpected exception here would abort the entire migration loop, leaving remaining notes unprocessed.

Consider catching a broader exception type (e.g., Exception) on the outer try (lines 90-97) or at minimum SQLiteException, so one corrupted row doesn't halt processing of the rest.

Also a minor nit: the inner e (line 93) shadows the outer e (line 82). Renaming one (e.g., e2) would improve clarity.

Proposed diff
-                try {
-                    truncateBodyAndFixSpans(dao, id)
-                    dao.get(id)
-                } catch (e: SQLiteBlobTooBigException) {
-                    log(TAG, "Note (id: $id) could not be repaired. Deleting...", e)
+                try {
+                    truncateBodyAndFixSpans(dao, id)
+                    dao.get(id)
+                } catch (e2: SQLiteBlobTooBigException) {
+                    log(TAG, "Note (id: $id) could not be repaired. Deleting...", e2)
                     dao.delete(id)
                     null
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/philkes/notallyx/utils/DataSchemaMigrations.kt` around
lines 90 - 97, The current try-catch around truncateBodyAndFixSpans(dao, id)
only catches SQLiteBlobTooBigException and uses a nested exception variable that
shadows the outer one; update the error handling so the outer try (surrounding
truncateBodyAndFixSpans and dao.get) also catches a broader exception type
(e.g., Exception or SQLiteException) to prevent one bad row from aborting the
migration loop, and rename the inner catch variable (e.g., change the inner
catch from "e" to "e2") to avoid shadowing the outer "e" defined earlier; locate
and modify the block around truncateBodyAndFixSpans, dao.get, and dao.delete to
implement these changes and log the broader exception appropriately before
continuing.
app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt (1)

296-312: SQLiteBlobTooBigException fallback is unreachable dead code; also, redundant fully-qualified reference at line 304.

The body column in baseNoteCursor is explicitly capped via SUBSTR(body, 1, ${MAX_BODY_CHAR_LENGTH}) in the projection (line 136), where MAX_BODY_CHAR_LENGTH = 393,216 characters (~1.57 MB worst-case UTF-8). This pre-truncation in the cursor means getString(getColumnIndexOrThrow("body")) at line 297 will not throw SQLiteBlobTooBigException—the catch block at lines 296–312 is unreachable dead code.

Line 304 uses the fully-qualified name com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH, which duplicates the import at line 18.

♻️ Proposed cleanup

Remove the dead catch block:

-    val body =
-        try {
-            getString(getColumnIndexOrThrow("body"))
-        } catch (_: SQLiteBlobTooBigException) {
-            // Fall back to truncated read from source DB to avoid cursor window overflow
-            val cursor =
-                sourceDb.rawQuery(
-                    "SELECT substr(body, 1, ?) AS body FROM BaseNote WHERE id = ?",
-                    arrayOf(
-                        com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
-                            .toString(),
-                        id.toString(),
-                    ),
-                )
-            val value = if (cursor.moveToFirst()) cursor.getString(0) else ""
-            cursor.close()
-            value
-        }
+    val body = getString(getColumnIndexOrThrow("body"))

Also remove the unused sourceDb parameter from the toBaseNote function signature.

Alternatively, if the catch block is retained as defensive code, at minimum fix the redundant FQ reference:

-                        com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH
+                        MAX_BODY_CHAR_LENGTH
🤖 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 296 - 312, The catch for SQLiteBlobTooBigException in toBaseNote is
dead because baseNoteCursor already projects SUBSTR(body, 1,
MAX_BODY_CHAR_LENGTH), so remove the try/catch fallback and its rawQuery usage
(including the redundant sourceDb dependency); also replace the fully-qualified
reference
com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH with
the existing imported MAX_BODY_CHAR_LENGTH or the symbol
BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH if needed, and then remove the
now-unused sourceDb parameter from the toBaseNote signature and all call sites.
🤖 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/LockedActivity.kt`:
- Around line 76-84: The process-wide uncaught-exception handler installed via
Thread.setDefaultUncaughtExceptionHandler in LockedActivity captures this and
then launches on lifecycleScope (which may be cancelled if the activity is
destroyed), so move the recovery work to an application-scoped coroutine context
(e.g., a GlobalScope or an Application-level CoroutineScope) instead of
lifecycleScope, keep using EXCEPTION_HANDLER_MUTEX and
isExceptionAlreadyBeingHandled as before, and before performing any UI work
(showing the progress dialog or interacting with this@LockedActivity) check the
activity is still valid/alive (e.g., isFinishing/isDestroyed or a safe flag) so
UI is only touched when safe.
- Around line 80-84: Reorder the conditional so the exception type is checked
first and only then apply the dedup guard: first test if throwable is a
SQLiteBlobTooBigException (or has a cause of that type) using the existing
throwable and SQLiteBlobTooBigException checks, and if it matches then consult
isExceptionAlreadyBeingHandled(time) to decide whether to handle it locally or
delegate to the previous uncaught-exception handler; update the branch that
currently contains isExceptionAlreadyBeingHandled(time) so that legitimate
SQLiteBlobTooBigException occurrences are not skipped when dedup returns false
and are properly forwarded to the previous handler when appropriate.
- Around line 75-109: Capture the existing default handler before replacing it
in setupGlobalExceptionHandler (val previousHandler =
Thread.getDefaultUncaughtExceptionHandler()), and then in the new
Thread.setDefaultUncaughtExceptionHandler implementation, only take the
coroutine-based migration path for handled SQLiteBlobTooBigException cases and
synchronously delegate any other throwables to previousHandler. Specifically, in
setupGlobalExceptionHandler keep the current coroutine logic for when (throwable
is SQLiteBlobTooBigException || throwable.cause is SQLiteBlobTooBigException)
but for the else branch call previousHandler?.uncaughtException(thread,
throwable) (or previousHandler?.uncaughtException(thread, throwable) after
returning from coroutine if you must run async work), ensuring you reference
setupGlobalExceptionHandler, Thread.setDefaultUncaughtExceptionHandler, and
previousHandler so non-handled exceptions are not swallowed.
- Around line 111-112: Rename the function isExceptionAlreadyBeingHandled to
reflect its actual semantics (returns true when the exception is NOT currently
being handled), e.g., shouldHandleException(time: Long) or
isExceptionNotBeingHandled(time: Long); update the implementation that reads
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) > 20 } ?:
true to the new name without changing logic, then update all call sites
(including the usage that currently checks the condition around
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP) to invoke the new function name so the
condition reads clearly (e.g., if (shouldHandleException(time)) { ... }).

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 900-903: The catch block in BaseNoteModel.kt currently uses the
Kotlin 2.0 discard syntax `catch (_: Exception)` which won't compile under
Kotlin 1.9.0; change the catch to use a named parameter (for example `catch (e:
Exception)`) and keep the existing body that sets
showRefreshBackupsFolderAfterThemeChange = false and calls disableBackups(), or
alternatively bump the project Kotlin version to 2.0+ if you prefer to use
discard syntax.
- Around line 117-119: deletedNotes and archivedNotes are pre-initialized with
an identity transform so their transform never becomes ::transform during init,
causing section headers to be missing; update their initialization to use the
same transform as baseNotes. Locate the properties deletedNotes and
archivedNotes (currently Content(MutableLiveData(), { t -> t })) and change
their initializer to use ::transform like baseNotes does so that when init calls
setObserver(baseNoteDao.getFrom(...)) the Content keeps the correct transform;
ensure this mirrors the behavior of labelsHiddenObserver/initBaseNotes which
sets baseNotes = null before init.

---

Outside diff comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 152-157: In startObserving(), change the unsafe non-null assertion
on searchResults inside folder.observeForever to a safe null check: ensure that
when folder.observeForever triggers (in BaseNoteModel::startObserving), you only
call searchResults.fetch(keyword, newFolder, currentLabel) if searchResults is
non-null (e.g., guard with an if (searchResults != null) or use safe-call), so
that asynchronous initialization via
NotallyDatabase.getDatabase(app).observeForever(::init) and init() can complete
without causing an NPE on searchResults.

---

Nitpick comments:
In `@app/src/main/java/com/philkes/notallyx/utils/backup/ImportExtensions.kt`:
- Around line 296-312: The catch for SQLiteBlobTooBigException in toBaseNote is
dead because baseNoteCursor already projects SUBSTR(body, 1,
MAX_BODY_CHAR_LENGTH), so remove the try/catch fallback and its rawQuery usage
(including the redundant sourceDb dependency); also replace the fully-qualified
reference
com.philkes.notallyx.data.dao.BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH with
the existing imported MAX_BODY_CHAR_LENGTH or the symbol
BaseNoteDao.Companion.MAX_BODY_CHAR_LENGTH if needed, and then remove the
now-unused sourceDb parameter from the toBaseNote signature and all call sites.

In `@app/src/main/java/com/philkes/notallyx/utils/DataSchemaMigrations.kt`:
- Around line 90-97: The current try-catch around truncateBodyAndFixSpans(dao,
id) only catches SQLiteBlobTooBigException and uses a nested exception variable
that shadows the outer one; update the error handling so the outer try
(surrounding truncateBodyAndFixSpans and dao.get) also catches a broader
exception type (e.g., Exception or SQLiteException) to prevent one bad row from
aborting the migration loop, and rename the inner catch variable (e.g., change
the inner catch from "e" to "e2") to avoid shadowing the outer "e" defined
earlier; locate and modify the block around truncateBodyAndFixSpans, dao.get,
and dao.delete to implement these changes and log the broader exception
appropriately before continuing.

In `@app/src/main/java/com/philkes/notallyx/utils/MiscExtensions.kt`:
- Around line 115-117: Replace the typealias TimeMillis = Long with a `@JvmInline`
value class to get compile-time type safety: define `@JvmInline` value class
TimeMillis(val ms: Long) and update the extension/function secondsBetween to use
the new wrapper (e.g., fun TimeMillis.secondsBetween(other: TimeMillis): Long =
abs(this.ms - other.ms) / 1000), and then call sites that use raw Longs (e.g.,
System.currentTimeMillis()) should be wrapped/unwrapped explicitly
(TimeMillis(System.currentTimeMillis())).

Comment thread app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt Outdated
Comment thread app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt Outdated
Comment on lines +111 to +112
private fun isExceptionAlreadyBeingHandled(time: Long): Boolean =
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) > 20 } ?: true
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.

🛠️ Refactor suggestion | 🟠 Major

Method name has inverted semantics — rename for clarity.

isExceptionAlreadyBeingHandled returns true on the first call (null timestamp) and when >20s have elapsed — i.e., when the exception is not currently being handled. The name suggests the opposite. This makes the condition on line 81 read as "if exception IS already being handled, handle it again," which is confusing.

Proposed rename
-    private fun isExceptionAlreadyBeingHandled(time: Long): Boolean =
-        EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) > 20 } ?: true
+    private fun canHandleException(time: Long): Boolean =
+        EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) > 20 } ?: true

And update the call site accordingly.

🤖 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/LockedActivity.kt`
around lines 111 - 112, Rename the function isExceptionAlreadyBeingHandled to
reflect its actual semantics (returns true when the exception is NOT currently
being handled), e.g., shouldHandleException(time: Long) or
isExceptionNotBeingHandled(time: Long); update the implementation that reads
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP?.let { it.secondsBetween(time) > 20 } ?:
true to the new name without changing logic, then update all call sites
(including the usage that currently checks the condition around
EXCEPTION_HANDLER_MUTEX_LAST_TIMESTAMP) to invoke the new function name so the
condition reads clearly (e.g., if (shouldHandleException(time)) { ... }).

Comment thread app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt Outdated
Comment on lines +900 to 903
} catch (_: Exception) {
showRefreshBackupsFolderAfterThemeChange = false
disableBackups()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Kotlin version declarations in gradle files and version catalogs
echo "=== Searching for Kotlin version in gradle files ==="
fd -e toml -e gradle -e "gradle.kts" | xargs grep -i "kotlin" 2>/dev/null | grep -iE "version|kotlin-version|kotlinVersion|kotlin-gradle-plugin" | head -40

Repository: Crustack/NotallyX

Length of output: 288


🏁 Script executed:

#!/bin/bash
# More comprehensive search for all Kotlin version configurations
echo "=== All Kotlin version-related declarations ==="
rg -i "kotlin.*version|version.*kotlin" --type gradle --type toml -A 2 -B 2 | head -60

Repository: Crustack/NotallyX

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Broader search for Kotlin version
echo "=== All references to Kotlin 1.9 and 2.0 ==="
rg -i "kotlin.*1\.9|1\.9.*kotlin|kotlin.*2\.0|2\.0.*kotlin" | head -20

echo ""
echo "=== Content of main build files ==="
for file in build.gradle.kts app/build.gradle.kts gradle/libs.versions.toml; do
  if [ -f "$file" ]; then
    echo "File: $file"
    cat "$file" | grep -i "kotlin\|version" | head -15
    echo ""
  fi
done

Repository: Crustack/NotallyX

Length of output: 1295


Use of catch (_: Exception) is incompatible with this project's Kotlin version 1.9.0.

The discard syntax for exception parameters (catch (_: Exception)) was introduced in Kotlin 2.0. This project targets Kotlin 1.9.0, where this syntax will fail to compile. Either update the Kotlin version to 2.0 or later, or use a named parameter instead (e.g., catch (_: Exception)catch (e: Exception)).

🤖 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 900 - 903, The catch block in BaseNoteModel.kt currently uses the
Kotlin 2.0 discard syntax `catch (_: Exception)` which won't compile under
Kotlin 1.9.0; change the catch to use a named parameter (for example `catch (e:
Exception)`) and keep the existing body that sets
showRefreshBackupsFolderAfterThemeChange = false and calls disableBackups(), or
alternatively bump the project Kotlin version to 2.0+ if you prefer to use
discard syntax.

@Crustack Crustack force-pushed the fix/data-migrations branch from 68c6cae to 0fffc9c Compare February 18, 2026 11:54
@Crustack Crustack merged commit 7a0f38d into main Feb 18, 2026
@Crustack Crustack deleted the fix/data-migrations branch February 18, 2026 11:54
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.

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/presentation/viewmodel/BaseNoteModel.kt (1)

152-157: ⚠️ Potential issue | 🟠 Major

Potential NPE: searchResults!! on line 155 if the database LiveData hasn't dispatched yet.

folder already has a value (Folder.NOTES), so observeForever on line 154 dispatches the observer callback immediately. If NotallyDatabase.getDatabase(app) on line 153 has not yet emitted a value (database still initializing), init() hasn't run and searchResults is still null — causing searchResults!!.fetch(...) to throw NullPointerException.

This depends on whether the database LiveData synchronously dispatches its cached value upon observeForever. If it does, init() runs first and searchResults is set. If it doesn't (e.g., first cold start), this is a crash.

#!/bin/bash
# Check how NotallyDatabase.getDatabase returns its LiveData - does it have an initial value?
echo "=== NotallyDatabase.getDatabase implementation ==="
ast-grep --pattern $'fun getDatabase($$$): $_ {
  $$$
}'

echo ""
echo "=== Search for getDatabase companion/factory ==="
rg -n "fun getDatabase" --type kotlin -A 20

echo ""
echo "=== Check if the returned LiveData is pre-populated ==="
rg -n "MutableLiveData|NotNullLiveData" --type kotlin -g '*NotallyDatabase*' -B2 -A2
Suggested defensive fix
     fun startObserving() {
         NotallyDatabase.getDatabase(app).observeForever(::init)
         folder.observeForever { newFolder ->
-            searchResults!!.fetch(keyword, newFolder, currentLabel)
+            searchResults?.fetch(keyword, newFolder, currentLabel)
         }
     }
🤖 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 152 - 157, startObserving can call searchResults!! before init()
runs because NotallyDatabase.getDatabase(app).observeForever(::init) may not
dispatch immediately; change startObserving to guard against a null
searchResults by making the folder observer check searchResults != null (or
early-return) before calling searchResults.fetch, or defer attaching the
folder.observeForever until after init has set searchResults; reference
functions/vars: startObserving, NotallyDatabase.getDatabase(app), init,
folder.observeForever, and searchResults.
🧹 Nitpick comments (1)
app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt (1)

181-191: Dead null branches: deletedNotes and archivedNotes are never null.

Since both are pre-initialized on lines 118–119 and never set to null, the if (deletedNotes == null) and if (archivedNotes == null) conditions are always false. The if branches (lines 182, 188) are unreachable.

Consider removing the dead branches for clarity, or if the null path is meant as a safety net, add a comment explaining the intent.

Simplified version
-        if (deletedNotes == null) {
-            deletedNotes = Content(baseNoteDao.getFrom(Folder.DELETED), ::transform)
-        } else {
-            deletedNotes!!.setObserver(baseNoteDao.getFrom(Folder.DELETED))
-        }
+        deletedNotes!!.setObserver(baseNoteDao.getFrom(Folder.DELETED))

-        if (archivedNotes == null) {
-            archivedNotes = Content(baseNoteDao.getFrom(Folder.ARCHIVED), ::transform)
-        } else {
-            archivedNotes!!.setObserver(baseNoteDao.getFrom(Folder.ARCHIVED))
-        }
+        archivedNotes!!.setObserver(baseNoteDao.getFrom(Folder.ARCHIVED))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`
around lines 181 - 191, The null-check branches for deletedNotes and
archivedNotes are dead because deletedNotes and archivedNotes are
pre-initialized and never set to null; remove the unreachable if (deletedNotes
== null) / if (archivedNotes == null) blocks and simply call
deletedNotes!!.setObserver(baseNoteDao.getFrom(Folder.DELETED)) and
archivedNotes!!.setObserver(baseNoteDao.getFrom(Folder.ARCHIVED)) (or better,
make them non-nullable and drop the !!), or if you intended a safety-net,
replace the dead branches with a concise comment explaining why a null-path
would be expected and keep one clear path that sets the observer on the existing
Content instances (symbols: deletedNotes, archivedNotes, Content,
baseNoteDao.getFrom(Folder.DELETED), baseNoteDao.getFrom(Folder.ARCHIVED)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 152-157: startObserving can call searchResults!! before init()
runs because NotallyDatabase.getDatabase(app).observeForever(::init) may not
dispatch immediately; change startObserving to guard against a null
searchResults by making the folder observer check searchResults != null (or
early-return) before calling searchResults.fetch, or defer attaching the
folder.observeForever until after init has set searchResults; reference
functions/vars: startObserving, NotallyDatabase.getDatabase(app), init,
folder.observeForever, and searchResults.

---

Duplicate comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt`:
- Around line 75-113: The global handler in setupGlobalExceptionHandler
currently captures this@LockedActivity and uses lifecycleScope (so recovery can
be cancelled and leaked across onCreate calls); fix it by installing the handler
only once (guard setupGlobalExceptionHandler with a static/companion boolean)
and move the recovery work out of lifecycleScope into an application-scoped
CoroutineScope (create/use an Application-level CoroutineScope or GlobalScope
with Dispatchers.IO) when launching the coroutine that calls
application.splitOversizedNotes() and uses EXCEPTION_HANDLER_MUTEX; do not hold
strong references to LockedActivity — only keep a WeakReference if you must
reference the activity for UI work; guard any UI calls to setupProgressDialog /
posting MigrationProgress with activity?.isFinishing/isDestroyed checks and
perform them on the main thread (or skip and call
previousHandler?.uncaughtException(thread, throwable)) so that if the activity
is gone the exception is delegated to previousHandler.

In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 900-903: The catch uses Kotlin 2.0 discard syntax "catch (_:
Exception)" which will not compile on Kotlin 1.9.x; update the catch to use an
explicit exception parameter (e.g., "catch (e: Exception)") or otherwise remove
the parameter and handle/log e, in the BaseNoteModel.kt block that sets
showRefreshBackupsFolderAfterThemeChange = false and calls disableBackups(),
and/or bump the project Kotlin version to 2.0+ in your Gradle/libs config;
ensure any references to the exception variable (if added) are either used or
explicitly ignored per your lint rules.

---

Nitpick comments:
In
`@app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt`:
- Around line 181-191: The null-check branches for deletedNotes and
archivedNotes are dead because deletedNotes and archivedNotes are
pre-initialized and never set to null; remove the unreachable if (deletedNotes
== null) / if (archivedNotes == null) blocks and simply call
deletedNotes!!.setObserver(baseNoteDao.getFrom(Folder.DELETED)) and
archivedNotes!!.setObserver(baseNoteDao.getFrom(Folder.ARCHIVED)) (or better,
make them non-nullable and drop the !!), or if you intended a safety-net,
replace the dead branches with a concise comment explaining why a null-path
would be expected and keep one clear path that sets the observer on the existing
Content instances (symbols: deletedNotes, archivedNotes, Content,
baseNoteDao.getFrom(Folder.DELETED), baseNoteDao.getFrom(Folder.ARCHIVED)).

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.

Bounty offered: Crash and inaccessible files after attempting to open large .txt with NotallyX

1 participant