Merge "Improving LauncherModel performance"
diff --git a/src/com/android/launcher2/LauncherApplication.java b/src/com/android/launcher2/LauncherApplication.java
index 68b1644..db3a4cb 100644
--- a/src/com/android/launcher2/LauncherApplication.java
+++ b/src/com/android/launcher2/LauncherApplication.java
@@ -24,11 +24,14 @@
 import android.database.ContentObserver;
 import android.os.Handler;
 
+import java.lang.ref.WeakReference;
+
 public class LauncherApplication extends Application {
     public LauncherModel mModel;
     public IconCache mIconCache;
     private static boolean sIsScreenLarge;
     private static float sScreenDensity;
+    WeakReference<LauncherProvider> mLauncherProvider;
 
     @Override
     public void onCreate() {
@@ -97,6 +100,14 @@
         return mModel;
     }
 
+    void setLauncherProvider(LauncherProvider provider) {
+        mLauncherProvider = new WeakReference<LauncherProvider>(provider);
+    }
+
+    LauncherProvider getLauncherProvider() {
+        return mLauncherProvider.get();
+    }
+
     public static boolean isScreenLarge() {
         return sIsScreenLarge;
     }
diff --git a/src/com/android/launcher2/LauncherModel.java b/src/com/android/launcher2/LauncherModel.java
index e8ae9d9..64b38c0 100644
--- a/src/com/android/launcher2/LauncherModel.java
+++ b/src/com/android/launcher2/LauncherModel.java
@@ -88,9 +88,27 @@
 
     private WeakReference<Callbacks> mCallbacks;
 
-    private AllAppsList mAllAppsList; // only access in worker thread
-    private IconCache mIconCache;
+    // < only access in worker thread >
+    private AllAppsList mAllAppsList;
 
+    // sItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by
+    // LauncherModel to their ids
+    static final HashMap<Long, ItemInfo> sItemsIdMap = new HashMap<Long, ItemInfo>();
+
+    // sItems is passed to bindItems, which expects a list of all folders and shortcuts created by
+    //       LauncherModel that are directly on the home screen (however, no widgets or shortcuts
+    //       within folders).
+    static final ArrayList<ItemInfo> sItems = new ArrayList<ItemInfo>();
+
+    // sAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget()
+    static final ArrayList<LauncherAppWidgetInfo> sAppWidgets =
+        new ArrayList<LauncherAppWidgetInfo>();
+
+    // sFolders is all FolderInfos created by LauncherModel. Passed to bindFolders()
+    static final HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>();
+    // </ only access in worker thread >
+
+    private IconCache mIconCache;
     private Bitmap mDefaultIcon;
 
     private static int mCellCountX;
@@ -148,9 +166,8 @@
     /**
      * Move an item in the DB to a new <container, screen, cellX, cellY>
      */
-    static void moveItemInDatabase(Context context, ItemInfo item, long container, int screen,
-            int cellX, int cellY) {
-
+    static void moveItemInDatabase(Context context, final ItemInfo item, final long container,
+            final int screen, final int cellX, final int cellY) {
         item.container = container;
         item.screen = screen;
         item.cellX = cellX;
@@ -168,6 +185,24 @@
         sWorker.post(new Runnable() {
                 public void run() {
                     cr.update(uri, values, null, null);
+                    ItemInfo modelItem = sItemsIdMap.get(item.id);
+                    if (item != modelItem) {
+                        // the modelItem needs to match up perfectly with item if our model is to be
+                        // consistent with the database-- for now, just require modelItem == item
+                        throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " +
+                                "doesn't match original");
+                    }
+
+                    // Items are added/removed from the corresponding FolderInfo elsewhere, such
+                    // as in Workspace.onDrop. Here, we just add/remove them from the list of items
+                    // that are on the desktop, as appropriate
+                    if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                        if (!sItems.contains(modelItem)) {
+                            sItems.add(modelItem);
+                        }
+                    } else {
+                        sItems.remove(modelItem);
+                    }
                 }
             });
     }
