OSDN Git Service

Moving various runnables in LauncherModel to individual tasks
authorSunny Goyal <sunnygoyal@google.com>
Fri, 9 Sep 2016 22:47:55 +0000 (15:47 -0700)
committerSunny Goyal <sunnygoyal@google.com>
Wed, 9 Nov 2016 23:50:08 +0000 (15:50 -0800)
> Adding tests for some of the runnable

Change-Id: I1a315d38878857df3371f0e69d622a41fc3b081a

22 files changed:
build.gradle
src/com/android/launcher3/AllAppsList.java
src/com/android/launcher3/IconCache.java
src/com/android/launcher3/InstallShortcutReceiver.java
src/com/android/launcher3/LauncherAppWidgetInfo.java
src/com/android/launcher3/LauncherModel.java
src/com/android/launcher3/ShortcutInfo.java
src/com/android/launcher3/model/AddWorkspaceItemsTask.java [new file with mode: 0644]
src/com/android/launcher3/model/CacheDataUpdatedTask.java [new file with mode: 0644]
src/com/android/launcher3/model/ExtendedModelTask.java [new file with mode: 0644]
src/com/android/launcher3/model/PackageInstallStateChangedTask.java [new file with mode: 0644]
src/com/android/launcher3/model/PackageUpdatedTask.java [new file with mode: 0644]
src/com/android/launcher3/model/ShortcutsChangedTask.java [new file with mode: 0644]
src/com/android/launcher3/model/UserLockStateChangedTask.java [new file with mode: 0644]
src/com/android/launcher3/util/ManagedProfileHeuristic.java
tests/Android.mk
tests/res/raw/cache_data_updated_task_data.txt [new file with mode: 0644]
tests/res/raw/package_install_state_change_task_data.txt [new file with mode: 0644]
tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java [new file with mode: 0644]
tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java [new file with mode: 0644]
tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java [new file with mode: 0644]
tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java [new file with mode: 0644]

index 0c00da9..6fccb5c 100644 (file)
@@ -45,6 +45,7 @@ android {
 
         androidTest {
             java.srcDirs = ['tests/src']
+            res.srcDirs = ['tests/res']
             manifest.srcFile "tests/AndroidManifest.xml"
         }
 
@@ -65,6 +66,9 @@ dependencies {
     compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-2'
 
     testCompile 'junit:junit:4.12'
+    androidTestCompile "org.mockito:mockito-core:1.+"
+    androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
+    androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
     androidTestCompile 'com.android.support.test:runner:0.5'
     androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
     androidTestCompile 'com.android.support:support-annotations:23.2.0'
index 0e465a4..b13c20b 100644 (file)
@@ -155,10 +155,9 @@ public class AllAppsList {
             // to the removed list.
             for (int i = data.size() - 1; i >= 0; i--) {
                 final AppInfo applicationInfo = data.get(i);
-                final ComponentName component = applicationInfo.intent.getComponent();
                 if (user.equals(applicationInfo.user)
-                        && packageName.equals(component.getPackageName())) {
-                    if (!findActivity(matches, component)) {
+                        && packageName.equals(applicationInfo.componentName.getPackageName())) {
+                    if (!findActivity(matches, applicationInfo.componentName)) {
                         removed.add(applicationInfo);
                         data.remove(i);
                     }
@@ -182,11 +181,10 @@ public class AllAppsList {
             // Remove all data for this package.
             for (int i = data.size() - 1; i >= 0; i--) {
                 final AppInfo applicationInfo = data.get(i);
-                final ComponentName component = applicationInfo.intent.getComponent();
                 if (user.equals(applicationInfo.user)
-                        && packageName.equals(component.getPackageName())) {
+                        && packageName.equals(applicationInfo.componentName.getPackageName())) {
                     removed.add(applicationInfo);
-                    mIconCache.remove(component, user);
+                    mIconCache.remove(applicationInfo.componentName, user);
                     data.remove(i);
                 }
             }
@@ -238,9 +236,8 @@ public class AllAppsList {
     private AppInfo findApplicationInfoLocked(String packageName, UserHandleCompat user,
             String className) {
         for (AppInfo info: data) {
-            final ComponentName component = info.intent.getComponent();
-            if (user.equals(info.user) && packageName.equals(component.getPackageName())
-                    && className.equals(component.getClassName())) {
+            if (user.equals(info.user) && packageName.equals(info.componentName.getPackageName())
+                    && className.equals(info.componentName.getClassName())) {
                 return info;
             }
         }
index 661f99b..04d0c8c 100644 (file)
@@ -79,7 +79,7 @@ public class IconCache {
 
     @Thunk static final Object ICON_UPDATE_TOKEN = new Object();
 
-    @Thunk static class CacheEntry {
+    public static class CacheEntry {
         public Bitmap icon;
         public CharSequence title = "";
         public CharSequence contentDescription = "";
@@ -544,7 +544,7 @@ public class IconCache {
      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
      * This method is not thread safe, it must be called from a synchronized method.
      */
-    private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
+    protected CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
             UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
index bd20e32..b810740 100644 (file)
@@ -241,7 +241,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver {
             // Add the new apps to the model and bind them
             if (!addShortcuts.isEmpty()) {
                 LauncherAppState app = LauncherAppState.getInstance();
-                app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts);
+                app.getModel().addAndBindAddedWorkspaceItems(addShortcuts);
             }
         }
     }
index 66d8957..78f5b8e 100644 (file)
@@ -84,12 +84,12 @@ public class LauncherAppWidgetInfo extends ItemInfo {
     /**
      * Indicates the restore status of the widget.
      */
-    int restoreStatus;
+    public int restoreStatus;
 
     /**
      * Indicates the installation progress of the widget provider
      */
-    int installProgress = -1;
+    public int installProgress = -1;
 
     /**
      * Optional extras sent during widget bind. See {@link #FLAG_DIRECT_CONFIG}.
@@ -98,7 +98,7 @@ public class LauncherAppWidgetInfo extends ItemInfo {
 
     private boolean mHasNotifiedInitialWidgetSizeChanged;
 
-    LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
+    public LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
         if (appWidgetId == CUSTOM_WIDGET_ID) {
             itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
         } else {
@@ -117,6 +117,11 @@ public class LauncherAppWidgetInfo extends ItemInfo {
         restoreStatus = RESTORE_COMPLETED;
     }
 
+    /** Used for testing **/
+    public LauncherAppWidgetInfo() {
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+    }
+
     public boolean isCustomWidget() {
         return appWidgetId == CUSTOM_WIDGET_ID;
     }
index 955f51f..c70a475 100644 (file)
@@ -27,7 +27,6 @@ import android.content.Intent;
 import android.content.Intent.ShortcutIconResource;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.net.Uri;
@@ -43,7 +42,6 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.MutableInt;
-import android.util.Pair;
 
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
@@ -59,11 +57,18 @@ import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.AddWorkspaceItemsTask;
+import com.android.launcher3.model.ExtendedModelTask;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.CacheDataUpdatedTask;
 import com.android.launcher3.model.GridSizeMigrationTask;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.SdCardAvailableReceiver;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.PackageInstallStateChangedTask;
+import com.android.launcher3.model.PackageUpdatedTask;
+import com.android.launcher3.model.ShortcutsChangedTask;
+import com.android.launcher3.model.UserLockStateChangedTask;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.provider.ImportDataTask;
 import com.android.launcher3.provider.LauncherDbUtils;
@@ -72,7 +77,6 @@ import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.CursorIconInfo;
-import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LongArrayMap;
@@ -87,7 +91,6 @@ import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
 import java.security.InvalidParameterException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -168,8 +171,8 @@ public class LauncherModel extends BroadcastReceiver
 
     // </ only access in worker thread >
 
-    private IconCache mIconCache;
-    private DeepShortcutManager mDeepShortcutManager;
+    private final IconCache mIconCache;
+    private final DeepShortcutManager mDeepShortcutManager;
 
     private final LauncherAppsCompat mLauncherApps;
     private final UserManagerCompat mUserManager;
@@ -241,286 +244,26 @@ public class LauncherModel extends BroadcastReceiver
         }
     }
 
-    public void setPackageState(final PackageInstallInfo installInfo) {
-        Runnable updateRunnable = new Runnable() {
-
-            @Override
-            public void run() {
-                synchronized (sBgDataModel) {
-                    final HashSet<ItemInfo> updates = new HashSet<>();
-
-                    if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) {
-                        // Ignore install success events as they are handled by Package add events.
-                        return;
-                    }
-
-                    for (ItemInfo info : sBgDataModel.itemsIdMap) {
-                        if (info instanceof ShortcutInfo) {
-                            ShortcutInfo si = (ShortcutInfo) info;
-                            ComponentName cn = si.getTargetComponent();
-                            if (si.isPromise() && (cn != null)
-                                    && installInfo.packageName.equals(cn.getPackageName())) {
-                                si.setInstallProgress(installInfo.progress);
-
-                                if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) {
-                                    // Mark this info as broken.
-                                    si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE;
-                                }
-                                updates.add(si);
-                            }
-                        }
-                    }
-
-                    for (LauncherAppWidgetInfo widget : sBgDataModel.appWidgets) {
-                        if (widget.providerName.getPackageName().equals(installInfo.packageName)) {
-                            widget.installProgress = installInfo.progress;
-                            updates.add(widget);
-                        }
-                    }
-
-                    if (!updates.isEmpty()) {
-                        // Push changes to the callback.
-                        Runnable r = new Runnable() {
-                            public void run() {
-                                Callbacks callbacks = getCallback();
-                                if (callbacks != null) {
-                                    callbacks.bindRestoreItemsChange(updates);
-                                }
-                            }
-                        };
-                        mHandler.post(r);
-                    }
-                }
-            }
-        };
-        runOnWorkerThread(updateRunnable);
+    public void setPackageState(PackageInstallInfo installInfo) {
+        enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo));
     }
 
     /**
      * Updates the icons and label of all pending icons for the provided package name.
      */
     public void updateSessionDisplayInfo(final String packageName) {
-        Runnable updateRunnable = new Runnable() {
-
-            @Override
-            public void run() {
-                synchronized (sBgDataModel) {
-                    ArrayList<ShortcutInfo> updates = new ArrayList<>();
-                    UserHandleCompat user = UserHandleCompat.myUserHandle();
-
-                    for (ItemInfo info : sBgDataModel.itemsIdMap) {
-                        if (info instanceof ShortcutInfo) {
-                            ShortcutInfo si = (ShortcutInfo) info;
-                            ComponentName cn = si.getTargetComponent();
-                            if (si.isPromise() && (cn != null)
-                                    && packageName.equals(cn.getPackageName())) {
-                                si.updateIcon(mIconCache);
-                                updates.add(si);
-                            }
-                        }
-                    }
-
-                    bindUpdatedShortcuts(updates, user);
-                }
-            }
-        };
-        runOnWorkerThread(updateRunnable);
-    }
-
-    public void addAppsToAllApps(final Context ctx, final ArrayList<AppInfo> allAppsApps) {
-        final Callbacks callbacks = getCallback();
-
-        if (allAppsApps == null) {
-            throw new RuntimeException("allAppsApps must not be null");
-        }
-        if (allAppsApps.isEmpty()) {
-            return;
-        }
-
-        // Process the newly added applications and add them to the database first
-        Runnable r = new Runnable() {
-            public void run() {
-                runOnMainThread(new Runnable() {
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.bindAppsAdded(null, null, null, allAppsApps);
-                        }
-                    }
-                });
-            }
-        };
-        runOnWorkerThread(r);
-    }
-
-    private static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos,
-            int[] xy, int spanX, int spanY) {
-        LauncherAppState app = LauncherAppState.getInstance();
-        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
-
-        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
-        if (occupiedPos != null) {
-            for (ItemInfo r : occupiedPos) {
-                occupied.markCells(r, true);
-            }
-        }
-        return occupied.findVacantCell(xy, spanX, spanY);
-    }
-
-    /**
-     * Find a position on the screen for the given size or adds a new screen.
-     * @return screenId and the coordinates for the item.
-     */
-    @Thunk Pair<Long, int[]> findSpaceForItem(
-            Context context,
-            ArrayList<Long> workspaceScreens,
-            ArrayList<Long> addedWorkspaceScreensFinal,
-            int spanX, int spanY) {
-        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
-
-        // Use sBgItemsIdMap as all the items are already loaded.
-        assertWorkspaceLoaded();
-        synchronized (sBgDataModel) {
-            for (ItemInfo info : sBgDataModel.itemsIdMap) {
-                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
-                    if (items == null) {
-                        items = new ArrayList<>();
-                        screenItems.put(info.screenId, items);
-                    }
-                    items.add(info);
-                }
-            }
-        }
-
-        // Find appropriate space for the item.
-        long screenId = 0;
-        int[] cordinates = new int[2];
-        boolean found = false;
-
-        int screenCount = workspaceScreens.size();
-        // First check the preferred screen.
-        int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
-        if (preferredScreenIndex < screenCount) {
-            screenId = workspaceScreens.get(preferredScreenIndex);
-            found = findNextAvailableIconSpaceInScreen(
-                    screenItems.get(screenId), cordinates, spanX, spanY);
-        }
-
-        if (!found) {
-            // Search on any of the screens starting from the first screen.
-            for (int screen = 1; screen < screenCount; screen++) {
-                screenId = workspaceScreens.get(screen);
-                if (findNextAvailableIconSpaceInScreen(
-                        screenItems.get(screenId), cordinates, spanX, spanY)) {
-                    // We found a space for it
-                    found = true;
-                    break;
-                }
-            }
-        }
-
-        if (!found) {
-            // Still no position found. Add a new screen to the end.
-            screenId = LauncherSettings.Settings.call(context.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
-                    .getLong(LauncherSettings.Settings.EXTRA_VALUE);
-
-            // Save the screen id for binding in the workspace
-            workspaceScreens.add(screenId);
-            addedWorkspaceScreensFinal.add(screenId);
-
-            // If we still can't find an empty space, then God help us all!!!
-            if (!findNextAvailableIconSpaceInScreen(
-                    screenItems.get(screenId), cordinates, spanX, spanY)) {
-                throw new RuntimeException("Can't find space to add the item");
-            }
-        }
-        return Pair.create(screenId, cordinates);
+        HashSet<String> packages = new HashSet<>();
+        packages.add(packageName);
+        enqueueModelUpdateTask(new CacheDataUpdatedTask(
+                CacheDataUpdatedTask.OP_SESSION_UPDATE, UserHandleCompat.myUserHandle(), packages));
     }
 
     /**
      * Adds the provided items to the workspace.
      */
-    public void addAndBindAddedWorkspaceItems(final Context context,
+    public void addAndBindAddedWorkspaceItems(
             final ArrayList<? extends ItemInfo> workspaceApps) {
-        final Callbacks callbacks = getCallback();
-        if (workspaceApps.isEmpty()) {
-            return;
-        }
-        // Process the newly added applications and add them to the database first
-        Runnable r = new Runnable() {
-            public void run() {
-                final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
-                final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
-
-                // Get the list of workspace screens.  We need to append to this list and
-                // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
-                // called.
-                ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context);
-                synchronized(sBgDataModel) {
-                    for (ItemInfo item : workspaceApps) {
-                        if (item instanceof ShortcutInfo) {
-                            // Short-circuit this logic if the icon exists somewhere on the workspace
-                            if (shortcutExists(context, item.getIntent(), item.user)) {
-                                continue;
-                            }
-                        }
-
-                        // Find appropriate space for the item.
-                        Pair<Long, int[]> coords = findSpaceForItem(context,
-                                workspaceScreens, addedWorkspaceScreensFinal, 1, 1);
-                        long screenId = coords.first;
-                        int[] cordinates = coords.second;
-
-                        ItemInfo itemInfo;
-                        if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
-                            itemInfo = item;
-                        } else if (item instanceof AppInfo) {
-                            itemInfo = ((AppInfo) item).makeShortcut();
-                        } else {
-                            throw new RuntimeException("Unexpected info type");
-                        }
-
-                        // Add the shortcut to the db
-                        addItemToDatabase(context, itemInfo,
-                                LauncherSettings.Favorites.CONTAINER_DESKTOP,
-                                screenId, cordinates[0], cordinates[1]);
-                        // Save the ShortcutInfo for binding in the workspace
-                        addedShortcutsFinal.add(itemInfo);
-                    }
-                }
-
-                // Update the workspace screens
-                updateWorkspaceScreenOrder(context, workspaceScreens);
-
-                if (!addedShortcutsFinal.isEmpty()) {
-                    runOnMainThread(new Runnable() {
-                        public void run() {
-                            Callbacks cb = getCallback();
-                            if (callbacks == cb && cb != null) {
-                                final ArrayList<ItemInfo> addAnimated = new ArrayList<ItemInfo>();
-                                final ArrayList<ItemInfo> addNotAnimated = new ArrayList<ItemInfo>();
-                                if (!addedShortcutsFinal.isEmpty()) {
-                                    ItemInfo info = addedShortcutsFinal.get(addedShortcutsFinal.size() - 1);
-                                    long lastScreenId = info.screenId;
-                                    for (ItemInfo i : addedShortcutsFinal) {
-                                        if (i.screenId == lastScreenId) {
-                                            addAnimated.add(i);
-                                        } else {
-                                            addNotAnimated.add(i);
-                                        }
-                                    }
-                                }
-                                callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
-                                        addNotAnimated, addAnimated, null);
-                            }
-                        }
-                    });
-                }
-            }
-        };
-        runOnWorkerThread(r);
+        enqueueModelUpdateTask(new AddWorkspaceItemsTask(workspaceApps));
     }
 
     /**
@@ -784,60 +527,6 @@ public class LauncherModel extends BroadcastReceiver
         updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase");
     }
 
-    private void assertWorkspaceLoaded() {
-        if (ProviderConfig.IS_DOGFOOD_BUILD) {
-            synchronized (mLock) {
-                if (!mHasLoaderCompletedOnce ||
-                        (mLoaderTask != null && mLoaderTask.mIsLoadingAndBindingWorkspace)) {
-                    throw new RuntimeException("Trying to add shortcut while loader is running");
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns true if the shortcuts already exists on the workspace. This must be called after
-     * the workspace has been loaded. We identify a shortcut by its intent.
-     */
-    @Thunk boolean shortcutExists(Context context, Intent intent, UserHandleCompat user) {
-        assertWorkspaceLoaded();
-        final String intentWithPkg, intentWithoutPkg;
-        if (intent.getComponent() != null) {
-            // If component is not null, an intent with null package will produce
-            // the same result and should also be a match.
-            String packageName = intent.getComponent().getPackageName();
-            if (intent.getPackage() != null) {
-                intentWithPkg = intent.toUri(0);
-                intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
-            } else {
-                intentWithPkg = new Intent(intent).setPackage(packageName).toUri(0);
-                intentWithoutPkg = intent.toUri(0);
-            }
-        } else {
-            intentWithPkg = intent.toUri(0);
-            intentWithoutPkg = intent.toUri(0);
-        }
-
-        synchronized (sBgDataModel) {
-            for (ItemInfo item : sBgDataModel.itemsIdMap) {
-                if (item instanceof ShortcutInfo) {
-                    ShortcutInfo info = (ShortcutInfo) item;
-                    Intent targetIntent = info.promisedIntent == null
-                            ? info.intent : info.promisedIntent;
-                    if (targetIntent != null && info.user.equals(user)) {
-                        Intent copyIntent = new Intent(targetIntent);
-                        copyIntent.setSourceBounds(intent.getSourceBounds());
-                        String s = copyIntent.toUri(0);
-                        if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
-                            return true;
-                        }
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
     /**
      * Add an item to the database in a specified container. Sets the container, screen, cellX and
      * cellY fields of the item. Also assigns an ID to the item.
@@ -899,7 +588,8 @@ public class LauncherModel extends BroadcastReceiver
     /**
      * Removes the specified items from the database
      */
-    static void deleteItemsFromDatabase(Context context, final Iterable<? extends ItemInfo> items) {
+    public static void deleteItemsFromDatabase(Context context,
+            final Iterable<? extends ItemInfo> items) {
         final ContentResolver cr = context.getContentResolver();
         Runnable r = new Runnable() {
             public void run() {
@@ -918,7 +608,7 @@ public class LauncherModel extends BroadcastReceiver
      * Update the order of the workspace screens in the database. The array list contains
      * a list of screen ids in the order that they should appear.
      */
-    public void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) {
+    public static void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) {
         final ArrayList<Long> screensCopy = new ArrayList<Long>(screens);
         final ContentResolver cr = context.getContentResolver();
         final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
@@ -997,8 +687,7 @@ public class LauncherModel extends BroadcastReceiver
     @Override
     public void onPackageChanged(String packageName, UserHandleCompat user) {
         int op = PackageUpdatedTask.OP_UPDATE;
-        enqueueItemUpdatedTask(new PackageUpdatedTask(op, new String[] { packageName },
-                user));
+        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
     }
 
     @Override
@@ -1008,56 +697,52 @@ public class LauncherModel extends BroadcastReceiver
 
     public void onPackagesRemoved(UserHandleCompat user, String... packages) {
         int op = PackageUpdatedTask.OP_REMOVE;
-        enqueueItemUpdatedTask(new PackageUpdatedTask(op, packages, user));
+        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages));
     }
 
     @Override
     public void onPackageAdded(String packageName, UserHandleCompat user) {
         int op = PackageUpdatedTask.OP_ADD;
-        enqueueItemUpdatedTask(new PackageUpdatedTask(op, new String[] { packageName },
-                user));
+        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
     }
 
     @Override
     public void onPackagesAvailable(String[] packageNames, UserHandleCompat user,
             boolean replacing) {
-        enqueueItemUpdatedTask(
-                new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, packageNames, user));
+        enqueueModelUpdateTask(
+                new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames));
     }
 
     @Override
     public void onPackagesUnavailable(String[] packageNames, UserHandleCompat user,
             boolean replacing) {
         if (!replacing) {
-            enqueueItemUpdatedTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_UNAVAILABLE, packageNames,
-                    user));
+            enqueueModelUpdateTask(new PackageUpdatedTask(
+                    PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames));
         }
     }
 
     @Override
     public void onPackagesSuspended(String[] packageNames, UserHandleCompat user) {
-        enqueueItemUpdatedTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_SUSPEND, packageNames,
-                user));
+        enqueueModelUpdateTask(new PackageUpdatedTask(
+                PackageUpdatedTask.OP_SUSPEND, user, packageNames));
     }
 
     @Override
     public void onPackagesUnsuspended(String[] packageNames, UserHandleCompat user) {
-        enqueueItemUpdatedTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_UNSUSPEND, packageNames,
-                user));
+        enqueueModelUpdateTask(new PackageUpdatedTask(
+                PackageUpdatedTask.OP_UNSUSPEND, user, packageNames));
     }
 
     @Override
     public void onShortcutsChanged(String packageName, List<ShortcutInfoCompat> shortcuts,
             UserHandleCompat user) {
-        enqueueItemUpdatedTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
+        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
     }
 
     public void updatePinnedShortcuts(String packageName, List<ShortcutInfoCompat> shortcuts,
             UserHandleCompat user) {
-        enqueueItemUpdatedTask(new ShortcutsChangedTask(packageName, shortcuts, user, false));
+        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false));
     }
 
     /**
@@ -1083,16 +768,15 @@ public class LauncherModel extends BroadcastReceiver
             if (user != null) {
                 if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) ||
                         Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
-                    enqueueItemUpdatedTask(new PackageUpdatedTask(
-                            PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE,
-                            new String[0], user));
+                    enqueueModelUpdateTask(new PackageUpdatedTask(
+                            PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
                 }
 
                 // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so
                 // we need to run the state change task again.
                 if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) ||
                         Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) {
-                    enqueueItemUpdatedTask(new UserLockStateChangedTask(user));
+                    enqueueModelUpdateTask(new UserLockStateChangedTask(user));
                 }
             }
         } else if (Intent.ACTION_WALLPAPER_CHANGED.equals(action)) {
@@ -2702,397 +2386,68 @@ public class LauncherModel extends BroadcastReceiver
      * Called when the icons for packages have been updated in the icon cache.
      */
     public void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandleCompat user) {
-        final Callbacks callbacks = getCallback();
-        final ArrayList<AppInfo> updatedApps = new ArrayList<>();
-        final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
-
         // If any package icon has changed (app was updated while launcher was dead),
         // update the corresponding shortcuts.
-        synchronized (sBgDataModel) {
-            for (ItemInfo info : sBgDataModel.itemsIdMap) {
-                if (info instanceof ShortcutInfo && user.equals(info.user)
-                        && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-                    ShortcutInfo si = (ShortcutInfo) info;
-                    ComponentName cn = si.getTargetComponent();
-                    if (cn != null && updatedPackages.contains(cn.getPackageName())) {
-                        si.updateIcon(mIconCache);
-                        updatedShortcuts.add(si);
-                    }
-                }
-            }
-            mBgAllAppsList.updateIconsAndLabels(updatedPackages, user, updatedApps);
-        }
-
-        bindUpdatedShortcuts(updatedShortcuts, user);
-
-        if (!updatedApps.isEmpty()) {
-            mHandler.post(new Runnable() {
-
-                public void run() {
-                    Callbacks cb = getCallback();
-                    if (cb != null && callbacks == cb) {
-                        cb.bindAppsUpdated(updatedApps);
-                    }
-                }
-            });
-        }
+        enqueueModelUpdateTask(new CacheDataUpdatedTask(
+                CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages));
     }
 
