Simplifying some DB managed logic

> Adding SQLiteTransaction to make it easier to manage DB transactions
> Using try-with resource for better resource handling
> Defining utility method for iterating over cursor

Change-Id: I20b1a62d61798342825ecfeb971e1a0c63c9b6d7
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 03ca242..ca789d4 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.LauncherDbUtils;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.NoLocaleSqliteContext;
@@ -70,6 +71,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
@@ -303,8 +305,7 @@
         SqlArguments args = new SqlArguments(uri);
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             int numValues = values.length;
             for (int i = 0; i < numValues; i++) {
                 addModifiedTime(values[i]);
@@ -312,9 +313,7 @@
                     return 0;
                 }
             }
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
+            t.commit();
         }
 
         notifyListeners();
@@ -326,15 +325,11 @@
     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
             throws OperationApplicationException {
         createDbIfNotExists();
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
+        try (SQLiteTransaction t = new SQLiteTransaction(mOpenHelper.getWritableDatabase())) {
             ContentProviderResult[] result =  super.applyBatch(operations);
-            db.setTransactionSuccessful();
+            t.commit();
             reloadLauncherIfExternal();
             return result;
-        } finally {
-            db.endTransaction();
         }
     }
 
@@ -440,31 +435,26 @@
     private ArrayList<Long> deleteEmptyFolders() {
         ArrayList<Long> folderIds = new ArrayList<>();
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             // Select folders whose id do not match any container value.
             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
                             LauncherSettings.Favorites.CONTAINER + " FROM "
                                 + Favorites.TABLE_NAME + ")";
-            Cursor c = db.query(Favorites.TABLE_NAME,
+            try (Cursor c = db.query(Favorites.TABLE_NAME,
                     new String[] {LauncherSettings.Favorites._ID},
-                    selection, null, null, null, null);
-            while (c.moveToNext()) {
-                folderIds.add(c.getLong(0));
+                    selection, null, null, null, null)) {
+                LauncherDbUtils.iterateCursor(c, 0, folderIds);
             }
-            c.close();
             if (!folderIds.isEmpty()) {
                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
                         LauncherSettings.Favorites._ID, folderIds), null);
             }
-            db.setTransactionSuccessful();
+            t.commit();
         } catch (SQLException ex) {
             Log.e(TAG, ex.getMessage(), ex);
             folderIds.clear();
-        } finally {
-            db.endTransaction();
         }
         return folderIds;
     }