@@ -175,8 +210,8 @@
     /**
      * Resize an item in the DB to a new <spanX, spanY, cellX, cellY>
      */
-    static void resizeItemInDatabase(Context context, ItemInfo item, int cellX, int cellY,
-            int spanX, int spanY) {
+    static void resizeItemInDatabase(Context context, final ItemInfo item, final int cellX,
+            final int cellY, final int spanX, final int spanY) {
         item.spanX = spanX;
         item.spanY = spanY;
         item.cellX = cellX;
@@ -195,6 +230,13 @@
         sWorker.post(new Runnable() {
                 public void run() {
                     cr.update(uri, values, null, null);
+                    ItemInfo modelItem = sItemsIdMap.get(item.id);
+                    if (item != modelItem) {
+                        // the modelItem needs to match up perfectly with item if our model is to be
+                        // consistent with the database-- for now, just require modelItem == item
+                        throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " +
+                            "doesn't match original");
+                    }
                 }
             });
     }
@@ -316,14 +358,37 @@
         final ContentResolver cr = context.getContentResolver();
         item.onAddToDatabase(values);
 
+        Launcher l = (Launcher) context;
+        LauncherApplication app = (LauncherApplication) l.getApplication();
+        item.id = app.getLauncherProvider().generateNewId();
+        values.put(LauncherSettings.Favorites._ID, item.id);
         item.updateValuesWithCoordinates(values, cellX, cellY);
 
-        Uri result = cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
-            LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
+        sWorker.post(new Runnable() {
+            public void run() {
+                cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
+                        LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
 
-        if (result != null) {
-            item.id = Integer.parseInt(result.getPathSegments().get(1));
-        }
+                sItemsIdMap.put(item.id, item);
+                switch (item.itemType) {
+                    case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                        sFolders.put(item.id, (FolderInfo) item);
+                        if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                            sItems.add(item);
+                        }
+                        break;
+                    case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                            sItems.add(item);
+                        }
+                        break;
+                    case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                        sAppWidgets.add((LauncherAppWidgetInfo) item);
+                        break;
+                }
+            }
+        });
     }
 
     /**
@@ -355,14 +420,26 @@
     /**
      * Update an item to the database in a specified container.
      */
-    static void updateItemInDatabase(Context context, ItemInfo item) {
+    static void updateItemInDatabase(Context context, final ItemInfo item) {
         final ContentValues values = new ContentValues();
         final ContentResolver cr = context.getContentResolver();
 
         item.onAddToDatabase(values);
         item.updateValuesWithCoordinates(values, item.cellX, item.cellY);
 
-        cr.update(LauncherSettings.Favorites.getContentUri(item.id, false), values, null, null);
+        sWorker.post(new Runnable() {
+            public void run() {
+                cr.update(LauncherSettings.Favorites.getContentUri(item.id, false),
+                        values, null, null);
+                final ItemInfo modelItem = sItemsIdMap.get(item.id);
+                if (item != modelItem) {
+                    // the modelItem needs to match up perfectly with item if our model is to be
+                    // consistent with the database-- for now, just require modelItem == item
+                    throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " +
+                        "doesn't match original");
+                }
+            }
+        });
     }
 
     /**
@@ -370,43 +447,50 @@
      * @param context
      * @param item
      */
-    static void deleteItemFromDatabase(Context context, ItemInfo item) {
+    static void deleteItemFromDatabase(Context context, final ItemInfo item) {
         final ContentResolver cr = context.getContentResolver();
         final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false);
         sWorker.post(new Runnable() {
                 public void run() {
                     cr.delete(uriToDelete, null, null);
+                    switch (item.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                            sFolders.remove(item.id);
+                            sItems.remove(item);
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                            sItems.remove(item);
+                            break;
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                            sAppWidgets.remove((LauncherAppWidgetInfo) item);
+                            break;
+                    }
+                    sItemsIdMap.remove(item.id);
                 }
             });
     }
 
-    static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) {
-        deleteFolderContentsFromDatabase(context, info, false);
-    }
-
     /**
      * Remove the contents of the specified folder from the database
      */