-    private void bindUpdatedShortcuts(
-            ArrayList<ShortcutInfo> updatedShortcuts, UserHandleCompat user) {
-        bindUpdatedShortcuts(updatedShortcuts, new ArrayList<ShortcutInfo>(), user);
+    void enqueueModelUpdateTask(BaseModelUpdateTask task) {
+        task.init(this);
+        runOnWorkerThread(task);
     }
 
-    private void bindUpdatedShortcuts(
-            final ArrayList<ShortcutInfo> updatedShortcuts,
-            final ArrayList<ShortcutInfo> removedShortcuts,
-            final UserHandleCompat user) {
-        if (!updatedShortcuts.isEmpty() || !removedShortcuts.isEmpty()) {
-            final Callbacks callbacks = getCallback();
-            mHandler.post(new Runnable() {
-
-                public void run() {
-                    Callbacks cb = getCallback();
-                    if (cb != null && callbacks == cb) {
-                        cb.bindShortcutsChanged(updatedShortcuts, removedShortcuts, user);
-                    }
-                }
-            });
-        }
-    }
+    /**
+     * A task to be executed on the current callbacks on the UI thread.
+     * If there is no current callbacks, the task is ignored.
+     */
+    public interface CallbackTask {
 
-    void enqueueItemUpdatedTask(Runnable task) {
-        sWorker.post(task);
+        void execute(Callbacks callbacks);
     }
 
-    private class PackageUpdatedTask implements Runnable {
-        final int mOp;
-        final String[] mPackages;
-        final UserHandleCompat mUser;
+    /**
+     * A runnable which changes/updates the data model of the launcher based on certain events.
+     */
+    public static abstract class BaseModelUpdateTask implements Runnable {
 
-        public static final int OP_NONE = 0;
-        public static final int OP_ADD = 1;
-        public static final int OP_UPDATE = 2;
-        public static final int OP_REMOVE = 3; // uninstlled
-        public static final int OP_UNAVAILABLE = 4; // external media unmounted
-        public static final int OP_SUSPEND = 5; // package suspended
-        public static final int OP_UNSUSPEND = 6; // package unsuspended
-        public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable
+        private LauncherModel mModel;
+        private DeferredHandler mUiHandler;
 
-        public PackageUpdatedTask(int op, String[] packages, UserHandleCompat user) {
-            mOp = op;
-            mPackages = packages;
-            mUser = user;
+        /* package private */
+        void init(LauncherModel model) {
+            mModel = model;
+            mUiHandler = mModel.mHandler;
         }
 
+        @Override
         public void run() {
-            if (!mHasLoaderCompletedOnce) {
+            if (!mModel.mHasLoaderCompletedOnce) {
                 // Loader has not yet run.
                 return;
             }
-            final Context context = mApp.getContext();
-
-            final String[] packages = mPackages;
-            final int N = packages.length;
-            FlagOp flagOp = FlagOp.NO_OP;
-            final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
-            switch (mOp) {
-                case OP_ADD: {
-                    for (int i=0; i<N; i++) {
-                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
-                        mIconCache.updateIconsForPkg(packages[i], mUser);
-                        mBgAllAppsList.addPackage(context, packages[i], mUser);
-                    }
-
-                    ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
-                    if (heuristic != null) {
-                        heuristic.processPackageAdd(mPackages);
-                    }
-                    break;
-                }
-                case OP_UPDATE:
-                    for (int i=0; i<N; i++) {
-                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
-                        mIconCache.updateIconsForPkg(packages[i], mUser);
-                        mBgAllAppsList.updatePackage(context, packages[i], mUser);
-                        mApp.getWidgetCache().removePackage(packages[i], mUser);
-                    }
-                    // Since package was just updated, the target must be available now.
-                    flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
-                    break;
-                case OP_REMOVE: {
-                    ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
-                    if (heuristic != null) {
-                        heuristic.processPackageRemoved(mPackages);
-                    }
-                    for (int i=0; i<N; i++) {
-                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
-                        mIconCache.removeIconsForPkg(packages[i], mUser);
-                    }
-                    // Fall through
-                }
-                case OP_UNAVAILABLE:
-                    for (int i=0; i<N; i++) {
-                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
-                        mBgAllAppsList.removePackage(packages[i], mUser);
-                        mApp.getWidgetCache().removePackage(packages[i], mUser);
-                    }
-                    flagOp = FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
-                    break;
-                case OP_SUSPEND:
-                case OP_UNSUSPEND:
-                    flagOp = mOp == OP_SUSPEND ?
-                            FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED) :
-                                    FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED);
-                    if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
-                    mBgAllAppsList.updateDisabledFlags(
-                            ItemInfoMatcher.ofPackages(packageSet, mUser), flagOp);
-                    break;
-                case OP_USER_AVAILABILITY_CHANGE:
-                    flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
-                            ? FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER)
-                            : FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER);
-                    // We want to update all packages for this user.
-                    mBgAllAppsList.updateDisabledFlags(ItemInfoMatcher.ofUser(mUser), flagOp);
-                    break;
-            }
-
-            ArrayList<AppInfo> added = null;
-            ArrayList<AppInfo> modified = null;
-            final ArrayList<AppInfo> removedApps = new ArrayList<AppInfo>();
-
-            if (mBgAllAppsList.added.size() > 0) {
-                added = new ArrayList<>(mBgAllAppsList.added);
-                mBgAllAppsList.added.clear();
-            }
-            if (mBgAllAppsList.modified.size() > 0) {
-                modified = new ArrayList<>(mBgAllAppsList.modified);
-                mBgAllAppsList.modified.clear();
-            }
-            if (mBgAllAppsList.removed.size() > 0) {
-                removedApps.addAll(mBgAllAppsList.removed);
-                mBgAllAppsList.removed.clear();
-            }
-
-            final HashMap<ComponentName, AppInfo> addedOrUpdatedApps = new HashMap<>();
-
-            if (added != null) {
-                addAppsToAllApps(context, added);
-                for (AppInfo ai : added) {
-                    addedOrUpdatedApps.put(ai.componentName, ai);
-                }
-            }
-
-            if (modified != null) {
-                final Callbacks callbacks = getCallback();
-                final ArrayList<AppInfo> modifiedFinal = modified;
-                for (AppInfo ai : modified) {
-                    addedOrUpdatedApps.put(ai.componentName, ai);
-                }
-
-                mHandler.post(new Runnable() {
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.bindAppsUpdated(modifiedFinal);
-                        }
-                    }
-                });
-            }
-
-            // Update shortcut infos
-            if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
-                final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
-                final ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<>();
-                final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
-
-                synchronized (sBgDataModel) {
-                    for (ItemInfo info : sBgDataModel.itemsIdMap) {
-                        if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
-                            ShortcutInfo si = (ShortcutInfo) info;
-                            boolean infoUpdated = false;
-                            boolean shortcutUpdated = false;
-
-                            // Update shortcuts which use iconResource.
-                            if ((si.iconResource != null)
-                                    && packageSet.contains(si.iconResource.packageName)) {
-                                Bitmap icon = LauncherIcons.createIconBitmap(
-                                        si.iconResource.packageName,
-                                        si.iconResource.resourceName, context);
-                                if (icon != null) {
-                                    si.setIcon(icon);
-                                    si.usingFallbackIcon = false;
-                                    infoUpdated = true;
-                                }
-                            }
-
-                            ComponentName cn = si.getTargetComponent();
-                            if (cn != null && packageSet.contains(cn.getPackageName())) {
-                                AppInfo appInfo = addedOrUpdatedApps.get(cn);
-
-                                if (si.isPromise()) {
-                                    if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
-                                        // Auto install icon
-                                        PackageManager pm = context.getPackageManager();
-                                        ResolveInfo matched = pm.resolveActivity(
-                                                new Intent(Intent.ACTION_MAIN)
-                                                .setComponent(cn).addCategory(Intent.CATEGORY_LAUNCHER),
-                                                PackageManager.MATCH_DEFAULT_ONLY);
-                                        if (matched == null) {
-                                            // Try to find the best match activity.
-                                            Intent intent = pm.getLaunchIntentForPackage(
-                                                    cn.getPackageName());
-                                            if (intent != null) {
-                                                cn = intent.getComponent();
-                                                appInfo = addedOrUpdatedApps.get(cn);
-                                            }
-
-                                            if ((intent == null) || (appInfo == null)) {
-                                                removedShortcuts.add(si);
-                                                continue;
-                                            }
-                                            si.promisedIntent = intent;
-                                        }
-                                    }
-
-                                    si.intent = si.promisedIntent;
-                                    si.promisedIntent = null;
-                                    si.status = ShortcutInfo.DEFAULT;
-                                    infoUpdated = true;
-                                    si.updateIcon(mIconCache);
-                                }
-
-                                if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction())
-                                        && si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-                                    si.updateIcon(mIconCache);
-                                    si.title = Utilities.trim(appInfo.title);
-                                    si.contentDescription = appInfo.contentDescription;
-                                    infoUpdated = true;
-                                }
-
-                                int oldDisabledFlags = si.isDisabled;
-                                si.isDisabled = flagOp.apply(si.isDisabled);
-                                if (si.isDisabled != oldDisabledFlags) {
-                                    shortcutUpdated = true;
-                                }
-                            }
-
-                            if (infoUpdated || shortcutUpdated) {
-                                updatedShortcuts.add(si);
-                            }
-                            if (infoUpdated) {
-                                updateItemInDatabase(context, si);
-                            }
-                        } else if (info instanceof LauncherAppWidgetInfo && mOp == OP_ADD) {
-                            LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
-                            if (mUser.equals(widgetInfo.user)
-                                    && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
-                                    && packageSet.contains(widgetInfo.providerName.getPackageName())) {
-                                widgetInfo.restoreStatus &=
-                                        ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
-                                        ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
-
-                                // adding this flag ensures that launcher shows 'click to setup'
-                                // if the widget has a config activity. In case there is no config
-                                // activity, it will be marked as 'restored' during bind.
-                                widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-
-                                widgets.add(widgetInfo);
-                                updateItemInDatabase(context, widgetInfo);
-                            }
-                        }
-                    }
-                }
-
-                bindUpdatedShortcuts(updatedShortcuts, removedShortcuts, mUser);
-                if (!removedShortcuts.isEmpty()) {
-                    deleteItemsFromDatabase(context, removedShortcuts);
-                }
-
-                if (!widgets.isEmpty()) {
-                    final Callbacks callbacks = getCallback();
-                    mHandler.post(new Runnable() {
-                        public void run() {
-                            Callbacks cb = getCallback();
-                            if (callbacks == cb && cb != null) {
-                                callbacks.bindWidgetsRestored(widgets);
-                            }
-                        }
-                    });
-                }
-            }
+            execute(mModel.mApp, sBgDataModel, mModel.mBgAllAppsList);
+        }
 
