Reset MediaStore database Uuid on schema reset.

MediaStore version is a combination of database version and uuid(derived
from randomUUID() and stored as xattr in MediaStore database file).

MediaStore version is used by apps to identify any changes in the data
in MediaStore database. This is useful for apps that cache MediaStore
database data. When there is a change in MediaStore version, apps are
advised to update their cache with changed data in MediaStore database.

MediaStore database version can change whenever there is an update in
the schema or change in data in any of the rows and columns that happens
during db upgrade. This shouldn't be confused with data changes in
database due to ContentResolver operations or any implicit database
updates in MediaProvider. Bulk data change in this case happens during
database upgrade or downgrade routine where we don't notify apps about a
particular db row change.

MediaStore Uuid is a random number derived from randomUUID() and stored
as xattr in database file. This will only change if database file is
deleted and recreated.

Hence, if there is database data corruption or database downgrade and
upgrade which doesn't change the database version and doesn't delete the
database file, we don't change the MediaStore version even if this has
dropped database tables and recreated a new table. This is a major
change in MediaStore database data and should be clearly indicated in
MediaStore version so that apps can resync their cache.

To solve this issue, we now reset the Uuid whenever we delete tables and
create new schema, we change the Uuid so that MediaStore version is
changed even if database version hasn't changed.

Bug: 192102594
Test: atest
com.android.providers.media.DatabaseHelperTest#testDowngradeChangesUUID

Change-Id: Ic59d583b35ce18ce7bf3d6cc87a55ac607db3a7d
Merged-In: Ic59d583b35ce18ce7bf3d6cc87a55ac607db3a7d
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 7bcdf62..53c579d 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -715,6 +715,11 @@
 
     @VisibleForTesting
     static void makePristineSchema(SQLiteDatabase db) {
+        // We are dropping all tables and recreating new schema. This
+        // is a clear indication of major change in MediaStore version.
+        // Hence reset the Uuid whenever we change the schema.
+        resetAndGetUuid(db);
+
         // drop all triggers
         Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'",
                 null, null, null, null);
@@ -1851,19 +1856,23 @@
         } catch (ErrnoException e) {
             if (e.errno == OsConstants.ENODATA) {
                 // Doesn't exist yet, so generate and persist a UUID
-                final String uuid = UUID.randomUUID().toString();
-                try {
-                    Os.setxattr(db.getPath(), XATTR_UUID, uuid.getBytes(), 0);
-                } catch (ErrnoException e2) {
-                    throw new RuntimeException(e);
-                }
-                return uuid;
+                return resetAndGetUuid(db);
             } else {
                 throw new RuntimeException(e);
             }
         }
     }
 
+    private static @NonNull String resetAndGetUuid(SQLiteDatabase db) {
+        final String uuid = UUID.randomUUID().toString();
+        try {
+            Os.setxattr(db.getPath(), XATTR_UUID, uuid.getBytes(), 0);
+        } catch (ErrnoException e) {
+            throw new RuntimeException(e);
+        }
+        return uuid;
+    }
+
     private static final long PASSTHROUGH_WAIT_TIMEOUT = 10 * DateUtils.SECOND_IN_MILLIS;
 
     /**
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index a159fd0..eef8bbd 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -18,9 +18,11 @@
 
 import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
 
+import static com.android.providers.media.DatabaseHelper.VERSION_LATEST;
 import static com.android.providers.media.DatabaseHelper.makePristineSchema;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -525,6 +527,55 @@
         }
     }
 
+    /**
+     * Test that database downgrade changed the UUID saved in database file.
+     */
+    @Test
+    public void testDowngradeChangesUUID() throws Exception {
+        Class<? extends DatabaseHelper> dbVersionHigher = DatabaseHelperS.class;
+        Class<? extends DatabaseHelper> dbVersionLower = DatabaseHelperR.class;
+        String originalUUID;
+        int originalVersion;
+
+        // Create the database with database version = dbVersionLower
+        try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
+                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
+            SQLiteDatabase db = helper.getWritableDatabaseForTest();
+            originalUUID = DatabaseHelper.getOrCreateUuid(db);
+            originalVersion = db.getVersion();
+            // Verify that original version of the database is dbVersionLower.
+            assertWithMessage("Current database version")
+                    .that(db.getVersion()).isEqualTo(DatabaseHelper.VERSION_R);
+        }
+
+        // Upgrade the database by changing the version to dbVersionHigher
+        try (DatabaseHelper helper = dbVersionHigher.getConstructor(Context.class, String.class)
+                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
+            SQLiteDatabase db = helper.getWritableDatabaseForTest();
+            // Verify that upgrade resulted in database version change.
+            assertWithMessage("Current database version after upgrade")
+                    .that(db.getVersion()).isNotEqualTo(originalVersion);
+            // Verify that upgrade resulted in database version same as latest version.
+            assertWithMessage("Current database version after upgrade")
+                    .that(db.getVersion()).isEqualTo(VERSION_LATEST);
+            // Verify that upgrade didn't change UUID
+            assertWithMessage("Current database UUID after upgrade")
+                    .that(DatabaseHelper.getOrCreateUuid(db)).isEqualTo(originalUUID);
+        }
+
+        // Downgrade the database by changing the version to dbVersionLower
+        try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
+                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
+            SQLiteDatabase db = helper.getWritableDatabaseForTest();
+            // Verify that downgraded version is same as original database version before upgrade
+            assertWithMessage("Current database version after downgrade")
+                    .that(db.getVersion()).isEqualTo(originalVersion);
+            // Verify that downgrade changed UUID
+            assertWithMessage("Current database UUID after downgrade")
+                    .that(DatabaseHelper.getOrCreateUuid(db)).isNotEqualTo(originalUUID);
+        }
+    }
+
     private static String normalize(String sql) {
         return sql != null ? sql.replace(", ", ",") : null;
     }