Merge "Preventing launcher crashes due to low disk space." into ub-launcher3-burnaby-polish
am: 60acb943c6

* commit '60acb943c68e2f949fc8ad4703c9156549263897':
  Preventing launcher crashes due to low disk space.
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index efb978d..d39ae66 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -28,7 +28,7 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteException;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
@@ -48,6 +48,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SQLiteCacheHelper;
 import com.android.launcher3.util.Thunk;
 
 import java.util.Collections;
@@ -231,7 +232,7 @@
     public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) {
         removeFromMemCacheLocked(packageName, user);
         long userSerial = mUserManager.getSerialNumberForUser(user);
-        mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
+        mIconDb.delete(
                 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
                 new String[]{packageName + "/%", Long.toString(userSerial)});
     }
@@ -276,58 +277,65 @@
             componentMap.put(app.getComponentName(), app);
         }
 
-        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
-                new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
-                    IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
-                    IconDB.COLUMN_SYSTEM_STATE},
-                IconDB.COLUMN_USER + " = ? ",
-                new String[] {Long.toString(userSerial)},
-                null, null, null);
-
-        final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
-        final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
-        final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
-        final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
-        final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
-
         HashSet<Integer> itemsToRemove = new HashSet<Integer>();
         Stack<LauncherActivityInfoCompat> appsToUpdate = new Stack<>();
 
-        while (c.moveToNext()) {
-            String cn = c.getString(indexComponent);
-            ComponentName component = ComponentName.unflattenFromString(cn);
-            PackageInfo info = pkgInfoMap.get(component.getPackageName());
-            if (info == null) {
-                if (!ignorePackages.contains(component.getPackageName())) {
+        Cursor c = null;
+        try {
+            c = mIconDb.query(
+                    new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+                            IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+                            IconDB.COLUMN_SYSTEM_STATE},
+                    IconDB.COLUMN_USER + " = ? ",
+                    new String[]{Long.toString(userSerial)});
+
+            final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+            final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+            final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+            final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+            final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+            while (c.moveToNext()) {
+                String cn = c.getString(indexComponent);
+                ComponentName component = ComponentName.unflattenFromString(cn);
+                PackageInfo info = pkgInfoMap.get(component.getPackageName());
+                if (info == null) {
+                    if (!ignorePackages.contains(component.getPackageName())) {
+                        remove(component, user);
+                        itemsToRemove.add(c.getInt(rowIndex));
+                    }
+                    continue;
+                }
+                if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+                    // Application is not present
+                    continue;
+                }
+
+                long updateTime = c.getLong(indexLastUpdate);
+                int version = c.getInt(indexVersion);
+                LauncherActivityInfoCompat app = componentMap.remove(component);
+                if (version == info.versionCode && updateTime == info.lastUpdateTime &&
+                        TextUtils.equals(mSystemState, c.getString(systemStateIndex))) {
+                    continue;
+                }
+                if (app == null) {
                     remove(component, user);
                     itemsToRemove.add(c.getInt(rowIndex));
+                } else {
+                    appsToUpdate.add(app);
                 }
-                continue;
             }
-            if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
-                // Application is not present
-                continue;
-            }
-
-            long updateTime = c.getLong(indexLastUpdate);
-            int version = c.getInt(indexVersion);
-            LauncherActivityInfoCompat app = componentMap.remove(component);
-            if (version == info.versionCode && updateTime == info.lastUpdateTime &&
-                    TextUtils.equals(mSystemState, c.getString(systemStateIndex))) {
-                continue;
-            }
-            if (app == null) {
-                remove(component, user);
-                itemsToRemove.add(c.getInt(rowIndex));
-            } else {
-                appsToUpdate.add(app);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Error reading icon cache", e);
+            // Continue updating whatever we have read so far
+        } finally {
+            if (c != null) {
+                c.close();
             }
         }
-        c.close();
         if (!itemsToRemove.isEmpty()) {
-            mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
-                    Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove),
-                    null);
+            mIconDb.delete(
+                    Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), null);
         }
 
         // Insert remaining apps.
@@ -357,8 +365,7 @@
         values.put(IconDB.COLUMN_USER, userSerial);
         values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
         values.put(IconDB.COLUMN_VERSION, info.versionCode);
-        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
-                SQLiteDatabase.CONFLICT_REPLACE);
+        mIconDb.insertOrReplace(values);
     }
 
     @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
@@ -679,19 +686,18 @@
         ContentValues values = newContentValues(icon, lowResIcon, label);
         values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString());
         values.put(IconDB.COLUMN_USER, userSerial);
-        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
-                SQLiteDatabase.CONFLICT_REPLACE);
+        mIconDb.insertOrReplace(values);
     }
 
     private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
-        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
-                new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
+        Cursor c = null;
+        try {
+            c = mIconDb.query(
+                new String[]{lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
                         IconDB.COLUMN_LABEL},
                 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
-                new String[] {cacheKey.componentName.flattenToString(),
-                    Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))},
-                null, null, null);
-        try {
+                new String[]{cacheKey.componentName.flattenToString(),
+                        Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))});
             if (c.moveToNext()) {
                 entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null);
                 entry.isLowResIcon = lowRes;
@@ -705,8 +711,12 @@
                 }
                 return true;
             }
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Error reading icon cache", e);
         } finally {
-            c.close();
+            if (c != null) {
+                c.close();
+            }
         }
         return false;
     }
@@ -752,9 +762,9 @@
                 LauncherActivityInfoCompat app = mAppsToUpdate.pop();
                 String cn = app.getComponentName().flattenToString();
                 ContentValues values = updateCacheAndGetContentValues(app, true);
-                mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values,
+                mIconDb.update(values,
                         IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
-                        new String[] {cn, Long.toString(mUserSerial)});
+                        new String[]{cn, Long.toString(mUserSerial)});
                 mUpdatedPackages.add(app.getComponentName().getPackageName());
 
                 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
@@ -789,7 +799,7 @@
         mSystemState = Locale.getDefault().toString();
     }
 
-    private static final class IconDB extends SQLiteOpenHelper {
+    private static final class IconDB extends SQLiteCacheHelper {
         private final static int DB_VERSION = 7;
 
         private final static int RELEASE_VERSION = DB_VERSION +
@@ -807,11 +817,11 @@
         private final static String COLUMN_SYSTEM_STATE = "system_state";
 
         public IconDB(Context context) {
-            super(context, LauncherFiles.APP_ICONS_DB, null, RELEASE_VERSION);
+            super(context, LauncherFiles.APP_ICONS_DB, RELEASE_VERSION, TABLE_NAME);
         }
 
         @Override
-        public void onCreate(SQLiteDatabase db) {
+        protected void onCreateTable(SQLiteDatabase db) {
             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
                     COLUMN_COMPONENT + " TEXT NOT NULL, " +
                     COLUMN_USER + " INTEGER NOT NULL, " +
@@ -824,25 +834,6 @@
                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
                     ");");
         }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        @Override
-        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        private void clearDB(SQLiteDatabase db) {
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
-            onCreate(db);
-        }
     }
 
     private ContentValues newContentValues(Bitmap icon, Bitmap lowResIcon, String label) {
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 10c1053..b27fa60 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -10,7 +10,6 @@
 import android.database.Cursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
@@ -32,6 +31,7 @@
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SQLiteCacheHelper;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.widget.WidgetCell;
 
@@ -104,7 +104,7 @@
      * The DB holds the generated previews for various components. Previews can also have different
      * sizes (landscape vs portrait).
      */
-    private static class CacheDb extends SQLiteOpenHelper {
+    private static class CacheDb extends SQLiteCacheHelper {
         private static final int DB_VERSION = 4;
 
         private static final String TABLE_NAME = "shortcut_and_widget_previews";
@@ -117,11 +117,11 @@
         private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
 
         public CacheDb(Context context) {
-            super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION);
+            super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
         }
 
         @Override
-        public void onCreate(SQLiteDatabase database) {
+        public void onCreateTable(SQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
                     COLUMN_COMPONENT + " TEXT NOT NULL, " +
                     COLUMN_USER + " INTEGER NOT NULL, " +
@@ -133,25 +133,6 @@
                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
                     ");");
         }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        @Override
-        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            if (oldVersion != newVersion) {
-                clearDB(db);
-            }
-        }
-
-        private void clearDB(SQLiteDatabase db) {
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
-            onCreate(db);
-        }
     }
 
     private WidgetCacheKey getObjectKey(Object o, String size) {
@@ -176,13 +157,7 @@
         values.put(CacheDb.COLUMN_VERSION, versions[0]);
         values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
         values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview));
-
-        try {
-            mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values,
-                    SQLiteDatabase.CONFLICT_REPLACE);
-        } catch (SQLException e) {
-            Log.e(TAG, "Error saving image to DB", e);
-        }
+        mDb.insertOrReplace(values);
     }
 
     public void removePackage(String packageName, UserHandleCompat user) {
@@ -194,13 +169,9 @@
             mPackageVersions.remove(packageName);
         }
 
