Skip to content

Conversation

@OrangeAndGreen
Copy link
Contributor

https://dimagi.atlassian.net/browse/CCCT-1597

Product Description

Navigates to Connect opportunity list when user selects Opportunities from the side drawer menu.

Also addresses visibility of menu in different scenarios:

  • Setup: Always shown (including logged out state)
  • Login: only shown when PersonalID is configured
  • App Home: Never shown

Technical Summary

Nothing special above what's described in the Product Description

Feature Flag

Side Drawer Navigation Menu

Safety Assurance

Safety story

Dev tested with:
-No apps installed vs. apps installed

  • PersonalID configured vs. not configured

Automated test coverage

None

QA Plan

  • On Setup page, verify menu always shows and in the correct state (depending on whether PersonalID is configured)
  • On Login page, verify menu only shows when PersonalID is configured
  • Verify that click actions always work correctly when menu is showing in any state on any page

Corner-cases:

  • User forgets PersonalID account while on Login page: drawer should go to logged out state, and not show at all the next time the app starts
  • User configures PersonalID from 3-dot menu on Login page: once configuration is complete, app should return to Login page and show the side drawer

…ide drawer menu.

Only showing side drawer in Login when PersonalID is configured.
Always showing side drawer in Setup.
Never showing side drawer in App Home.
@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

📝 Walkthrough

Walkthrough

The PR migrates the app’s database layer to a new IDatabase abstraction, replacing direct SQLCipher SQLiteDatabase usage across app, user, global, and connect databases. It upgrades SQLCipher dependency to net.zetetic:sqlcipher-android:4.9.0, updates ProGuard rules, adds EncryptedDatabaseAdapter/UnencryptedDatabaseAdapter, and introduces schema managers for app/global/user DBs with corresponding upgraders. CommCareApplication and multiple services, models, storage, and parsers are refactored to use IDatabase. DB name resolution moves to schema managers. Navigation drawer logic changes: removal of Personal ID sign-in in setup, drawer visibility gating tweaks, and OPPORTUNITIES now navigates to Connect via unlock flow. Unit tests updated accordingly.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI/Feature
  participant App as CommCareApplication
  participant Helper as *DatabaseOpenHelper
  participant Adapt as EncryptedDatabaseAdapter
  participant DB as SQLCipher DB

  UI->>App: getUserDbOpenHelper(userId, key)
  App->>Helper: construct/open with app context
  Helper->>DB: getWritableDatabase()
  Helper-->>Adapt: wrap(SQLiteDatabase)
  Adapt-->>App: IDatabase
  App-->>UI: IDatabase handle
  UI->>Adapt: beginTransaction/queries/commit via IDatabase
Loading
sequenceDiagram
  actor User
  participant BDA as BaseDrawerActivity
  participant PIM as PersonalIdManager
  participant Nav as ConnectNavHelper

  User->>BDA: Select OPPORTUNITIES
  BDA->>PIM: init + unlockConnect(callback)
  alt unlock success
    BDA->>Nav: goToConnectJobsList(context)
    BDA-->>User: setResult(OK) + finish()
  else unlock failure
    BDA-->>User: remain on screen
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • shubham1g5
  • Jignesh-dimagi
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dv/side_drawer_nav

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🔭 Outside diff range comments (10)
app/src/org/commcare/models/database/user/UserDatabaseUpgrader.java (1)

66-238: Upgrade flow ignores target newVersion; consider honoring it to prevent overshooting

upgrade(IDatabase db, int oldVersion, int newVersion) never uses newVersion, so the chain runs through all steps up to the latest known version regardless of the requested target. This is risky if the helper calls with an intermediate target.

Two options:

  • Minimal: Gate each step with newVersion (e.g., only run step N→N+1 when newVersion >= N+1).
  • Preferred: Replace the linear if-chain with a while/switch that iterates until oldVersion < newVersion.

Example sketch:

-    public void upgrade(IDatabase db, int oldVersion, int newVersion) {
+    public void upgrade(IDatabase db, int oldVersion, int newVersion) {
         CommCareApplication.instance().setCustomServiceBindTimeout(5 * 60 * 1000);
-        int startVersion = oldVersion;
+        int startVersion = oldVersion;
+        while (oldVersion < newVersion) {
+            switch (oldVersion) {
+                case 1: if (upgradeOneTwo(db)) { oldVersion = 2; } break;
+                case 2: if (upgradeTwoThree(db)) { oldVersion = 3; } break;
+                // ...continue for each step...
+                case 28: if (updateTwentyEightTwentyNine(db)) { oldVersion = 29; } break;
+                default:
+                    // Unknown path; bail to avoid looping
+                    return;
+            }
+        }
         // existing startVersion > 22 special-case for 24->25 would be handled inside its case
     }
app/src/org/commcare/activities/CommCareSetupActivity.java (1)

655-661: Don’t navigate to Connect if unlock fails

The unlockConnect callback ignores the success flag and navigates unconditionally. This can route users to Connect screens without a valid session.

-        installFragment.updateConnectButton(!fromManager && !fromExternal && PersonalIdManager.getInstance().isloggedIn(), v -> {
-            PersonalIdManager.getInstance().unlockConnect(this, success -> {
-                ConnectNavHelper.INSTANCE.goToConnectJobsList(this);
-            });
-        });
+        installFragment.updateConnectButton(
+                !fromManager && !fromExternal && PersonalIdManager.getInstance().isloggedIn(),
+                v -> PersonalIdManager.getInstance().unlockConnect(this, success -> {
+                    if (success) {
+                        ConnectNavHelper.INSTANCE.goToConnectJobsList(this);
+                    } else {
+                        Toast.makeText(this, Localization.get("connect.unlock.failed"), Toast.LENGTH_SHORT).show();
+                    }
+                })
+        );
app/unit-tests/src/org/commcare/android/util/TestUtils.java (1)

161-167: Close InputStreams on all paths (use try-with-resources)

InputStreams are only closed on the success path. If parsing throws, the streams leak. Wrap in try-with-resources in both methods.

Apply these diffs:

--- a/app/unit-tests/src/org/commcare/android/util/TestUtils.java
+++ b/app/unit-tests/src/org/commcare/android/util/TestUtils.java
@@ -159,12 +159,11 @@
-        DataModelPullParser parser;
-
-        try {
-            InputStream is = TestUtils.class.getResourceAsStream(resourcePath);
-
-            parser = new DataModelPullParser(is, getFactory(db, bulkProcessingEnabled), true, true);
-            parser.parse();
-            is.close();
+        try (InputStream is = TestUtils.class.getResourceAsStream(resourcePath)) {
+            DataModelPullParser parser =
+                    new DataModelPullParser(is, getFactory(db, bulkProcessingEnabled),
+                            true, true);
+            parser.parse();
         } catch (IOException ioe) {
             throw wrapError(ioe, "IO Error parsing transactions");
         } catch (InvalidStructureException e) {
@@ -191,12 +190,11 @@
-        DataModelPullParser parser;
-
         try {
-            InputStream is = TestUtils.class.getResourceAsStream(resourcePath);
-
-            parser = new DataModelPullParser(is, androidTransactionFactory, true, true);
-            parser.parse();
-            is.close();
+            try (InputStream is = TestUtils.class.getResourceAsStream(resourcePath)) {
+                DataModelPullParser parser =
+                        new DataModelPullParser(is, androidTransactionFactory,
+                                true, true);
+                parser.parse();
+            }
         } catch (IOException ioe) {
             throw wrapError(ioe, "IO Error parsing transactions");
         } catch (InvalidStructureException e) {

Also applies to: 191-197

app/src/org/commcare/adapters/EntityStringFilterer.java (1)

75-99: Avoid holding a DB transaction while doing in-memory sorting

buildMatchList() opens a transaction on the user DB but does no DB work inside the lambda; this unnecessarily blocks other DB users and can hurt concurrency. If the only purpose is to periodically yield, use CPU yielding instead and drop the transaction.

Apply this refactor:

@@
-    private void buildMatchList() {
+    private void buildMatchList() {
         Locale currentLocale = Locale.getDefault();
-        //It's a bit sketchy here, because this DB lock will prevent
-        //anything else from processing
-        IDatabase db;
-        try {
-            db = CommCareApplication.instance().getUserDbHandle();
-        } catch (SessionUnavailableException e) {
-            this.cancelSearch();
-            return;
-        }
-        db.beginTransaction();
-        try {
-            EntitySortUtil.sortEntities(fullEntityList,
-                    searchTerms,
-                    currentLocale,
-                    isFuzzySearchEnabled,
-                    matchScores,
-                    matchList,
-                    index -> getEntityAtIndex(db, index));
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
+        EntitySortUtil.sortEntities(
+                fullEntityList,
+                searchTerms,
+                currentLocale,
+                isFuzzySearchEnabled,
+                matchScores,
+                matchList,
+                this::getEntityAtIndex
+        );
     }

And adjust the helper to remove DB coupling and use CPU yield:

-    private Entity<TreeReference> getEntityAtIndex(IDatabase db, int index) {
-        if (index % 500 == 0) {
-            db.yieldIfContendedSafely();
-        }
+    private Entity<TreeReference> getEntityAtIndex(int index) {
+        if (index % 500 == 0) {
+            Thread.yield();
+        }
         Entity<TreeReference> e = fullEntityList.get(index);
         if (isCancelled()) {
             return null;
         }
         return e;
     }
app/src/org/commcare/xml/AndroidBulkCaseXmlParser.java (1)

72-95: Refactor AndroidCaseIndexTable.indexCase to avoid nested transactions

Our investigation shows:

  • SqlStorage.write(c) calls helper.getHandle() to grab the shared IDatabase connection and does not wrap its inserts in its own transaction—so it participates in the outer transaction as intended.
  • However, AndroidCaseIndexTable.indexCase(Case) (app/src/org/commcare/models/database/user/models/AndroidCaseIndexTable.java:76–92) begins and ends its own transaction on the same connection. When invoked inside the bulk‐write transaction, those inner commits will persist index rows even if the outer transaction later aborts, breaking atomicity.

Please update as follows:

  • In AndroidCaseIndexTable.indexCase, remove the calls to
    • db.beginTransaction();
    • db.setTransactionSuccessful();
    • db.endTransaction();
    so that indexCase simply performs the inserts under the caller’s transaction.
  • Audit clearCaseIndices(...) to confirm it likewise does not open its own transaction.
app/src/org/commcare/models/database/user/models/AndroidCaseIndexTablePreV21.java (1)

29-44: Eliminate nested transactions during re-indexing.

Avoid starting a transaction in indexCase when reIndexAllCases already has one open. Extract a no-TX helper and reuse it in both paths.

Apply this diff:

@@
-    public void indexCase(Case c) {
-        db.beginTransaction();
-        try {
-            for (CaseIndex ci : c.getIndices()) {
-                ContentValues cv = new ContentValues();
-                cv.put(COL_CASE_RECORD_ID, c.getID());
-                cv.put(COL_INDEX_NAME, ci.getName());
-                cv.put(COL_INDEX_TYPE, ci.getTargetType());
-                cv.put(COL_INDEX_TARGET, ci.getTarget());
-                db.insert(TABLE_NAME, null, cv);
-            }
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-    }
+    public void indexCase(Case c) {
+        db.beginTransaction();
+        try {
+            indexCaseNoTx(c);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    // Inserts indices without starting/ending a transaction.
+    private void indexCaseNoTx(Case c) {
+        for (CaseIndex ci : c.getIndices()) {
+            ContentValues cv = new ContentValues();
+            cv.put(COL_CASE_RECORD_ID, c.getID());
+            cv.put(COL_INDEX_NAME, ci.getName());
+            cv.put(COL_INDEX_TYPE, ci.getTargetType());
+            cv.put(COL_INDEX_TARGET, ci.getTarget());
+            db.insert(TABLE_NAME, null, cv);
+        }
+    }
@@
-    public void reIndexAllCases(SqlStorage<ACase> caseStorage) {
+    public void reIndexAllCases(SqlStorage<ACase> caseStorage) {
         db.beginTransaction();
         try {
             wipeTable();
             for (ACase c : caseStorage) {
-                indexCase(c);
+                indexCaseNoTx(c);
             }
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
         }
     }

Also applies to: 56-67

app/src/org/commcare/models/database/HybridFileBackedSqlHelpers.java (1)

96-102: Fix typo in log message

There's a typo in the log message on line 100.

-                    "Unable to unset orphaned file: " + deleteCount + " entries effected.b");
+                    "Unable to unset orphaned file: " + deleteCount + " entries affected");
app/src/org/commcare/tasks/DataPullTask.java (1)

584-589: Wrap wipeStorageForFourTwelveSync in the transaction try block

Currently, in app/src/org/commcare/tasks/DataPullTask.java (around lines 563–578), you call wipeStorageForFourTwelveSync(userDb) immediately after beginTransaction() but before entering the try { … } finally { endTransaction(); } block. If any of the delete calls inside wipeStorageForFourTwelveSync throw, execution will never reach the finally and the transaction remains open.

• Move the wipe call inside the try so that endTransaction() always runs:

--- a/app/src/org/commcare/tasks/DataPullTask.java
+++ b/app/src/org/commcare/tasks/DataPullTask.java
@@ -563,7 +563,8 @@
         IDatabase userDb = CommCareApplication.instance().getUserDbHandle();
-        userDb.beginTransaction();
-        wipeStorageForFourTwelveSync(userDb);
-        
-        try {
+        userDb.beginTransaction();
+        try {
+            wipeStorageForFourTwelveSync(userDb);
             String syncToken = readInputWithoutCommit(cache.retrieveCache(), factory);
             updateUserSyncToken(syncToken);
             Logger.log(LogTypes.TYPE_USER, "Sync Recovery Successful");
@@ -570,7 +571,6 @@
             userDb.setTransactionSuccessful();
             return new Pair<>(PROGRESS_DONE, "");
         } catch (InvalidStructureException | XmlPullParserException
-        // …
         ) {
             Logger.exception("Sync recovery failed|" + e.getLocalizedMessage(), e);
             return new Pair<>(PROGRESS_RECOVERY_FAIL_BAD, e.getLocalizedMessage());

This guarantees that, even if a wipe operation fails, endTransaction() (and cache.release()) runs and the database isn’t left in an open transaction.

app/src/org/commcare/models/database/app/AppDatabaseUpgrader.java (2)

141-152: Verify table name constant usage.

The upgradeOneTwo method uses the hardcoded string "RECOVERY_RESOURCE_TABLE" instead of the constant RECOVERY_RESOURCE_TABLE_NAME. This inconsistency could lead to maintenance issues.

-            TableBuilder builder = new TableBuilder("RECOVERY_RESOURCE_TABLE");
+            TableBuilder builder = new TableBuilder(RECOVERY_RESOURCE_TABLE_NAME);

154-165: Duplicate RECOVERY_RESOURCE_TABLE creation in upgradeTwoThree
The upgradeTwoThree method in AppDatabaseUpgrader.java (lines 154–165) is identical to upgradeOneTwo and re-creates RECOVERY_RESOURCE_TABLE, even though that table was already created during the v1→v2 migration. This will either fail at runtime or silently skip the new descriptor field added in v3. You should instead alter the existing table to add the optional descriptor column (or guard against duplicate creation with IF NOT EXISTS) rather than re-running a full CREATE.

• File: app/src/org/commcare/models/database/app/AppDatabaseUpgrader.java
• Method: upgradeTwoThree (around lines 154–165)

🧹 Nitpick comments (30)
app/unit-tests/src/org/commcare/android/shadows/SQLiteCursorNative.java (1)

13-19: Update class documentation to reflect interface change.

The comment mentions "SQL-Cipher compatible cursor" but the class now implements the standard Android Cursor interface. Consider updating the documentation to reflect this change.

 /**
- * Wrapper which translates a plain android SQLite cursor 
- * into a SQL-Cipher compatible cursor 
+ * Wrapper which adapts a plain android SQLite cursor 
+ * to the standard Android Cursor interface for testing
  * 
  * @author ctsims
  *
  */
app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java (2)

205-206: Typo: “factor” → “factory” in exception messages

Minor copy edit in error strings.

-        throw new UnsupportedOperationException("Can't perform queries with a factor in the mock db");
+        throw new UnsupportedOperationException("Can't perform queries with a factory in the mock db");
-        throw new UnsupportedOperationException("Can't perform queries with a factor in the mock db");
+        throw new UnsupportedOperationException("Can't perform queries with a factory in the mock db");

Also applies to: 228-229


31-33: Document intentional no-ops for password/hook in the shadow constructors

Clarify that password and hook are intentionally ignored under Robolectric to prevent confusion during future maintenance.

 public void __constructor__(String path, char[] password, CursorFactory factory, int flags, SQLiteDatabaseHook hook) {
-        db = android.database.sqlite.SQLiteDatabase.openDatabase(path, null, flags);
+        // Note: password and hook are intentionally ignored in Robolectric shadow.
+        db = android.database.sqlite.SQLiteDatabase.openDatabase(path, null, flags);
     }
app/src/org/commcare/models/database/user/UserDatabaseUpgrader.java (3)

333-349: Misleading log message content

The log says "Case model update complete..." in upgradeSevenEight, but this step migrates AUser to User. Consider updating the message for clarity.

-            Log.d(TAG, "Case model update complete in " + (System.currentTimeMillis() - start) + "ms");
+            Log.d(TAG, "AUser -> User migration complete in " + (System.currentTimeMillis() - start) + "ms");

596-614: Possible NoSuchElementException when re-indexing fixtures with empty tables

exampleChildElement = storageForThisFixture.iterate().nextRecord() will throw if a fixture table exists but has no rows. Guard against empty iterators before building indices.

-                StorageIndexedTreeElementModel exampleChildElement =
-                        storageForThisFixture.iterate().nextRecord();
-                IndexedFixturePathUtils.buildFixtureIndices(db, tableName, exampleChildElement.getIndexColumnNames());
+                SqlStorageIterator<StorageIndexedTreeElementModel> it = storageForThisFixture.iterate();
+                if (it.hasMore()) {
+                    StorageIndexedTreeElementModel exampleChildElement = it.nextRecord();
+                    IndexedFixturePathUtils.buildFixtureIndices(db, tableName, exampleChildElement.getIndexColumnNames());
+                }

753-766: Method name inconsistent with others (update vs upgrade)

For consistency with the rest of the upgrader, consider renaming updateTwentySixTwentySeven to upgradeTwentySixTwentySeven.

app/src/org/commcare/activities/CommCareSetupActivity.java (1)

510-514: Nit: method naming style for PersonalIdManager.isloggedIn()

If feasible, prefer isLoggedIn() to match Java bean naming conventions. If the API is fixed elsewhere, ignore.

app/src/org/commcare/models/database/app/AppDatabaseSchemaManager.java (1)

86-89: Sanitize appId used in DB filename

If appId can contain characters not ideal for filenames or SQLite DB names, consider sanitizing (e.g., alphanumerics, underscore, dash) to avoid edge cases.

-    public static String getDbName(String appId) {
-        return DB_LOCATOR_PREF_APP + appId;
-    }
+    public static String getDbName(String appId) {
+        String sanitized = appId == null ? "" : appId.replaceAll("[^A-Za-z0-9._-]", "_");
+        return DB_LOCATOR_PREF_APP + sanitized;
+    }
app/src/org/commcare/models/database/IDatabase.java (2)

31-34: Add execSQL overload with bind args

Many call sites (and SQLiteDatabase API) support execSQL with bind arguments. Consider exposing it on the interface to avoid raw string concatenation.

     void execSQL(String sql);
+    void execSQL(String sql, Object[] bindArgs);

6-9: Tiny Javadoc nit

Grammar and extra space fix for clarity.

- * Interface to abstracts database operations to allow for different  and heterogeneous implementations, such as
- * SQLCipher and SQLite databases.
+ * Interface that abstracts database operations to allow different and heterogeneous implementations,
+ * such as SQLCipher and SQLite databases.
app/src/org/commcare/adapters/EntityStringFilterer.java (1)

64-73: Decouple helper from DB to simplify testing and reduce coupling

getEntityAtIndex now only uses the DB to yield. With the above change, it no longer needs a DB parameter, making it easier to test and reason about.

app/src/org/commcare/models/database/AndroidSandbox.java (1)

59-61: Reuse a single DB handle for drop/build/index to avoid repeated lookups.
Minor cleanup; also ensures all steps use the same handle instance.

-        UserDatabaseSchemaManager.dropTable(app.getUserDbHandle(), tableName);
-        UserDatabaseSchemaManager.buildTable(app.getUserDbHandle(), tableName, exampleEntry);
-        IndexedFixturePathUtils.buildFixtureIndices(app.getUserDbHandle(), tableName, indices);
+        IDatabase db = app.getUserDbHandle();
+        UserDatabaseSchemaManager.dropTable(db, tableName);
+        UserDatabaseSchemaManager.buildTable(db, tableName, exampleEntry);
+        IndexedFixturePathUtils.buildFixtureIndices(db, tableName, indices);
app/src/org/commcare/xml/AndroidCaseXmlParser.java (1)

87-89: Add @OverRide to ensure compile-time contract with parent.
If the parent now exposes the same signature, adding @OverRide prevents accidental mismatches.

-    protected IDatabase getDbHandle() {
+    @Override
+    protected IDatabase getDbHandle() {
         return CommCareApplication.instance().getUserDbHandle();
     }
app/src/org/commcare/sync/FormSubmissionHelper.java (1)

541-542: Fix typos in comment (“progress”, “appropriate”).

Minor, but visible in code search and IDE tooltips.

Apply this diff:

-    // parses prpgress and calls the approproate methods for form submission listeners
+    // Parses progress and calls the appropriate methods for form submission listeners
app/src/org/commcare/services/CommCareSessionService.java (2)

97-97: Document thread-safety expectations for userDatabase.

userDatabase is guarded by the service’s lock for reads/writes, but the handle will be used outside of that lock by callers. Add a brief Javadoc noting:

  • The handle is valid only while the session is active.
  • Callers must not cache it across session boundaries.
  • It may be closed by logout/expiration paths.

This will reduce misuse after the IDatabase migration.


221-225: Consider annotating getUserDbHandle() with @nullable (or throw on inactive session).

This can return null before storage is prepared or after logout. Annotating reduces downstream NPE risk and improves static analysis. Alternatively, throw SessionUnavailableException to make the contract explicit, but that would be a stricter, potentially breaking API change.

Apply this diff:

-    public IDatabase getUserDbHandle() {
+    @Nullable
+    public IDatabase getUserDbHandle() {
         synchronized (lock) {
             return userDatabase;
         }
     }
app/src/org/commcare/navdrawer/BaseDrawerActivity.kt (1)

40-41: Document intentional no-op for COMMCARE_APPS

The comment indicates this is intentional behavior (expands/collapses menu without navigation). Consider making this more explicit.

-            NavItemType.COMMCARE_APPS -> { /* No nav, expands/collapses menu */}
+            NavItemType.COMMCARE_APPS -> { 
+                // Intentional no-op: menu item only expands/collapses submenu
+            }
app/unit-tests/src/org/commcare/models/database/user/DatabaseUserOpenHelperMock.java (1)

14-16: Nit: Use the 4-arg SQLiteOpenHelper constructor for consistency

Other test helpers (e.g., DatabaseAppOpenHelperMock) use the 4-arg constructor. Unless you rely on a custom DatabaseErrorHandler, simplify for consistency.

-        super(context, getDbName(userKeyRecordId), null, USER_DB_VERSION, null);
+        super(context, getDbName(userKeyRecordId), null, USER_DB_VERSION);
app/src/org/commcare/activities/LoginActivity.java (1)

1023-1034: Guard selectedAppIndex assignment to avoid -1 when recordId is not in the list

If recordId isn’t present, indexOf returns -1 and we overwrite any previous selection with an invalid index. Guarding this keeps selectedAppIndex stable.

-            if (recordId != null) {
-                if (!appIdDropdownList.isEmpty()) {
-                    selectedAppIndex = appIdDropdownList.indexOf(recordId);
-                }
-                seatAppIfNeeded(recordId);
-            }
+            if (recordId != null) {
+                if (!appIdDropdownList.isEmpty()) {
+                    final int idx = appIdDropdownList.indexOf(recordId);
+                    if (idx >= 0) {
+                        selectedAppIndex = idx;
+                    }
+                }
+                seatAppIfNeeded(recordId);
+            }
app/unit-tests/src/org/commcare/CommCareTestApplication.java (1)

338-356: Test-time IDatabase providers look good; clarify ignored parameters

These overrides correctly return unencrypted adapters for tests. Minor clarity nit: keys/passwords are intentionally ignored in test implementations—consider a short comment to avoid confusion for readers.

     @Override
     public IDatabase getGlobalDbOpenHelper() {
-        return new UnencryptedDatabaseAdapter(new DatabaseGlobalOpenHelperMock(this));
+        // Tests use unencrypted DBs. No password/key is required.
+        return new UnencryptedDatabaseAdapter(new DatabaseGlobalOpenHelperMock(this));
     }

     @Override
     public IDatabase getUserDbOpenHelper(String userKeyRecordId, String key) {
-        return new UnencryptedDatabaseAdapter(new DatabaseUserOpenHelperMock(this, userKeyRecordId));
+        // 'key' is intentionally ignored in tests
+        return new UnencryptedDatabaseAdapter(new DatabaseUserOpenHelperMock(this, userKeyRecordId));
     }

     @Override
     public IDatabase getUserDbOpenHelperFromFile(String path, String password) {
-        return new UnencryptedDatabaseAdapter(path);
+        // 'password' is intentionally ignored in tests
+        return new UnencryptedDatabaseAdapter(path);
     }

     @Override
     public IDatabase getAppDbOpenHelper(String appId) {
-        return new UnencryptedDatabaseAdapter(new DatabaseAppOpenHelperMock(this, appId));
+        // Tests use unencrypted DBs. No password/key is required.
+        return new UnencryptedDatabaseAdapter(new DatabaseAppOpenHelperMock(this, appId));
     }
app/src/org/commcare/models/database/global/DatabaseGlobalOpenHelper.java (1)

17-21: Docstring is outdated (says “unencrypted” while using SQLCipher adapter)

The class Javadoc still says “global (unencrypted) db space” but we now use SQLCipher APIs and EncryptedDatabaseAdapter. Update to avoid confusion.

- * The helper for opening/updating the global (unencrypted) db space for CommCare.
+ * Helper for opening/updating the global database for CommCare (SQLCipher-backed).
app/src/org/commcare/models/database/app/DatabaseAppOpenHelper.java (1)

28-28: Hardcoded key value may cause confusion.

The field key is hardcoded to "null" string, which is then passed to DbUtil.trySqlCipherDbUpdate. This appears to be a placeholder for unencrypted app databases. Consider using a more descriptive constant name or adding a comment to clarify the intent.

-    private final String key = "null";
+    // App databases are not encrypted, using "null" as a placeholder key
+    private static final String UNENCRYPTED_KEY = "null";

Then update line 47:

-            DbUtil.trySqlCipherDbUpdate(key, context, getDbName(mAppId));
+            DbUtil.trySqlCipherDbUpdate(UNENCRYPTED_KEY, context, getDbName(mAppId));
app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java (1)

154-167: Consider more specific exception handling.

The catch block at line 161-165 catches and rethrows the exception after logging. Since we're already in a catch block from a retry attempt, consider adding more context to help diagnose migration failures.

             } catch (SQLiteException e) {
                 // Handle the exception, log the error, or inform the user
-                CrashUtil.log(e.getMessage());
+                CrashUtil.log("Failed to open Connect database after migration attempt: " + e.getMessage());
                 throw e;
             }
app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (3)

28-28: Remove unnecessary import

The Base64 import is added but Base64 is already available through android.util.Base64 (line 28). This appears to be a duplicate import.

-import android.util.Base64;

31-31: Remove empty line

Unnecessary blank line added.

-

79-91: Consider simplifying the legacy path detection

The current logic uses the remote/local passphrase equality check to determine the path, but the legacy comment indicates this is for backward compatibility. Consider adding more context about when this legacy path would be triggered and if there's a migration plan to deprecate it.

Can you provide more context about when the legacy path would be triggered? Is there a plan to migrate all users off this path?

app/src/org/commcare/models/database/migration/FixtureSerializationMigration.java (1)

82-92: Verify error handling behavior

The code sets transaction as successful even on exception to prevent app crashes. While this allows users to sync and recover, it could mask data corruption issues. Consider logging metrics or creating a recovery mechanism.

Consider implementing a recovery mechanism or telemetry to track when fixture migration fails, so you can monitor the impact and potentially provide targeted fixes for affected users.

app/src/org/commcare/models/database/user/UserDatabaseSchemaManager.java (1)

82-83: Fix inconsistent indentation.

The indentation is inconsistent - Line 82 uses 5 spaces while surrounding lines use 4 spaces.

-         builder = new TableBuilder(DeviceReportRecord.class);
-         database.execSQL(builder.getTableCreateString());
+        builder = new TableBuilder(DeviceReportRecord.class);
+        database.execSQL(builder.getTableCreateString());
app/src/org/commcare/CommCareApplication.java (2)

185-185: Consider documenting the IDatabase abstraction layer

The introduction of IDatabase as an abstraction over SQLiteDatabase is a significant architectural change. Consider adding documentation to explain why this abstraction was introduced and its benefits.


1264-1278: Consider centralizing database open helper creation

The new database open helper methods provide a good abstraction. However, consider adding error handling or validation for the parameters, especially for the password in getUserDbOpenHelperFromFile.

Consider adding parameter validation:

 public IDatabase getUserDbOpenHelperFromFile(String path, String password) {
+    if (path == null || path.isEmpty()) {
+        throw new IllegalArgumentException("Database path cannot be null or empty");
+    }
+    if (password == null || password.isEmpty()) {
+        throw new IllegalArgumentException("Database password cannot be null or empty");
+    }
     return new EncryptedDatabaseAdapter(path, password);
 }

Comment on lines 135 to 137
for (String id : dbIdsToRemove) {
CommCareApplication.instance().getDatabasePath(DatabaseUserOpenHelper.getDbName(id)).delete();
CommCareApplication.instance().getDatabasePath(UserDatabaseSchemaManager.getDbName(id)).delete();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use Context.deleteDatabase to remove WAL/SHM sidecar files and add failure logging.
Deleting via File.delete() can leave -wal/-shm files behind and ignores failures. Prefer deleteDatabase(name), and fall back to deleting the path and sidecars if needed.

Apply this diff:

-        for (String id : dbIdsToRemove) {
-            CommCareApplication.instance().getDatabasePath(UserDatabaseSchemaManager.getDbName(id)).delete();
-        }
+        for (String id : dbIdsToRemove) {
+            String dbName = UserDatabaseSchemaManager.getDbName(id);
+            boolean deleted = CommCareApplication.instance().deleteDatabase(dbName);
+            if (!deleted) {
+                // Fallback: try direct file deletion and sidecars
+                File dbFile = CommCareApplication.instance().getDatabasePath(dbName);
+                if (dbFile.exists() && !FileUtil.deleteFileOrDir(dbFile)) {
+                    Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB file for id: " + id);
+                }
+                File wal = new File(dbFile.getPath() + "-wal");
+                File shm = new File(dbFile.getPath() + "-shm");
+                if (wal.exists() && !wal.delete()) {
+                    Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB WAL for id: " + id);
+                }
+                if (shm.exists() && !shm.delete()) {
+                    Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB SHM for id: " + id);
+                }
+            }
+        }
📝 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
for (String id : dbIdsToRemove) {
CommCareApplication.instance().getDatabasePath(DatabaseUserOpenHelper.getDbName(id)).delete();
CommCareApplication.instance().getDatabasePath(UserDatabaseSchemaManager.getDbName(id)).delete();
}
for (String id : dbIdsToRemove) {
String dbName = UserDatabaseSchemaManager.getDbName(id);
boolean deleted = CommCareApplication.instance().deleteDatabase(dbName);
if (!deleted) {
// Fallback: try direct file deletion and sidecars
File dbFile = CommCareApplication.instance().getDatabasePath(dbName);
if (dbFile.exists() && !FileUtil.deleteFileOrDir(dbFile)) {
Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB file for id: " + id);
}
File wal = new File(dbFile.getPath() + "-wal");
File shm = new File(dbFile.getPath() + "-shm");
if (wal.exists() && !wal.delete()) {
Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB WAL for id: " + id);
}
if (shm.exists() && !shm.delete()) {
Logger.log(LogTypes.TYPE_ERROR_STORAGE, "Failed to delete user DB SHM for id: " + id);
}
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/AppUtils.java around lines 135 to 137, the code
currently deletes databases by calling File.delete() on the DB path which can
leave -wal/-shm sidecar files and ignores failures; replace that with
CommCareApplication.instance().deleteDatabase(UserDatabaseSchemaManager.getDbName(id))
to properly remove DB and sidecars, and if deleteDatabase returns false fall
back to attempting to delete the DB file and its -wal and -shm sidecars
explicitly while logging any failures via the app logger (include DB name and
which file failed) so unsuccessful deletions are recorded.

Comment on lines 167 to 178
private boolean upgradeThreeFour(IDatabase db) {
db.beginTransaction();
try {
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE"));
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE"));
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE"));
db.setTransactionSuccessful();
return true;
} finally {
db.endTransaction();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Verify hardcoded table names in index creation.

The upgradeThreeFour method uses hardcoded table name strings instead of constants. This is inconsistent with the rest of the codebase and could cause issues if table names change.

-            db.execSQL(indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE"));
-            db.execSQL(indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE"));
-            db.execSQL(indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE"));
+            db.execSQL(indexOnTableWithPGUIDCommand("global_index_id", GLOBAL_RESOURCE_TABLE_NAME));
+            db.execSQL(indexOnTableWithPGUIDCommand("upgrade_index_id", UPGRADE_RESOURCE_TABLE_NAME));
+            db.execSQL(indexOnTableWithPGUIDCommand("recovery_index_id", RECOVERY_RESOURCE_TABLE_NAME));
📝 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 boolean upgradeThreeFour(IDatabase db) {
db.beginTransaction();
try {
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE"));
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE"));
db.execSQL(DatabaseAppOpenHelper.indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("global_index_id", "GLOBAL_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("upgrade_index_id", "UPGRADE_RESOURCE_TABLE"));
db.execSQL(indexOnTableWithPGUIDCommand("recovery_index_id", "RECOVERY_RESOURCE_TABLE"));
db.setTransactionSuccessful();
return true;
} finally {
db.endTransaction();
}
}
private boolean upgradeThreeFour(IDatabase db) {
db.beginTransaction();
try {
db.execSQL(indexOnTableWithPGUIDCommand("global_index_id", GLOBAL_RESOURCE_TABLE_NAME));
db.execSQL(indexOnTableWithPGUIDCommand("upgrade_index_id", UPGRADE_RESOURCE_TABLE_NAME));
db.execSQL(indexOnTableWithPGUIDCommand("recovery_index_id", RECOVERY_RESOURCE_TABLE_NAME));
db.setTransactionSuccessful();
return true;
} finally {
db.endTransaction();
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/models/database/app/AppDatabaseUpgrader.java around
lines 167 to 178, the index creation calls use hardcoded table name strings;
replace those string literals with the appropriate table name constants used
elsewhere in the codebase (e.g., GLOBAL_RESOURCE_TABLE, UPGRADE_RESOURCE_TABLE,
RECOVERY_RESOURCE_TABLE) so the calls become:
indexOnTableWithPGUIDCommand(<CONSTANT>, ...). Ensure you import or reference
the same constants as other classes (or the DB constants holder) and run the
build to confirm the constant names are correct.

Comment on lines 22 to 24
public EncryptedDatabaseAdapter(String path, String password) {
this.db = SQLiteDatabase.openDatabase(path, password, null, SQLiteDatabase.OPEN_READWRITE, null, null);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding error handling for database opening

The constructor that opens a database from a path should handle potential exceptions when the database cannot be opened or the password is incorrect.

Consider wrapping the database opening in error handling:

 public EncryptedDatabaseAdapter(String path, String password) {
-    this.db = SQLiteDatabase.openDatabase(path, password, null, SQLiteDatabase.OPEN_READWRITE, null, null);
+    try {
+        this.db = SQLiteDatabase.openDatabase(path, password, null, SQLiteDatabase.OPEN_READWRITE, null, null);
+    } catch (net.zetetic.database.sqlcipher.SQLiteException e) {
+        throw new RuntimeException("Failed to open encrypted database at path: " + path, 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
public EncryptedDatabaseAdapter(String path, String password) {
this.db = SQLiteDatabase.openDatabase(path, password, null, SQLiteDatabase.OPEN_READWRITE, null, null);
}
public EncryptedDatabaseAdapter(String path, String password) {
try {
this.db = SQLiteDatabase.openDatabase(path, password, null, SQLiteDatabase.OPEN_READWRITE, null, null);
} catch (net.zetetic.database.sqlcipher.SQLiteException e) {
throw new RuntimeException("Failed to open encrypted database at path: " + path, e);
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/models/database/EncryptedDatabaseAdapter.java around
lines 22-24, the constructor directly calls SQLiteDatabase.openDatabase which
can throw when the file is missing or the password is wrong; wrap the
openDatabase call in a try-catch that catches SQLiteException (or Exception),
log the error, clean up any partially opened resources, and throw a clear
checked or runtime exception (or rethrow with context) so callers can handle
failed opens; ensure the db field remains null on failure and include a helpful
message (e.g., path and reason) in the thrown/logged error.

Comment on lines 83 to 91
public long runCompileStatement(String sql) {
SQLiteStatement stmt = null;
try {
stmt = db.compileStatement(sql);
return stmt.simpleQueryForLong();
} finally{
stmt.close();
}
}
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 potential NPE in runCompileStatement

The finally block attempts to close stmt which could be null if compileStatement throws an exception.

Apply this fix to prevent NPE:

 @Override
 public long runCompileStatement(String sql) {
     SQLiteStatement stmt = null;
     try {
         stmt = db.compileStatement(sql);
         return stmt.simpleQueryForLong();
-    } finally{
-        stmt.close();
+    } finally {
+        if (stmt != null) {
+            stmt.close();
+        }
     }
 }
🤖 Prompt for AI Agents
In app/src/org/commcare/models/database/EncryptedDatabaseAdapter.java around
lines 83 to 91, the finally block calls stmt.close() which can throw a
NullPointerException if db.compileStatement(sql) fails and stmt remains null;
update the finally block to null-check stmt before calling close (or convert to
try-with-resources if Java version permits) so close() is only invoked when stmt
!= null.

Comment on lines 4 to 8
import android.database.sqlite.SQLiteException;

import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteException;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Catching the wrong SQLiteException class breaks cipher migration fallback

With net.zetetic classes, getWritableDatabase() throws net.zetetic.database.sqlcipher.SQLiteException, not android.database.sqlite.SQLiteException. As written, the catch won’t fire on SQLCipher errors, and DbUtil.trySqlCipherDbUpdate(...) won’t be invoked, risking startup crashes on cipher version mismatches.

Apply this diff to import and catch the correct exception:

-import android.database.sqlite.SQLiteException;
+import net.zetetic.database.sqlcipher.SQLiteException;

No further code changes needed since the short name remains SQLiteException in the catch block.

Also applies to: 38-45

🤖 Prompt for AI Agents
In app/src/org/commcare/models/database/global/DatabaseGlobalOpenHelper.java
around lines 4-8 and 38-45, the code imports
android.database.sqlite.SQLiteException but SQLCipher throws
net.zetetic.database.sqlcipher.SQLiteException; replace the android import with
import net.zetetic.database.sqlcipher.SQLiteException (remove the
android.database.sqlite import) so the existing catch(SQLiteException e) blocks
will catch the correct exception and allow DbUtil.trySqlCipherDbUpdate(...) to
run.

SqlStorage<UserKeyRecord> userDatabase = app.getStorage(UserKeyRecord.class);
for (UserKeyRecord user : userDatabase) {
File f = ccInstance.getDatabasePath(DatabaseUserOpenHelper.getDbName(user.getUuid()));
File f = ccInstance.getDatabasePath(UserDatabaseSchemaManager.getDbName(user.getUuid()));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prefer deleteDatabase over deleting the file path to remove sidecars reliably.
Using Context.deleteDatabase(name) removes the main file and -wal/-shm/-journal files; file deletion may leave sidecars behind and fail silently.

-            File f = ccInstance.getDatabasePath(UserDatabaseSchemaManager.getDbName(user.getUuid()));
-            if (!FileUtil.deleteFileOrDir(f)) {
+            String dbName = UserDatabaseSchemaManager.getDbName(user.getUuid());
+            if (!ccInstance.deleteDatabase(dbName)) {
                 Logger.log(LogTypes.TYPE_RESOURCES, "A user database was unable to be " +
                         "deleted during app uninstall. Aborting uninstall process for now.");
                 // If we failed to delete a file, it is likely because there is an open pointer
                 // to that db still in use, so stop the uninstall for now, and rely on it to
                 // complete the next time the app starts up
                 return;
             }
📝 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
File f = ccInstance.getDatabasePath(UserDatabaseSchemaManager.getDbName(user.getUuid()));
String dbName = UserDatabaseSchemaManager.getDbName(user.getUuid());
if (!ccInstance.deleteDatabase(dbName)) {
Logger.log(LogTypes.TYPE_RESOURCES, "A user database was unable to be " +
"deleted during app uninstall. Aborting uninstall process for now.");
// If we failed to delete a file, it is likely because there is an open pointer
// to that db still in use, so stop the uninstall for now, and rely on it to
// complete the next time the app starts up
return;
}
🤖 Prompt for AI Agents
In app/src/org/commcare/utils/AppLifecycleUtils.java around line 56, the code
deletes the DB by removing the File path (File f =
ccInstance.getDatabasePath(...)), which can leave -wal/-shm/-journal sidecar
files behind; replace that approach by calling Context.deleteDatabase(dbName)
using the same UserDatabaseSchemaManager.getDbName(user.getUuid()) value so
Android removes the main DB and any sidecars reliably, and remove any direct
File.delete() usage related to the database path.


// 7) Delete the app database
File f = ccInstance.getDatabasePath(DatabaseAppOpenHelper.getDbName(app.getAppRecord().getApplicationId()));
File f = ccInstance.getDatabasePath(AppDatabaseSchemaManager.getDbName(app.getAppRecord().getApplicationId()));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply the same deleteDatabase approach for the app DB.
Consistent with user DB cleanup and ensures WAL/SHM removal.

-        File f = ccInstance.getDatabasePath(AppDatabaseSchemaManager.getDbName(app.getAppRecord().getApplicationId()));
-        if (!FileUtil.deleteFileOrDir(f)) {
+        String appDbName = AppDatabaseSchemaManager.getDbName(app.getAppRecord().getApplicationId());
+        if (!ccInstance.deleteDatabase(appDbName)) {
             Logger.log(LogTypes.TYPE_RESOURCES, "The app database was unable to be deleted" +
                     "during app uninstall. Aborting uninstall process for now.");
             return;
         }
📝 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
File f = ccInstance.getDatabasePath(AppDatabaseSchemaManager.getDbName(app.getAppRecord().getApplicationId()));
String appDbName = AppDatabaseSchemaManager.getDbName(app.getAppRecord().getApplicationId());
if (!ccInstance.deleteDatabase(appDbName)) {
Logger.log(LogTypes.TYPE_RESOURCES, "The app database was unable to be deleted" +
"during app uninstall. Aborting uninstall process for now.");
return;
}

Comment on lines 8 to 12
import net.zetetic.database.DatabaseErrorHandler;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
import net.zetetic.database.sqlcipher.SQLiteStatement;
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

Fix DatabaseErrorHandler package and wire it through to openDatabase(...)

  • The import for DatabaseErrorHandler should come from android.database, not net.zetetic.database (which likely doesn’t exist and will cause linkage issues).
  • The constructor that accepts a DatabaseErrorHandler currently ignores it; pass it through to the underlying openDatabase call to preserve error handling behavior.

Apply this diff:

- import net.zetetic.database.DatabaseErrorHandler;
+ import android.database.DatabaseErrorHandler;
@@
-    public void __constructor__(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) {
-        db = android.database.sqlite.SQLiteDatabase.openDatabase(path, null, flags);
+    public void __constructor__(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) {
+        db = android.database.sqlite.SQLiteDatabase.openDatabase(path, null, flags, errorHandler);
     }

Also applies to: 35-37

🤖 Prompt for AI Agents
In app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java
around lines 8-12 and also affecting lines 35-37, replace the incorrect import
net.zetetic.database.DatabaseErrorHandler with
android.database.DatabaseErrorHandler, and update the constructor(s) that accept
a DatabaseErrorHandler so they no longer ignore that parameter: pass the
provided DatabaseErrorHandler into the underlying openDatabase(...) invocation
(or the equivalent delegate call) so the handler is preserved and used by the
opened database.

* @author ctsims
*/
@Implements(net.sqlcipher.database.SQLiteDatabase.class)
@Implements(SQLiteDatabase.class)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Make shadow method matching resilient to shaded types with looseSignatures

Given the target is a non-Android SDK class (net.zetetic...) and some method signatures/return types differ from android.* (e.g., Cursor, exceptions), enabling looseSignatures reduces brittleness across library versions and shaded types.

Apply this diff:

-@Implements(SQLiteDatabase.class)
+@Implements(value = SQLiteDatabase.class, looseSignatures = true)

#!/bin/bash
# Display the top of the shadow file including imports and annotation
sed -n '1,50p' app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java

# Show explicit import of SQLiteDatabase to confirm its package
grep -R "import .*SQLiteDatabase" -n app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java

# Locate the @Implements line with context
grep -R "@Implements" -n app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java -A2 -B2

#!/bin/bash
# Search for any existing use of looseSignatures in the repo
grep -R "looseSignatures" -n .

# List all @Implements annotations to check for other patterns or usage
grep -R "@Implements" -n .

Enable looseSignatures on Shadow SQLiteDatabase for better resilience

To ensure your shadow methods continue matching even when using shaded or variant SQLiteDatabase signatures, update the @Implements annotation:

• File: app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java (around line 23)

- @Implements(SQLiteDatabase.class)
+ @Implements(value = SQLiteDatabase.class, looseSignatures = true)

This makes the shadowing less brittle across library versions and shaded types.

🤖 Prompt for AI Agents
In app/unit-tests/src/org/commcare/android/shadows/SQLiteDatabaseNative.java
around line 23, the @Implements annotation should enable looseSignatures to make
the shadow resilient to shaded/variant SQLiteDatabase signatures; update the
annotation on that line to include the looseSignatures attribute (set it to
true) so the shadow methods match across library versions.

Comment on lines 81 to 90
@Override
public long runCompileStatement(String sql) {
SQLiteStatement stmt = null;
try {
stmt = db.compileStatement(sql);
return stmt.simpleQueryForLong();
} finally{
stmt.close();
}
}
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 potential NullPointerException in runCompileStatement.

The finally block unconditionally calls stmt.close() which will throw NPE if compileStatement fails before assigning to stmt.

 @Override
 public long runCompileStatement(String sql) {
     SQLiteStatement stmt = null;
     try {
         stmt = db.compileStatement(sql);
         return stmt.simpleQueryForLong();
-    } finally{
-        stmt.close();
+    } finally {
+        if (stmt != null) {
+            stmt.close();
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public long runCompileStatement(String sql) {
SQLiteStatement stmt = null;
try {
stmt = db.compileStatement(sql);
return stmt.simpleQueryForLong();
} finally{
stmt.close();
}
}
@Override
public long runCompileStatement(String sql) {
SQLiteStatement stmt = null;
try {
stmt = db.compileStatement(sql);
return stmt.simpleQueryForLong();
} finally {
if (stmt != null) {
stmt.close();
}
}
}
🤖 Prompt for AI Agents
In
app/unit-tests/src/org/commcare/models/database/UnencryptedDatabaseAdapter.java
around lines 81 to 90, the finally block always calls stmt.close() which can
throw a NullPointerException if db.compileStatement(sql) fails and stmt is still
null; change the finally block to only call stmt.close() when stmt is non-null
(or use try-with-resources if available) so the statement is closed safely
without risking NPE.

Showing Messaging menu option when user has at least one channel.
@shubham1g5 shubham1g5 changed the base branch from master to commcare_2.59 August 13, 2025 02:59
@Override
protected boolean shouldShowDrawer() {
return true;
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to show drawer on App Home

Comment on lines 56 to 68
private fun navigateToConnectMenu() {
val personalIdManager: PersonalIdManager = PersonalIdManager.getInstance()
personalIdManager.init(this)
personalIdManager.unlockConnect(
this
) { success: Boolean ->
if (success) {
goToConnectJobsList(this)
setResult(RESULT_OK)
finish()
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can we put all of this into ConnectNavHelper ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created a couple helper methods in ConnectNavHelper to add the unlock code, now calling the helper methods wherever possible. ad5fbbc

) { success: Boolean ->
if (success) {
goToMessaging(this)
drawerController?.closeDrawer()
Copy link
Contributor

Choose a reason for hiding this comment

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

think the drawer logic should live out of this method so that we can move this method to ConnectNavHelper as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done as part of ad5fbbc

…to jobs list or messaging.

Using the new helper methods anywhere applicable.
Re-enabled side drawer in app home
@OrangeAndGreen OrangeAndGreen merged commit e5b3468 into commcare_2.59 Aug 13, 2025
1 check was pending
@OrangeAndGreen OrangeAndGreen deleted the dv/side_drawer_nav branch August 13, 2025 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants