OSDN Git Service

Preventing launcher crashes due to low disk space.
authorSunny Goyal <sunnygoyal@google.com>
Fri, 18 Dec 2015 01:09:36 +0000 (17:09 -0800)
committerSunny Goyal <sunnygoyal@google.com>
Fri, 18 Dec 2015 18:20:52 +0000 (10:20 -0800)
In case of low disk space, all write operations to the IconCache are
silently ignored. This will not affect the Launcher behavior and
user will still see the latest icons, but in some cases, icon loading
would appear slow

Bug: 24585352
Change-Id: I85ccc519046fc3708403388bba89e019a3f2ce3d

src/com/android/launcher3/IconCache.java
src/com/android/launcher3/WidgetPreviewLoader.java
src/com/android/launcher3/util/SQLiteCacheHelper.java [new file with mode: 0644]

index 1016137..05ad538 100644 (file)
@@ -28,7 +28,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
 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.compat.UserManagerCompat;
 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;
@@ -230,9 +231,9 @@ public class IconCache {
     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)});
+                new String[]{packageName + "/%", Long.toString(userSerial)});
     }
 
     public void updateDbIcons(Set<String> ignorePackagesForMainUser) {
@@ -275,58 +276,65 @@ public class IconCache {
             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.
@@ -356,8 +364,7 @@ public class IconCache {
         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,
@@ -672,19 +679,18 @@ public class IconCache {
                 label, Color.TRANSPARENT);
         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;
@@ -698,8 +704,12 @@ public class IconCache {
                 }
                 return true;
             }
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Error reading icon cache", e);
         } finally {
-            c.close();
+            if (c != null) {
+                c.close();
+            }
         }
         return false;
     }
@@ -745,9 +755,9 @@ public class IconCache {
                 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()) {
@@ -782,7 +792,7 @@ public class IconCache {
         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 +
@@ -800,11 +810,11 @@ public class IconCache {
         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, " +
@@ -817,25 +827,6 @@ public class IconCache {
                     "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, String label, int lowResBackgroundColor) {
index 10c1053..b27fa60 100644 (file)
@@ -10,7 +10,6 @@ import android.content.res.Resources;
 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.AppWidgetManagerCompat;
 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 @@ public class WidgetPreviewLoader {
      * 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 @@ public class WidgetPreviewLoader {
         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 @@ public class WidgetPreviewLoader {
                     "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 @@ public class WidgetPreviewLoader {
         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 @@ public class WidgetPreviewLoader {
             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 @@ public class WidgetPreviewLoader {
         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 @@ public class WidgetPreviewLoader {
                 }
             }
         } 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 @@ public class WidgetPreviewLoader {
     @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 (file)
index 0000000..62a30d0
--- /dev/null
@@ -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);
+        }
+    }
+}