From f0ba8b7ca1dc8fd53451d3d16e9f4fc306cddcd4 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 9 Sep 2016 15:47:55 -0700 Subject: Moving various runnables in LauncherModel to individual tasks > Adding tests for some of the runnable Change-Id: I1a315d38878857df3371f0e69d622a41fc3b081a --- build.gradle | 4 + src/com/android/launcher3/AllAppsList.java | 15 +- src/com/android/launcher3/IconCache.java | 4 +- .../android/launcher3/InstallShortcutReceiver.java | 2 +- .../android/launcher3/LauncherAppWidgetInfo.java | 11 +- src/com/android/launcher3/LauncherModel.java | 968 ++------------------- src/com/android/launcher3/ShortcutInfo.java | 8 +- .../launcher3/model/AddWorkspaceItemsTask.java | 262 ++++++ .../launcher3/model/CacheDataUpdatedTask.java | 95 ++ .../android/launcher3/model/ExtendedModelTask.java | 61 ++ .../model/PackageInstallStateChangedTask.java | 86 ++ .../launcher3/model/PackageUpdatedTask.java | 378 ++++++++ .../launcher3/model/ShortcutsChangedTask.java | 113 +++ .../launcher3/model/UserLockStateChangedTask.java | 114 +++ .../launcher3/util/ManagedProfileHeuristic.java | 5 +- tests/Android.mk | 3 +- tests/res/raw/cache_data_updated_task_data.txt | 28 + .../raw/package_install_state_change_task_data.txt | 24 + .../launcher3/model/AddWorkspaceItemsTaskTest.java | 190 ++++ .../model/BaseModelUpdateTaskTestCase.java | 208 +++++ .../launcher3/model/CacheDataUpdatedTaskTest.java | 81 ++ .../model/PackageInstallStateChangedTaskTest.java | 61 ++ 22 files changed, 1812 insertions(+), 909 deletions(-) create mode 100644 src/com/android/launcher3/model/AddWorkspaceItemsTask.java create mode 100644 src/com/android/launcher3/model/CacheDataUpdatedTask.java create mode 100644 src/com/android/launcher3/model/ExtendedModelTask.java create mode 100644 src/com/android/launcher3/model/PackageInstallStateChangedTask.java create mode 100644 src/com/android/launcher3/model/PackageUpdatedTask.java create mode 100644 src/com/android/launcher3/model/ShortcutsChangedTask.java create mode 100644 src/com/android/launcher3/model/UserLockStateChangedTask.java create mode 100644 tests/res/raw/cache_data_updated_task_data.txt create mode 100644 tests/res/raw/package_install_state_change_task_data.txt create mode 100644 tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java create mode 100644 tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java create mode 100644 tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java create mode 100644 tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java diff --git a/build.gradle b/build.gradle index 0c00da9d3..6fccb5cb5 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 0e465a41e..b13c20b20 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -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; } } diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 661f99b9e..04d0c8c91 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -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); diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index bd20e324b..b81074041 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -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); } } } diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index 66d895726..78f5b8edb 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -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; } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 955f51f5f..c70a47595 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -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 // - 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 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 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 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 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 findSpaceForItem( - Context context, - ArrayList workspaceScreens, - ArrayList addedWorkspaceScreensFinal, - int spanX, int spanY) { - LongSparseArray> 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 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 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 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 addedShortcutsFinal = new ArrayList(); - final ArrayList addedWorkspaceScreensFinal = new ArrayList(); - - // 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 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 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 addAnimated = new ArrayList(); - final ArrayList addNotAnimated = new ArrayList(); - 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 items) { + public static void deleteItemsFromDatabase(Context context, + final Iterable 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 screens) { + public static void updateWorkspaceScreenOrder(Context context, final ArrayList screens) { final ArrayList screensCopy = new ArrayList(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 shortcuts, UserHandleCompat user) { - enqueueItemUpdatedTask(new ShortcutsChangedTask(packageName, shortcuts, user, true)); + enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true)); } public void updatePinnedShortcuts(String packageName, List 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 updatedPackages, UserHandleCompat user) { - final Callbacks callbacks = getCallback(); - final ArrayList updatedApps = new ArrayList<>(); - final ArrayList 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 updatedShortcuts, UserHandleCompat user) { - bindUpdatedShortcuts(updatedShortcuts, new ArrayList(), user); + void enqueueModelUpdateTask(BaseModelUpdateTask task) { + task.init(this); + runOnWorkerThread(task); } - private void bindUpdatedShortcuts( - final ArrayList updatedShortcuts, - final ArrayList 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 packageSet = new HashSet<>(Arrays.asList(packages)); - switch (mOp) { - case OP_ADD: { - for (int i=0; i added = null; - ArrayList modified = null; - final ArrayList removedApps = new ArrayList(); - - 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 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 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 updatedShortcuts = new ArrayList<>(); - final ArrayList removedShortcuts = new ArrayList<>(); - final ArrayList 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 removedPackages = new HashSet<>(); - final HashSet 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 update = new ArrayList(); + public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { + info.updateFromDeepShortcutInfo(fullDetail, app.getContext()); + + ArrayList update = new ArrayList<>(); update.add(info); bindUpdatedShortcuts(update, fullDetail.getUserHandle()); } }); } - private class ShortcutsChangedTask implements Runnable { - private final String mPackageName; - private final List mShortcuts; - private final UserHandleCompat mUser; - private final boolean mUpdateIdMap; - - public ShortcutsChangedTask(String packageName, List 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 removedShortcutInfos = new ArrayList<>(); - MultiHashMap 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 updatedShortcutInfos = new ArrayList<>(); - if (!idsToWorkspaceShortcutInfos.isEmpty()) { - // Update the workspace to reflect the changes to updated shortcuts residing on it. - List shortcuts = mDeepShortcutManager.queryForFullDetails( - mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser); - for (ShortcutInfoCompat fullDetails : shortcuts) { - List 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 pinnedShortcuts = new HashMap<>(); - if (isUserUnlocked) { - List 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 updatedShortcutInfos = new ArrayList<>(); - ArrayList 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 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 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. diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 9a9287234..fc087363a 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -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 index 000000000..986e163e6 --- /dev/null +++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java @@ -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 mWorkspaceApps; + + /** + * @param workspaceApps items to add on the workspace + */ + public AddWorkspaceItemsTask(ArrayList workspaceApps) { + mWorkspaceApps = workspaceApps; + } + + @Override + public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { + if (mWorkspaceApps.isEmpty()) { + return; + } + Context context = app.getContext(); + + final ArrayList addedShortcutsFinal = new ArrayList(); + final ArrayList addedWorkspaceScreensFinal = new ArrayList(); + + // 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 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 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 addAnimated = new ArrayList(); + final ArrayList addNotAnimated = new ArrayList(); + 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 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 findSpaceForItem( + LauncherAppState app, BgDataModel dataModel, + ArrayList workspaceScreens, + ArrayList addedWorkspaceScreensFinal, + int spanX, int spanY) { + LongSparseArray> 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 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 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 index 000000000..9f24e9035 --- /dev/null +++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java @@ -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 mPackages; + + public CacheDataUpdatedTask(int op, UserHandleCompat user, HashSet packages) { + mOp = op; + mUser = user; + mPackages = packages; + } + + @Override + public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { + IconCache iconCache = app.getIconCache(); + + final ArrayList updatedApps = new ArrayList<>(); + + ArrayList 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 index 000000000..ccc600768 --- /dev/null +++ b/src/com/android/launcher3/model/ExtendedModelTask.java @@ -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 updatedShortcuts, UserHandleCompat user) { + bindUpdatedShortcuts(updatedShortcuts, new ArrayList(), user); + } + + public void bindUpdatedShortcuts( + final ArrayList updatedShortcuts, + final ArrayList 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 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 index 000000000..5d04325e8 --- /dev/null +++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java @@ -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 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 index 000000000..7286bf51f --- /dev/null +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -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 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 added = null; + ArrayList modified = null; + final ArrayList removedApps = new ArrayList(); + + 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 addedOrUpdatedApps = new HashMap<>(); + + if (added != null) { + final ArrayList 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 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 updatedShortcuts = new ArrayList<>(); + final ArrayList removedShortcuts = new ArrayList<>(); + final ArrayList 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 removedPackages = new HashSet<>(); + final HashSet 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 mShortcuts; + private final UserHandleCompat mUser; + private final boolean mUpdateIdMap; + + public ShortcutsChangedTask(String packageName, List 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 removedShortcutInfos = new ArrayList<>(); + MultiHashMap 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 updatedShortcutInfos = new ArrayList<>(); + if (!idsToWorkspaceShortcutInfos.isEmpty()) { + // Update the workspace to reflect the changes to updated shortcuts residing on it. + List shortcuts = deepShortcutManager.queryForFullDetails( + mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser); + for (ShortcutInfoCompat fullDetails : shortcuts) { + List 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 index 000000000..b7b52a448 --- /dev/null +++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java @@ -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 pinnedShortcuts = new HashMap<>(); + if (isUserUnlocked) { + List 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 updatedShortcutInfos = new ArrayList<>(); + ArrayList 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 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); + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java index 6661429c1..78b7a3eee 100644 --- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -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 itemList = new ArrayList(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. */ diff --git a/tests/Android.mk b/tests/Android.mk index 61ee220e8..5103ced7c 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -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 index 000000000..9095476f6 --- /dev/null +++ b/tests/res/raw/cache_data_updated_task_data.txt @@ -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 index 000000000..84f9c161e --- /dev/null +++ b/tests/res/raw/package_install_state_change_task_data.txt @@ -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 index 000000000..ecb3782fc --- /dev/null +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java @@ -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 existingScreens; + private ArrayList newScreens; + private LongArrayMap 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 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 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 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 oldScreens = new ArrayList<>(existingScreens); + Pair 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 notAnimated = ArgumentCaptor.forClass(ArrayList.class); + ArgumentCaptor 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 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 index 000000000..5628e8291 --- /dev/null +++ b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java @@ -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 { + + public final HashMap> 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 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 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 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 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 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 index 000000000..25b8df933 --- /dev/null +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -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 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 index 000000000..d6555620c --- /dev/null +++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java @@ -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 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); + } + } + } +} -- cgit v1.2.3