KeyChain: Upgrade keys database

A new table was added to KeyChain's grants.db, storing the
user-visibilty state of each alias KeyChain manages.

Formerly it was not created during an upgrade, which led to existing
keys being classified as non-user-selectable and essentially not usable.

Now, during upgrade, the table for user-visibility is created and the
existing aliases are all set as user-selectable in it.

Test: m -j RunKeyChainRoboTests
Bug: 73898958
Change-Id: I3b92a957f4e949c13363769ece531af438895ff9
diff --git a/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java b/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java
index 68b28e3..a1d4990 100644
--- a/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java
+++ b/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java
@@ -19,7 +19,11 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.content.ContentValues;
+import android.content.Context;
 import android.content.pm.PackageManager;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import com.android.keychain.TestConfig;
 import org.junit.Assert;
 import org.junit.Before;
@@ -37,6 +41,12 @@
     private static final String DUMMY_ALIAS2 = "another_dummy_alias";
     private static final int DUMMY_UID = 1000;
     private static final int DUMMY_UID2 = 1001;
+    // Constants duplicated from GrantsDatabase to make sure the upgrade tests catch if the
+    // name of one of the fields in the DB changes.
+    static final String DATABASE_NAME = "grants.db";
+    static final String TABLE_GRANTS = "grants";
+    static final String GRANTS_ALIAS = "alias";
+    static final String GRANTS_GRANTEE_UID = "uid";
 
     private GrantsDatabase mGrantsDB;
 
@@ -107,7 +117,7 @@
     public void testPurgeOldGrantsDoesNotDeleteGrantsForExistingPackages() {
         mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
         PackageManager pm = mock(PackageManager.class);
-        when(pm.getPackagesForUid(DUMMY_UID)).thenReturn(new String[]{"p"});
+        when(pm.getPackagesForUid(DUMMY_UID)).thenReturn(new String[] {"p"});
         mGrantsDB.purgeOldGrants(pm);
         Assert.assertTrue(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
     }
@@ -146,4 +156,57 @@
         mGrantsDB.setIsUserSelectable(DUMMY_ALIAS, true);
         Assert.assertTrue(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
     }
+
+    private class V1DatabaseHelper extends SQLiteOpenHelper {
+        public V1DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null /* CursorFactory */, 1);
+        }
+
+        @Override
+        public void onCreate(final SQLiteDatabase db) {
+            db.execSQL(
+                    "CREATE TABLE "
+                            + TABLE_GRANTS
+                            + " (  "
+                            + GRANTS_ALIAS
+                            + " STRING NOT NULL,  "
+                            + GRANTS_GRANTEE_UID
+                            + " INTEGER NOT NULL,  "
+                            + "UNIQUE ("
+                            + GRANTS_ALIAS
+                            + ","
+                            + GRANTS_GRANTEE_UID
+                            + "))");
+        }
+
+        @Override
+        public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
+            throw new IllegalStateException("Existing DB must be dropped first.");
+        }
+    }
+
+    @Test
+    public void testUpgradeDatabase() {
+        // Close old DB
+        mGrantsDB.destroy();
+        // Create a new, V1 database.
+        Context context = RuntimeEnvironment.application;
+        context.deleteDatabase(DATABASE_NAME);
+        V1DatabaseHelper v1DBHelper = new V1DatabaseHelper(context);
+        // Fill it up with a few records
+        final SQLiteDatabase db = v1DBHelper.getWritableDatabase();
+        String[] aliases = {"alias-1", "alias-2", "alias-3"};
+        for (String alias : aliases) {
+            final ContentValues values = new ContentValues();
+            values.put(GRANTS_ALIAS, alias);
+            values.put(GRANTS_GRANTEE_UID, 123456);
+            db.insert(TABLE_GRANTS, GRANTS_ALIAS, values);
+        }
+
+        // Test that the aliases were made user-selectable during the upgrade.
+        mGrantsDB = new GrantsDatabase(RuntimeEnvironment.application);
+        for (String alias : aliases) {
+            Assert.assertTrue(mGrantsDB.isUserSelectable(alias));
+        }
+    }
 }
diff --git a/src/com/android/keychain/internal/GrantsDatabase.java b/src/com/android/keychain/internal/GrantsDatabase.java
index 1e333c1..ae11505 100644
--- a/src/com/android/keychain/internal/GrantsDatabase.java
+++ b/src/com/android/keychain/internal/GrantsDatabase.java
@@ -29,7 +29,7 @@
     private static final String TAG = "KeyChain";
 
     private static final String DATABASE_NAME = "grants.db";
-    private static final int DATABASE_VERSION = 1;
+    private static final int DATABASE_VERSION = 2;
     private static final String TABLE_GRANTS = "grants";
     private static final String GRANTS_ALIAS = "alias";
     private static final String GRANTS_GRANTEE_UID = "uid";
@@ -62,6 +62,20 @@
             super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION);
         }
 
+        void createSelectableTable(final SQLiteDatabase db) {
+            db.execSQL(
+                    "CREATE TABLE "
+                            + TABLE_SELECTABLE
+                            + " (  "
+                            + GRANTS_ALIAS
+                            + " STRING NOT NULL,  "
+                            + SELECTABLE_IS_SELECTABLE
+                            + " STRING NOT NULL,  "
+                            + "UNIQUE ("
+                            + GRANTS_ALIAS
+                            + "))");
+        }
+
         @Override
         public void onCreate(final SQLiteDatabase db) {
             db.execSQL(
@@ -78,26 +92,39 @@
                             + GRANTS_GRANTEE_UID
                             + "))");
 
-            db.execSQL(
-                    "CREATE TABLE "
-                            + TABLE_SELECTABLE
-                            + " (  "
-                            + GRANTS_ALIAS
-                            + " STRING NOT NULL,  "
-                            + SELECTABLE_IS_SELECTABLE
-                            + " STRING NOT NULL,  "
-                            + "UNIQUE ("
-                            + GRANTS_ALIAS
-                            + "))");
+            createSelectableTable(db);
         }
 
         @Override
         public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
-            Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
+            Log.w(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
 
             if (oldVersion == 1) {
-                // the first upgrade step goes here
+                // Version 1 of the database does not have the 'userselectable' table, meaning
+                // upgraded keys could not be selected by users.
+                // The upgrade from version 1 to 2 consists of creating the 'userselectable'
+                // table and adding all existing keys as user-selectable ones into that table.
                 oldVersion++;
+                createSelectableTable(db);
+
+                try (Cursor cursor =
+                        db.query(
+                                TABLE_GRANTS,
+                                new String[] {GRANTS_ALIAS},
+                                null,
+                                null,
+                                GRANTS_ALIAS,
+                                null,
+                                null)) {
+
+                    while ((cursor != null) && (cursor.moveToNext())) {
+                        final String alias = cursor.getString(0);
+                        final ContentValues values = new ContentValues();
+                        values.put(GRANTS_ALIAS, alias);
+                        values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(true));
+                        db.replace(TABLE_SELECTABLE, null, values);
+                    }
+                }
             }
         }
     }