-            final HashSet<String> removedPackages = new HashSet<>();
-            final HashSet<ComponentName> removedComponents = new HashSet<>();
-            if (mOp == OP_REMOVE) {
-                // Mark all packages in the broadcast to be removed
-                Collections.addAll(removedPackages, packages);
+        /**
+         * Execute the actual task. Called on the worker thread.
+         */
+        public abstract void execute(
+                LauncherAppState app, BgDataModel dataModel, AllAppsList apps);
 
-                // No need to update the removedComponents as
-                // removedPackages is a super-set of removedComponents
-            } else if (mOp == OP_UPDATE) {
-                // Mark disabled packages in the broadcast to be removed
-                for (int i=0; i<N; i++) {
-                    if (isPackageDisabled(context, packages[i], mUser)) {
-                        removedPackages.add(packages[i]);
+        /**
+         * Schedules a {@param task} to be executed on the current callbacks.
+         */
+        public final void scheduleCallbackTask(final CallbackTask task) {
+            final Callbacks callbacks = mModel.getCallback();
+            mUiHandler.post(new Runnable() {
+                public void run() {
+                    Callbacks cb = mModel.getCallback();
+                    if (callbacks == cb && cb != null) {
+                        task.execute(callbacks);
                     }
                 }
-
-                // Update removedComponents as some components can get removed during package update
-                for (AppInfo info : removedApps) {
-                    removedComponents.add(info.componentName);
-                }
-            }
-
-            if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
-                deleteItemsFromDatabase(
-                        context, ItemInfoMatcher.ofPackages(removedPackages, mUser));
-                deleteItemsFromDatabase(
-                        context, ItemInfoMatcher.ofComponents(removedComponents, mUser));
-
-                // Remove any queued items from the install queue
-                InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
-
-                // Call the components-removed callback
-                final Callbacks callbacks = getCallback();
-                mHandler.post(new Runnable() {
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.bindWorkspaceComponentsRemoved(
-                                    removedPackages, removedComponents, mUser);
-                        }
-                    }
-                });
-            }
-
-            if (!removedApps.isEmpty()) {
-                // Remove corresponding apps from All-Apps
-                final Callbacks callbacks = getCallback();
-                mHandler.post(new Runnable() {
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.bindAppInfosRemoved(removedApps);
-                        }
-                    }
-                });
-            }
-
-            // Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to
-            // get widget update signals.
-            if (!Utilities.ATLEAST_MARSHMALLOW &&
-                    (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) {
-                final Callbacks callbacks = getCallback();
-                mHandler.post(new Runnable() {
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.notifyWidgetProvidersChanged();
-                        }
-                    }
-                });
-            }
+            });
         }
     }
 
@@ -3100,171 +2455,18 @@ public class LauncherModel extends BroadcastReceiver
      * Repopulates the shortcut info, possibly updating any icon already on the workspace.
      */
     public void updateShortcutInfo(final ShortcutInfoCompat fullDetail, final ShortcutInfo info) {
-        enqueueItemUpdatedTask(new Runnable() {
+        enqueueModelUpdateTask(new ExtendedModelTask() {
             @Override
-            public void run() {
-                info.updateFromDeepShortcutInfo(
-                        fullDetail, LauncherAppState.getInstance().getContext());
-                ArrayList<ShortcutInfo> update = new ArrayList<ShortcutInfo>();
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                info.updateFromDeepShortcutInfo(fullDetail, app.getContext());
+
+                ArrayList<ShortcutInfo> update = new ArrayList<>();
                 update.add(info);
                 bindUpdatedShortcuts(update, fullDetail.getUserHandle());
             }
         });
     }
 
-    private class ShortcutsChangedTask implements Runnable {
-        private final String mPackageName;
-        private final List<ShortcutInfoCompat> mShortcuts;
-        private final UserHandleCompat mUser;
-        private final boolean mUpdateIdMap;
-
-        public ShortcutsChangedTask(String packageName, List<ShortcutInfoCompat> shortcuts,
-                UserHandleCompat user, boolean updateIdMap) {
-            mPackageName = packageName;
-            mShortcuts = shortcuts;
-            mUser = user;
-            mUpdateIdMap = updateIdMap;
-        }
-
-        @Override
-        public void run() {
-            mDeepShortcutManager.onShortcutsChanged(mShortcuts);
-
-            // Find ShortcutInfo's that have changed on the workspace.
-            final ArrayList<ShortcutInfo> removedShortcutInfos = new ArrayList<>();
-            MultiHashMap<String, ShortcutInfo> idsToWorkspaceShortcutInfos = new MultiHashMap<>();
-            for (ItemInfo itemInfo : sBgDataModel.itemsIdMap) {
-                if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                    ShortcutInfo si = (ShortcutInfo) itemInfo;
-                    if (si.getPromisedIntent().getPackage().equals(mPackageName)
-                            && si.user.equals(mUser)) {
-                        idsToWorkspaceShortcutInfos.addToList(si.getDeepShortcutId(), si);
-                    }
-                }
-            }
-
-            final Context context = LauncherAppState.getInstance().getContext();
-            final ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
-            if (!idsToWorkspaceShortcutInfos.isEmpty()) {
-                // Update the workspace to reflect the changes to updated shortcuts residing on it.
-                List<ShortcutInfoCompat> shortcuts = mDeepShortcutManager.queryForFullDetails(
-                        mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser);
-                for (ShortcutInfoCompat fullDetails : shortcuts) {
-                    List<ShortcutInfo> shortcutInfos = idsToWorkspaceShortcutInfos
-                            .remove(fullDetails.getId());
-                    if (!fullDetails.isPinned()) {
-                        // The shortcut was previously pinned but is no longer, so remove it from
-                        // the workspace and our pinned shortcut counts.
-                        // Note that we put this check here, after querying for full details,
-                        // because there's a possible race condition between pinning and
-                        // receiving this callback.
-                        removedShortcutInfos.addAll(shortcutInfos);
-                        continue;
-                    }
-                    for (ShortcutInfo shortcutInfo : shortcutInfos) {
-                        shortcutInfo.updateFromDeepShortcutInfo(fullDetails, context);
-                        updatedShortcutInfos.add(shortcutInfo);
-                    }
-                }
-            }
-
-            // If there are still entries in idsToWorkspaceShortcutInfos, that means that
-            // the corresponding shortcuts weren't passed in onShortcutsChanged(). This
-            // means they were cleared, so we remove and unpin them now.
-            for (String id : idsToWorkspaceShortcutInfos.keySet()) {
-                removedShortcutInfos.addAll(idsToWorkspaceShortcutInfos.get(id));
-            }
-
-            bindUpdatedShortcuts(updatedShortcutInfos, removedShortcutInfos, mUser);
-            if (!removedShortcutInfos.isEmpty()) {
-                deleteItemsFromDatabase(context, removedShortcutInfos);
-            }
-
-            if (mUpdateIdMap) {
-                // Update the deep shortcut map if the list of ids has changed for an activity.
-                sBgDataModel.updateDeepShortcutMap(mPackageName, mUser, mShortcuts);
-                bindDeepShortcuts();
-            }
-        }
-    }
-
-    /**
-     * Task to handle changing of lock state of the user
-     */
-    private class UserLockStateChangedTask implements Runnable {
-
-        private final UserHandleCompat mUser;
-
-        public UserLockStateChangedTask(UserHandleCompat user) {
-            mUser = user;
-        }
-
-        @Override
-        public void run() {
-            boolean isUserUnlocked = mUserManager.isUserUnlocked(mUser);
-            Context context = mApp.getContext();
-
-            HashMap<ShortcutKey, ShortcutInfoCompat> pinnedShortcuts = new HashMap<>();
-            if (isUserUnlocked) {
-                List<ShortcutInfoCompat> shortcuts =
-                        mDeepShortcutManager.queryForPinnedShortcuts(null, mUser);
-                if (mDeepShortcutManager.wasLastCallSuccess()) {
-                    for (ShortcutInfoCompat shortcut : shortcuts) {
-                        pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
-                    }
-                } else {
-                    // Shortcut manager can fail due to some race condition when the lock state
-                    // changes too frequently. For the purpose of the update,
-                    // consider it as still locked.
-                    isUserUnlocked = false;
-                }
-            }
-
-            // Update the workspace to reflect the changes to updated shortcuts residing on it.
-            ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
-            ArrayList<ShortcutInfo> deletedShortcutInfos = new ArrayList<>();
-            for (ItemInfo itemInfo : sBgDataModel.itemsIdMap) {
-                if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                        && mUser.equals(itemInfo.user)) {
-                    ShortcutInfo si = (ShortcutInfo) itemInfo;
-                    if (isUserUnlocked) {
-                        ShortcutInfoCompat shortcut =
-                                pinnedShortcuts.get(ShortcutKey.fromShortcutInfo(si));
-                        // We couldn't verify the shortcut during loader. If its no longer available
-                        // (probably due to clear data), delete the workspace item as well
-                        if (shortcut == null) {
-                            deletedShortcutInfos.add(si);
-                            continue;
-                        }
-                        si.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
-                        si.updateFromDeepShortcutInfo(shortcut, context);
-                    } else {
-                        si.isDisabled |= ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
-                    }
-                    updatedShortcutInfos.add(si);
-                }
-            }
-            bindUpdatedShortcuts(updatedShortcutInfos, deletedShortcutInfos, mUser);
-            if (!deletedShortcutInfos.isEmpty()) {
-                deleteItemsFromDatabase(context, deletedShortcutInfos);
-            }
-
-            // Remove shortcut id map for that user
-            Iterator<ComponentKey> keysIter = sBgDataModel.deepShortcutMap.keySet().iterator();
-            while (keysIter.hasNext()) {
-                if (keysIter.next().user.equals(mUser)) {
-                    keysIter.remove();
-                }
-            }
-
-            if (isUserUnlocked) {
-                sBgDataModel.updateDeepShortcutMap(
-                        null, mUser, mDeepShortcutManager.queryForAllShortcuts(mUser));
-            }
-            bindDeepShortcuts();
-        }
-    }
-
     private void bindWidgetsModel(final Callbacks callbacks) {
         final MultiHashMap<PackageItemInfo, WidgetItem> widgets
                 = mBgWidgetsModel.getWidgetsMap().clone();
@@ -3296,12 +2498,6 @@ public class LauncherModel extends BroadcastReceiver
         });
     }
 
-    @Thunk static boolean isPackageDisabled(Context context, String packageName,
-            UserHandleCompat user) {
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
-        return !launcherApps.isPackageEnabledForProfile(packageName, user);
-    }
-
     /**
      * Make an ShortcutInfo object for a restored application or shortcut item that points
      * to a package that is not yet installed on the system.
index 9a92872..fc08736 100644 (file)
@@ -80,7 +80,7 @@ public class ShortcutInfo extends ItemInfo {
      * Indicates whether we're using the default fallback icon instead of something from the
      * app.
      */