-        try {
-            mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME,
-                    CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
-                    new String[] {packageName, Long.toString(userSerial)});
-        } catch (SQLException e) {
-            Log.e(TAG, "Unable to delete items from DB", e);
-        }
+        mDb.delete(
+                CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
+                new String[]{packageName, Long.toString(userSerial)});
     }
 
     /**
@@ -238,10 +209,10 @@
         LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
         Cursor c = null;
         try {
-            c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME,
-                    new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
-                        CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
-                    null, null, null, null, null);
+            c = mDb.query(
+                    new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
+                            CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
+                    null, null);
             while (c.moveToNext()) {
                 long userId = c.getLong(0);
                 String pkg = c.getString(1);
@@ -274,7 +245,7 @@
                 }
             }
         } catch (SQLException e) {
-            Log.e(TAG, "Error updatating widget previews", e);
+            Log.e(TAG, "Error updating widget previews", e);
         } finally {
             if (c != null) {
                 c.close();
@@ -288,16 +259,15 @@
     @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
         Cursor cursor = null;
         try {
-            cursor = mDb.getReadableDatabase().query(
-                    CacheDb.TABLE_NAME,
-                    new String[] { CacheDb.COLUMN_PREVIEW_BITMAP },
-                    CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?",
-                    new String[] {
+            cursor = mDb.query(
+                    new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
+                    CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
+                            + CacheDb.COLUMN_SIZE + " = ?",
+                    new String[]{
                             key.componentName.flattenToString(),
                             Long.toString(mUserManager.getSerialNumberForUser(key.user)),
                             key.size
-                    },
-                    null, null, null);
+                    });
             // If cancelled, skip getting the blob and decoding it into a bitmap
             if (loadTask.isCancelled()) {
                 return null;
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java
new file mode 100644
index 0000000..62a30d0
--- /dev/null
+++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -0,0 +1,128 @@
+package com.android.launcher3.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+/**
+ * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB.
+ * Any exception during write operations are ignored, and any version change causes a DB reset.
+ */
+public abstract class SQLiteCacheHelper {
+    private static final String TAG = "SQLiteCacheHelper";
+
+    private final String mTableName;
+    private final MySQLiteOpenHelper mOpenHelper;
+
+    private boolean mIgnoreWrites;
+
+    public SQLiteCacheHelper(Context context, String name, int version, String tableName) {
+        mTableName = tableName;
+        mOpenHelper = new MySQLiteOpenHelper(context, name, version);
+
+        mIgnoreWrites = false;
+    }
+
+    /**
+     * @see SQLiteDatabase#update(String, ContentValues, String, String[])
+     */
+    public void update(ContentValues values, String whereClause, String[] whereArgs) {
+        if (mIgnoreWrites) {
+            return;
+        }
+        try {
+            mOpenHelper.getWritableDatabase().update(mTableName, values, whereClause, whereArgs);
+        } catch (SQLiteFullException e) {
+            onDiskFull(e);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Ignoring sqlite exception", e);
+        }
+    }
+
+    /**
+     * @see SQLiteDatabase#delete(String, String, String[])
+     */
+    public void delete(String whereClause, String[] whereArgs) {
+        if (mIgnoreWrites) {
+            return;
+        }
+        try {
+            mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs);
+        } catch (SQLiteFullException e) {
+            onDiskFull(e);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Ignoring sqlite exception", e);
+        }
+    }
+
+    /**
+     * @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int)
+     */
+    public void insertOrReplace(ContentValues values) {
+        if (mIgnoreWrites) {
+            return;
+        }
+        try {
+            mOpenHelper.getWritableDatabase().insertWithOnConflict(
+                    mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+        } catch (SQLiteFullException e) {
+            onDiskFull(e);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Ignoring sqlite exception", e);
+        }
+    }
+
+    private void onDiskFull(SQLiteFullException e) {
+        Log.e(TAG, "Disk full, all write operations will be ignored", e);
+        mIgnoreWrites = true;
+    }
+
+    /**
+     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
+     */
+    public Cursor query(String[] columns, String selection, String[] selectionArgs) {
+        return mOpenHelper.getReadableDatabase().query(
+                mTableName, columns, selection, selectionArgs, null, null, null);
+    }
+
+    protected abstract void onCreateTable(SQLiteDatabase db);
+
+    /**
+     * A private inner class to prevent direct DB access.
+     */
+    private class MySQLiteOpenHelper extends SQLiteOpenHelper {
+
+        public MySQLiteOpenHelper(Context context, String name, int version) {
+            super(context, name, null, version);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            onCreateTable(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        private void clearDB(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + mTableName);
+            onCreate(db);
+        }
+    }
+}