-    static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info,
-            boolean post) {
-        // TODO: this post flag is temporary to fix an ordering of commands issue. In future, 
-        // all db operations will be moved to the worker thread, so this can be discarded at
-        // that time.
+    static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) {
         final ContentResolver cr = context.getContentResolver();
 
-        if (!post) {
-            cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null);
-            cr.delete(LauncherSettings.Favorites.CONTENT_URI,
-                    LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
-        } else {
-            sWorker.post(new Runnable() {
+        sWorker.post(new Runnable() {
                 public void run() {
                     cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null);
+                    sItemsIdMap.remove(info.id);
+                    sFolders.remove(info.id);
+                    sItems.remove(info);
+
                     cr.delete(LauncherSettings.Favorites.CONTENT_URI,
                             LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
+                    for (ItemInfo childInfo : info.contents) {
+                        sItemsIdMap.remove(childInfo.id);
+                    }
                 }
             });
-        }
     }
 
     /**
@@ -547,10 +631,6 @@
         private boolean mStopped;
         private boolean mLoadAndBindStepFinished;
 
-        final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
-        final ArrayList<LauncherAppWidgetInfo> mAppWidgets = new ArrayList<LauncherAppWidgetInfo>();
-        final HashMap<Long, FolderInfo> mFolders = new HashMap<Long, FolderInfo>();
-
         LoaderTask(Context context, boolean isLaunching) {
             mContext = context;
             mIsLaunching = isLaunching;
@@ -562,14 +642,10 @@
 
         private void loadAndBindWorkspace() {
             // Load the workspace
-
-            // For now, just always reload the workspace.  It's ~100 ms vs. the
-            // binding which takes many hundreds of ms.
-            // We can reconsider.
             if (DEBUG_LOADERS) {
                 Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
             }
-            if (true || !mWorkspaceLoaded) {
+            if (!mWorkspaceLoaded) {
                 loadWorkspace();
                 if (mStopped) {
                     return;
@@ -743,9 +819,10 @@
             final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
             final boolean isSafeMode = manager.isSafeMode();
 
-            mItems.clear();
-            mAppWidgets.clear();
-            mFolders.clear();
+            sItems.clear();
+            sAppWidgets.clear();
+            sFolders.clear();
+            sItemsIdMap.clear();
 
             final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
 
@@ -834,15 +911,16 @@
 
                                 switch (container) {
                                 case LauncherSettings.Favorites.CONTAINER_DESKTOP:
-                                    mItems.add(info);
+                                    sItems.add(info);
                                     break;
                                 default:
                                     // Item is in a user folder
                                     FolderInfo folderInfo =
-                                            findOrMakeFolder(mFolders, container);
+                                            findOrMakeFolder(sFolders, container);
                                     folderInfo.add(info);
                                     break;
                                 }
+                                sItemsIdMap.put(info.id, info);
 
                                 // now that we've loaded everthing re-save it with the
                                 // icon in case it disappears somehow.
@@ -861,7 +939,7 @@
 
                         case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                             id = c.getLong(idIndex);
-                            FolderInfo folderInfo = findOrMakeFolder(mFolders, id);
+                            FolderInfo folderInfo = findOrMakeFolder(sFolders, id);
 
                             folderInfo.title = c.getString(titleIndex);
                             folderInfo.id = id;
@@ -877,11 +955,12 @@
                             }
                             switch (container) {
                                 case LauncherSettings.Favorites.CONTAINER_DESKTOP:
-                                    mItems.add(folderInfo);
+                                    sItems.add(folderInfo);
                                     break;
                             }
 
-                            mFolders.put(folderInfo.id, folderInfo);
+                            sItemsIdMap.put(folderInfo.id, folderInfo);
+                            sFolders.put(folderInfo.id, folderInfo);
                             break;
 
                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -918,8 +997,8 @@
                                 if (!checkItemPlacement(occupied, appWidgetInfo)) {
                                     break;
                                 }
-
-                                mAppWidgets.add(appWidgetInfo);
+                                sItemsIdMap.put(appWidgetInfo.id, appWidgetInfo);
+                                sAppWidgets.add(appWidgetInfo);
                             }
                             break;
                         }
@@ -993,7 +1072,7 @@
                 }
             });
             // Add the items to the workspace.
-            N = mItems.size();
+            N = sItems.size();
             for (int i=0; i<N; i+=ITEMS_CHUNK) {
                 final int start = i;
                 final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
@@ -1001,7 +1080,7 @@
                     public void run() {
                         Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                         if (callbacks != null) {
-                            callbacks.bindItems(mItems, start, start+chunkSize);
+                            callbacks.bindItems(sItems, start, start+chunkSize);
                         }
                     }
                 });
@@ -1010,7 +1089,7 @@
                 public void run() {
                     Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                     if (callbacks != null) {
-                        callbacks.bindFolders(mFolders);
+                        callbacks.bindFolders(sFolders);
                     }
                 }
             });
@@ -1028,10 +1107,10 @@
             // is just a hint for the order, and if it's wrong, we'll be okay.
             // TODO: instead, we should have that push the current screen into here.
             final int currentScreen = oldCallbacks.getCurrentWorkspaceScreen();
-            N = mAppWidgets.size();
+            N = sAppWidgets.size();
             // once for the current screen
             for (int i=0; i<N; i++) {
-                final LauncherAppWidgetInfo widget = mAppWidgets.get(i);
+                final LauncherAppWidgetInfo widget = sAppWidgets.get(i);
                 if (widget.screen == currentScreen) {
                     mHandler.post(new Runnable() {
                         public void run() {
@@ -1045,7 +1124,7 @@
             }
             // once for the other screens
             for (int i=0; i<N; i++) {
-                final LauncherAppWidgetInfo widget = mAppWidgets.get(i);
+                final LauncherAppWidgetInfo widget = sAppWidgets.get(i);
                 if (widget.screen != currentScreen) {
                     mHandler.post(new Runnable() {
                         public void run() {
@@ -1238,7 +1317,7 @@
             Log.d(TAG, "mLoaderTask.mIsLaunching=" + mIsLaunching);
             Log.d(TAG, "mLoaderTask.mStopped=" + mStopped);
             Log.d(TAG, "mLoaderTask.mLoadAndBindStepFinished=" + mLoadAndBindStepFinished);
-            Log.d(TAG, "mItems size=" + mItems.size());
+            Log.d(TAG, "mItems size=" + sItems.size());
         }
     }
 
diff --git a/src/com/android/launcher2/LauncherProvider.java b/src/com/android/launcher2/LauncherProvider.java
index a02e449..5ffd984 100644
--- a/src/com/android/launcher2/LauncherProvider.java
+++ b/src/com/android/launcher2/LauncherProvider.java
@@ -32,6 +32,7 @@
 import android.content.res.TypedArray;
 import android.content.pm.PackageManager;
 import android.content.pm.ActivityInfo;
+import android.content.SharedPreferences;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteStatement;
@@ -80,11 +81,12 @@
     static final Uri CONTENT_APPWIDGET_RESET_URI =
             Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
     
-    private SQLiteOpenHelper mOpenHelper;
+    private DatabaseHelper mOpenHelper;
 
     @Override
     public boolean onCreate() {
         mOpenHelper = new DatabaseHelper(getContext());
+        ((LauncherApplication) getContext()).setLauncherProvider(this);
         return true;
     }
 
@@ -113,12 +115,20 @@
         return result;
     }
 
+    private static long dbInsertAndCheck(DatabaseHelper helper,
+            SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
+        if (!values.containsKey(LauncherSettings.Favorites._ID)) {
+            throw new RuntimeException("Error: attempting to add item without specifying an id");
+        }
+        return db.insert(table, nullColumnHack, values);
+    }
+
     @Override
     public Uri insert(Uri uri, ContentValues initialValues) {
         SqlArguments args = new SqlArguments(uri);
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        final long rowId = db.insert(args.table, null, initialValues);
+        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
         if (rowId <= 0) return null;
 
         uri = ContentUris.withAppendedId(uri, rowId);
@@ -136,7 +146,9 @@
         try {
             int numValues = values.length;
             for (int i = 0; i < numValues; i++) {
-                if (db.insert(args.table, null, values[i]) < 0) return 0;
+                if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
+                    return 0;
+                }
             }
             db.setTransactionSuccessful();
         } finally {
@@ -176,6 +188,10 @@
         }
     }
 
+    public long generateNewId() {
+        return mOpenHelper.generateNewId();
+    }
+
     private static class DatabaseHelper extends SQLiteOpenHelper {
         private static final String TAG_FAVORITES = "favorites";
         private static final String TAG_FAVORITE = "favorite";
@@ -186,11 +202,13 @@
         
         private final Context mContext;
         private final AppWidgetHost mAppWidgetHost;
+        private long mMaxId = -1;
 
         DatabaseHelper(Context context) {
             super(context, DATABASE_NAME, null, DATABASE_VERSION);
             mContext = context;
             mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
+            mMaxId = initializeMaxId(getWritableDatabase());
         }
 
         /**
@@ -207,7 +225,9 @@
         @Override
         public void onCreate(SQLiteDatabase db) {
             if (LOGD) Log.d(TAG, "creating new launcher database");
-            
+
+            mMaxId = 1;
+
             db.execSQL("CREATE TABLE favorites (" +
                     "_id INTEGER PRIMARY KEY," +
                     "title TEXT," +
@@ -253,7 +273,7 @@
             try {
                 cursor = resolver.query(uri, null, null, null, null);
             } catch (Exception e) {
-	            // Ignore
+                // Ignore
             }
 
             // We already have a favorites database in the old provider
@@ -321,7 +341,7 @@
             try {
                 int numValues = rows.length;
                 for (i = 0; i < numValues; i++) {
-                    if (db.insert(TABLE_FAVORITES, null, rows[i]) < 0) {
+                    if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
                         return 0;
                     } else {
                         total++;
@@ -535,7 +555,36 @@
                     c.close();
                 }
             }
-            
+        }
+
+        // Generates a new ID to use for an object in your database. This method should be only
+        // called from the main UI thread. As an exception, we do call it when we call the
+        // constructor from the worker thread; however, this doesn't extend until after the
+        // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
+        // after that point
+        public long generateNewId() {
+            if (mMaxId < 0) {
+                throw new RuntimeException("Error: max id was not initialized");
+            }
+            mMaxId += 1;
+            return mMaxId;
+        }
+
+        private long initializeMaxId(SQLiteDatabase db) {
+            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
+
+            // get the result
+            final int maxIdIndex = 0;
+            long id = -1;
+            if (c != null && c.moveToNext()) {
+                id = c.getLong(maxIdIndex);
+            }
+
+            if (id == -1) {
+                throw new RuntimeException("Error: could not query max id");
+            }
+
+            return id;
         }
 
         /**
@@ -712,7 +761,8 @@
                 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
                 values.put(Favorites.SPANX, 1);
                 values.put(Favorites.SPANY, 1);
-                db.insert(TABLE_FAVORITES, null, values);
+                values.put(Favorites._ID, generateNewId());
+                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
             } catch (PackageManager.NameNotFoundException e) {
                 Log.w(TAG, "Unable to add favorite: " + packageName +
                         "/" + className, e);
@@ -804,7 +854,8 @@
                 values.put(Favorites.SPANX, spanX);
                 values.put(Favorites.SPANY, spanY);
                 values.put(Favorites.APPWIDGET_ID, appWidgetId);
-                db.insert(TABLE_FAVORITES, null, values);
+                values.put(Favorites._ID, generateNewId());
+                dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
 
                 allocatedAppWidgets = true;
                 
@@ -847,8 +898,8 @@
             values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
             values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
             values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId));
-
-            db.insert(TABLE_FAVORITES, null, values);
+            values.put(Favorites._ID, generateNewId());
+            dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
 
             return true;
         }