Skip to content

Propagate local tag deletions to Google Drive - #165

Merged
anod merged 7 commits into
masterfrom
anod-fix-tag-deletion-drive-sync
Aug 29, 2026
Merged

anod merged 7 commits into
masterfrom
anod-fix-tag-deletion-drive-sync

Conversation

@anod

@anod anod commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • track tag deletion with Tag.status, matching the existing app deletion model without a separate tombstone table
  • hide deleted tags from normal queries and Drive snapshots while retaining deletion intent until upload succeeds
  • prevent stale Drive tags and app assignments from being restored during merge
  • preserve create, update, rename, recreate, backup restore, duplicate-name, and concurrent deletion behavior
  • add Room schema v20 and focused regression coverage

Closes #163

Validation

  • ./gradlew :app:testDebugUnitTest --tests "com.anod.appwatcher.backup.gdrive.GDriveTagSyncTest"
  • ./gradlew ktlintCheck
  • 2/2 isolated Room status cases passed on a physical A024 running Android 16
  • earlier 4/4 focused sync instrumentation cases passed on the same physical device

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI lite review requested due to automatic review settings August 28, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are coherent across DB/migration/sync paths and are backed by focused regression tests, with only a minor efficiency nit noted.

Pull request overview

Implements tag-deletion propagation semantics for Google Drive backup/sync by introducing a local “deleted tag name” tombstone table that prevents stale remote tags and assignments from being re-merged until an upload is acknowledged (Issue #163).

Changes:

  • Add deleted_tags Room table/entity, wire it into AppsDatabase v20 with migration + schema export.
  • Tombstone tag names on local delete/rename and clear tombstones on recreate/rename to ensure correct merge semantics.
  • Update Google Drive sync/upload to clear acknowledged tombstones after a successful write, and add focused unit regression coverage.
File summaries
File Description
app/src/test/java/com/anod/appwatcher/backup/gdrive/GDriveTagSyncTest.kt Adds regression tests for tombstoning, stale-remote merge suppression, and tombstone acknowledgement behavior.
app/src/main/java/com/anod/appwatcher/tags/EditTagViewModel.kt Routes tag updates through TagsTable.Queries.update so rename/update flows maintain tombstones correctly.
app/src/main/java/com/anod/appwatcher/database/TagsTable.kt Adds tombstone-aware tag insert/update/delete logic and a countByName helper to avoid incorrect tombstoning when duplicates exist.
app/src/main/java/com/anod/appwatcher/database/entities/DeletedTag.kt Introduces DeletedTag entity keyed by tag name.
app/src/main/java/com/anod/appwatcher/database/DeletedTagsTable.kt Adds DAO for loading/inserting/deleting deleted-tag-name tombstones.
app/src/main/java/com/anod/appwatcher/database/AppsDatabase.kt Bumps DB to v20, adds DeletedTag entity + deletedTags() DAO, and provides migration 19→20.
app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveUpload.kt Captures and clears deleted-tag tombstones only after a successful Drive write.
app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt Prevents stale remote tags/app-assignments from being merged if tombstoned; clears acknowledged tombstones post-upload.
app/src/main/java/com/anod/appwatcher/backup/DbBackupManager.kt Clears tombstones on full restore reset to avoid carrying deleted-tag state across restores.
app/schemas/com.anod.appwatcher.database.AppsDatabase/20.json Exports Room schema v20 including the new deleted_tags table.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 28, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

TagsTable.Queries.update() can clear a deleted-tag tombstone even when the tag row is missing (0-row update), which can re-enable stale Drive restoration after concurrent deletion.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

app/src/main/java/com/anod/appwatcher/database/TagsTable.kt:72

  • Queries.update() unconditionally clears the tombstone for tag.name even if the tag row no longer exists (e.g., the tag was deleted concurrently while the edit screen is open). In that case db.tags().update(tag) affects 0 rows but db.deletedTags().delete(tag.name) will still run, which can allow the deleted tag name to be restored from stale Drive data on a later merge.
            val previousTag = db.tags().loadById(tag.id)
            db.tags().update(tag)
            db.deletedTags().delete(tag.name)
            if (previousTag != null && previousTag.name != tag.name) {
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Replace the separate deleted-tag tombstone table with a status on Tag, matching the existing app deletion model while keeping deleted tags out of normal queries and Drive snapshots.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 28, 2026 20:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Restoring a deleted tag by name can leave duplicate deleted rows for the same name, which can cause Drive merge to still treat the recreated name as deleted and skip restoring its remote app assignments.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread app/src/main/java/com/anod/appwatcher/database/TagsTable.kt
When a tag is recreated, revive one matching row and remove any remaining deleted rows for that name so stale tombstones cannot suppress Drive assignments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 28, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It introduces a hashCode/equals contract violation in Tag and leaves the Drive Reader unclosed during remote merge, both of which can cause correctness/resource issues.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt:55

  • Tag.equals() compares id, but hashCode() does not include id, which violates the equals/hashCode contract and can cause incorrect behavior when Tag is used in hashed collections (e.g., Set, Map).
    override fun hashCode() = hashCodeOf(name, color, status)

app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt:84

  • insertRemoteItems(file, db) obtains a Reader from DriveIdFile.read() but never closes it, which can leak file descriptors/resources during sync. Wrap the reader in use { ... } (or otherwise close it) after parsing completes.
    private suspend fun insertRemoteItems(file: DriveIdFile, db: AppsDatabase) {
        val reader = file.read() ?: throw IllegalStateException("Cannot read file")
        insertRemoteItems(reader, db)
    }
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Comment thread app/src/main/java/com/anod/appwatcher/database/TagsTable.kt
Comment thread app/src/main/java/com/anod/appwatcher/database/TagsTable.kt Outdated
Comment thread app/src/main/java/com/anod/appwatcher/database/TagsTable.kt Outdated
Rename the deleted-row cleanup APIs and document why duplicate active tag names are physically removed instead of retained as deletion markers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 29, 2026 07:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

It introduces a likely crash on Navigation 3 state restore due to a non-optional new Tag.status field and also leaves a newly introduced Reader unclosed during Drive merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt:84

  • DriveIdFile.read() returns a FileReader backed by a temp file; the current implementation passes it down without ever closing it. Over time this can leak file descriptors and keep temp files locked longer than necessary, especially if sync runs frequently or fails mid-parse.
    private suspend fun insertRemoteItems(file: DriveIdFile, db: AppsDatabase) {
        val reader = file.read() ?: throw IllegalStateException("Cannot read file")
        insertRemoteItems(reader, db)
    }
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt Outdated
Default missing tag status to normal so Navigation 3 can restore back stacks saved before the status field was introduced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 29, 2026 07:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

There are two correctness issues in the diff (an unclosed Drive Reader in sync merge and an equals/hashCode contract violation in Tag) that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

app/src/main/java/com/anod/appwatcher/database/entities/Tag.kt:55

  • equals() compares id, but hashCode() does not include it. This violates the equals/hashCode contract and can cause incorrect behavior when Tag is used in hash-based collections (e.g., sets/maps) after this PR changes equality to also include status.
    override fun hashCode() = hashCodeOf(name, color, status)
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread app/src/main/java/com/anod/appwatcher/backup/gdrive/GDriveSync.kt
Scope the Drive reader so parse exceptions cannot leak its temp-file descriptor, and include tag IDs in hashes to mirror equality.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a41cc6b5-b40a-471d-b859-e382092b8ac0
Copilot AI review requested due to automatic review settings August 29, 2026 08:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The tombstone-based deletion model is implemented consistently across Room queries, migrations, Drive sync/upload cleanup, and is backed by focused regression tests for the main stale-remote and recreation edge cases.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@anod
anod merged commit 3bcb32d into master Aug 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Propagate local tag deletions to Google Drive sync

3 participants