@@ -718,15 +708,12 @@
             if (oldVersion != DATA_VERSION) {
                 // Only run the data upgrade path for an existing db.
                 if (!Utilities.getPrefs(mContext).getBoolean(EMPTY_DATABASE_CREATED, false)) {
-                    db.beginTransaction();
-                    try {
+                    try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                         onDataUpgrade(db, oldVersion);
-                        db.setTransactionSuccessful();
+                        t.commit();
                     } catch (Exception e) {
                         Log.d(TAG, "Error updating data version, ignoring", e);
                         return;
-                    } finally {
-                        db.endTransaction();
                     }
                 }
                 prefs.edit().putInt(PREF_KEY_DATA_VERISON, DATA_VERSION).apply();
@@ -773,35 +760,29 @@
                     addWorkspacesTable(db, false);
                 }
                 case 13: {
-                    db.beginTransaction();
-                    try {
+                    try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                         // Insert new column for holding widget provider name
                         db.execSQL("ALTER TABLE favorites " +
                                 "ADD COLUMN appWidgetProvider TEXT;");
-                        db.setTransactionSuccessful();
+                        t.commit();
                     } catch (SQLException ex) {
                         Log.e(TAG, ex.getMessage(), ex);
                         // Old version remains, which means we wipe old data
                         break;
-                    } finally {
-                        db.endTransaction();
                     }
                 }
                 case 14: {
-                    db.beginTransaction();
-                    try {
+                    try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                         // Insert new column for holding update timestamp
                         db.execSQL("ALTER TABLE favorites " +
                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
                         db.execSQL("ALTER TABLE workspaceScreens " +
                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
-                        db.setTransactionSuccessful();
+                        t.commit();
                     } catch (SQLException ex) {
                         Log.e(TAG, ex.getMessage(), ex);
                         // Old version remains, which means we wipe old data
                         break;
-                    } finally {
-                        db.endTransaction();
                     }
                 }
                 case 15: {
@@ -884,14 +865,11 @@
          * Clears all the data for a fresh start.
          */
         public void createEmptyDB(SQLiteDatabase db) {
-            db.beginTransaction();
-            try {
+            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                 db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
                 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
                 onCreate(db);
-                db.setTransactionSuccessful();
-            } finally {
-                db.endTransaction();
+                t.commit();
             }
         }
 
@@ -911,28 +889,26 @@
                 Log.e(TAG, "getAppWidgetIds not supported", e);
                 return;
             }
-            try {
-                Cursor c = db.query(Favorites.TABLE_NAME,
-                        new String[] {Favorites.APPWIDGET_ID },
-                        "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null, null, null);
-                HashSet<Integer> validWidgets = new HashSet<>();
+            final HashSet<Integer> validWidgets = new HashSet<>();
+            try (Cursor c = db.query(Favorites.TABLE_NAME,
+                    new String[] {Favorites.APPWIDGET_ID },
+                    "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null, null, null)) {
                 while (c.moveToNext()) {
                     validWidgets.add(c.getInt(0));
                 }
-                c.close();
-
-                for (int widgetId : allWidgets) {
-                    if (!validWidgets.contains(widgetId)) {
-                        try {
-                            FileLog.d(TAG, "Deleting invalid widget " + widgetId);
-                            host.deleteAppWidgetId(widgetId);
-                        } catch (RuntimeException e) {
-                            // Ignore
-                        }
-                    }
-                }
             } catch (SQLException ex) {
                 Log.w(TAG, "Error getting widgets list", ex);
+                return;
+            }
+            for (int widgetId : allWidgets) {
+                if (!validWidgets.contains(widgetId)) {
+                    try {
+                        FileLog.d(TAG, "Deleting invalid widget " + widgetId);
+                        host.deleteAppWidgetId(widgetId);
+                    } catch (RuntimeException e) {
+                        // Ignore
+                    }
+                }
             }
         }
 
@@ -941,22 +917,16 @@
          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
          */
         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
-            db.beginTransaction();
-            Cursor c = null;
-            SQLiteStatement updateStmt = null;
-
-            try {
-                // Only consider the primary user as other users can't have a shortcut.
-                long userSerial = getDefaultUserSerial();
-                c = db.query(Favorites.TABLE_NAME, new String[] {
-                        Favorites._ID,
-                        Favorites.INTENT,
-                    }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
-                    null, null, null, null);
-
-                updateStmt = db.compileStatement("UPDATE favorites SET itemType="
-                        + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
-
+            try (SQLiteTransaction t = new SQLiteTransaction(db);
+                 // Only consider the primary user as other users can't have a shortcut.
+                 Cursor c = db.query(Favorites.TABLE_NAME,
+                         new String[] { Favorites._ID, Favorites.INTENT},
+                         "itemType=" + Favorites.ITEM_TYPE_SHORTCUT +
+                                 " AND profileId=" + getDefaultUserSerial(),
+                         null, null, null, null);
+                 SQLiteStatement updateStmt = db.compileStatement("UPDATE favorites SET itemType="
+                         + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?")
+            ) {
                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
 
@@ -978,17 +948,9 @@
                     updateStmt.bindLong(1, id);
                     updateStmt.executeUpdateDelete();
                 }
-                db.setTransactionSuccessful();
+                t.commit();
             } catch (SQLException ex) {
                 Log.w(TAG, "Error deduping shortcuts", ex);
-            } finally {
-                db.endTransaction();
-                if (c != null) {
-                    c.close();
-                }
-                if (updateStmt != null) {
-                    updateStmt.close();
-                }
             }
         }
 
@@ -996,26 +958,17 @@
          * Recreates workspace table and migrates data to the new table.
          */
         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
-            db.beginTransaction();
-            try {
-                Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
+            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+                final ArrayList<Long> sortedIDs;
+
+                try (Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
                         new String[] {LauncherSettings.WorkspaceScreens._ID},
                         null, null, null, null,
-                        LauncherSettings.WorkspaceScreens.SCREEN_RANK);
-                ArrayList<Long> sortedIDs = new ArrayList<Long>();
-                long maxId = 0;
-                try {
-                    while (c.moveToNext()) {
-                        Long id = c.getLong(0);
-                        if (!sortedIDs.contains(id)) {
-                            sortedIDs.add(id);
-                            maxId = Math.max(maxId, id);
-                        }
-                    }
-                } finally {
-                    c.close();
+                        LauncherSettings.WorkspaceScreens.SCREEN_RANK)) {
+                    // Use LinkedHashSet so that ordering is preserved
+                    sortedIDs = new ArrayList<>(
+                            LauncherDbUtils.iterateCursor(c, 0, new LinkedHashSet<Long>()));
                 }
-
                 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
                 addWorkspacesTable(db, false);
 
@@ -1028,21 +981,18 @@
                     addModifiedTime(values);
                     db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
                 }
-                db.setTransactionSuccessful();
-                mMaxScreenId = maxId;
+                t.commit();
+                mMaxScreenId = sortedIDs.isEmpty() ? 0 : Collections.max(sortedIDs);
             } catch (SQLException ex) {
                 // Old version remains, which means we wipe old data
                 Log.e(TAG, ex.getMessage(), ex);
                 return false;
-            } finally {
-                db.endTransaction();
             }
             return true;
         }
 
         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
-            db.beginTransaction();
-            try {
+            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                 if (addRankColumn) {
                     // Insert new column for holding rank
                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
@@ -1061,13 +1011,11 @@
                 }
 
                 c.close();
-                db.setTransactionSuccessful();
+                t.commit();
             } catch (SQLException ex) {
                 // Old version remains, which means we wipe old data
                 Log.e(TAG, ex.getMessage(), ex);
                 return false;
-            } finally {
-                db.endTransaction();
             }
             return true;
         }
@@ -1077,16 +1025,13 @@
         }
 
         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
