Skip to content

Conversation

@shubertm
Copy link
Member

@shubertm shubertm commented Feb 27, 2023

Re-implementation of #12

Summary by CodeRabbit

  • New Features

    • Added Versions screen to browse note history, mark latest versions, and support forking/tracking versions from note items.
    • Enhanced audio playback controls with pause, resume, and stop UI updates.
    • Expanded brush color options.
  • UI

    • Updated note list layout with new actions and latest-version indicator.
    • Added history, note, and copy icons.
    • Improved focus behavior in the edit note screen.
  • Documentation

    • Added initial README.
  • Tests

    • Introduced tests for version storage.
  • Chores

    • Improved build token handling via local properties/env.

@shubertm shubertm temporarily deployed to Development February 27, 2023 13:15 — with GitHub Actions Inactive
@shubertm shubertm temporarily deployed to Development February 28, 2023 15:38 — with GitHub Actions Inactive
@kirillt kirillt changed the title Re-implementation of version tracking #11: Re-implementation of version tracking May 27, 2023
@kirillt
Copy link
Member

kirillt commented Sep 7, 2023

@kirillt kirillt temporarily deployed to Development September 13, 2023 15:30 — with GitHub Actions Inactive
@kirillt
Copy link
Member

kirillt commented Sep 13, 2023

I'm getting this crash right now:

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property instance has not been initialized
	at space.taran.arkfilepicker.folders.FoldersRepo$Companion.getInstance(FoldersRepo.kt:249)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerViewModel.<init>(ArkFilePickerViewModel.kt:46)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerViewModelFactory.create(ArkFilePickerViewModel.kt:150)
	at androidx.lifecycle.ViewModelProvider$Factory.create(ViewModelProvider.kt:83)
	at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
	at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
	at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
	at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.getViewModel(ArkFilePickerFragment.kt:74)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.getPages(ArkFilePickerFragment.kt:287)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.initUI(ArkFilePickerFragment.kt:126)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.onViewCreated(ArkFilePickerFragment.kt:89)
	at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128)
	at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1899)
	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1823)
	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760)
	at androidx.fragment.app.FragmentController.execPendingActions(FragmentController.java:495)
	at androidx.fragment.app.FragmentActivity.onResume(FragmentActivity.java:312)
	at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1488)
	at android.app.Activity.performResume(Activity.java:8197)
	at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4814)
	... 13 more

@kirillt kirillt mentioned this pull request Sep 14, 2023
@shubertm
Copy link
Member Author

I'm getting this crash right now:

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property instance has not been initialized
	at space.taran.arkfilepicker.folders.FoldersRepo$Companion.getInstance(FoldersRepo.kt:249)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerViewModel.<init>(ArkFilePickerViewModel.kt:46)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerViewModelFactory.create(ArkFilePickerViewModel.kt:150)
	at androidx.lifecycle.ViewModelProvider$Factory.create(ViewModelProvider.kt:83)
	at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
	at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
	at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
	at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.getViewModel(ArkFilePickerFragment.kt:74)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.getPages(ArkFilePickerFragment.kt:287)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.initUI(ArkFilePickerFragment.kt:126)
	at space.taran.arkfilepicker.presentation.filepicker.ArkFilePickerFragment.onViewCreated(ArkFilePickerFragment.kt:89)
	at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3128)
	at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:552)
	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1899)
	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1823)
	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760)
	at androidx.fragment.app.FragmentController.execPendingActions(FragmentController.java:495)
	at androidx.fragment.app.FragmentActivity.onResume(FragmentActivity.java:312)
	at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1488)
	at android.app.Activity.performResume(Activity.java:8197)
	at android.app.ActivityThread.performResumeActivity(ActivityThread.java:4814)
	... 13 more

This crash happens because filepicker changed, we have to initialise FoldersRepo during app creation.

@shubertm shubertm temporarily deployed to Development September 21, 2023 20:26 — with GitHub Actions Inactive
@shubertm shubertm temporarily deployed to Development September 27, 2023 11:26 — with GitHub Actions Inactive
@shubertm shubertm temporarily deployed to Development September 29, 2023 13:16 — with GitHub Actions Inactive
@kirillt
Copy link
Member

kirillt commented Oct 6, 2023

We should merge #18 first.

@kirillt
Copy link
Member

kirillt commented Nov 18, 2023

@ShubertMunthali we need to resolve conflicts before merging.

# Conflicts:
#	app/build.gradle
#	app/src/main/AndroidManifest.xml
#	app/src/main/java/dev/arkbuilders/arkmemo/App.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/files/FilePicker.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/files/parsers/JsonParser.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/TextNotesListAdapter.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/NoteDeleteDialogFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/SettingsFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/TextNoteVersionsFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/views/PathPreference.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/utils/Config.kt
#	app/src/main/java/space/taran/arkmemo/data/repo/notes/text/TextNotesRepo.kt
#	app/src/main/java/space/taran/arkmemo/data/viewmodels/EditTextNotesViewModel.kt
#	app/src/main/java/space/taran/arkmemo/data/viewmodels/TextNotesViewModel.kt
#	app/src/main/java/space/taran/arkmemo/preferences/MemoPreferences.kt
#	app/src/main/res/values/strings.xml
#	app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt
#	build.gradle
#	gradle/wrapper/gradle-wrapper.properties
#	settings.gradle
@shubertm
Copy link
Member Author

@ShubertMunthali sorry, we need update it again

Yes, I will update it

@kirillt
Copy link
Member

kirillt commented Dec 21, 2023

It works! I've checked both text notes and graphic notes. Versions are displayed correctly and forking functions correctly, too. This is really cool.

We have new reusable storage primitives in arklib-android, let's use them to reduce amount of code. See FileStorage arklib-android.

I think, it's possible to implement VersionStorage similarly to ScoreStorage. Scores storage is id-to-integer mapping, and versions storage is id-to-id. The code will be very similar to the scores storage.

Also, we should find another icon for forking feature, I'll ask our UX team.

@shubertm
Copy link
Member Author

You are right @kirillt
I think it will be efficient to just extract VeraionStorage to arklib-android now
I see that we may need it in Retouch soon

@kirillt
Copy link
Member

kirillt commented Dec 24, 2023

@ShubertMunthali it's OK to keep VersionStorage in this repo for the moment. More important is to derive it from ready-made base classes.

But yes, we'll move it eventually to arklib-android.

@shubertm
Copy link
Member Author

Alright @kirillt, so let's make important storage API from arklib-android public, I think we can make it internal after all storages are moved there

@kirillt
Copy link
Member

kirillt commented Dec 25, 2023

@ShubertMunthali yes, if our ready-made primitives can't be used to implement custom storages — this should be fixed. Because it would be great if 3rd party developers could write their own storages for their apps.

It's possible to compile arklib-android and inject the local version into the app. It would be easier for experimentation.

@coderabbitai
Copy link

coderabbitai bot commented Aug 14, 2025

Walkthrough

Adds note versioning capability: new storage interfaces/implementations, models, ViewModel, Fragment, utilities, tests, and resources. Extends Note to Parcelable and introduces isForked across note types. Updates adapters for media side-effects. Numerous XML/Kotlin formatting changes and minor ID/ID rename, plus CI/settings tweaks.

Changes

Cohort / File(s) Summary
Versioning core (storage, models, repo)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt, .../VersionStorage.kt, .../RootVersionStorage.kt, .../VersionStorageRepo.kt, .../Versions.kt, .../VersionsStorage.kt, .../RootVersionsStorage.kt, .../VersionsStorageRepo.kt, app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt
Introduces version graph model (Version, Versions alias), storage interfaces and file-backed implementations, repo for lazy per-root provisioning, and a consolidated VersionsResult.
Note model changes
app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt, .../TextNote.kt, .../GraphicNote.kt, .../VoiceNote.kt
Makes Note Parcelable and adds isForked property; implements isForked in TextNote, GraphicNote, and VoiceNote constructors.
Versioning UI
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt, .../ui/viewmodels/VersionsViewModel.kt, app/src/main/res/values/strings.xml, app/src/main/res/drawable/ic_history.xml, app/src/main/res/drawable/ic_note.xml
Adds VersionsFragment and VersionsViewModel to display and manage note histories; adds related strings and icons.
Utilities and constants
app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt, .../utils/Constants.kt
Adds Path.arkVersions() extension and NOTE_KEY/NOTE_PASTE_KEY constants.
Tests
app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt
Adds tests for RootVersionStorage add/parents tree/forget logic.
Playback side-effects UI updates
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt, .../ui/fragments/ArkMediaPlayerFragment.kt
Handles Pause/Stop/Resume side-effects for media UI states in list and player fragment.
Brush color options
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/BrushAdapter.kt
Adds multiple color cases for brush tinting.
Layouts adjusted for versions and IDs
app/src/main/res/layout/note.xml, app/src/main/res/layout/recyclerview.xml, app/src/main/res/layout/fragment_edit_notes.xml, app/src/main/res/layout/dialog_common_action.xml
Updates note item layout (LinearLayout, version/fork buttons, latest indicator), renames RecyclerView id to rv_notes and makes it match_parent, tweaks focus behavior, and switches several IDs from @+id to @id.
General formatting-only changes
README.md, app/build.gradle, app/src/main/java/dev/arkbuilders/arkmemo/... (multiple minor files), app/src/main/res/drawable/*, app/src/main/res/layout/* (various), app/src/main/AndroidManifest.xml, .github/workflows/build.yml
Whitespace/indentation/tag self-closing reflows across Kotlin/XML and workflow/manifest; no logic changes except one vector path tweak noted separately.
Drawable additions
app/src/main/res/drawable/ic_content_copy.xml
Adds copy icon resource.
Vector path tweak
app/src/main/res/drawable/ic_download.xml
Alters pathData sequence for download icon, changing rendered arrow shape.
Build/Settings
settings.gradle
Loads local.properties; retrieves GitHub token from local props or env; repository credential wiring updated.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant VF as VersionsFragment
    participant NVm as NotesViewModel
    participant VVm as VersionsViewModel
    participant VRepo as VersionStorageRepo
    participant VStore as RootVersionStorage

    User->>VF: Open Versions screen (with Note)
    VF->>VVm: init()
    VVm->>VRepo: provide()
    VRepo->>VStore: create/init (if absent)
    VRepo-->>VVm: VersionStorage

    VF->>NVm: getNotes()
    NVm-->>VF: List<Note>
    VF->>VVm: updateLatestResourceId(note.id)
    VF->>VVm: getLatestNoteFamilyTree(notes, emit)
    VVm->>VStore: parentsTreeByChild(id)
    VStore-->>VVm: Map<ResourceId, List<ResourceId>>
    VVm-->>VF: Filtered notes (family chain)
    VF-->>User: Render history list
Loading
sequenceDiagram
    autonumber
    actor User
    participant VF as VersionsFragment
    participant VVm as VersionsViewModel
    participant VStore as RootVersionStorage

    User->>VF: Create new version (fork/save)
    VF->>VVm: createVersion(oldId, newId)
    VVm->>VStore: add(Version(parent=oldId, child=newId))
    VStore->>VStore: persist() to file
    VStore-->>VVm: OK
    VVm-->>VF: Completion
    VF-->>User: UI updates (latest markers)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • kirillt
  • hieuwu

Poem

A nibble of notes in a warren of time,
I burrow through versions, in orderly rhyme.
Forks sprout like clover, green on the hill,
Latest leaves shimmer—still, still, still.
I tap my paw: “Persist!”—and hop away,
History tucked in my pouch for the day. 🐇✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title precisely captures the main change—re-implementing version tracking—and aligns with the bulk of the modifications, giving reviewers immediate clarity on the PR’s purpose.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch version-tracking-2

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

@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: 40

🔭 Outside diff range comments (3)
app/build.gradle (1)

90-92: Update dev.arkbuilders dependencies to latest published versions

Web lookup shows newer releases: dev.arkbuilders:arklib → 0.3.4-blake3 and dev.arkbuilders:arkfilepicker → 0.1.2. Current lines use 0.3.3 and 0.1.1 — please bump and verify compatibility (AndroidX/Hilt/transitives) and CI access to GitHub Packages.

  • File to change:
    • app/build.gradle (around lines 90–92)

Diff suggestion:

-    implementation 'dev.arkbuilders:arkfilepicker:0.1.1'
-    implementation 'dev.arkbuilders:arklib:0.3.3'
+    implementation 'dev.arkbuilders:arkfilepicker:0.1.2'
+    implementation 'dev.arkbuilders:arklib:0.3.4-blake3'

Also ensure GitHub Packages is configured in repositories (and CI auth) so the artifacts can be resolved. Run a full build and smoke tests after bumping to confirm no transitive breakage.

app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt (1)

121-123: Invalidate options menu after navigating to Settings

After replacing the fragment with SettingsFragment, the menu should be invalidated so onCreateOptionsMenu can re-evaluate visibility. Otherwise, the Settings button may remain visible on the Settings screen.

Apply this diff:

             R.id.settings -> {
                 fragment = SettingsFragment()
                 replaceFragment(fragment, SettingsFragment.TAG)
+                invalidateOptionsMenu()
             }
app/src/main/res/layout/note.xml (1)

15-17: Remove unused ConstraintLayout attributes

This container is a LinearLayout; ConstraintLayout attributes have no effect and will trigger lint warnings. Remove them.

Apply this diff:

-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
🧹 Nitpick comments (30)
README.md (1)

3-3: Fix markdownlint MD036: avoid emphasis as a heading

Use a proper heading instead of italic text for the status line.

Apply:

- _Implementation in progress_
+## Status
+Implementation in progress
app/src/main/res/drawable/ic_content_copy.xml (1)

1-5: Avoid hardcoded tint for better theming (dark mode, dynamic color)

Hardcoding @color/purple_700 limits theme adaptability. Prefer a theme attribute so the icon respects light/dark and Material dynamic color.

Minimal change:

-<vector android:height="24dp" android:tint="@color/purple_700"
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"

Optionally, remove tint from the vector and tint at usage sites (ImageView/Button), which is often cleaner for shared assets. Also ensure usage sets contentDescription appropriately (or null if decorative).

gradle/wrapper/gradle-wrapper.properties (1)

4-4: Confirm AGP/Kotlin/JDK compatibility & add Gradle wrapper checksum

Gradle 9 requires AGP ≥ 8.4.0; ensure Kotlin Gradle Plugin ≥ 2.0.0 and JDK ≥ 17 before upgrading. Add the Gradle wrapper SHA-256 for supply-chain hardening.

  • Files to update / verify:
    • gradle/wrapper/gradle-wrapper.properties (line ~4) — add checksum
    • Check project AGP/Kotlin/JDK in build.gradle(.kts), settings.gradle(.kts), and gradle.properties

Suggested diff:

 distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
+distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b

Please confirm your project’s AGP, Kotlin, and JDK versions meet the minima above; if they do, apply the checksum change.

app/src/main/res/drawable/ic_note.xml (1)

1-5: Use theme-based tint to support dynamic theming

Same as ic_content_copy.xml: avoid hardcoded purple tint to ensure consistency across themes and dark mode.

-<vector android:height="24dp" android:tint="@color/purple_700"
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"

Consider tinting at usage sites for greater flexibility.

app/src/main/res/drawable/ic_history.xml (1)

1-5: Use theme-aware tint for better dark/light mode support

Hardcoding tint to @color/purple_700 makes the icon ignore theme, and may reduce contrast in dark mode. Prefer using a theme attribute.

-<vector android:height="24dp" android:tint="@color/purple_700"
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"
     android:viewportHeight="24" android:viewportWidth="24"
     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">

Optionally add auto-mirroring if this icon is considered directional:

-    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    android:width="24dp" android:autoMirrored="true" xmlns:android="http://schemas.android.com/apk/res/android">
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt (2)

5-5: Rename Version2 to a clearer alias (e.g., VersionId)

typealias Version2 = ResourceId is unclear and looks like a temporary name. If the intent is to indicate “a single version identifier”, consider a clearer alias such as VersionId.

-typealias Version2 = ResourceId
+typealias VersionId = ResourceId

Note: This is a breaking rename across the codebase (Version2 usages must be updated).

If you’d like, I can generate a patch for the whole repo to rename Version2VersionId. Do you want me to proceed?


7-10: Add brief KDoc to clarify semantics of parent/child

Document whether parent → child implies “child is derived from parent” and if multiple parents or cycles are allowed. This will help future maintainers and storage/graph operations.

Example:

/**

  • parent: the source/origin version
  • child: the derived/forked version
  • Semantics: DAG with no cycles (enforced at storage level)
    */