-    boolean usingFallbackIcon;
+    public boolean usingFallbackIcon;
 
     /**
      * Indicates whether we're using a low res icon
@@ -132,7 +132,7 @@ public class ShortcutInfo extends ItemInfo {
      * Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when
      * sd-card is not available).
      */
-    int isDisabled = DEFAULT;
+    public int isDisabled = DEFAULT;
 
     /**
      * A message to display when the user tries to start a disabled shortcut.
@@ -140,7 +140,7 @@ public class ShortcutInfo extends ItemInfo {
      */
     CharSequence disabledMessage;
 
-    int status;
+    public int status;
 
     /**
      * The installation progress [0-100] of the package that this shortcut represents.
@@ -152,7 +152,7 @@ public class ShortcutInfo extends ItemInfo {
      * this will hold the original intent from the database.  Otherwise, null.
      * Refer {@link #FLAG_RESTORED_ICON}, {@link #FLAG_AUTOINTALL_ICON}
      */
-    Intent promisedIntent;
+    public Intent promisedIntent;
 
     public ShortcutInfo() {
         itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
new file mode 100644 (file)
index 0000000..986e163
--- /dev/null
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.LongSparseArray;
+import android.util.Pair;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.GridOccupancy;
+
+import java.util.ArrayList;
+
+/**
+ * Task to add auto-created workspace items.
+ */
+public class AddWorkspaceItemsTask extends ExtendedModelTask {
+
+    private final ArrayList<? extends ItemInfo> mWorkspaceApps;
+
+    /**
+     * @param workspaceApps items to add on the workspace
+     */
+    public AddWorkspaceItemsTask(ArrayList<? extends ItemInfo> workspaceApps) {
+        mWorkspaceApps = workspaceApps;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        if (mWorkspaceApps.isEmpty()) {
+            return;
+        }
+        Context context = app.getContext();
+
+        final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
+        final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
+
+        // Get the list of workspace screens.  We need to append to this list and
+        // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
+        // called.
+        ArrayList<Long> workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context);
+        synchronized(dataModel) {
+            for (ItemInfo item : mWorkspaceApps) {
+                if (item instanceof ShortcutInfo) {
+                    // Short-circuit this logic if the icon exists somewhere on the workspace
+                    if (shortcutExists(dataModel, item.getIntent(), item.user)) {
+                        continue;
+                    }
+                }
+
+                // Find appropriate space for the item.
+                Pair<Long, int[]> coords = findSpaceForItem(
+                        app, dataModel, workspaceScreens, addedWorkspaceScreensFinal, 1, 1);
+                long screenId = coords.first;
+                int[] cordinates = coords.second;
+
+                ItemInfo itemInfo;
+                if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
+                    itemInfo = item;
+                } else if (item instanceof AppInfo) {
+                    itemInfo = ((AppInfo) item).makeShortcut();
+                } else {
+                    throw new RuntimeException("Unexpected info type");
+                }
+
+                // Add the shortcut to the db
+                addItemToDatabase(context, itemInfo, screenId, cordinates);
+
+                // Save the ShortcutInfo for binding in the workspace
+                addedShortcutsFinal.add(itemInfo);
+            }
+        }
+
+        // Update the workspace screens
+        updateScreens(context, workspaceScreens);
+
+        if (!addedShortcutsFinal.isEmpty()) {
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    final ArrayList<ItemInfo> addAnimated = new ArrayList<ItemInfo>();
+                    final ArrayList<ItemInfo> addNotAnimated = new ArrayList<ItemInfo>();
+                    if (!addedShortcutsFinal.isEmpty()) {
+                        ItemInfo info = addedShortcutsFinal.get(addedShortcutsFinal.size() - 1);
+                        long lastScreenId = info.screenId;
+                        for (ItemInfo i : addedShortcutsFinal) {
+                            if (i.screenId == lastScreenId) {
+                                addAnimated.add(i);
+                            } else {
+                                addNotAnimated.add(i);
+                            }
+                        }
+                    }
+                    callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
+                            addNotAnimated, addAnimated, null);
+                }
+            });
+        }
+    }
+
+    protected void addItemToDatabase(Context context, ItemInfo item, long screenId, int[] pos) {
+        LauncherModel.addItemToDatabase(context, item,
+                LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, pos[0], pos[1]);
+    }
+
+    protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) {
+        LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens);
+    }
+
+    /**
+     * Returns true if the shortcuts already exists on the workspace. This must be called after
+     * the workspace has been loaded. We identify a shortcut by its intent.
+     */
+    protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandleCompat user) {
+        final String intentWithPkg, intentWithoutPkg;
+        if (intent.getComponent() != null) {
+            // If component is not null, an intent with null package will produce
+            // the same result and should also be a match.
+            String packageName = intent.getComponent().getPackageName();
+            if (intent.getPackage() != null) {
+                intentWithPkg = intent.toUri(0);
+                intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
+            } else {
+                intentWithPkg = new Intent(intent).setPackage(packageName).toUri(0);
+                intentWithoutPkg = intent.toUri(0);
+            }
+        } else {
+            intentWithPkg = intent.toUri(0);
+            intentWithoutPkg = intent.toUri(0);
+        }
+
+        synchronized (dataModel) {
+            for (ItemInfo item : dataModel.itemsIdMap) {
+                if (item instanceof ShortcutInfo) {
+                    ShortcutInfo info = (ShortcutInfo) item;
+                    Intent targetIntent = info.promisedIntent == null
+                            ? info.intent : info.promisedIntent;
+                    if (targetIntent != null && info.user.equals(user)) {
+                        Intent copyIntent = new Intent(targetIntent);
+                        copyIntent.setSourceBounds(intent.getSourceBounds());
+                        String s = copyIntent.toUri(0);
+                        if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Find a position on the screen for the given size or adds a new screen.
+     * @return screenId and the coordinates for the item.
+     */
+    protected Pair<Long, int[]> findSpaceForItem(
+            LauncherAppState app, BgDataModel dataModel,
+            ArrayList<Long> workspaceScreens,
+            ArrayList<Long> addedWorkspaceScreensFinal,
+            int spanX, int spanY) {
+        LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
+
+        // Use sBgItemsIdMap as all the items are already loaded.
+        synchronized (dataModel) {
+            for (ItemInfo info : dataModel.itemsIdMap) {
+                if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                    ArrayList<ItemInfo> items = screenItems.get(info.screenId);
+                    if (items == null) {
+                        items = new ArrayList<>();
+                        screenItems.put(info.screenId, items);
+                    }
+                    items.add(info);
+                }
+            }
+        }
+
+        // Find appropriate space for the item.
+        long screenId = 0;
+        int[] cordinates = new int[2];
+        boolean found = false;
+
+        int screenCount = workspaceScreens.size();
+        // First check the preferred screen.
+        int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
+        if (preferredScreenIndex < screenCount) {
+            screenId = workspaceScreens.get(preferredScreenIndex);
+            found = findNextAvailableIconSpaceInScreen(
+                    app, screenItems.get(screenId), cordinates, spanX, spanY);
+        }
+
+        if (!found) {
+            // Search on any of the screens starting from the first screen.
+            for (int screen = 1; screen < screenCount; screen++) {
+                screenId = workspaceScreens.get(screen);
+                if (findNextAvailableIconSpaceInScreen(
+                        app, screenItems.get(screenId), cordinates, spanX, spanY)) {
+                    // We found a space for it
+                    found = true;
+                    break;
+                }
+            }
+        }
+
+        if (!found) {
+            // Still no position found. Add a new screen to the end.
+            screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                    .getLong(LauncherSettings.Settings.EXTRA_VALUE);
+
+            // Save the screen id for binding in the workspace
+            workspaceScreens.add(screenId);
+            addedWorkspaceScreensFinal.add(screenId);
+
+            // If we still can't find an empty space, then God help us all!!!
+            if (!findNextAvailableIconSpaceInScreen(
+                    app, screenItems.get(screenId), cordinates, spanX, spanY)) {
+                throw new RuntimeException("Can't find space to add the item");
+            }
+        }
+        return Pair.create(screenId, cordinates);
+    }
+
+    private boolean findNextAvailableIconSpaceInScreen(
+            LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
+            int[] xy, int spanX, int spanY) {
+        InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
+
+        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
+        if (occupiedPos != null) {
+            for (ItemInfo r : occupiedPos) {
+                occupied.markCells(r, true);
+            }
+        }
+        return occupied.findVacantCell(xy, spanX, spanY);
+    }
+
+}
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
new file mode 100644 (file)
index 0000000..9f24e90
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/**
+ * Handles changes due to cache updates.
+ */
+public class CacheDataUpdatedTask extends ExtendedModelTask {
+
+    public static final int OP_CACHE_UPDATE = 1;
+    public static final int OP_SESSION_UPDATE = 2;
+
+    private final int mOp;
+    private final UserHandleCompat mUser;
+    private final HashSet<String> mPackages;
+
+    public CacheDataUpdatedTask(int op, UserHandleCompat user, HashSet<String> packages) {
+        mOp = op;
+        mUser = user;
+        mPackages = packages;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        IconCache iconCache = app.getIconCache();
+
+        final ArrayList<AppInfo> updatedApps = new ArrayList<>();
+
+        ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
+        synchronized (dataModel) {
+            for (ItemInfo info : dataModel.itemsIdMap) {
+                if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
+                    ShortcutInfo si = (ShortcutInfo) info;
+                    ComponentName cn = si.getTargetComponent();
+                    if (isValidShortcut(si) &&
+                            cn != null && mPackages.contains(cn.getPackageName())) {
+                        si.updateIcon(iconCache);
+                        updatedShortcuts.add(si);
+                    }
+                }
+            }
+            apps.updateIconsAndLabels(mPackages, mUser, updatedApps);
+        }
+        bindUpdatedShortcuts(updatedShortcuts, mUser);
+
+        if (!updatedApps.isEmpty()) {
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindAppsUpdated(updatedApps);
+                }
+            });
+        }
+    }
+
+    public boolean isValidShortcut(ShortcutInfo si) {
+        switch (mOp) {
+            case OP_CACHE_UPDATE:
+                return si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+            case OP_SESSION_UPDATE:
+                return si.isPromise();
+            default:
+                return false;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/ExtendedModelTask.java b/src/com/android/launcher3/model/ExtendedModelTask.java
new file mode 100644 (file)
index 0000000..ccc6007
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.LauncherModel.BaseModelUpdateTask;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.MultiHashMap;
+
+import java.util.ArrayList;
+
+/**
+ * Extension of {@link BaseModelUpdateTask} with some utility methods
+ */
+public abstract class ExtendedModelTask extends BaseModelUpdateTask {
+
+    public void bindUpdatedShortcuts(
+            ArrayList<ShortcutInfo> updatedShortcuts, UserHandleCompat user) {
+        bindUpdatedShortcuts(updatedShortcuts, new ArrayList<ShortcutInfo>(), user);
+    }
+
+    public void bindUpdatedShortcuts(
+            final ArrayList<ShortcutInfo> updatedShortcuts,
+            final ArrayList<ShortcutInfo> removedShortcuts,
+            final UserHandleCompat user) {
+        if (!updatedShortcuts.isEmpty() || !removedShortcuts.isEmpty()) {
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindShortcutsChanged(updatedShortcuts, removedShortcuts, user);
+                }
+            });
+        }
+    }
+
+    public void bindDeepShortcuts(BgDataModel dataModel) {
+        final MultiHashMap<ComponentKey, String> shortcutMapCopy = dataModel.deepShortcutMap.clone();
+        scheduleCallbackTask(new CallbackTask() {
+            @Override
+            public void execute(Callbacks callbacks) {
+                callbacks.bindDeepShortcutMap(shortcutMapCopy);
+            }
+        });
+    }
+}
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
new file mode 100644 (file)
index 0000000..5d04325
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+
+import java.util.HashSet;
+
+/**
+ * Handles changes due to a sessions updates for a currently installing app.
+ */
+public class PackageInstallStateChangedTask extends ExtendedModelTask {
+
+    private final PackageInstallInfo mInstallInfo;
+
+    public PackageInstallStateChangedTask(PackageInstallInfo installInfo) {
+        mInstallInfo = installInfo;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        if (mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLED) {
+            // Ignore install success events as they are handled by Package add events.
+            return;
+        }
+
+        synchronized (dataModel) {
+            final HashSet<ItemInfo> updates = new HashSet<>();
+            for (ItemInfo info : dataModel.itemsIdMap) {
+                if (info instanceof ShortcutInfo) {
+                    ShortcutInfo si = (ShortcutInfo) info;
+                    ComponentName cn = si.getTargetComponent();
+                    if (si.isPromise() && (cn != null)
+                            && mInstallInfo.packageName.equals(cn.getPackageName())) {
+                        si.setInstallProgress(mInstallInfo.progress);
+
+                        if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
+                            // Mark this info as broken.
+                            si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE;
+                        }
+                        updates.add(si);
+                    }
+                }
+            }
+
+            for (LauncherAppWidgetInfo widget : dataModel.appWidgets) {
+                if (widget.providerName.getPackageName().equals(mInstallInfo.packageName)) {
+                    widget.installProgress = mInstallInfo.progress;
+                    updates.add(widget);
+                }
+            }
+
+            if (!updates.isEmpty()) {
+                scheduleCallbackTask(new CallbackTask() {
+                    @Override
+                    public void execute(Callbacks callbacks) {
+                        callbacks.bindRestoreItemsChange(updates);
+                    }
+                });
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
new file mode 100644 (file)
index 0000000..7286bf5
--- /dev/null
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InstallShortcutReceiver;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.CallbackTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.util.FlagOp;
+import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.ManagedProfileHeuristic;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Handles updates due to changes in package manager (app installed/updated/removed)
+ * or when a user availability changes.
+ */
+public class PackageUpdatedTask extends ExtendedModelTask {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "PackageUpdatedTask";
+
+    public static final int OP_NONE = 0;
+    public static final int OP_ADD = 1;
+    public static final int OP_UPDATE = 2;
+    public static final int OP_REMOVE = 3; // uninstalled
+    public static final int OP_UNAVAILABLE = 4; // external media unmounted
+    public static final int OP_SUSPEND = 5; // package suspended
+    public static final int OP_UNSUSPEND = 6; // package unsuspended
+    public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable
+
+    private final int mOp;
+    private final UserHandleCompat mUser;
+    private final String[] mPackages;
+
+    public PackageUpdatedTask(int op, UserHandleCompat user, String... packages) {
+        mOp = op;
+        mUser = user;
+        mPackages = packages;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
+        final Context context = app.getContext();
+        final IconCache iconCache = app.getIconCache();
+
+        final String[] packages = mPackages;
+        final int N = packages.length;
+        FlagOp flagOp = FlagOp.NO_OP;
+        final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
+        switch (mOp) {
+            case OP_ADD: {
+                for (int i = 0; i < N; i++) {
+                    if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
+                    iconCache.updateIconsForPkg(packages[i], mUser);
+                    appsList.addPackage(context, packages[i], mUser);
+                }
+
+                ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
+                if (heuristic != null) {
+                    heuristic.processPackageAdd(mPackages);
+                }
+                break;
+            }
+            case OP_UPDATE:
+                for (int i = 0; i < N; i++) {
+                    if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
+                    iconCache.updateIconsForPkg(packages[i], mUser);
+                    appsList.updatePackage(context, packages[i], mUser);
+                    app.getWidgetCache().removePackage(packages[i], mUser);
+                }
+                // Since package was just updated, the target must be available now.
+                flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
+                break;
+            case OP_REMOVE: {
+                ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
+                if (heuristic != null) {
+                    heuristic.processPackageRemoved(mPackages);
+                }
+                for (int i = 0; i < N; i++) {
+                    iconCache.removeIconsForPkg(packages[i], mUser);
+                }
+                // Fall through
+            }
+            case OP_UNAVAILABLE:
+                for (int i = 0; i < N; i++) {
+                    if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
+                    appsList.removePackage(packages[i], mUser);
+                    app.getWidgetCache().removePackage(packages[i], mUser);
+                }
+                flagOp = FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
+                break;
+            case OP_SUSPEND:
+            case OP_UNSUSPEND:
+                flagOp = mOp == OP_SUSPEND ?
+                        FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED) :
+                        FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED);
+                if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
+                appsList.updateDisabledFlags(
+                        ItemInfoMatcher.ofPackages(packageSet, mUser), flagOp);
+                break;
+            case OP_USER_AVAILABILITY_CHANGE:
+                flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
+                        ? FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER)
+                        : FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER);
+                // We want to update all packages for this user.
+                appsList.updateDisabledFlags(ItemInfoMatcher.ofUser(mUser), flagOp);
+                break;
+        }
+
+        ArrayList<AppInfo> added = null;
+        ArrayList<AppInfo> modified = null;
+        final ArrayList<AppInfo> removedApps = new ArrayList<AppInfo>();
+
+        if (appsList.added.size() > 0) {
+            added = new ArrayList<>(appsList.added);
+            appsList.added.clear();
+        }
+        if (appsList.modified.size() > 0) {
+            modified = new ArrayList<>(appsList.modified);
+            appsList.modified.clear();
+        }
+        if (appsList.removed.size() > 0) {
+            removedApps.addAll(appsList.removed);
+            appsList.removed.clear();
+        }
+
+        final HashMap<ComponentName, AppInfo> addedOrUpdatedApps = new HashMap<>();
+
+        if (added != null) {
+            final ArrayList<AppInfo> addedApps = added;
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindAppsAdded(null, null, null, addedApps);
+                }
+            });
+            for (AppInfo ai : added) {
+                addedOrUpdatedApps.put(ai.componentName, ai);
+            }
+        }
+
+        if (modified != null) {
+            final ArrayList<AppInfo> modifiedFinal = modified;
+            for (AppInfo ai : modified) {
+                addedOrUpdatedApps.put(ai.componentName, ai);
+            }
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindAppsUpdated(modifiedFinal);
+                }
+            });
+        }
+
+        // Update shortcut infos
+        if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
+            final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
+            final ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<>();
+            final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
+
+            synchronized (dataModel) {
+                for (ItemInfo info : dataModel.itemsIdMap) {
+                    if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
+                        ShortcutInfo si = (ShortcutInfo) info;
+                        boolean infoUpdated = false;
+                        boolean shortcutUpdated = false;
+
+                        // Update shortcuts which use iconResource.
+                        if ((si.iconResource != null)
+                                && packageSet.contains(si.iconResource.packageName)) {
+                            Bitmap icon = LauncherIcons.createIconBitmap(
+                                    si.iconResource.packageName,
+                                    si.iconResource.resourceName, context);
+                            if (icon != null) {
+                                si.setIcon(icon);
+                                si.usingFallbackIcon = false;
+                                infoUpdated = true;
+                            }
+                        }
+
+                        ComponentName cn = si.getTargetComponent();
+                        if (cn != null && packageSet.contains(cn.getPackageName())) {
+                            AppInfo appInfo = addedOrUpdatedApps.get(cn);
+
+                            if (si.isPromise()) {
+                                if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
+                                    // Auto install icon
+                                    PackageManager pm = context.getPackageManager();
+                                    ResolveInfo matched = pm.resolveActivity(
+                                            new Intent(Intent.ACTION_MAIN)
+                                                    .setComponent(cn).addCategory(Intent.CATEGORY_LAUNCHER),
+                                            PackageManager.MATCH_DEFAULT_ONLY);
+                                    if (matched == null) {
+                                        // Try to find the best match activity.
+                                        Intent intent = pm.getLaunchIntentForPackage(
+                                                cn.getPackageName());
+                                        if (intent != null) {
+                                            cn = intent.getComponent();
+                                            appInfo = addedOrUpdatedApps.get(cn);
+                                        }
+
+                                        if ((intent == null) || (appInfo == null)) {
+                                            removedShortcuts.add(si);
+                                            continue;
+                                        }
+                                        si.promisedIntent = intent;
+                                    }
+                                }
+
+                                si.intent = si.promisedIntent;
+                                si.promisedIntent = null;
+                                si.status = ShortcutInfo.DEFAULT;
+                                infoUpdated = true;
+                                si.updateIcon(iconCache);
+                            }
+
+                            if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction())
+                                    && si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
+                                si.updateIcon(iconCache);
+                                si.title = Utilities.trim(appInfo.title);
+                                si.contentDescription = appInfo.contentDescription;
+                                infoUpdated = true;
+                            }
+
+                            int oldDisabledFlags = si.isDisabled;
+                            si.isDisabled = flagOp.apply(si.isDisabled);
+                            if (si.isDisabled != oldDisabledFlags) {
+                                shortcutUpdated = true;
+                            }
+                        }
+
+                        if (infoUpdated || shortcutUpdated) {
+                            updatedShortcuts.add(si);
+                        }
+                        if (infoUpdated) {
+                            LauncherModel.updateItemInDatabase(context, si);
+                        }
+                    } else if (info instanceof LauncherAppWidgetInfo && mOp == OP_ADD) {
+                        LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
+                        if (mUser.equals(widgetInfo.user)
+                                && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                                && packageSet.contains(widgetInfo.providerName.getPackageName())) {
+                            widgetInfo.restoreStatus &=
+                                    ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
+                                            ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+
+                            // adding this flag ensures that launcher shows 'click to setup'
+                            // if the widget has a config activity. In case there is no config
+                            // activity, it will be marked as 'restored' during bind.
+                            widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+
+                            widgets.add(widgetInfo);
+                            LauncherModel.updateItemInDatabase(context, widgetInfo);
+                        }
+                    }
+                }
+            }
+
+            bindUpdatedShortcuts(updatedShortcuts, removedShortcuts, mUser);
+            if (!removedShortcuts.isEmpty()) {
+                LauncherModel.deleteItemsFromDatabase(context, removedShortcuts);
+            }
+
+            if (!widgets.isEmpty()) {
+                scheduleCallbackTask(new CallbackTask() {
+                    @Override
+                    public void execute(Callbacks callbacks) {
+                        callbacks.bindWidgetsRestored(widgets);
+                    }
+                });
+            }
+        }
+
+        final HashSet<String> removedPackages = new HashSet<>();
+        final HashSet<ComponentName> removedComponents = new HashSet<>();
+        if (mOp == OP_REMOVE) {
+            // Mark all packages in the broadcast to be removed
+            Collections.addAll(removedPackages, packages);
+
+            // No need to update the removedComponents as
+            // removedPackages is a super-set of removedComponents
+        } else if (mOp == OP_UPDATE) {
+            // Mark disabled packages in the broadcast to be removed
+            final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
+            for (int i=0; i<N; i++) {
+                if (!launcherApps.isPackageEnabledForProfile(packages[i], mUser)) {
+                    removedPackages.add(packages[i]);
+                }
+            }
+
+            // Update removedComponents as some components can get removed during package update
+            for (AppInfo info : removedApps) {
+                removedComponents.add(info.componentName);
+            }
+        }
+
+        if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
+            LauncherModel.deleteItemsFromDatabase(
+                    context, ItemInfoMatcher.ofPackages(removedPackages, mUser));
+            LauncherModel.deleteItemsFromDatabase(
+                    context, ItemInfoMatcher.ofComponents(removedComponents, mUser));
+
+            // Remove any queued items from the install queue
+            InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
+
+            // Call the components-removed callback
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindWorkspaceComponentsRemoved(
+                            removedPackages, removedComponents, mUser);
+                }
+            });
+        }
+
+        if (!removedApps.isEmpty()) {
+            // Remove corresponding apps from All-Apps
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.bindAppInfosRemoved(removedApps);
+                }
+            });
+        }
+
+        // Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to
+        // get widget update signals.
+        if (!Utilities.ATLEAST_MARSHMALLOW &&
+                (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) {
+            scheduleCallbackTask(new CallbackTask() {
+                @Override
+                public void execute(Callbacks callbacks) {
+                    callbacks.notifyWidgetProvidersChanged();
+                }
+            });
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
new file mode 100644 (file)
index 0000000..8f7c21d
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.Context;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+import com.android.launcher3.util.MultiHashMap;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles changes due to shortcut manager updates (deep shortcut changes)
+ */
+public class ShortcutsChangedTask extends ExtendedModelTask {
+
+    private final String mPackageName;
+    private final List<ShortcutInfoCompat> mShortcuts;
+    private final UserHandleCompat mUser;
+    private final boolean mUpdateIdMap;
+
+    public ShortcutsChangedTask(String packageName, List<ShortcutInfoCompat> shortcuts,
+            UserHandleCompat user, boolean updateIdMap) {
+        mPackageName = packageName;
+        mShortcuts = shortcuts;
+        mUser = user;
+        mUpdateIdMap = updateIdMap;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        DeepShortcutManager deepShortcutManager = app.getShortcutManager();
+        deepShortcutManager.onShortcutsChanged(mShortcuts);
+
+        // Find ShortcutInfo's that have changed on the workspace.
+        final ArrayList<ShortcutInfo> removedShortcutInfos = new ArrayList<>();
+        MultiHashMap<String, ShortcutInfo> idsToWorkspaceShortcutInfos = new MultiHashMap<>();
+        for (ItemInfo itemInfo : dataModel.itemsIdMap) {
+            if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                ShortcutInfo si = (ShortcutInfo) itemInfo;
+                if (si.getPromisedIntent().getPackage().equals(mPackageName)
+                        && si.user.equals(mUser)) {
+                    idsToWorkspaceShortcutInfos.addToList(si.getDeepShortcutId(), si);
+                }
+            }
+        }
+
+        final Context context = LauncherAppState.getInstance().getContext();
+        final ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
+        if (!idsToWorkspaceShortcutInfos.isEmpty()) {
+            // Update the workspace to reflect the changes to updated shortcuts residing on it.
+            List<ShortcutInfoCompat> shortcuts = deepShortcutManager.queryForFullDetails(
+                    mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser);
+            for (ShortcutInfoCompat fullDetails : shortcuts) {
+                List<ShortcutInfo> shortcutInfos = idsToWorkspaceShortcutInfos
+                        .remove(fullDetails.getId());
+                if (!fullDetails.isPinned()) {
+                    // The shortcut was previously pinned but is no longer, so remove it from
+                    // the workspace and our pinned shortcut counts.
+                    // Note that we put this check here, after querying for full details,
+                    // because there's a possible race condition between pinning and
+                    // receiving this callback.
+                    removedShortcutInfos.addAll(shortcutInfos);
+                    continue;
+                }
+                for (ShortcutInfo shortcutInfo : shortcutInfos) {
+                    shortcutInfo.updateFromDeepShortcutInfo(fullDetails, context);
+                    updatedShortcutInfos.add(shortcutInfo);
+                }
+            }
+        }
+
+        // If there are still entries in idsToWorkspaceShortcutInfos, that means that
+        // the corresponding shortcuts weren't passed in onShortcutsChanged(). This
+        // means they were cleared, so we remove and unpin them now.
+        for (String id : idsToWorkspaceShortcutInfos.keySet()) {
+            removedShortcutInfos.addAll(idsToWorkspaceShortcutInfos.get(id));
+        }
+
+        bindUpdatedShortcuts(updatedShortcutInfos, removedShortcutInfos, mUser);
+        if (!removedShortcutInfos.isEmpty()) {
+            LauncherModel.deleteItemsFromDatabase(context, removedShortcutInfos);
+        }
+
+        if (mUpdateIdMap) {
+            // Update the deep shortcut map if the list of ids has changed for an activity.
+            dataModel.updateDeepShortcutMap(mPackageName, mUser, mShortcuts);
+            bindDeepShortcuts(dataModel);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
new file mode 100644 (file)
index 0000000..b7b52a4
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import android.content.Context;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Task to handle changing of lock state of the user
+ */
+public class UserLockStateChangedTask extends ExtendedModelTask {
+
+    private final UserHandleCompat mUser;
+
+    public UserLockStateChangedTask(UserHandleCompat user) {
+        mUser = user;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        Context context = app.getContext();
+        boolean isUserUnlocked = UserManagerCompat.getInstance(context).isUserUnlocked(mUser);
+        DeepShortcutManager deepShortcutManager = app.getShortcutManager();
+
+        HashMap<ShortcutKey, ShortcutInfoCompat> pinnedShortcuts = new HashMap<>();
+        if (isUserUnlocked) {
+            List<ShortcutInfoCompat> shortcuts =
+                    deepShortcutManager.queryForPinnedShortcuts(null, mUser);
+            if (deepShortcutManager.wasLastCallSuccess()) {
+                for (ShortcutInfoCompat shortcut : shortcuts) {
+                    pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
+                }
+            } else {
+                // Shortcut manager can fail due to some race condition when the lock state
+                // changes too frequently. For the purpose of the update,
+                // consider it as still locked.
+                isUserUnlocked = false;
+            }
+        }
+
+        // Update the workspace to reflect the changes to updated shortcuts residing on it.
+        ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
+        ArrayList<ShortcutInfo> deletedShortcutInfos = new ArrayList<>();
+        for (ItemInfo itemInfo : dataModel.itemsIdMap) {
+            if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                    && mUser.equals(itemInfo.user)) {
+                ShortcutInfo si = (ShortcutInfo) itemInfo;
+                if (isUserUnlocked) {
+                    ShortcutInfoCompat shortcut =
+                            pinnedShortcuts.get(ShortcutKey.fromShortcutInfo(si));
+                    // We couldn't verify the shortcut during loader. If its no longer available
+                    // (probably due to clear data), delete the workspace item as well
+                    if (shortcut == null) {
+                        deletedShortcutInfos.add(si);
+                        continue;
+                    }
+                    si.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
+                    si.updateFromDeepShortcutInfo(shortcut, context);
+                } else {
+                    si.isDisabled |= ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
+                }
+                updatedShortcutInfos.add(si);
+            }
+        }
+        bindUpdatedShortcuts(updatedShortcutInfos, deletedShortcutInfos, mUser);
+        if (!deletedShortcutInfos.isEmpty()) {
+            LauncherModel.deleteItemsFromDatabase(context, deletedShortcutInfos);
+        }
+
+        // Remove shortcut id map for that user
+        Iterator<ComponentKey> keysIter = dataModel.deepShortcutMap.keySet().iterator();
+        while (keysIter.hasNext()) {
+            if (keysIter.next().user.equals(mUser)) {
+                keysIter.remove();
+            }
+        }
+
+        if (isUserUnlocked) {
+            dataModel.updateDeepShortcutMap(
+                    null, mUser, deepShortcutManager.queryForAllShortcuts(mUser));
+        }
+        bindDeepShortcuts(dataModel);
+    }
+}
index 6661429..78b7a3e 100644 (file)
@@ -121,7 +121,7 @@ public class ManagedProfileHeuristic {
             // getting filled with the managed user apps, when it start with a fresh DB (or after
             // a very long time).
             if (userAppsExisted && !homescreenApps.isEmpty()) {
-                mModel.addAndBindAddedWorkspaceItems(mContext, homescreenApps);
+                mModel.addAndBindAddedWorkspaceItems(homescreenApps);
             }
         }
 
@@ -175,7 +175,7 @@ public class ManagedProfileHeuristic {
                 // Add the item to home screen and DB. This also generates an item id synchronously.
                 ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1);
                 itemList.add(workFolder);
-                mModel.addAndBindAddedWorkspaceItems(mContext, itemList);
+                mModel.addAndBindAddedWorkspaceItems(itemList);
                 mPrefs.edit().putLong(folderIdKey, workFolder.id).apply();
 
                 saveWorkFolderShortcuts(workFolder.id, 0, workFolderApps);
@@ -200,7 +200,6 @@ public class ManagedProfileHeuristic {
         }
     }
 
-
     /**
      * Verifies that entries corresponding to {@param users} exist and removes all invalid entries.
      */
index 61ee220..5103ced 100644 (file)
@@ -17,11 +17,12 @@ LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator mockito-target-minus-junit4
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_SDK_VERSION := current
+LOCAL_MIN_SDK_VERSION := 21
 
 LOCAL_PACKAGE_NAME := Launcher3Tests
 
diff --git a/tests/res/raw/cache_data_updated_task_data.txt b/tests/res/raw/cache_data_updated_task_data.txt
new file mode 100644 (file)
index 0000000..9095476
--- /dev/null
@@ -0,0 +1,28 @@
+# Model data used by CacheDataUpdatedTaskTest
+
+classMap s com.android.launcher3.ShortcutInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Auto install app shortcut
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+
+# Custom shortcuts
+bgItem s itemType=1 title=app1-shrt intent=component=app1/class3 id=7
+bgItem s itemType=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Restored custom shortcut
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=9
+bgItem s itemType=1 status=1 title=app5-shrt intent=component=app5/class1 id=10
+
+allApps componentName=app1/class1
+allApps componentName=app1/class2
+allApps componentName=app2/class1
+allApps componentName=app2/class2
\ No newline at end of file
diff --git a/tests/res/raw/package_install_state_change_task_data.txt b/tests/res/raw/package_install_state_change_task_data.txt
new file mode 100644 (file)
index 0000000..84f9c16
--- /dev/null
@@ -0,0 +1,24 @@
+# Model data used by PackageInstallStateChangeTaskTest
+
+classMap s com.android.launcher3.ShortcutInfo
+classMap w com.android.launcher3.LauncherAppWidgetInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Promise icons for app3
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
+
+# Promise icon for app4
+bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Widget
+bgItem w providerName=app4/provider1 id=9
+bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
new file mode 100644 (file)
index 0000000..ecb3782
--- /dev/null
@@ -0,0 +1,190 @@
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.Pair;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.LongArrayMap;
+
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link AddWorkspaceItemsTask}
+ */
+public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
+
+    private final ComponentName mComponent1 = new ComponentName("a", "b");
+    private final ComponentName mComponent2 = new ComponentName("b", "b");
+
+    private ArrayList<Long> existingScreens;
+    private ArrayList<Long> newScreens;
+    private LongArrayMap<GridOccupancy> screenOccupancy;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        existingScreens = new ArrayList<>();
+        screenOccupancy = new LongArrayMap<>();
+        newScreens = new ArrayList<>();
+
+        idp.numColumns = 5;
+        idp.numRows = 5;
+    }
+
+    private <T extends ItemInfo> AddWorkspaceItemsTask newTask(T... items) {
+        return new AddWorkspaceItemsTask(new ArrayList<>(Arrays.asList(items))) {
+
+            @Override
+            protected void addItemToDatabase(Context context, ItemInfo item,
+                    long screenId, int[] pos) {
+                item.screenId = screenId;
+                item.cellX = pos[0];
+                item.cellY = pos[1];
+            }
+
+            @Override
+            protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) { }
+        };
+    }
+
+    public void testFindSpaceForItem_prefers_second() {
+        // First screen has only one hole of size 1
+        int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+        // Second screen has 2 holes of sizes 3x2 and 2x3
+        setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+
+        Pair<Long, int[]> spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
+        assertEquals(2L, (long) spaceFound.first);
+        assertTrue(screenOccupancy.get(spaceFound.first)
+                .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 1, 1));
+
+        // Find a larger space
+        spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
+        assertEquals(2L, (long) spaceFound.first);
+        assertTrue(screenOccupancy.get(spaceFound.first)
+                .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 2, 3));
+    }
+
+    public void testFindSpaceForItem_adds_new_screen() throws Exception {
+        // First screen has 2 holes of sizes 3x2 and 2x3
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        ArrayList<Long> oldScreens = new ArrayList<>(existingScreens);
+        Pair<Long, int[]> spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
+        assertFalse(oldScreens.contains(spaceFound.first));
+        assertTrue(newScreens.contains(spaceFound.first));
+    }
+
+    public void testAddItem_existing_item_ignored() throws Exception {
+        ShortcutInfo info = new ShortcutInfo();
+        info.intent = new Intent().setComponent(mComponent1);
+
+        // Setup a screen with a hole
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        // Nothing was added
+        assertTrue(executeTaskForTest(newTask(info)).isEmpty());
+    }
+
+    public void testAddItem_some_items_added() throws Exception {
+        ShortcutInfo info = new ShortcutInfo();
+        info.intent = new Intent().setComponent(mComponent1);
+
+        ShortcutInfo info2 = new ShortcutInfo();
+        info2.intent = new Intent().setComponent(mComponent2);
+
+        // Setup a screen with a hole
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        executeTaskForTest(newTask(info, info2)).get(0).run();
+        ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
+        ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
+
+        // only info2 should be added because info was already added to the workspace
+        // in setupWorkspaceWithHoles()
+        verify(callbacks).bindAppsAdded(any(ArrayList.class), notAnimated.capture(),
+                animated.capture(), any(ArrayList.class));
+        assertTrue(notAnimated.getValue().isEmpty());
+
+        assertEquals(1, animated.getValue().size());
+        assertTrue(animated.getValue().contains(info2));
+    }
+
+    private int setupWorkspaceWithHoles(int startId, long screenId, Rect... holes) {
+        GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
+        occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
+        for (Rect r : holes) {
+            occupancy.markCells(r, false);
+        }
+
+        existingScreens.add(screenId);
+        screenOccupancy.append(screenId, occupancy);
+
+        for (int x = 0; x < idp.numColumns; x++) {
+            for (int y = 0; y < idp.numRows; y++) {
+                if (!occupancy.cells[x][y]) {
+                    continue;
+                }
+
+                ShortcutInfo info = new ShortcutInfo();
+                info.intent = new Intent().setComponent(mComponent1);
+                info.id = startId++;
+                info.screenId = screenId;
+                info.cellX = x;
+                info.cellY = y;
+                info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+                bgDataModel.addItem(info, false);
+            }
+        }
+        return startId;
+    }
+
+    private void commitScreensToDb() throws Exception {
+        LauncherSettings.Settings.call(getMockContentResolver(),
+                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+
+        Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
+        ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+        // Clear the table
+        ops.add(ContentProviderOperation.newDelete(uri).build());
+        int count = existingScreens.size();
+        for (int i = 0; i < count; i++) {
+            ContentValues v = new ContentValues();
+            long screenId = existingScreens.get(i);
+            v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
+            v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+            ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
+        }
+        getMockContentResolver().applyBatch(ProviderConfig.AUTHORITY, ops);
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
new file mode 100644 (file)
index 0000000..5628e82
--- /dev/null
@@ -0,0 +1,208 @@
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.support.test.InstrumentationRegistry;
+import android.test.ProviderTestCase2;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.DeferredHandler;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.LauncherModel.BaseModelUpdateTask;
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.TestLauncherProvider;
+
+import org.mockito.ArgumentCaptor;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Base class for writing tests for Model update tasks.
+ */
+public class BaseModelUpdateTaskTestCase extends ProviderTestCase2<TestLauncherProvider> {
+
+    public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
+
+    public Context targetContext;
+    public UserHandleCompat myUser;
+
+    public InvariantDeviceProfile idp;
+    public LauncherAppState appState;
+    public MyIconCache iconCache;
+
+    public BgDataModel bgDataModel;
+    public AllAppsList allAppsList;
+    public Callbacks callbacks;
+
+    public BaseModelUpdateTaskTestCase() {
+        super(TestLauncherProvider.class, ProviderConfig.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        callbacks = mock(Callbacks.class);
+        appState = mock(LauncherAppState.class);
+        myUser = UserHandleCompat.myUserHandle();
+
+        bgDataModel = new BgDataModel();
+        targetContext = InstrumentationRegistry.getTargetContext();
+        idp = new InvariantDeviceProfile();
+        iconCache = new MyIconCache(targetContext, idp);
+
+        allAppsList = new AllAppsList(iconCache, null);
+
+        when(appState.getIconCache()).thenReturn(iconCache);
+        when(appState.getInvariantDeviceProfile()).thenReturn(idp);
+    }
+
+    /**
+     * Synchronously executes the task and returns all the UI callbacks posted.
+     */
+    public List<Runnable> executeTaskForTest(BaseModelUpdateTask task) throws Exception {
+        LauncherModel mockModel = mock(LauncherModel.class);
+        when(mockModel.getCallback()).thenReturn(callbacks);
+
+        Field f = BaseModelUpdateTask.class.getDeclaredField("mModel");
+        f.setAccessible(true);
+        f.set(task, mockModel);
+
+        DeferredHandler mockHandler = mock(DeferredHandler.class);
+        f = BaseModelUpdateTask.class.getDeclaredField("mUiHandler");
+        f.setAccessible(true);
+        f.set(task, mockHandler);
+
+        task.execute(appState, bgDataModel, allAppsList);
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mockHandler, atLeast(0)).post(captor.capture());
+
+        return captor.getAllValues();
+    }
+
+    /**
+     * Initializes mock data for the test.
+     */
+    public void initializeData(String resourceName) throws Exception {
+        Context myContext = InstrumentationRegistry.getContext();
+        Resources res = myContext.getResources();
+        int id = res.getIdentifier(resourceName, "raw", myContext.getPackageName());
+        try (BufferedReader reader =
+                     new BufferedReader(new InputStreamReader(res.openRawResource(id)))) {
+            String line;
+            HashMap<String, Class> classMap = new HashMap<>();
+            while((line = reader.readLine()) != null) {
+                line = line.trim();
+                if (line.startsWith("#") || line.isEmpty()) {
+                    continue;
+                }
+                String[] commands = line.split(" ");
+                switch (commands[0]) {
+                    case "classMap":
+                        classMap.put(commands[1], Class.forName(commands[2]));
+                        break;
+                    case "bgItem":
+                        bgDataModel.addItem(
+                                (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
+                        break;
+                    case "allApps":
+                        allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1));
+                        break;
+                }
+            }
+        }
+    }
+
+    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
+        HashMap<String, Field> cache = fieldCache.get(clazz);
+        if (cache == null) {
+            cache = new HashMap<>();
+            Class c = clazz;
+            while (c != null) {
+                for (Field f : c.getDeclaredFields()) {
+                    f.setAccessible(true);
+                    cache.put(f.getName(), f);
+                }
+                c = c.getSuperclass();
+            }
+            fieldCache.put(clazz, cache);
+        }
+
+        Object item = clazz.newInstance();
+        for (int i = startIndex; i < fieldDef.length; i++) {
+            String[] fieldData = fieldDef[i].split("=", 2);
+            Field f = cache.get(fieldData[0]);
+            Class type = f.getType();
+            if (type == int.class || type == long.class) {
+                f.set(item, Integer.parseInt(fieldData[1]));
+            } else if (type == CharSequence.class || type == String.class) {
+                f.set(item, fieldData[1]);
+            } else if (type == Intent.class) {
+                if (!fieldData[1].startsWith("#Intent")) {
+                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
+                }
+                f.set(item, Intent.parseUri(fieldData[1], 0));
+            } else if (type == ComponentName.class) {
+                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
+            } else {
+                throw new Exception("Added parsing logic for "
+                        + f.getName() + " of type " + f.getType());
+            }
+        }
+        return item;
+    }
+
+    public static class MyIconCache extends IconCache {
+
+        private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
+
+        public MyIconCache(Context context, InvariantDeviceProfile idp) {
+            super(context, idp);
+        }
+
+        @Override
+        protected CacheEntry cacheLocked(ComponentName componentName,
+                LauncherActivityInfoCompat info, UserHandleCompat user,
+                boolean usePackageIcon, boolean useLowResIcon) {
+            CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
+            if (entry == null) {
+                entry = new CacheEntry();
+                entry.icon = getDefaultIcon(user);
+            }
+            return entry;
+        }
+
+        public void addCache(ComponentName key, String title) {
+            CacheEntry entry = new CacheEntry();
+            entry.icon = newIcon();
+            entry.title = title;
+            mCache.put(new ComponentKey(key, UserHandleCompat.myUserHandle()), entry);
+        }
+
+        public Bitmap newIcon() {
+            return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
new file mode 100644 (file)
index 0000000..25b8df9
--- /dev/null
@@ -0,0 +1,81 @@
+package com.android.launcher3.model;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ShortcutInfo;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link CacheDataUpdatedTask}
+ */
+public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
+
+    private static final String NEW_LABEL_PREFIX = "new-label-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        initializeData("cache_data_updated_task_data");
+        // Add dummy entries in the cache to simulate update
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
+        }
+    }
+
+    private CacheDataUpdatedTask newTask(int op, String... pkg) {
+        return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
+    }
+
+    public void testCacheUpdate_update_apps() throws Exception {
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+
+        // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
+        // is not updated
+        verifyUpdate(1L, 2L);
+
+        // Verify that only app1 var updated in allAppsList
+        assertFalse(allAppsList.data.isEmpty());
+        for (AppInfo info : allAppsList.data) {
+            if (info.componentName.getPackageName().equals("app1")) {
+                assertNotNull(info.iconBitmap);
+            } else {
+                assertNull(info.iconBitmap);
+            }
+        }
+    }
+
+    public void testSessionUpdate_ignores_normal_apps() throws Exception {
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+
+        // app1 has no restored shortcuts. Verify that nothing was updated.
+        verifyUpdate();
+    }
+
+    public void testSessionUpdate_updates_pending_apps() throws Exception {
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+
+        // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
+        // were updated
+        verifyUpdate(5L, 6L);
+    }
+
+    private void verifyUpdate(Long... idsUpdated) {
+        HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
+        IconCache noOpIconCache = mock(IconCache.class);
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            if (updates.contains(info.id)) {
+                assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
+                assertNotNull(((ShortcutInfo) info).getIcon(noOpIconCache));
+            } else {
+                assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
+                assertNull(((ShortcutInfo) info).getIcon(noOpIconCache));
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
new file mode 100644 (file)
index 0000000..d655562
--- /dev/null
@@ -0,0 +1,61 @@
+package com.android.launcher3.model;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Tests for {@link PackageInstallStateChangedTask}
+ */
+public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        initializeData("package_install_state_change_task_data");
+    }
+
+    private PackageInstallStateChangedTask newTask(String pkg, int progress) {
+        PackageInstallInfo installInfo = new PackageInstallInfo(pkg);
+        installInfo.progress = progress;
+        installInfo.state = PackageInstallerCompat.STATUS_INSTALLING;
+        return new PackageInstallStateChangedTask(installInfo);
+    }
+
+    public void testSessionUpdate_ignore_installed() throws Exception {
+        executeTaskForTest(newTask("app1", 30));
+
+        // No shortcuts were updated
+        verifyProgressUpdate(0);
+    }
+
+    public void testSessionUpdate_shortcuts_updated() throws Exception {
+        executeTaskForTest(newTask("app3", 30));
+
+        verifyProgressUpdate(30, 5L, 6L, 7L);
+    }
+
+    public void testSessionUpdate_widgets_updated() throws Exception {
+        executeTaskForTest(newTask("app4", 30));
+
+        verifyProgressUpdate(30, 8L, 9L);
+    }
+
+    private void verifyProgressUpdate(int progress, Long... idsUpdated) {
+        HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            if (info instanceof ShortcutInfo) {
+                assertEquals(updates.contains(info.id) ? progress: 0,
+                        ((ShortcutInfo) info).getInstallProgress());
+            } else {
+                assertEquals(updates.contains(info.id) ? progress: -1,
+                        ((LauncherAppWidgetInfo) info).installProgress);
+            }
+        }
+    }
+}