-            db.beginTransaction();
-            try {
+            try (SQLiteTransaction t = new SQLiteTransaction(db)) {
                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
-                db.setTransactionSuccessful();
+                t.commit();
             } catch (SQLException ex) {
                 Log.e(TAG, ex.getMessage(), ex);
                 return false;
-            } finally {
-                db.endTransaction();
             }
             return true;
         }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 1758350..6325040 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -19,15 +19,16 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
-import com.android.launcher3.logging.FileLog;
 
 import java.util.ArrayList;
+import java.util.Collection;
 
 /**
  * A set of utility methods for Launcher DB used for DB updates and migration.
@@ -44,8 +45,7 @@
      * items are simply deleted.
      */
     public static boolean prepareScreenZeroToHostQsb(Context context, SQLiteDatabase db) {
-        db.beginTransaction();
-        try {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             // Get the existing screens
             ArrayList<Long> screenIds = getScreenIdsFromCursor(db.query(WorkspaceScreens.TABLE_NAME,
                     null, null, null, null, null, WorkspaceScreens.SCREEN_RANK));
@@ -68,23 +68,19 @@
             }
 
             // Check if the first row is empty
-            try (Cursor c = db.query(Favorites.TABLE_NAME, null,
-                    "container = -100 and screen = 0 and cellY = 0", null, null, null, null)) {
-                if (c.getCount() == 0) {
-                    // First row is empty, no need to migrate.
-                    return true;
-                }
+            if (DatabaseUtils.queryNumEntries(db, Favorites.TABLE_NAME,
+                    "container = -100 and screen = 0 and cellY = 0") == 0) {
+                // First row is empty, no need to migrate.
+                return true;
             }
 
             new LossyScreenMigrationTask(context, LauncherAppState.getIDP(context), db)
                     .migrateScreen0();
-            db.setTransactionSuccessful();
+            t.commit();
             return true;
         } catch (Exception e) {
             Log.e(TAG, "Failed to update workspace size", e);
             return false;
-        } finally {
-            db.endTransaction();
         }
     }
 
@@ -104,19 +100,40 @@
      * Parses the cursor containing workspace screens table and returns the list of screen IDs
      */
     public static ArrayList<Long> getScreenIdsFromCursor(Cursor sc) {
-        ArrayList<Long> screenIds = new ArrayList<Long>();
         try {
-            final int idIndex = sc.getColumnIndexOrThrow(WorkspaceScreens._ID);
-            while (sc.moveToNext()) {
-                try {
-                    screenIds.add(sc.getLong(idIndex));
-                } catch (Exception e) {
-                    FileLog.d(TAG, "Invalid screen id", e);
-                }
-            }
+            return iterateCursor(sc,
+                    sc.getColumnIndexOrThrow(WorkspaceScreens._ID),
+                    new ArrayList<Long>());
         } finally {
             sc.close();
         }
-        return screenIds;
+    }
+
+    public static <T extends Collection<Long>> T iterateCursor(Cursor c, int columnIndex, T out) {
+        while (c.moveToNext()) {
+            out.add(c.getLong(columnIndex));
+        }
+        return out;
+    }
+
+    /**
+     * Utility class to simplify managing sqlite transactions
+     */
+    public static class SQLiteTransaction implements AutoCloseable {
+        private final SQLiteDatabase mDb;
+
+        public SQLiteTransaction(SQLiteDatabase db) {
+            mDb = db;
+            db.beginTransaction();
+        }
+
+        public void commit() {
+            mDb.setTransactionSuccessful();
+        }
+
+        @Override
+        public void close() {
+            mDb.endTransaction();
+        }
     }
 }
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index dc85aba..00e2644 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.LogConfig;
 
 import java.io.InvalidObjectException;
@@ -47,16 +48,13 @@
 
     public static boolean performRestore(DatabaseHelper helper) {
         SQLiteDatabase db = helper.getWritableDatabase();
-        db.beginTransaction();
-        try {
+        try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             new RestoreDbTask().sanitizeDB(helper, db);
-            db.setTransactionSuccessful();
+            t.commit();
             return true;
         } catch (Exception e) {
             FileLog.e(TAG, "Failed to verify db", e);
             return false;
-        } finally {
-            db.endTransaction();
         }
     }