settings.gradle (1)

28-29: Give GitHub Maven repositories unique names and keep the token fallback

Good: falling back to System.getenv("GITHUB_TOKEN") is CI-friendly.

To avoid confusion in dependency resolution logs and IDE UIs, give the two GitHub repos distinct names:

-        maven {
-            name = "GitHubPackages"
+        maven {
+            name = "GitHubPackages-arklib-android"
             url = "https://maven.pkg.github.com/ARK-Builders/arklib-android"
             credentials {
                 username = "token"
                 password = localProps.getProperty("gpr.token") ?: System.getenv("GITHUB_TOKEN")
             }
         }
         maven {
-            name = "GitHubPackages"
+            name = "GitHubPackages-ark-android"
             url = "https://maven.pkg.github.com/ARK-Builders/ark-android"
             credentials {
                 username = "token"
                 password = localProps.getProperty("gpr.token") ?: System.getenv("GITHUB_TOKEN")
             }
         }

Also applies to: 31-37

app/src/main/res/values/strings.xml (1)

30-33: Add contentDescription strings for accessibility (for the new action buttons)

If these strings are used as labels/tooltips only, also add dedicated content descriptions for the image buttons to support TalkBack.

Consider adding alongside the current entries:

<string name="ark_memo_cd_track_versions">Track versions</string>
<string name="ark_memo_cd_fork_versions">Fork versions</string>
<string name="ark_memo_cd_history">History</string>
<string name="ark_memo_cd_old_version">Older version</string>

Then, reference them via android:contentDescription in the layout where the new icons are used.

I can patch the layout(s) to wire these up if you confirm the desired wording.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Versions.kt (2)

11-17: Prefer emptySet() and guard logging in Monoid.combine to keep it pure and low-noise

  • Neutral can use emptySet() (clearer intent).
  • Consider guarding or removing logging inside a pure combiner to avoid noisy logs and side-effects.
-    override val neutral: Versions = setOf()
+    override val neutral: Versions = emptySet()

-        val result = a.union(b)
-        Log.d(LOG_PREFIX, "merging $a and $b into $result")
+        val result = a.union(b)
+        if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
+            Log.d(LOG_TAG, "merging $a and $b into $result")
+        }
         return result

20-20: Use a proper Log tag constant and keep it private to this file

Android Log tags are typically identifiers (no brackets). Also prefer const val for inlining and make it private to this file.

-internal val LOG_PREFIX: String = "[versions]"
+private const val LOG_TAG: String = "Versions"
app/build.gradle (1)

13-14: Remove the duplicate namespace declaration

The namespace is already set on Line 10. The second declaration is redundant and can confuse readers.

-    namespace 'dev.arkbuilders.arkmemo'
app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt (1)

29-33: Consider scoping NotesRepoHelper as Singleton for consistency with other repos

NotesRepoHelper likely encapsulates shared state/config. For parity with PropertiesStorageRepo and VersionStorageRepo, consider annotating this provider with @singleton as well.

Proposed change outside the selected range:

@Singleton
@Provides
fun provideNotesRepoHelper(
    memoPreferences: MemoPreferences,
    propertiesStorageRepo: PropertiesStorageRepo
): NotesRepoHelper = NotesRepoHelper(memoPreferences, propertiesStorageRepo)
app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt (1)

5-5: Add brief KDoc to clarify purpose and expected existence of the "versions" dir

Minor, but documenting whether the caller must ensure directory existence helps avoid misuse.

Example:

/**
 * Returns a path to the "versions" subdirectory under this path.
 * Note: The directory may not exist; callers should create it when needed.
 */
fun Path.arkVersions(): Path = resolve("versions")
app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt (1)

139-143: Provide a counterpart to restore default UI after editing

initEditUI() sets edit mode UI but there’s no public API to restore defaults (title, home-as-up, settings visibility) when leaving edit screens. Introduce a reset method for consistency.

Example implementation outside the selected range:

fun resetMainUI() {
    title = getString(R.string.app_name)
    supportActionBar?.setDisplayHomeAsUpEnabled(false)
    showSettingsButton(fragment is NotesFragment || fragment is VersionsFragment)
    invalidateOptionsMenu()
}
app/src/main/res/layout/note.xml (2)

25-31: Decorative ImageView missing contentDescription

For accessibility, mark decorative images with a null contentDescription so they’re ignored by TalkBack.

Apply this diff:

                 android:visibility="gone"
                 app:srcCompat="@drawable/ic_note"
+                android:contentDescription="@null"
                 />

55-76: Use ripple/selectable background instead of null for ImageButtons

Null backgrounds remove visual feedback and tap targets may feel unresponsive. Use the standard borderless ripple.

Apply this diff:

-        <ImageButton
+        <ImageButton
             android:id="@+id/btn_track_versions"
             android:layout_width="50dp"
             android:layout_height="50dp"
-            android:background="@null"
+            android:background="?attr/selectableItemBackgroundBorderless"
             android:contentDescription="@string/ark_memo_track_versions"
             android:src="@drawable/ic_history"
             android:visibility="gone" />
-        <ImageButton
+        <ImageButton
             android:id="@+id/btn_fork_version"
             android:layout_width="50dp"
             android:layout_height="50dp"
-            android:background="@null"
+            android:background="?attr/selectableItemBackgroundBorderless"
             android:contentDescription="@string/ark_memo_fork_versions"
             android:src="@drawable/ic_content_copy"
             android:visibility="gone" />
 
-        <ImageButton
+        <ImageButton
             android:id="@+id/btn_delete"
             android:layout_width="50dp"
             android:layout_height="50dp"
-            android:background="@null"
+            android:background="?attr/selectableItemBackgroundBorderless"
             android:contentDescription="@string/delete"
             android:src="@drawable/delete" />
app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt (1)

6-11: Document isForked semantics

Add a brief KDoc on what constitutes a “forked” note vs “latest” and how UI should treat it (e.g., read-only for old versions). This will help third-party storage implementations later.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt (1)

13-21: Consider cache invalidation when root changes or storage needs a refresh

If the notes storage root is changed in preferences, old entries remain cached indefinitely. Expose an invalidate API.

Example addition (outside the diffed lines, for illustration):

suspend fun invalidate(root: Path? = null) = mutex.withLock {
    if (root == null) storageByRoot.clear() else storageByRoot.remove(root)
}
app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (1)

43-45: Avoid duplicating SVG updates in the View

The View writes SVG commands directly while viewModel.onDrawPath likely updates the SVG too (per GraphicNotesViewModel.onDrawPath). Duplicating these responsibilities risks divergence and double writes.

Consider removing the direct SVG mutations from the View and let the ViewModel be the single source of truth:

-                    viewModel.svg().apply {
-                        addCommand(SVGCommand.MoveTo(x, y))
-                    }
+                    // Let ViewModel update the SVG when the stroke is finalized
-                    viewModel.svg().apply {
-                        addCommand(SVGCommand.AbsQuadTo(currentX, currentY, x2, y2))
-                    }
+                    // Let ViewModel update the SVG when the stroke is finalized

If you need live-preview of the stroke, render the Path on canvas (as you already do) and persist to SVG once on ACTION_UP.

Also applies to: 54-56

app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/NoteDeleteDialog.kt (1)

28-33: Optional: Strengthen parent dependency and add content description

Use requireParentFragment() if the dialog must be attached to a parent that implements deleteNote. Also, consider making the positive action’s intent accessible.

-            .setPositiveButton(R.string.ark_memo_ok){ dialog, _ ->
-                parentFragment?.deleteNote(note)
+            .setPositiveButton(R.string.ark_memo_ok){ dialog, _ ->
+                requireParentFragment().deleteNote(note)
                 toast(requireContext(), getString(R.string.note_deleted))
                 dialog.cancel()
             }

If the parent isn’t guaranteed, keep the safe call but surface a clear error or disable the button.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt (1)

6-15: Add KDoc and clarify naming to avoid confusion with VersionStorage

There are both VersionStorage (singular, Version2) and VersionsStorage (plural, Versions) in the codebase. A short KDoc on the interface and methods will help contributors distinguish them quickly.

Example:

/**
 * Stores parent versions for a child resource (child -> set of parent ResourceIds).
 * Parallel to VersionStorage which stores single parent-child edges.
 */
interface VersionsStorage : Storage<Versions> {
    /** Replace the set of parents for the given child. */
    fun setParents(child: ResourceId, parents: Versions) = setValue(child, parents)
    /** Retrieve the set of parents for the given child. */
    fun getParents(child: ResourceId): Versions = getValue(child)
    /** Remove all parents for the given child. */
    fun removeParents(child: ResourceId) = remove(child)
}
app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt (1)

71-114: Nit: remove unused constants and imports after refactor

ROOT_PATH will be unused after switching to TemporaryFolder. Clean up to keep tests tidy.

app/src/main/res/layout/fragment_notes.xml (2)

13-23: Add contentDescription for accessibility on the extended FAB

Extended FAB should have a contentDescription for non-text modes or assistive tech. Use the same string as its label.

 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
         android:id="@+id/fab_new"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/ark_memo_close"
         android:layout_gravity="bottom|end"
         android:visibility="gone"
+        android:contentDescription="@string/ark_memo_close"
         android:layout_marginEnd="16dp"
         android:layout_marginBottom="20dp"
         app:icon="@drawable/add"/>

49-55: Naming consistency: ‘fab_paste’ is a Button

Minor naming mismatch: the id implies a FAB, but the widget is a Button. Consider renaming to btn_paste for clarity (and update references).

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (1)

131-148: Inefficient text parsing and potential index out of bounds.

The code parses the first line of text twice (Lines 144 and 154) and uses take(20) which could be extracted to a constant. Also, split("\n")[0] could throw if the text is empty.

Extract common logic and add safety:

+companion object{
+    const val TAG = "text-notes-fragment"
+    private const val TITLE_MAX_LENGTH = 20
+
+    private fun extractTitleFromText(text: String): String =
+        text.lines().firstOrNull()?.take(TITLE_MAX_LENGTH) ?: ""

 private fun readArguments() {
     if(arguments != null) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
             requireArguments().getParcelable(NOTE_KEY, TextNote::class.java)?.let {
                 prepare(it)
                 checkNoteForReadOnly()
             }
         else requireArguments().getParcelable<TextNote>(NOTE_KEY)?.let {
             prepare(it)
             checkNoteForReadOnly()
         }
         requireArguments().getString(NOTE_PASTE_KEY)?.let {
             data = it
-            title = it.split("\n")[0].take(20)
+            title = extractTitleFromText(it)
         }
         etNote.setText(data)
     }
 }

 private fun prepare(newNote: TextNote) {
     note = newNote
     data = newNote.text
     title = newNote.title.ifEmpty {
         // For backward compatibility, text notes before dedicated properties storage
-        data.split("\n")[0].take(20)
+        extractTitleFromText(data)
     }
 }
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt (1)

3-4: Remove duplicate import statement.

There are two imports for Build - one direct import and one for Build.VERSION.SDK_INT. This is redundant.

 import android.os.Build
-import android.os.Build.VERSION.SDK_INT

Then update Line 95:

-if (SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (1)

115-129: Consider extracting SDK version check logic

The repeated SDK version check pattern for getParcelable could be extracted into a utility function to improve maintainability and reduce duplication.

Consider creating an extension function:

// In a utilities file
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        getParcelable(key, T::class.java)
    } else {
        @Suppress("DEPRECATION")
        getParcelable(key)
    }
}

Then simplify the code:

 private fun readArguments() {
     if (arguments != null) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
-            requireArguments().getParcelable(GRAPHIC_NOTE_KEY, GraphicNote::class.java)?.let {
-                note = it
-                graphicNotesViewModel.onNoteOpened(note)
-                checkNoteForReadOnly()
-            }
-        else requireArguments().getParcelable<GraphicNote>(GRAPHIC_NOTE_KEY)?.let {
+        requireArguments().getParcelableCompat<GraphicNote>(GRAPHIC_NOTE_KEY)?.let {
             note = it
             graphicNotesViewModel.onNoteOpened(note)
             checkNoteForReadOnly()
         }
     }
 }
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (1)

88-92: Inconsistent dialog method naming

The NoteDeleteDialog uses setNote() method which differs from typical Android dialog patterns that use with or create builders.

Consider using a more idiomatic builder pattern or factory method for consistency with Android conventions.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1)

176-195: TODO methods should throw NotImplementedError

The unimplemented methods use TODO() which will throw at runtime, but it's better to be explicit about the error type.

 override fun getValue(id: ResourceId): Version2 {
-    TODO("Not yet implemented")
+    throw NotImplementedError("getValue is not yet implemented")
 }

 override fun remove(id: ResourceId) {
-    TODO("Not yet implemented")
+    throw NotImplementedError("remove is not yet implemented")
 }

 override fun setValue(
     id: ResourceId,
     value: Version2
 ) {
-    TODO("Not yet implemented")
+    throw NotImplementedError("setValue is not yet implemented")
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c2332f0 and 15add0b.

📒 Files selected for processing (40)
  • README.md (1 hunks)
  • app/build.gradle (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt (2 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Versions.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorageRepo.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (4 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/NoteDeleteDialog.kt (2 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (4 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (2 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/utils/Config.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/utils/Constants.kt (1 hunks)
  • app/src/main/res/drawable/ic_content_copy.xml (1 hunks)
  • app/src/main/res/drawable/ic_history.xml (1 hunks)
  • app/src/main/res/drawable/ic_note.xml (1 hunks)
  • app/src/main/res/layout/fragment_edit_notes.xml (1 hunks)
  • app/src/main/res/layout/fragment_notes.xml (3 hunks)
  • app/src/main/res/layout/note.xml (3 hunks)
  • app/src/main/res/layout/recyclerview.xml (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
  • app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt (1 hunks)
  • build.gradle (1 hunks)
  • gradle/wrapper/gradle-wrapper.properties (1 hunks)
  • settings.gradle (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T05:40:19.372Z
Learnt from: tuancoltech
PR: ARK-Builders/ARK-Memo#0
File: :0-0
Timestamp: 2025-07-18T05:40:19.372Z
Learning: In the ARK-Memo Android app, the notesViewModel.init {} callback properly handles threading by dispatching operations to worker threads, preventing main thread blocking during view restoration.

Applied to files:

  • README.md
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt
📚 Learning: 2025-07-18T05:33:52.382Z
Learnt from: tuancoltech
PR: ARK-Builders/ARK-Memo#0
File: :0-0
Timestamp: 2025-07-18T05:33:52.382Z
Learning: In the ARK-Memo Android app, TextNotesRepo.findNote() intentionally returns null because text notes don't need special handling during view restoration. This is a design decision to ignore text notes when restoring view state, as users can easily continue editing text notes without complex state recovery mechanisms.

Applied to files:

  • app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt
  • app/src/main/res/layout/fragment_notes.xml
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt
🧬 Code Graph Analysis (8)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt (1)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (3)
  • setValue (190-195)
  • getValue (176-178)
  • remove (186-188)
app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (1)
app/src/main/java/dev/arkbuilders/arkmemo/graphics/SVG.kt (1)
  • addCommand (31-33)
app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/NoteDeleteDialog.kt (1)
app/src/main/java/dev/arkbuilders/arkmemo/ui/views/Toast.kt (1)
  • toast (6-8)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (2)
app/src/main/java/dev/arkbuilders/arkmemo/utils/Utils.kt (1)
  • observeSaveResult (17-30)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (3)
  • initUI (66-113)
  • readArguments (115-129)
  • checkNoteForReadOnly (131-143)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt (2)
  • childrenNotParents (20-20)
  • parentsTreeByChild (16-18)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt (5)
app/src/main/java/dev/arkbuilders/arkmemo/ui/views/Toast.kt (1)
  • toast (6-8)
app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (1)
  • init (25-29)
app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt (2)
  • init (35-44)
  • readAllNotes (46-50)
app/src/main/java/dev/arkbuilders/arkmemo/repo/NotesRepo.kt (2)
  • init (5-14)
  • init (7-7)
app/src/main/java/dev/arkbuilders/arkmemo/repo/text/TextNotesRepo.kt (1)
  • init (36-39)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt (3)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (2)
  • readArguments (131-148)
  • initUI (88-129)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (2)
  • readArguments (115-129)
  • initUI (66-113)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt (1)
  • initUI (103-145)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (3)
app/src/main/java/dev/arkbuilders/arkmemo/utils/Utils.kt (1)
  • observeSaveResult (17-30)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (3)
  • initUI (88-129)
  • readArguments (131-148)
  • checkNoteForReadOnly (166-180)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt (2)
  • initUI (87-91)
  • readArguments (93-103)
🪛 detekt (1.23.8)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorageRepo.kt

[warning] 3-4: The class or object VersionsStorageRepo is empty.

(detekt.empty-blocks.EmptyClassBlock)

🪛 markdownlint-cli2 (0.17.2)
README.md

3-3: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (17)
app/src/main/java/dev/arkbuilders/arkmemo/utils/Config.kt (1)

4-4: Good use of SharedPreferences.edit DSL

Concise, safe, and consistent with AndroidX best practices.

app/src/main/res/values/strings.xml (1)

30-33: LGTM: Strings added for versioning UX are clear and consistent

The new labels read well and align with the newly introduced UI/actions.

app/build.gradle (1)

94-97: Hilt upgrade confirmed — optional KSP migration

Verified: Hilt 2.57 and androidx.hilt:hilt-compiler:1.2.0 are present; the only kapt usage in the repo is the Hilt compilers and the kotlin-kapt plugin — no other kapt, annotationProcessor, or ksp references found.

  • app/build.gradle
    • plugins: id 'kotlin-kapt' (near top)
    • dependencies: kapt "com.google.dagger:hilt-compiler:2.57" and kapt 'androidx.hilt:hilt-compiler:1.2.0' (lines ~95-96)

Snippet (unchanged):

    implementation "com.google.dagger:hilt-android:2.57"
    kapt "com.google.dagger:hilt-compiler:2.57"
    kapt 'androidx.hilt:hilt-compiler:1.2.0'

Optional: you can migrate these kapt entries to KSP for faster builds (add the KSP plugin and replace kapt with ksp) — I can prepare a migration patch if you’d like.

app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt (2)

6-10: LGTM: cohesive model to carry version graph slices

The data shape is clear and aligns with storage reads.


7-9: Confirm Set/List semantics for parents vs children

Parents is a Set while children is a List. If both are identifiers, asymmetry may be accidental unless children ordering or duplicates matter. Please confirm intended semantics.

Would you like me to scan usages of VersionsResult.children to detect ordering/duplication reliance?

app/src/main/res/layout/recyclerview.xml (1)

8-10: Renamed ID verified — no stale references to R.id.recyclerView found

Short explanation: searches for R.id.recyclerView and @id/recyclerView returned no hits. The code uses the new ID (rv_notes) and view binding.

Files checked / evidence:

  • app/src/main/res/layout/recyclerview.xml — android:id="@+id/rv_notes"
  • app/src/main/res/layout/fragment_notes.xml — android:id="@+id/rv_notes_binding", includes layout="@layout/recyclerview"
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt — rvNotes = binding.rvNotesBinding.rvNotes
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt — rvNotes = binding.rvNotesBinding.rvNotes

Action: none required — rename appears fully migrated.

app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt (1)

12-13: Alignment with updated Note contract looks good

Overriding description and adding the isForked flag correctly aligns GraphicNote with the new Note API. Defaults and mutability are appropriate.

app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt (1)

11-16: TextNote correctly adopts new Note members

The overrides for description and isForked match the updated Note interface. Parcelable usage remains consistent. No issues spotted here.

app/src/main/res/layout/note.xml (2)

2-7: Layout switch to LinearLayout is reasonable

Simplifies the layout and matches current child structure. No functional issues spotted with the container change.


81-89: Divider placement is fine

Moving the divider outside the header row makes sense visually. No issues.

app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt (1)

6-11: Note implementers updated — no action required

Verified implementers:

  • app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt — overrides description & isForked; @parcelize present; resource is annotated @IgnoredOnParcel. (OK)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt — overrides description & isForked; @parcelize present; resource is annotated @IgnoredOnParcel. (OK)

Other files that reference Note (ViewModels, Fragments, Repo, Dialog) are callers, not implementers, and need no changes.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt (1)

13-21: Ignore — no early-init risk for memoPreferences.getNotesStorage()

Quick verification: VersionStorageRepo.provide() is only invoked from VersionsViewModel.init(), and that init() is called from Fragments (VersionsFragment, NotesFragment) — after Application.onCreate. App is annotated with @HiltAndroidApp and MemoPreferencesImpl is injected with @ApplicationContext and constructs SharedPreferences in its constructor; getNotesStorage() simply returns Path(getPath()). Given this, memoPreferences is available by the time provide() runs and the original worry is not applicable.

Relevant locations:

  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt — provide()
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt — versionStorageRepo.provide()
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt and NotesFragment.kt — calls to VersionsViewModel.init()
  • app/src/main/java/dev/arkbuilders/arkmemo/preferences/MemoPreferencesImpl.kt — getNotesStorage() uses SharedPreferences + Path(...)
  • app/src/main/java/dev/arkbuilders/arkmemo/App.kt — @HiltAndroidApp (Hilt/App init)

If you still want extra defensiveness, validate that getPath() is non-empty or wrap Path(...) with a clear exception message — otherwise no change required.

Likely an incorrect or invalid review comment.

app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (1)

21-24: Guard against uninitialized ViewModel in NotesCanvas

onSizeChanged and onDraw access viewModel before it may be set via setViewModel(...), which can throw UninitializedPropertyAccessException for a lateinit property.

  • File: app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt
    • onSizeChanged: lines 21–24
    • onDraw: lines 26–34

Suggested change:

 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
     super.onSizeChanged(w, h, oldw, oldh)
-    viewModel.svg().setViewBox(w.toFloat(), h.toFloat())
+    if (::viewModel.isInitialized) {
+        viewModel.svg().setViewBox(w.toFloat(), h.toFloat())
+    }
 }
 
 override fun onDraw(canvas: Canvas) {
     super.onDraw(canvas)
-    val paths = viewModel.paths()
+    if (!::viewModel.isInitialized) return
+    val paths = viewModel.paths()
     if (paths.isNotEmpty()) {
         paths.forEach {
             canvas.drawPath(it.path, it.paint)
         }
     }
 }

Please verify call sites ensure setViewModel(viewModel) is invoked before the view is laid out/drawn.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt (1)

1-15: LGTM: Minimal, clear forwarding API

The interface cleanly wraps Storage with intention-revealing methods. Naming reads well and the defaults are concise.

app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt (1)

52-56: saveVersion is provided by all callers — no change needed

I searched the repo for onSaveClick usages. Every call site passes a saveVersion lambda; there’s no ArkRecorderFragment call that omits it. Keeping the parameter required is correct.

  • Callers found:
    • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (passes saveVersion — creates version & updates latest ID)
    • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (passes saveVersion — creates version & updates latest ID)
  • Definition:
    • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt (onSaveClick at ~line 52)

Remove the earlier suggestion to make saveVersion optional — the original review comment is incorrect.

Likely an incorrect or invalid review comment.

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (1)

61-64: No leak found — MainActivity.fragment is used as a current-fragment pointer and is updated consistently

MainActivity declares and restores var fragment: Fragment and the codebase updates that field when replacing/restoring fragments; fragments also set activity.fragment = this in onResume to keep the pointer current. Given the repository usage, this does not introduce a retained/stale reference leak.

Relevant locations inspected:

  • app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt — var fragment: Fragment = NotesFragment() and updates in showFragment()/onCreate/onOptionsItemSelected/onSaveInstanceState
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt — onResume sets activity.fragment = this
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt — sets activity.fragment = this
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt — assigns activity.fragment for editor fragments and in onResume
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/SettingsFragment.kt — sets activity.fragment = this
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt — assigns activity.fragment before replacing

Conclusion: no change required for the EditGraphicNotesFragment onResume assignment.

app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (1)

63-68: Missing closing brace in createVersion function

There's an extra closing brace on line 68 that doesn't match the opening braces, indicating a syntax error.

Fix the brace mismatch:

 fun createVersion(oldId: ResourceId, newId: ResourceId) {
     viewModelScope.launch(Dispatchers.IO) {
-            val version = Version(oldId, newId)
-            versionStorage.add(version)
-        }
+        val version = Version(oldId, newId)
+        versionStorage.add(version)
     }
-    }
+}

Likely an incorrect or invalid review comment.

Comment on lines 35 to 38
@Provides
fun provideVersionStorageRepo(
memoPreferences: MemoPreferences
) = VersionStorageRepo(memoPreferences)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Scope VersionStorageRepo as a Singleton and make the return type explicit

Without @singleton, Hilt may create multiple instances, defeating the cache-per-root behavior and increasing I/O. Also, adding an explicit return type improves DI clarity.

Apply this diff:

-        @Provides
-        fun provideVersionStorageRepo(
-            memoPreferences: MemoPreferences
-        ) = VersionStorageRepo(memoPreferences)
+        @Singleton
+        @Provides
+        fun provideVersionStorageRepo(
+            memoPreferences: MemoPreferences
+        ): VersionStorageRepo = VersionStorageRepo(memoPreferences)

Additionally, add the missing import:

import javax.inject.Singleton
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt around lines
35 to 38, the provideVersionStorageRepo provider is missing a @Singleton scope
and an explicit return type; annotate the function with @Singleton, change its
signature to return VersionStorageRepo explicitly, and add the import
javax.inject.Singleton at the top of the file so Hilt will create a single
instance and the DI contract is clear.

Comment on lines +17 to +20
override fun valueFromString(raw: String): Versions =
raw.split(",").filter { it.isNotEmpty() }.map {
ResourceId.fromString(it)
}.toSet()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential parsing errors when converting strings to ResourceIds.

The valueFromString method could throw exceptions if ResourceId.fromString() encounters malformed input. This could happen if the storage file gets corrupted or manually edited.

Add error handling to prevent crashes:

 override fun valueFromString(raw: String): Versions =
-    raw.split(",").filter { it.isNotEmpty() }.map {
-        ResourceId.fromString(it)
-    }.toSet()
+    raw.split(",").filter { it.isNotEmpty() }.mapNotNull {
+        try {
+            ResourceId.fromString(it)
+        } catch (e: Exception) {
+            Log.e("RootVersionsStorage", "Failed to parse ResourceId: $it", e)
+            null
+        }
+    }.toSet()

Don't forget to add the import:

import android.util.Log
🤖 Prompt for AI Agents
In
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt
around lines 17 to 20, the current valueFromString blindly calls
ResourceId.fromString and can throw on malformed items; wrap the mapping in safe
parsing (try/catch per item), skip or filter out invalid entries, and log parse
failures with android.util.Log so the method never throws—return the set of
successfully parsed ResourceId objects (or an empty set if none) and add the
import android.util.Log.

Comment on lines 87 to 89
val newVersion = Version(
versionIdIsChild?.parent!!,
versionIdIsParent?.child!!
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Force unwraps could cause NPE in version graph operations

Lines 88-89 use !! which will crash if either versionIdIsChild or versionIdIsParent is null. This could happen if the version graph is corrupted or if there's a race condition.

Add null safety checks:

+    if (versionIdIsChild == null || versionIdIsParent == null) {
+        Log.e(VERSIONS_STORAGE, "Inconsistent version graph state for id: $id")
+        return
+    }
     val newVersion = Version(
-        versionIdIsChild?.parent!!,
-        versionIdIsParent?.child!!
+        versionIdIsChild.parent,
+        versionIdIsParent.child
     )
📝 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 newVersion = Version(
versionIdIsChild?.parent!!,
versionIdIsParent?.child!!
if (versionIdIsChild == null || versionIdIsParent == null) {
Log.e(VERSIONS_STORAGE, "Inconsistent version graph state for id: $id")
return
}
val newVersion = Version(
versionIdIsChild.parent,
versionIdIsParent.child
)
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 87 to 89, the code force-unwraps versionIdIsChild and
versionIdIsParent with !! which can throw an NPE when either is null; replace
the force-unwraps with null-safe handling: check both variables before
constructing the new Version, and if either is null return a controlled failure
(e.g. throw a clear IllegalStateException/IllegalArgumentException with
contextual message or return a Result/nullable value) so you avoid crashing on
corrupted or racing version graphs and log the offending state for debugging.

Comment on lines 101 to 118
override fun parentsTreeByChild(
child: ResourceId
): Map<ResourceId, List<ResourceId>> {
var localChild = child
var parent: ResourceId?
val parents = mutableListOf<ResourceId>()
for (version in versions) {
parent = versions.find {
it.child == localChild
}?.parent
if (parent != null && children.contains(parent))
localChild = parent
if (parent != null) parents.add(parent)
if (!children.contains(parent))
break
}
return mapOf(child to parents)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inefficient parent tree traversal algorithm

The parentsTreeByChild function has nested loops that could be optimized. The outer loop iterates through all versions, but the inner logic only needs to traverse the parent chain once.

Optimize the traversal:

 override fun parentsTreeByChild(
     child: ResourceId
 ): Map<ResourceId, List<ResourceId>> {
     var localChild = child
-    var parent: ResourceId?
     val parents = mutableListOf<ResourceId>()
-    for (version in versions) {
-        parent = versions.find {
-            it.child == localChild
-        }?.parent
+    while (true) {
+        val parent = versions.find { it.child == localChild }?.parent ?: break
         if (parent != null && children.contains(parent))
             localChild = parent
-        if (parent != null) parents.add(parent)
-        if (!children.contains(parent))
-            break
+        parents.add(parent)
+        if (!children.contains(parent)) break
     }
     return mapOf(child to parents)
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 101 to 118, replace the current nested-loop traversal with a
single-parent-chain walk using a precomputed map: build a Map<ResourceId,
ResourceId?> from version.child -> version.parent once, then set localChild =
child and loop while true to lookup parent = map[localChild]; if parent is null
break; add parent to parents; if children.contains(parent) then set localChild =
parent and continue, otherwise break; return mapOf(child to parents). This
eliminates repeated versions.find calls and reduces complexity to O(n) for the
map build plus O(depth) per lookup.

Comment on lines 140 to 174
private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile)
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

No error handling for malformed storage file

The readStorage() function could throw various exceptions (IndexOutOfBoundsException, IllegalArgumentException) if the storage file is corrupted or malformed.

Add proper error handling:

 private suspend fun readStorage(): VersionsResult =
     withContext(Dispatchers.IO) {
+        try {
             val lines = Files.readAllLines(storageFile)
+            if (lines.isEmpty()) {
+                Log.w(VERSIONS_STORAGE, "Storage file is empty")
+                return@withContext VersionsResult(emptyList(), emptySet(), emptyList())
+            }
             val storageVersion = lines.removeAt(0)
             verifyVersion(storageVersion)
             val versions = lines.map {
                 val parts = it.split(KEY_VALUE_SEPARATOR)
+                if (parts.size != 2) {
+                    Log.e(VERSIONS_STORAGE, "Malformed version line: $it")
+                    throw IllegalStateException("Malformed storage file")
+                }
                 val parent = ResourceId.fromString(parts[0])
                 val child = ResourceId.fromString(parts[1])
                 // ... rest of the code
             }
             // ... rest of the code
+        } catch (e: Exception) {
+            Log.e(VERSIONS_STORAGE, "Failed to read storage", e)
+            throw e
+        }
     }
📝 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
private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile)
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
}
private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
try {
val lines = Files.readAllLines(storageFile)
if (lines.isEmpty()) {
Log.w(VERSIONS_STORAGE, "Storage file is empty")
return@withContext VersionsResult(emptyList(), emptySet(), emptyList())
}
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
if (parts.size != 2) {
Log.e(VERSIONS_STORAGE, "Malformed version line: $it")
throw IllegalStateException("Malformed storage file")
}
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
} catch (e: Exception) {
Log.e(VERSIONS_STORAGE, "Failed to read storage", e)
throw e
}
}
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 140 to 174, readStorage() currently assumes the file format is
correct and will throw on malformed input; wrap the file parsing in a try/catch
that catches IO and parsing-related exceptions (IndexOutOfBoundsException,
IllegalArgumentException, any exceptions from ResourceId.fromString), validate
the file has at least one line before removeAt(0), ensure each data line splits
into exactly two parts before using parts[0]/parts[1], handle or log malformed
lines (skip them) and either return an empty/safe VersionsResult or rethrow a
clear, custom exception with context; ensure errors are logged with details so
callers can react appropriately.

Comment on lines 30 to 50
fun parents_tree_is_correct() {
scope.launch {
versionStorage.add(version1)
versionStorage.add(version2)
versionStorage.add(version3)
versionStorage.add(version5)
versionStorage.add(version4)
val map1 = versionStorage.parentsTreeByChild(
child1
)
assertEquals(parentsTree1, map1)
val map2 = versionStorage.parentsTreeByChild(
parent1
)
assertEquals(parentsTree2, map2)
val map3 = versionStorage.parentsTreeByChild(
parent2
)
assertEquals(parentsTree3, map3)
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid shared mutable storage across tests and filesystem coupling

The same RootVersionStorage instance and ./ path are reused across tests, causing cross-test contamination. Use a per-test temp directory and recreate storage in @before.

🤖 Prompt for AI Agents
In app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt around lines
30 to 50, the test reuses the same RootVersionStorage and hard-coded "./" path
which causes cross-test contamination and filesystem coupling; change the test
setup to create a fresh temp directory for each test and initialize a new
VersionStorage instance in a @Before method (and delete the temp directory in
@After), then update the test to use that per-test storage (and use runBlocking
or equivalent to execute suspending calls synchronously instead of scope.launch
so the test waits for storage operations to complete).

build.gradle Outdated
Comment on lines 10 to 12
id 'com.android.application' version '8.11.1' apply false
id 'org.jetbrains.kotlin.android' version '2.2.0' apply false
id 'com.google.dagger.hilt.android' version '2.57' apply false
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify AGP/Kotlin plugin versions and Gradle wrapper compatibility

AGP 8.11.1 and Kotlin 2.2.0 are quite new; ensure your Gradle wrapper and Android Gradle Plugin combo are supported and available.

Check current wrapper and declared plugin versions:

And confirm latest stable versions and compatibility:


🏁 Script executed:

#!/bin/bash
set -e
echo "AGP in root build.gradle:"
rg -n "id 'com.android.application' version" -n build.gradle

echo -e "\nGradle wrapper distributionUrl:"
fd gradle-wrapper.properties | xargs -I{} sh -c 'echo "== {} ==" && rg -n "distributionUrl" "{}"'

Length of output: 513


🌐 Web query:

What is the latest stable Android Gradle Plugin version and which Gradle versions does it support?

💡 Result:

The latest stable Android Gradle Plugin (AGP) version is 8.11.1, released in July 2025[8][10]. (Versions 8.12.x and 8.13.x are available only as alpha or release candidates[10].)

Gradle compatibility:
AGP 8.11.x requires Gradle 8.13 as the minimum and default supported version[9].
Older AGP 8.10.x and 8.9.x require Gradle 8.11.1[1][7].
There is no official stable AGP release yet supporting Gradle 9.0 (the latest Gradle release[6][3]), so use Gradle 8.13 when using AGP 8.11.x[9].

Summary Table

AGP Version Stable Release? Required Gradle Version
8.11.1 Yes 8.13
8.12.x Alpha/RC Not officially stable
8.10.x Yes 8.11.1
  • Official compatibility and further requirements (JDK 17, SDK Build Tools 35.0.0) can be found in the AGP release notes[9].
  • Using Gradle 9.0 with AGP is not currently recommended or officially supported as of August 2025[6][8].

Citations:


🌐 Web query:

Is Kotlin 2.2.0 officially compatible with the latest AGP, and what are the minimum supported Gradle versions?

💡 Result:

Kotlin 2.2.0 is officially compatible with the latest stable Android Gradle Plugin (AGP), and the minimum supported Gradle version for Kotlin 2.2.0 is 7.6.3.

  • According to the official Kotlin documentation, Kotlin 2.2.0 is fully compatible with Gradle 7.6.3 through 8.14[4][7]. You can also use it with newer Gradle versions (up to the latest release), but if you do, you might see deprecation warnings and some new Gradle features may not be available or stable[4][7].
  • The minimum supported Gradle version for Kotlin 2.2.0 is 7.6.3. Your project will not build with older Gradle versions[4][7][10].

Regarding AGP compatibility:

  • Kotlin 2.2.0 can be used with the latest Android Gradle Plugin. As of August 2025, the latest AGP version is 8.12, which requires at least Gradle 8.13[9]. This means using Kotlin 2.2.0 with the latest AGP is supported, provided your Gradle version is at least 8.13 (to meet AGP’s requirements)[9].
  • The official compatibility matrix and documentation note that forward compatibility between the Kotlin Gradle plugin and Kotlin compiler or libraries is not always guaranteed, so it’s best to keep all related tooling aligned as much as possible[2][4].

Summary Table

Kotlin Version Minimum Gradle Version Latest Fully Supported Gradle Version Notes on AGP
2.2.0 7.6.3 8.14 Compatible with latest AGP (e.g., 8.12, which requires Gradle 8.13+)
  • You should ensure your build tools are at least the minimum versions listed above for reliable support.
  • For maximum stability, match Kotlin, AGP, and Gradle versions as closely as possible, and pay attention to any warnings about deprecations or incompatibilities in your build output.

References:

  • Official documentation and compatibility guidance[4][7][10].
  • AGP and Gradle version requirements[9].
  • Recent discussions in the Kotlin community confirming all of the above[2][10].

Citations:


Action required — align Gradle wrapper with AGP 8.11.1 (or upgrade AGP)

Root build.gradle uses AGP 8.11.1 but gradle-wrapper is set to Gradle 9.0.0. AGP 8.11.1 requires Gradle 8.13; Gradle 9.0 is not officially supported for this AGP release.

Files to fix:

  • build.gradle — lines ~10-12: id 'com.android.application' version '8.11.1' apply false
  • gradle/wrapper/gradle-wrapper.properties — distributionUrl currently points to gradle-9.0.0

Suggested change (preferred): downgrade wrapper to Gradle 8.13:
--- gradle/wrapper/gradle-wrapper.properties
+++ gradle/wrapper/gradle-wrapper.properties
@@
-distributionUrl=https://services.gradle.org/distributions/gradle-9.0.0-bin.zip
+distributionUrl=https://services.gradle.org/distributions/gradle-8.13-bin.zip

Notes: Kotlin 2.2.0 is compatible with AGP 8.11.1; the primary issue is the Gradle wrapper mismatch. If you intentionally want Gradle 9.0, update AGP to a version that officially supports Gradle 9 (alpha/RC only as of Aug 2025) or accept instability.

🤖 Prompt for AI Agents
In build.gradle around lines 10-12 the project uses AGP 8.11.1 while
gradle/wrapper/gradle-wrapper.properties points to Gradle 9.0.0; change the
wrapper to Gradle 8.13 by updating distributionUrl in
gradle/wrapper/gradle-wrapper.properties to point to gradle-8.13-bin.zip (or
alternatively upgrade the AGP to a version that officially supports Gradle 9 if
that is intentional), then run ./gradlew wrapper to regenerate wrapper files and
verify builds succeed.

Copy link

@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

🔭 Outside diff range comments (2)
.github/workflows/build.yml (2)

9-14: Set explicit token permissions and avoid exporting GITHUB_TOKEN globally

If Gradle resolves dependencies from GitHub Packages, you’ll need packages: read. Also, avoid exposing GITHUB_TOKEN at job scope; inject it only where needed to minimize leak surface.

  • Add permissions at the job level:
   build:
     runs-on: ubuntu-latest
     environment: Development
+    permissions:
+      contents: read
+      packages: read
     env:
       ACRA_LOGIN: ${{ secrets.ACRARIUM_BASIC_AUTH_LOGIN }}
       ACRA_PASS: ${{ secrets.ACRARIUM_BASIC_AUTH_PASSWORD }}
       ACRA_URI: ${{ secrets.ACRARIUM_URI }}
-      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • If Gradle needs the token, pass it only to the build step (outside this range), e.g.:
- name: Build Release APK
  run: ./gradlew --no-daemon --stacktrace assembleRelease
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Note: GITHUB_TOKEN is provided automatically by GitHub; you typically don’t need to re-expose it unless your build expects it in the environment explicitly. Ensure no tasks log it.


35-52: Fail fast on missing APKs and reduce artifact surprises

Upload step should error if files are missing to catch build or path issues early.

Apply these diffs:

       - name: Upload Release arm64-v8a APK
         uses: actions/upload-artifact@v4
         with:
           name: release-arm64-v8a-apk
           path: ./app/build/outputs/apk/release/app-arm64-v8a-release.apk
+          if-no-files-found: error
       - name: Upload Release armeabi-v7a APK
         uses: actions/upload-artifact@v4
         with:
           name: release-armeabi-v7a-apk
           path: ./app/build/outputs/apk/release/app-armeabi-v7a-release.apk
+          if-no-files-found: error
       - name: Upload Release universal APK
         uses: actions/upload-artifact@v4
         with:
           name: release-universal-apk
           path: ./app/build/outputs/apk/release/app-universal-release.apk
+          if-no-files-found: error

Optional:

  • Add retention-days to control storage costs.
  • Alternatively, upload all APKs in one step using a glob if you don’t need separate artifacts.
🧹 Nitpick comments (3)
.github/workflows/build.yml (3)

15-15: Optional: Pin actions to commit SHA for supply-chain hardening

Pinning to a specific SHA reduces the risk of supply-chain attacks. Keep the major version as a comment for clarity.

Example:

- uses: actions/checkout@v4
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82c2376cf9d2c1a0f0 # v4

32-34: Improve Gradle invocation for CI ergonomics

Add --no-daemon and --stacktrace for CI. Optionally adopt gradle/actions/setup-gradle for better caching and config-cache integration.

Apply this diff:

-      - name: Build Release APK
-        run: ./gradlew assembleRelease
+      - name: Build Release APK
+        run: ./gradlew --no-daemon --stacktrace assembleRelease

Optional caching improvement (place before the build step):

- name: Setup Gradle
  uses: gradle/actions/setup-gradle@v3

5-9: Consider removing the environment binding for a pure build job

Binding the job to an environment (Development) can require approvals and block CI. If you only need repo secrets (not environment-scoped secrets), drop the environment to streamline builds.

Proposed change:

   build:
     runs-on: ubuntu-latest
-    environment: Development
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 15add0b and fecaa2f.

📒 Files selected for processing (1)
  • .github/workflows/build.yml (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
.github/workflows/build.yml (2)

24-26: LGTM: Using the official Gradle wrapper validation

Correct placement and version. This helps protect against tampered wrappers.


15-26: Order of steps looks correct

Checkout precedes wrapper validation, and Java setup precedes Gradle. No issues here after fixing the JDK distribution.

name: Build the app

on: [push]
on: [ push ]
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Broaden triggers: build on PRs and allow manual runs

Currently runs only on push. Recommend adding pull_request and workflow_dispatch and restricting branches to reduce noise.

Apply this diff:

-on: [ push ]
+on:
+  push:
+    branches: [ main, develop ]
+  pull_request:
+    branches: [ main, develop ]
+  workflow_dispatch:
📝 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
on: [ push ]
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
🤖 Prompt for AI Agents
In .github/workflows/build.yml at line 3, the workflow is only triggered on push
which is noisy and misses PRs and manual runs; update the workflow "on:" section
to include pull_request (restricted to the same target branches as push), add
workflow_dispatch for manual runs, and scope push/pull_request to a branches
list (e.g., main, release/*) to reduce noise; ensure YAML indentation is correct
when adding these keys.

Comment on lines +17 to +23
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'adopt'
cache: gradle

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix: setup-java v4 no longer supports distribution 'adopt'

actions/setup-java@v4 does not accept 'adopt'. Use 'temurin' for Java 17 (AdoptOpenJDK migrated to Eclipse Temurin).

Apply this diff:

-      - name: Set up JDK 17
+      - name: Set up Temurin JDK 17
         uses: actions/setup-java@v4
         with:
           java-version: '17'
-          distribution: 'adopt'
+          distribution: 'temurin'
           cache: gradle
📝 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
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
🤖 Prompt for AI Agents
.github/workflows/build.yml around lines 17 to 23: the workflow uses
actions/setup-java@v4 with distribution: 'adopt' which is no longer supported;
update the distribution value to 'temurin' (keeping java-version: '17' and
cache: gradle) so the setup-java step uses Eclipse Temurin for Java 17.

Comment on lines 27 to 31
- name: Decrypt the keystore for signing
run: |
echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc
gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" --batch keystore.asc > keystore.jks
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden GPG decryption and ensure non-interactive mode

Add --pinentry-mode loopback for non-interactive passphrase entry, use --output for clarity, remove stray spaces, and consider cleanup to avoid leaving secrets on disk.

Apply this diff:

       - name: Decrypt the keystore for signing
         run: |
           echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc
-          gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD   }}" --batch keystore.asc > keystore.jks
+          gpg --batch --yes --pinentry-mode loopback \
+              --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" \
+              --output keystore.jks \
+              --decrypt keystore.asc

Additionally, add a cleanup step after uploads:

- name: Cleanup signing material
  if: always()
  run: |
    rm -f keystore.asc keystore.jks
🤖 Prompt for AI Agents
.github/workflows/build.yml around lines 27 to 31: the GPG decryption step is
not fully non-interactive, includes stray spaces in the secret variable
reference, doesn’t use --output for clarity, and leaves secrets on disk; update
the gpg command to add --pinentry-mode loopback, remove the extra spaces in the
passphrase variable, and use --output keystore.jks (or equivalent) instead of
redirection, and add a post-upload cleanup step that always runs to remove
keystore.asc and keystore.jks so signing material is not left on disk.

…tracking-2

# Conflicts:
#	.github/workflows/build.yml
#	app/build.gradle
#	app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/activities/MainActivity.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/NoteDeleteDialog.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/NotesViewModel.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/utils/Config.kt
#	app/src/main/java/dev/arkbuilders/arkmemo/utils/Constants.kt
#	app/src/main/res/layout/fragment_edit_notes.xml
#	app/src/main/res/layout/fragment_notes.xml
#	app/src/main/res/layout/note.xml
#	build.gradle
#	gradle/wrapper/gradle-wrapper.properties
#	settings.gradle
Copy link

@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

Caution

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

⚠️ Outside diff range comments (3)
app/src/main/res/layout/dialog_common_action.xml (1)

20-56: Restore @+id when declaring these views

Switching the first declaration of each view from @+id/... to @id/... means aapt now expects those IDs to be pre-defined (e.g., via ids.xml). Because they aren’t, the layout no longer provides IDs for tv_title, iv_close, tv_message, and tv_positive, so references to them resolve to 0 and the layout fails to build/inflate. Please revert these attributes back to @+id/....

-        android:id="@id/tv_title" />
+        android:id="@+id/tv_title" />
...
-        android:id="@id/iv_close" />
+        android:id="@+id/iv_close" />
...
-        android:id="@id/tv_message" />
+        android:id="@+id/tv_message" />
...
-        android:id="@id/tv_positive" />
+        android:id="@+id/tv_positive" />
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (1)

183-195: Keep data model in sync on Pause/Stop/Resume

Only the UI is updated; the underlying VoiceNote flags aren’t adjusted. On rebind/recycle, the row can show an incorrect state. Update the item’s fields for consistency.

Apply this diff:

             is ArkMediaPlayerSideEffect.PausePlaying -> {
                 showPlaybackIdleState(holder, isPaused = true)
+                val pos = holder.bindingAdapterPosition
+                if (pos >= 0) {
+                    val item = notes[pos]
+                    if (item is VoiceNote) {
+                        item.isPlaying = false
+                        item.waitToBeResumed = true
+                    }
+                }
             }
 
             is ArkMediaPlayerSideEffect.StopPlaying -> {
                 showPlaybackIdleState(holder)
                 holder.tvPlayingPosition.gone()
+                val pos = holder.bindingAdapterPosition
+                if (pos >= 0) {
+                    val item = notes[pos]
+                    if (item is VoiceNote) {
+                        item.isPlaying = false
+                        item.pendingForPlaybackReset = true
+                        item.waitToBeResumed = false
+                    }
+                }
             }
 
             is ArkMediaPlayerSideEffect.ResumePlaying -> {
                 showPlayingState(holder)
+                val pos = holder.bindingAdapterPosition
+                if (pos >= 0) {
+                    val item = notes[pos]
+                    if (item is VoiceNote) {
+                        item.isPlaying = true
+                        item.waitToBeResumed = false
+                    }
+                }
             }
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (1)

193-205: Save button enablement wrongly depends on clipboard content

This disables Save when clipboard is empty, even if note content changed.

Fix to use content state only:

-            enableSaveText(!clipboardTextEmpty && isContentChanged())
+            enableSaveText(isContentChanged() && !isContentEmpty())
🧹 Nitpick comments (9)
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/BrushAdapter.kt (1)

78-97: Consider generalizing BrushColor tint resolution.

With the additional color variants, the when branch now repeats the same imageTintList assignment multiple times. If the BrushColor sealed hierarchy can expose a @ColorRes (e.g., via constructor parameter), you could collapse these into a single is BrushColor branch and keep future color additions self-contained. Optional clean-up, no behavior change.

app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (1)

303-306: Fail‑safe default for storage check

Defaulting to true enables editing when not in NotesFragment. If the intent is to block editing when storage can’t be verified, prefer a false default.

Apply this diff:

-                val storageFolderExist =
-                    (activity.fragment as? NotesFragment)?.checkForStorageExistence() ?: true
+                val storageFolderExist =
+                    (activity.fragment as? NotesFragment)?.checkForStorageExistence() ?: false

Please confirm the desired behavior outside NotesFragment.

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (2)

101-102: Remove empty TextWatcher method or switch to the lambda extension

The empty beforeTextChanged block triggers detekt. Prefer the addTextChangedListener lambda to avoid no-op overrides.

Apply this refactor:

-        val noteTitleChangeListener =
-            object : TextWatcher {
-                override fun beforeTextChanged(
-                    s: CharSequence?,
-                    start: Int,
-                    count: Int,
-                    after: Int,
-                ) {
-                }
-
-                override fun onTextChanged(
-                    s: CharSequence?,
-                    start: Int,
-                    before: Int,
-                    count: Int,
-                ) {
-                    title = s?.toString() ?: ""
-                    if (title.isEmpty()) {
-                        binding.edtTitle.hint = getString(R.string.hint_new_text_note)
-                    }
-                    enableSaveText(isContentChanged() && !isContentEmpty())
-                }
-
-                override fun afterTextChanged(s: Editable?) {}
-            }
+        noteTitle.addTextChangedListener(
+            onTextChanged = { s, _, _, _ ->
+                title = s?.toString() ?: ""
+                if (title.isEmpty()) {
+                    binding.edtTitle.hint = getString(R.string.hint_new_text_note)
+                }
+                enableSaveText(isContentChanged() && !isContentEmpty())
+            },
+        )

And remove the usage below:

-        noteTitle.addTextChangedListener(noteTitleChangeListener)

175-178: Unregister listener before calling super.onDestroyView()

Avoid relying on view being non-null after super.

-    override fun onDestroyView() {
-        super.onDestroyView()
-        view?.viewTreeObserver?.removeOnWindowFocusChangeListener(windowFocusedListener)
-    }
+    override fun onDestroyView() {
+        view?.viewTreeObserver?.removeOnWindowFocusChangeListener(windowFocusedListener)
+        super.onDestroyView()
+    }
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt (1)

21-21: Make serialization deterministic

Ordering a Set is undefined; sort for stable files and diffs.

-    override fun valueToString(value: Versions): String = value.joinToString(",")
+    override fun valueToString(value: Versions): String =
+        value.map { it.toString() }.sorted().joinToString(",")
app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (2)

81-86: Guard reads on initialization and remove nested call

Return safe defaults before init completes.

-        fun getChildNotParentIds(): List<ResourceId> = versionStorage.childrenNotParents()
+        fun getChildNotParentIds(): List<ResourceId> =
+            if (isInitialized()) versionStorage.childrenNotParents() else emptyList()
 
-        fun isNotVersioned(id: ResourceId): Boolean = !isVersioned(id)
-
-        fun isVersioned(id: ResourceId): Boolean = versionStorage.contains(id)
+        fun isNotVersioned(id: ResourceId): Boolean = !isVersioned(id)
+        fun isVersioned(id: ResourceId): Boolean =
+            if (isInitialized()) versionStorage.contains(id) else false

87-94: Ensure init for delete and latest-version checks

Avoids access before storage is ready.

         fun onDelete(id: ResourceId) {
             viewModelScope.launch {
+                ensureInitialized()
                 versionStorage.forget(id)
             }
         }
 
-        fun isLatestResource(id: ResourceId): Boolean = versionStorage.isLatestResourceVersion(id)
+        fun isLatestResource(id: ResourceId): Boolean =
+            if (isInitialized()) versionStorage.isLatestResourceVersion(id) else false

Add missing import once (outside shown range):

import kotlinx.coroutines.CompletableDeferred
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1)

108-126: Optimize parent-chain traversal to O(depth)

Current loop redundantly scans versions on each step.

-    override fun parentsTreeByChild(child: ResourceId): Map<ResourceId, List<ResourceId>> {
-        var localChild = child
-        var parent: ResourceId?
-        val parents = mutableListOf<ResourceId>()
-        for (version in versions) {
-            parent =
-                versions.find {
-                    it.child == localChild
-                }?.parent
-            if (parent != null && children.contains(parent)) {
-                localChild = parent
-            }
-            if (parent != null) parents.add(parent)
-            if (!children.contains(parent)) {
-                break
-            }
-        }
-        return mapOf(child to parents)
-    }
+    override fun parentsTreeByChild(child: ResourceId): Map<ResourceId, List<ResourceId>> {
+        val parentByChild = versions.associate { it.child to it.parent }
+        val chain = mutableListOf<ResourceId>()
+        var current = child
+        while (true) {
+            val parent = parentByChild[current] ?: break
+            chain.add(parent)
+            if (!children.contains(parent)) break
+            current = parent
+        }
+        return mapOf(child to chain)
+    }
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (1)

86-92: Tidy up the empty beforeTextChanged override

Detekt still flags this empty block. Please collapse it to an expression body (or add a no-op comment) so the warning goes away. One option:

-                override fun beforeTextChanged(
-                    s: CharSequence?,
-                    start: Int,
-                    count: Int,
-                    after: Int,
-                ) {
-                }
+                override fun beforeTextChanged(
+                    s: CharSequence?,
+                    start: Int,
+                    count: Int,
+                    after: Int,
+                ) = Unit

Based on static analysis hints.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fecaa2f and 7654b46.

📒 Files selected for processing (107)
  • .github/workflows/build.yml (1 hunks)
  • app/build.gradle (1 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/graphics/SVG.kt (4 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Versions.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorageRepo.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/BrushAdapter.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (4 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/AboutFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt (6 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (2 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt (3 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (2 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/views/SettingTextView.kt (1 hunks)
  • app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt (1 hunks)
  • app/src/main/res/drawable/audio_wave_thumb.xml (1 hunks)
  • app/src/main/res/drawable/bg_audio_view.xml (1 hunks)
  • app/src/main/res/drawable/bg_big_radius.xml (1 hunks)
  • app/src/main/res/drawable/bg_border_r8.xml (1 hunks)
  • app/src/main/res/drawable/bg_brush_color_black.xml (1 hunks)
  • app/src/main/res/drawable/bg_brush_option_selected.xml (1 hunks)
  • app/src/main/res/drawable/bg_brush_size.xml (1 hunks)
  • app/src/main/res/drawable/bg_graphic_control_text_selected.xml (1 hunks)
  • app/src/main/res/drawable/bg_negative_button.xml (1 hunks)
  • app/src/main/res/drawable/bg_positive_button.xml (1 hunks)
  • app/src/main/res/drawable/bg_rect_button.xml (1 hunks)
  • app/src/main/res/drawable/bg_red_button.xml (1 hunks)
  • app/src/main/res/drawable/bg_round_button.xml (1 hunks)
  • app/src/main/res/drawable/bg_selected_brush.xml (1 hunks)
  • app/src/main/res/drawable/bg_tag.xml (1 hunks)
  • app/src/main/res/drawable/ic_add.xml (1 hunks)
  • app/src/main/res/drawable/ic_app_logo.xml (1 hunks)
  • app/src/main/res/drawable/ic_attach.xml (1 hunks)
  • app/src/main/res/drawable/ic_brush.xml (1 hunks)
  • app/src/main/res/drawable/ic_btc.xml (1 hunks)
  • app/src/main/res/drawable/ic_chevron_down.xml (1 hunks)
  • app/src/main/res/drawable/ic_chevron_left.xml (1 hunks)
  • app/src/main/res/drawable/ic_chevron_right.xml (1 hunks)
  • app/src/main/res/drawable/ic_clipboard.xml (1 hunks)
  • app/src/main/res/drawable/ic_close.xml (1 hunks)
  • app/src/main/res/drawable/ic_coffee.xml (1 hunks)
  • app/src/main/res/drawable/ic_copy.xml (1 hunks)
  • app/src/main/res/drawable/ic_crash_report.xml (1 hunks)
  • app/src/main/res/drawable/ic_debug_log.xml (1 hunks)
  • app/src/main/res/drawable/ic_delete_note.xml (1 hunks)
  • app/src/main/res/drawable/ic_discord.xml (1 hunks)
  • app/src/main/res/drawable/ic_download.xml (1 hunks)
  • app/src/main/res/drawable/ic_draw.xml (1 hunks)
  • app/src/main/res/drawable/ic_edit.xml (1 hunks)
  • app/src/main/res/drawable/ic_eraser.xml (1 hunks)
  • app/src/main/res/drawable/ic_eth.xml (1 hunks)
  • app/src/main/res/drawable/ic_feedback.xml (1 hunks)
  • app/src/main/res/drawable/ic_globe.xml (1 hunks)
  • app/src/main/res/drawable/ic_loading.xml (1 hunks)
  • app/src/main/res/drawable/ic_mic.xml (1 hunks)
  • app/src/main/res/drawable/ic_no_notes.xml (1 hunks)
  • app/src/main/res/drawable/ic_patreon.xml (1 hunks)
  • app/src/main/res/drawable/ic_pause.xml (1 hunks)
  • app/src/main/res/drawable/ic_pause_circle.xml (1 hunks)
  • app/src/main/res/drawable/ic_photo.xml (1 hunks)
  • app/src/main/res/drawable/ic_pin.xml (1 hunks)
  • app/src/main/res/drawable/ic_play.xml (1 hunks)
  • app/src/main/res/drawable/ic_play_circle.xml (1 hunks)
  • app/src/main/res/drawable/ic_privacy.xml (1 hunks)
  • app/src/main/res/drawable/ic_rate.xml (1 hunks)
  • app/src/main/res/drawable/ic_record.xml (1 hunks)
  • app/src/main/res/drawable/ic_record_big.xml (1 hunks)
  • app/src/main/res/drawable/ic_record_new.xml (1 hunks)
  • app/src/main/res/drawable/ic_record_ongoing.xml (1 hunks)
  • app/src/main/res/drawable/ic_record_start_over.xml (1 hunks)
  • app/src/main/res/drawable/ic_search.xml (1 hunks)
  • app/src/main/res/drawable/ic_search_result_empty.xml (1 hunks)
  • app/src/main/res/drawable/ic_selected_brush_type.xml (1 hunks)
  • app/src/main/res/drawable/ic_settings.xml (1 hunks)
  • app/src/main/res/drawable/ic_stop.xml (1 hunks)
  • app/src/main/res/drawable/ic_stop_record.xml (1 hunks)
  • app/src/main/res/drawable/ic_telegram.xml (1 hunks)
  • app/src/main/res/drawable/ic_terms_of_service.xml (1 hunks)
  • app/src/main/res/drawable/ic_theme.xml (1 hunks)
  • app/src/main/res/drawable/ic_toolbar_about.xml (1 hunks)
  • app/src/main/res/drawable/ripple_border_primary_r8.xml (1 hunks)
  • app/src/main/res/drawable/ripple_donate_copy_text.xml (1 hunks)
  • app/src/main/res/drawable/ripple_note_item.xml (1 hunks)
  • app/src/main/res/drawable/ripple_support_text.xml (1 hunks)
  • app/src/main/res/drawable/ripple_web_link_text.xml (1 hunks)
  • app/src/main/res/layout/activity_main.xml (1 hunks)
  • app/src/main/res/layout/adapter_tag.xml (1 hunks)
  • app/src/main/res/layout/adapter_text_note.xml (7 hunks)
  • app/src/main/res/layout/dialog_common_action.xml (5 hunks)
⛔ Files not processed due to max files limit (24)
  • app/src/main/res/layout/dialog_donate_qr.xml
  • app/src/main/res/layout/fragment_about.xml
  • app/src/main/res/layout/fragment_edit_notes.xml
  • app/src/main/res/layout/fragment_home.xml
  • app/src/main/res/layout/fragment_settings.xml
  • app/src/main/res/layout/layout_audio_record.xml
  • app/src/main/res/layout/layout_brush_color_chooser.xml
  • app/src/main/res/layout/layout_brush_size_chooser.xml
  • app/src/main/res/layout/layout_graphic_bottom_control.xml
  • app/src/main/res/layout/layout_setting_text.xml
  • app/src/main/res/layout/layout_support_text.xml
  • app/src/main/res/layout/layout_web_link_text.xml
  • app/src/main/res/layout/media_player_view.xml
  • app/src/main/res/layout/note.xml
  • app/src/main/res/layout/toolbar.xml
  • app/src/main/res/values/attrs.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values/styles.xml
  • app/src/main/res/values/switch_button_attrs.xml
  • app/src/test/java/dev/arkbuilders/arkmemo/VersionStorageTest.kt
  • gradle/libs.versions.toml
  • logging/src/main/AndroidManifest.xml
  • logging/src/main/java/dev/arkbuilders/logging/ArkLogImpl.kt
  • settings.gradle
✅ Files skipped from review due to trivial changes (56)
  • app/src/main/res/drawable/ic_chevron_left.xml
  • app/src/main/res/drawable/ic_record_start_over.xml
  • app/src/main/res/drawable/ic_eth.xml
  • app/src/main/res/drawable/ic_attach.xml
  • app/src/main/res/drawable/ripple_note_item.xml
  • app/src/main/res/drawable/ic_stop.xml
  • app/src/main/res/drawable/bg_rect_button.xml
  • app/src/main/res/drawable/ic_pause.xml
  • app/src/main/res/drawable/bg_round_button.xml
  • app/src/main/res/drawable/ic_record_ongoing.xml
  • app/src/main/res/drawable/ic_brush.xml
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkRecorderFragment.kt
  • app/src/main/res/drawable/bg_red_button.xml
  • app/src/main/res/drawable/bg_brush_size.xml
  • app/src/main/res/drawable/bg_graphic_control_text_selected.xml
  • app/src/main/res/drawable/ic_chevron_down.xml
  • app/src/main/res/drawable/ic_edit.xml
  • app/src/main/res/drawable/bg_audio_view.xml
  • app/src/main/res/drawable/ic_draw.xml
  • app/src/main/res/drawable/ic_no_notes.xml
  • app/src/main/res/drawable/audio_wave_thumb.xml
  • app/src/main/res/drawable/ic_coffee.xml
  • app/src/main/res/drawable/ic_pin.xml
  • app/src/main/res/drawable/ic_app_logo.xml
  • app/src/main/res/drawable/bg_brush_option_selected.xml
  • app/src/main/res/drawable/ic_settings.xml
  • app/src/main/res/drawable/bg_border_r8.xml
  • app/src/main/java/dev/arkbuilders/arkmemo/graphics/SVG.kt
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/res/drawable/bg_selected_brush.xml
  • app/src/main/res/drawable/ic_toolbar_about.xml
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/BaseEditNoteFragment.kt
  • app/src/main/res/drawable/ic_record_new.xml
  • app/src/main/res/drawable/ripple_web_link_text.xml
  • app/src/main/res/drawable/ic_feedback.xml
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/dialogs/FilePickerDialog.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/views/SettingTextView.kt
  • app/src/main/res/drawable/ic_chevron_right.xml
  • app/src/main/res/drawable/ripple_border_primary_r8.xml
  • app/src/main/res/drawable/ic_clipboard.xml
  • app/src/main/res/drawable/ic_globe.xml
  • app/src/main/res/drawable/bg_brush_color_black.xml
  • app/src/main/res/drawable/ic_play.xml
  • app/src/main/res/layout/adapter_tag.xml
  • app/src/main/res/drawable/ic_pause_circle.xml
  • app/build.gradle
  • app/src/main/res/drawable/bg_positive_button.xml
  • app/src/main/res/drawable/ic_record_big.xml
  • app/src/main/res/drawable/ic_stop_record.xml
  • app/src/main/res/drawable/bg_negative_button.xml
  • app/src/main/res/drawable/ic_discord.xml
  • app/src/main/res/drawable/ic_close.xml
  • app/src/main/res/drawable/ic_privacy.xml
  • app/src/main/res/drawable/ic_search.xml
  • app/src/main/res/drawable/ic_rate.xml
  • app/src/main/res/drawable/ic_patreon.xml
🚧 Files skipped from review as they are similar to previous changes (9)
  • app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorageRepo.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorageRepo.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/utils/ArkFiles.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Versions.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionsStorage.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/VersionsFragment.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt
  • app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/NotesFragment.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T05:33:52.382Z
Learnt from: tuancoltech
PR: ARK-Builders/ARK-Memo#0
File: :0-0
Timestamp: 2025-07-18T05:33:52.382Z
Learning: In the ARK-Memo Android app, TextNotesRepo.findNote() intentionally returns null because text notes don't need special handling during view restoration. This is a design decision to ignore text notes when restoring view state, as users can easily continue editing text notes without complex state recovery mechanisms.

Applied to files:

  • app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt
📚 Learning: 2025-07-18T05:40:19.372Z
Learnt from: tuancoltech
PR: ARK-Builders/ARK-Memo#0
File: :0-0
Timestamp: 2025-07-18T05:40:19.372Z
Learning: In the ARK-Memo Android app, the notesViewModel.init {} callback properly handles threading by dispatching operations to worker threads, preventing main thread blocking during view restoration.

Applied to files:

  • app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
🧬 Code graph analysis (1)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (1)
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt (2)
  • childrenNotParents (17-17)
  • parentsTreeByChild (15-15)
🪛 detekt (1.23.8)
app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditGraphicNotesFragment.kt

[warning] 91-92: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt

[warning] 101-102: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (43)
app/src/main/res/drawable/ripple_support_text.xml (1)

2-11: Formatting-only touch looks fine

The XML attributes are just reflowed; behavior stays unchanged. No further action needed.

app/src/main/res/drawable/ic_search_result_empty.xml (1)

6-12: Formatting-only vector tweak looks good

No semantic or rendering changes detected; indentation now matches the surrounding XML style.

app/src/main/res/drawable/bg_tag.xml (1)

4-8: Formatting looks good

Splitting the stroke, corners, and solid attributes onto their own lines improves readability without changing behavior.

app/src/main/res/drawable/ic_crash_report.xml (1)

1-11: Vector drawable formatting LGTM

Only indentation and line breaks changed; drawable attributes remain intact.

app/src/main/res/drawable/ic_record.xml (1)

1-9: Formatting update looks good

No functional changes introduced; vector definition remains consistent.

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/AboutFragment.kt (1)

34-35: Formatting update looks good

No behavioral changes introduced; the version string binding remains the same.

app/src/main/res/drawable/ic_selected_brush_type.xml (1)

6-14: Formatting update looks good

Re-indentation only; vector drawing data remains unchanged.

app/src/main/res/drawable/ic_theme.xml (1)

6-12: LGTM — formatting only

Vector path attributes remain unchanged; reformatting keeps rendering identical while matching project style.

app/src/main/res/drawable/ic_telegram.xml (1)

6-12: Vector formatting change looks good

Only whitespace/layout adjustments; rendering remains identical.

app/src/main/res/drawable/bg_big_radius.xml (1)

4-8: Formatting cleanup looks good

Attribute layout is clearer now with no functional changes introduced.

app/src/main/res/drawable/ic_delete_note.xml (1)

6-12: LGTM – formatting only. Rendering semantics remain unchanged.

app/src/main/res/drawable/ic_copy.xml (1)

6-12: LGTM – formatting only. Icon attributes are untouched, so behavior is preserved.

app/src/main/res/drawable/ic_mic.xml (1)

1-9: Formatting change looks good

Attribute values are untouched and the drawable will render identically; thanks for cleaning up the formatting.

app/src/main/res/drawable/ic_add.xml (1)

6-12: Vector drawable formatting looks good

Indentation and self-closing tag updates are purely cosmetic; no rendering or behavioral changes introduced.

app/src/main/AndroidManifest.xml (1)

12-13: No functional change detected

Whitespace-only tweak on the self-closing permission tags—manifest semantics remain identical.

app/src/main/res/drawable/ic_terms_of_service.xml (1)

6-12: LGTM on the drawable formatting

Indentation-only tweak; vector semantics remain untouched.

app/src/main/res/layout/adapter_text_note.xml (1)

1-128: Touched-up layout reads cleanly

Only formatting/ordering tweaks; view hierarchy and attrs remain unchanged. Looks good.

app/src/main/res/drawable/ripple_donate_copy_text.xml (1)

2-13: LGTM — ripple formatting

Only whitespace/formatting adjustments; drawable behavior stays the same.

app/src/main/res/drawable/ic_btc.xml (1)

6-11: Vector formatting cleanup looks good.

The path data and colors remain unchanged, so this is a safe formatting-only touchup.

app/src/main/res/drawable/ic_eraser.xml (1)

6-12: Vector formatting update looks good

Attribute set is unchanged and the reformatting keeps the drawable behavior identical while improving readability.

app/src/main/res/drawable/ic_download.xml (1)

6-12: Arrow path update looks good

Stroke-only path still renders the download arrow cleanly with the correct cap/join settings.

.github/workflows/build.yml (2)

51-94: Harden keystore handling and ensure cleanup.

Please make the GPG step fully non-interactive and remove signing material after artifact uploads. Suggested update:

       - name: Decrypt the keystore for signing
         run: |
           echo "${{ secrets.KEYSTORE_ENCRYPTED }}" > keystore.asc
-          gpg -d --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" --batch keystore.asc > keystore.jks
+          gpg --batch --yes --pinentry-mode loopback \
+              --passphrase "${{ secrets.KEYSTORE_PASSWORD }}" \
+              --output keystore.jks \
+              --decrypt keystore.asc
+
+      - name: Cleanup signing material
+        if: always()
+        run: |
+          rm -f keystore.asc keystore.jks

21-26: Fix broken JDK setup inputs.

actions/setup-java@v4 no longer accepts distribution: 'adopt', so every job that hits this step fails before running Gradle. Switch to temurin (the Eclipse successor) in each setup block.

-          distribution: 'adopt'
+          distribution: 'temurin'

Also applies to: 41-46, 103-107, 124-128

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/ArkMediaPlayerFragment.kt (1)

109-120: Playback controls now stay in sync with pause/stop/resume

Thanks for mirroring the pause/stop/resume icon handling from the list adapter—the fragment’s controls no longer get stuck in the wrong state when side effects come in.

app/src/main/java/dev/arkbuilders/arkmemo/ui/views/NotesCanvas.kt (1)

61-87: Still emitting duplicate DrawPath updates per stroke

finishDrawing prevents the empty Path, but we still call onDrawPath on every DOWN/MOVE, so the view model keeps receiving the same mutable instance multiple times. Please finalize the stroke inside ACTION_UP (as previously discussed) and drop the global dispatch.

-        var finishDrawing = false
         when (event.action) {
             MotionEvent.ACTION_DOWN -> {
                 path.moveTo(x, y)
                 viewModel.svg().apply {
                     addCommand(
                         SVGCommand.MoveTo(x, y).apply {
                             paintColor = viewModel.paint.color.getStrokeColor()
                             brushSizeId = viewModel.paint.strokeWidth.getBrushSizeId()
                         },
                     )
                 }
                 currentX = x
                 currentY = y
             }
 
             MotionEvent.ACTION_MOVE -> {
                 val x2 = (currentX + x) / 2
                 val y2 = (currentY + y) / 2
                 path.quadTo(currentX, currentY, x2, y2)
                 viewModel.svg().apply {
                     addCommand(
                         SVGCommand.AbsQuadTo(currentX, currentY, x2, y2).apply {
                             paintColor = viewModel.paint.color.getStrokeColor()
                             brushSizeId = viewModel.paint.strokeWidth.getBrushSizeId()
                         },
                     )
                 }
                 currentX = x
                 currentY = y
             }
 
             MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
-                path = Path()
-                finishDrawing = true
+                val drawPath = DrawPath(path, viewModel.paint)
+                viewModel.onDrawPath(drawPath)
+                invalidate()
+                path = Path()
             }
         }
 
-        if (!finishDrawing) {
-            val drawPath = DrawPath(path, viewModel.paint)
-            viewModel.onDrawPath(drawPath)
-            invalidate()
-        }
-
         return true
app/src/main/java/dev/arkbuilders/arkmemo/ui/adapters/NotesListAdapter.kt (3)

69-71: LGTM: cleaner inflation

Using view binding inflation directly is fine and clearer.


316-319: Avoid sharing mutable note instance with editor

If EditTextNotesFragment mutates the note, it can cause unexpected adapter state. Pass an immutable copy or an ID and load inside the fragment.

If TextNote is a data class:

-                            EditTextNotesFragment.newInstance(selectedNote)
+                            EditTextNotesFragment.newInstance(selectedNote.copy())

Otherwise, ensure the fragment treats the object as read‑only or creates its own copy.


325-328: Same for VoiceNote: pass copy or ID

To prevent shared‑mutable state issues between adapter and ArkRecorderFragment, pass a copy or an ID.

If VoiceNote is a data class:

-                        activity.fragment = ArkRecorderFragment.newInstance(selectedNote)
+                        activity.fragment = ArkRecorderFragment.newInstance(selectedNote.copy())

Or pass a stable identifier and load within the fragment.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt (1)

6-19: Document VersionStorage contract semantics before exposing

We still don’t spell out ordering guarantees or graph semantics for the public methods, so third‑party storage implementers can’t tell how to implement versions, forget, parentsTreeByChild, etc. Re-applying the earlier ask: add concise KDoc (or equivalent) describing edge/node behavior, ordering guarantees, and “latest” semantics before we freeze this API.

app/src/main/res/drawable/ic_loading.xml (1)

6-12: Vector formatting looks good

Only indentation changed; rendering stays identical.

app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt (1)

3-13: Parcelable move to the interface makes sense

Centralizing Parcelable on the interface keeps implementations consistent and avoids accidental omissions.

app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt (1)

10-18: Fork flag integration is straightforward

Adding isForked alongside the other mutable flags keeps the parcelable state aligned with the new versioning needs.

app/src/main/java/dev/arkbuilders/arkmemo/models/VoiceNote.kt (1)

12-27: Voice note fork state exposure looks good

The new flag mirrors other note types and participates in parceling, so UI/state flows can track forks consistently.

app/src/main/java/dev/arkbuilders/arkmemo/models/GraphicNote.kt (1)

12-22: Graphic note fork flag matches the shared contract

Keeping the default false and exposing it in the primary constructor aligns with the updated Note interface.

app/src/main/java/dev/arkbuilders/arkmemo/ui/fragments/EditTextNotesFragment.kt (1)

44-49: LGTM: safer paste handling with try/catch retained

Formatting-only change; behavior unchanged and exception path preserved.

app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt (1)

16-19: Harden deserialization against malformed entries

A bad line will throw and break storage init. Add per-item safe parsing and logging.

-    override fun valueFromString(raw: String): Versions =
-        raw.split(",").filter { it.isNotEmpty() }.map {
-            ResourceId.fromString(it)
-        }.toSet()
+    override fun valueFromString(raw: String): Versions =
+        raw.split(",")
+            .map { it.trim() }
+            .filter { it.isNotEmpty() }
+            .mapNotNull {
+                try {
+                    ResourceId.fromString(it)
+                } catch (_: Exception) {
+                    null
+                }
+            }
+            .toSet()
app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt (1)

69-69: Avoid !! in getParentIds() and handle pre-init state

Return emptyList when not initialized or missing key.

-        fun getParentIds(id: ResourceId): List<ResourceId> = versionStorage.parentsTreeByChild(id)[id]!!
+        fun getParentIds(id: ResourceId): List<ResourceId> =
+            if (isInitialized()) versionStorage.parentsTreeByChild(id)[id].orEmpty() else emptyList()
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt (2)

93-101: Compile-time type mismatch and potential NPE in forget()

Passing nullable versions to replace(...) and force-unwrapping fields will crash if not found.

-            val newVersion =
-                Version(
-                    versionIdIsChild?.parent!!,
-                    versionIdIsParent?.child!!,
-                )
-            replace(versionIdIsChild, newVersion)
-            versions.remove(versionIdIsParent)
+            if (versionIdIsChild == null || versionIdIsParent == null) {
+                Log.e(VERSIONS_STORAGE, "Inconsistent version graph for id=$id")
+                persist()
+                return
+            }
+            val newVersion = Version(versionIdIsChild.parent, versionIdIsParent.child)
+            replace(versionIdIsChild, newVersion)
+            versions.remove(versionIdIsParent)

148-185: Add robust parsing and logging in readStorage()

Malformed files will throw; validate and skip bad lines with diagnostics.

-    private suspend fun readStorage(): VersionsResult =
-        withContext(Dispatchers.IO) {
-            val lines = Files.readAllLines(storageFile)
-            val storageVersion = lines.removeAt(0)
-            verifyVersion(storageVersion)
-            val versions =
-                lines.map {
-                    val parts = it.split(KEY_VALUE_SEPARATOR)
-                    val parent = ResourceId.fromString(parts[0])
-                    val child = ResourceId.fromString(parts[1])
-                    Log.d(
-                        VERSIONS_STORAGE,
-                        it,
-                    )
-                    Version(parent, child)
-                }
-            val parents =
-                versions.map {
-                    Log.d(
-                        VERSIONS_STORAGE,
-                        "parent: ${it.parent}",
-                    )
-                    it.parent
-                }.toSet()
-            val children =
-                versions.map {
-                    Log.d(
-                        VERSIONS_STORAGE,
-                        "child: ${it.child}",
-                    )
-                    it.child
-                }
-            return@withContext VersionsResult(
-                versions,
-                parents,
-                children,
-            )
-        }
+    private suspend fun readStorage(): VersionsResult =
+        withContext(Dispatchers.IO) {
+            try {
+                val lines = Files.readAllLines(storageFile).toMutableList()
+                if (lines.isEmpty()) {
+                    Log.w(VERSIONS_STORAGE, "Storage file is empty: $storageFile")
+                    return@withContext VersionsResult(emptyList(), emptySet(), emptyList())
+                }
+                val storageVersion = lines.removeAt(0)
+                verifyVersion(storageVersion)
+                val versions = lines.mapNotNull { line ->
+                    val parts = line.split(KEY_VALUE_SEPARATOR)
+                    if (parts.size != 2) {
+                        Log.e(VERSIONS_STORAGE, "Malformed line: $line")
+                        return@mapNotNull null
+                    }
+                    try {
+                        val parent = ResourceId.fromString(parts[0])
+                        val child = ResourceId.fromString(parts[1])
+                        Log.d(VERSIONS_STORAGE, line)
+                        Version(parent, child)
+                    } catch (e: Exception) {
+                        Log.e(VERSIONS_STORAGE, "Failed to parse line: $line", e)
+                        null
+                    }
+                }
+                val parents = versions.map { it.parent }.toSet()
+                val children = versions.map { it.child }
+                VersionsResult(versions, parents, children)
+            } catch (e: Exception) {
+                Log.e(VERSIONS_STORAGE, "Failed to read storage", e)
+                throw e
+            }
+        }
app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/GraphicNotesViewModel.kt (1)

30-39: LGTM on the paint getter refactor

Just a formatting tweak; behavior remains unchanged.

app/src/main/res/drawable/ic_debug_log.xml (1)

6-8: Vector drawable formatting looks good

Whitespace-only change; renders identically.

app/src/main/res/drawable/ic_photo.xml (1)

6-12: No issues spotted with the drawable reformat

Attribute values are intact; purely cosmetic change.

app/src/main/res/drawable/ic_play_circle.xml (1)

6-13: Drawable reformat verified

All path data is unchanged; safe formatting adjustment.

Comment on lines +23 to +31
private lateinit var versionStorage: VersionStorage
private val latestResourceId = MutableStateFlow<ResourceId?>(null)

fun init() {
viewModelScope.launch {
versionStorage = versionStorageRepo.provide()
}
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Guard all storage access with initialization

versionStorage is set asynchronously; direct access can crash with UninitializedPropertyAccessException.

         private lateinit var versionStorage: VersionStorage
         private val latestResourceId = MutableStateFlow<ResourceId?>(null)
+        private val initComplete = kotlinx.coroutines.CompletableDeferred<Unit>()
 
         fun init() {
             viewModelScope.launch {
                 versionStorage = versionStorageRepo.provide()
+                initComplete.complete(Unit)
             }
         }

Add helpers outside the shown range:

private suspend fun ensureInitialized() = initComplete.await()
private fun isInitialized() = initComplete.isCompleted
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
around lines 23 to 31, versionStorage is assigned asynchronously and can be
accessed before initialization causing UninitializedPropertyAccessException; add
a CompletableDeferred<Unit> initComplete that you complete after versionStorage
is set, implement two helpers: a suspend ensureInitialized() that awaits
initComplete and a non-suspending isInitialized() that returns
initComplete.isCompleted, then update all places that access versionStorage to
call ensureInitialized() (suspend) or check isInitialized() and handle the
uninitialized case appropriately so every storage access is guarded.

Comment on lines +36 to +43
viewModelScope.launch {
val latestNotes =
notes.filter {
getChildNotParentIds().contains(it.resource?.id) ||
isNotVersioned(it.resource?.id!!)
}
emit(latestNotes)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix NPE and reduce per-item storage calls in getLatestNotes()

Avoid !! on nullable id, ensure storage is ready, and compute latest IDs once.

-            viewModelScope.launch {
-                val latestNotes =
-                    notes.filter {
-                        getChildNotParentIds().contains(it.resource?.id) ||
-                            isNotVersioned(it.resource?.id!!)
-                    }
-                emit(latestNotes)
-            }
+            viewModelScope.launch {
+                ensureInitialized()
+                val latestIds = versionStorage.childrenNotParents().toSet()
+                val latestNotes = notes.filter {
+                    val id = it.resource?.id ?: return@filter false
+                    latestIds.contains(id) || !versionStorage.contains(id)
+                }
+                emit(latestNotes)
+            }
📝 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
viewModelScope.launch {
val latestNotes =
notes.filter {
getChildNotParentIds().contains(it.resource?.id) ||
isNotVersioned(it.resource?.id!!)
}
emit(latestNotes)
}
viewModelScope.launch {
ensureInitialized()
val latestIds = versionStorage.childrenNotParents().toSet()
val latestNotes = notes.filter {
val id = it.resource?.id ?: return@filter false
latestIds.contains(id) || !versionStorage.contains(id)
}
emit(latestNotes)
}
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
around lines 36–43, the filter uses !! on a nullable id and calls
getChildNotParentIds() inside the per-item predicate causing NPE risk and
repeated storage calls; first ensure the storage or repository is ready (check
or await a readiness method and return/emit empty if not ready), then call
getChildNotParentIds() once before launching the filter to compute a Set of
latest IDs, and rewrite the filter to use safe calls (e.g., it.resource?.id)
with null-safe checks (filter out null ids with let or filterNotNull) instead of
!! so no null is dereferenced.

Comment on lines +50 to +61
viewModelScope.launch {
latestResourceId.collectLatest { id ->
id?.let {
val familyTree =
notes.filter {
getParentIds(id).contains(it.resource?.id) || it.resource?.id == id
}
emit(familyTree)
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Ensure init and handle null IDs in getLatestNoteFamilyTree()

Precompute parents set and avoid nullable comparisons inside filter.

-            viewModelScope.launch {
-                latestResourceId.collectLatest { id ->
-                    id?.let {
-                        val familyTree =
-                            notes.filter {
-                                getParentIds(id).contains(it.resource?.id) || it.resource?.id == id
-                            }
-                        emit(familyTree)
-                    }
-                }
-            }
+            viewModelScope.launch {
+                ensureInitialized()
+                latestResourceId.collectLatest { id ->
+                    id?.let { target ->
+                        val parentIds = versionStorage.parentsTreeByChild(target)[target].orEmpty().toSet()
+                        val familyTree = notes.filter {
+                            val rid = it.resource?.id ?: return@filter false
+                            rid == target || parentIds.contains(rid)
+                        }
+                        emit(familyTree)
+                    }
+                }
+            }
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
around lines 50 to 61, getLatestNoteFamilyTree() currently calls
getParentIds(id) inside the filter and compares nullable resource ids directly;
precompute a non-null parents Set by returning early when id is null, call
getParentIds(id) once into a Set (e.g., val parentIds =
getParentIds(id).toSet()), then filter notes by checking non-null
note.resource?.id against parentIds or equality to id using safe, non-null
comparisons, and emit the resulting list; this removes repeated calls and
nullable comparisons inside the lambda.

Comment on lines +75 to +78
viewModelScope.launch(Dispatchers.IO) {
val version = Version(oldId, newId)
versionStorage.add(version)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Wait for init before mutating storage in createVersion()

Prevents races on first use.

-            viewModelScope.launch(Dispatchers.IO) {
-                val version = Version(oldId, newId)
-                versionStorage.add(version)
-            }
+            viewModelScope.launch(Dispatchers.IO) {
+                ensureInitialized()
+                versionStorage.add(Version(oldId, newId))
+            }
📝 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
viewModelScope.launch(Dispatchers.IO) {
val version = Version(oldId, newId)
versionStorage.add(version)
}
viewModelScope.launch(Dispatchers.IO) {
ensureInitialized()
versionStorage.add(Version(oldId, newId))
}
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/ui/viewmodels/VersionsViewModel.kt
around lines 75 to 78, the coroutine mutates versionStorage without ensuring the
storage has finished initialization, which can cause a race on first use; update
createVersion() to wait for the storage to be initialized before calling
versionStorage.add(version) — either make createVersion suspend and call an
existing initialization/ready awaiter on versionStorage (or expose one), or
perform the initialization check/await on the main coroutine before launching
the Dispatchers.IO block, then add the version after that wait completes so no
concurrent access occurs during first-use initialization.

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

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants