Moving various runnables in LauncherModel to individual tasks

> Adding tests for some of the runnable

Change-Id: I1a315d38878857df3371f0e69d622a41fc3b081a
This commit is contained in:
Sunny Goyal 2016-09-09 15:47:55 -07:00
parent 0d547bfd57
commit f0ba8b7ca1
22 changed files with 1812 additions and 909 deletions

View File

@ -45,6 +45,7 @@ android {
androidTest {
java.srcDirs = ['tests/src']
res.srcDirs = ['tests/res']
manifest.srcFile "tests/AndroidManifest.xml"
}
@ -65,6 +66,9 @@ dependencies {
compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-2'
testCompile 'junit:junit:4.12'
androidTestCompile "org.mockito:mockito-core:1.+"
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
androidTestCompile 'com.android.support:support-annotations:23.2.0'

View File

@ -155,10 +155,9 @@ public class AllAppsList {
// to the removed list.
for (int i = data.size() - 1; i >= 0; i--) {
final AppInfo applicationInfo = data.get(i);
final ComponentName component = applicationInfo.intent.getComponent();
if (user.equals(applicationInfo.user)
&& packageName.equals(component.getPackageName())) {
if (!findActivity(matches, component)) {
&& packageName.equals(applicationInfo.componentName.getPackageName())) {
if (!findActivity(matches, applicationInfo.componentName)) {
removed.add(applicationInfo);
data.remove(i);
}
@ -182,11 +181,10 @@ public class AllAppsList {
// Remove all data for this package.
for (int i = data.size() - 1; i >= 0; i--) {
final AppInfo applicationInfo = data.get(i);
final ComponentName component = applicationInfo.intent.getComponent();
if (user.equals(applicationInfo.user)
&& packageName.equals(component.getPackageName())) {
&& packageName.equals(applicationInfo.componentName.getPackageName())) {
removed.add(applicationInfo);
mIconCache.remove(component, user);
mIconCache.remove(applicationInfo.componentName, user);
data.remove(i);
}
}
@ -238,9 +236,8 @@ public class AllAppsList {
private AppInfo findApplicationInfoLocked(String packageName, UserHandleCompat user,
String className) {
for (AppInfo info: data) {
final ComponentName component = info.intent.getComponent();
if (user.equals(info.user) && packageName.equals(component.getPackageName())
&& className.equals(component.getClassName())) {
if (user.equals(info.user) && packageName.equals(info.componentName.getPackageName())
&& className.equals(info.componentName.getClassName())) {
return info;
}
}

View File

@ -79,7 +79,7 @@ public class IconCache {
@Thunk static final Object ICON_UPDATE_TOKEN = new Object();
@Thunk static class CacheEntry {
public static class CacheEntry {
public Bitmap icon;
public CharSequence title = "";
public CharSequence contentDescription = "";
@ -544,7 +544,7 @@ public class IconCache {
* Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
* This method is not thread safe, it must be called from a synchronized method.
*/
private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
protected CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
ComponentKey cacheKey = new ComponentKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);

View File

@ -241,7 +241,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver {
// Add the new apps to the model and bind them
if (!addShortcuts.isEmpty()) {
LauncherAppState app = LauncherAppState.getInstance();
app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts);
app.getModel().addAndBindAddedWorkspaceItems(addShortcuts);
}
}
}

View File

@ -84,12 +84,12 @@ public class LauncherAppWidgetInfo extends ItemInfo {
/**
* Indicates the restore status of the widget.
*/
int restoreStatus;
public int restoreStatus;
/**
* Indicates the installation progress of the widget provider
*/
int installProgress = -1;
public int installProgress = -1;
/**
* Optional extras sent during widget bind. See {@link #FLAG_DIRECT_CONFIG}.
@ -98,7 +98,7 @@ public class LauncherAppWidgetInfo extends ItemInfo {
private boolean mHasNotifiedInitialWidgetSizeChanged;
LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
public LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) {
if (appWidgetId == CUSTOM_WIDGET_ID) {
itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
} else {
@ -117,6 +117,11 @@ public class LauncherAppWidgetInfo extends ItemInfo {
restoreStatus = RESTORE_COMPLETED;
}
/** Used for testing **/
public LauncherAppWidgetInfo() {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
}
public boolean isCustomWidget() {
return appWidgetId == CUSTOM_WIDGET_ID;
}

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@ public class ShortcutInfo extends ItemInfo {
* Indicates whether we're using the default fallback icon instead of something from the
* app.
*/
boolean usingFallbackIcon;
public boolean usingFallbackIcon;
/**
* Indicates whether we're using a low res icon
@ -132,7 +132,7 @@ public class ShortcutInfo extends ItemInfo {
* Could be disabled, if the the app is installed but unavailable (eg. in safe mode or when
* sd-card is not available).
*/
int isDisabled = DEFAULT;
public int isDisabled = DEFAULT;
/**
* A message to display when the user tries to start a disabled shortcut.
@ -140,7 +140,7 @@ public class ShortcutInfo extends ItemInfo {
*/
CharSequence disabledMessage;
int status;
public int status;
/**
* The installation progress [0-100] of the package that this shortcut represents.
@ -152,7 +152,7 @@ public class ShortcutInfo extends ItemInfo {
* this will hold the original intent from the database. Otherwise, null.
* Refer {@link #FLAG_RESTORED_ICON}, {@link #FLAG_AUTOINTALL_ICON}
*/
Intent promisedIntent;
public Intent promisedIntent;
public ShortcutInfo() {
itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT;

View File

@ -0,0 +1,262 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.Context;
import android.content.Intent;
import android.util.LongSparseArray;
import android.util.Pair;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.AppInfo;
import com.android.launcher3.FolderInfo;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.util.GridOccupancy;
import java.util.ArrayList;
/**
* Task to add auto-created workspace items.
*/
public class AddWorkspaceItemsTask extends ExtendedModelTask {
private final ArrayList<? extends ItemInfo> mWorkspaceApps;
/**
* @param workspaceApps items to add on the workspace
*/
public AddWorkspaceItemsTask(ArrayList<? extends ItemInfo> workspaceApps) {
mWorkspaceApps = workspaceApps;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
if (mWorkspaceApps.isEmpty()) {
return;
}
Context context = app.getContext();
final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
// Get the list of workspace screens. We need to append to this list and
// can not use sBgWorkspaceScreens because loadWorkspace() may not have been
// called.
ArrayList<Long> workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context);
synchronized(dataModel) {
for (ItemInfo item : mWorkspaceApps) {
if (item instanceof ShortcutInfo) {
// Short-circuit this logic if the icon exists somewhere on the workspace
if (shortcutExists(dataModel, item.getIntent(), item.user)) {
continue;
}
}
// Find appropriate space for the item.
Pair<Long, int[]> coords = findSpaceForItem(
app, dataModel, workspaceScreens, addedWorkspaceScreensFinal, 1, 1);
long screenId = coords.first;
int[] cordinates = coords.second;
ItemInfo itemInfo;
if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
itemInfo = item;
} else if (item instanceof AppInfo) {
itemInfo = ((AppInfo) item).makeShortcut();
} else {
throw new RuntimeException("Unexpected info type");
}
// Add the shortcut to the db
addItemToDatabase(context, itemInfo, screenId, cordinates);
// Save the ShortcutInfo for binding in the workspace
addedShortcutsFinal.add(itemInfo);
}
}
// Update the workspace screens
updateScreens(context, workspaceScreens);
if (!addedShortcutsFinal.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
final ArrayList<ItemInfo> addAnimated = new ArrayList<ItemInfo>();
final ArrayList<ItemInfo> addNotAnimated = new ArrayList<ItemInfo>();
if (!addedShortcutsFinal.isEmpty()) {
ItemInfo info = addedShortcutsFinal.get(addedShortcutsFinal.size() - 1);
long lastScreenId = info.screenId;
for (ItemInfo i : addedShortcutsFinal) {
if (i.screenId == lastScreenId) {
addAnimated.add(i);
} else {
addNotAnimated.add(i);
}
}
}
callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
addNotAnimated, addAnimated, null);
}
});
}
}
protected void addItemToDatabase(Context context, ItemInfo item, long screenId, int[] pos) {
LauncherModel.addItemToDatabase(context, item,
LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, pos[0], pos[1]);
}
protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) {
LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens);
}
/**
* Returns true if the shortcuts already exists on the workspace. This must be called after
* the workspace has been loaded. We identify a shortcut by its intent.
*/
protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandleCompat user) {
final String intentWithPkg, intentWithoutPkg;
if (intent.getComponent() != null) {
// If component is not null, an intent with null package will produce
// the same result and should also be a match.
String packageName = intent.getComponent().getPackageName();
if (intent.getPackage() != null) {
intentWithPkg = intent.toUri(0);
intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
} else {
intentWithPkg = new Intent(intent).setPackage(packageName).toUri(0);
intentWithoutPkg = intent.toUri(0);
}
} else {
intentWithPkg = intent.toUri(0);
intentWithoutPkg = intent.toUri(0);
}
synchronized (dataModel) {
for (ItemInfo item : dataModel.itemsIdMap) {
if (item instanceof ShortcutInfo) {
ShortcutInfo info = (ShortcutInfo) item;
Intent targetIntent = info.promisedIntent == null
? info.intent : info.promisedIntent;
if (targetIntent != null && info.user.equals(user)) {
Intent copyIntent = new Intent(targetIntent);
copyIntent.setSourceBounds(intent.getSourceBounds());
String s = copyIntent.toUri(0);
if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
return true;
}
}
}
}
}
return false;
}
/**
* Find a position on the screen for the given size or adds a new screen.
* @return screenId and the coordinates for the item.
*/
protected Pair<Long, int[]> findSpaceForItem(
LauncherAppState app, BgDataModel dataModel,
ArrayList<Long> workspaceScreens,
ArrayList<Long> addedWorkspaceScreensFinal,
int spanX, int spanY) {
LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
// Use sBgItemsIdMap as all the items are already loaded.
synchronized (dataModel) {
for (ItemInfo info : dataModel.itemsIdMap) {
if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
ArrayList<ItemInfo> items = screenItems.get(info.screenId);
if (items == null) {
items = new ArrayList<>();
screenItems.put(info.screenId, items);
}
items.add(info);
}
}
}
// Find appropriate space for the item.
long screenId = 0;
int[] cordinates = new int[2];
boolean found = false;
int screenCount = workspaceScreens.size();
// First check the preferred screen.
int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
if (preferredScreenIndex < screenCount) {
screenId = workspaceScreens.get(preferredScreenIndex);
found = findNextAvailableIconSpaceInScreen(
app, screenItems.get(screenId), cordinates, spanX, spanY);
}
if (!found) {
// Search on any of the screens starting from the first screen.
for (int screen = 1; screen < screenCount; screen++) {
screenId = workspaceScreens.get(screen);
if (findNextAvailableIconSpaceInScreen(
app, screenItems.get(screenId), cordinates, spanX, spanY)) {
// We found a space for it
found = true;
break;
}
}
}
if (!found) {
// Still no position found. Add a new screen to the end.
screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
.getLong(LauncherSettings.Settings.EXTRA_VALUE);
// Save the screen id for binding in the workspace
workspaceScreens.add(screenId);
addedWorkspaceScreensFinal.add(screenId);
// If we still can't find an empty space, then God help us all!!!
if (!findNextAvailableIconSpaceInScreen(
app, screenItems.get(screenId), cordinates, spanX, spanY)) {
throw new RuntimeException("Can't find space to add the item");
}
}
return Pair.create(screenId, cordinates);
}
private boolean findNextAvailableIconSpaceInScreen(
LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
int[] xy, int spanX, int spanY) {
InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
if (occupiedPos != null) {
for (ItemInfo r : occupiedPos) {
occupied.markCells(r, true);
}
}
return occupied.findVacantCell(xy, spanX, spanY);
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.ComponentName;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.AppInfo;
import com.android.launcher3.IconCache;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.UserHandleCompat;
import java.util.ArrayList;
import java.util.HashSet;
/**
* Handles changes due to cache updates.
*/
public class CacheDataUpdatedTask extends ExtendedModelTask {
public static final int OP_CACHE_UPDATE = 1;
public static final int OP_SESSION_UPDATE = 2;
private final int mOp;
private final UserHandleCompat mUser;
private final HashSet<String> mPackages;
public CacheDataUpdatedTask(int op, UserHandleCompat user, HashSet<String> packages) {
mOp = op;
mUser = user;
mPackages = packages;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
IconCache iconCache = app.getIconCache();
final ArrayList<AppInfo> updatedApps = new ArrayList<>();
ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
synchronized (dataModel) {
for (ItemInfo info : dataModel.itemsIdMap) {
if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
ShortcutInfo si = (ShortcutInfo) info;
ComponentName cn = si.getTargetComponent();
if (isValidShortcut(si) &&
cn != null && mPackages.contains(cn.getPackageName())) {
si.updateIcon(iconCache);
updatedShortcuts.add(si);
}
}
}
apps.updateIconsAndLabels(mPackages, mUser, updatedApps);
}
bindUpdatedShortcuts(updatedShortcuts, mUser);
if (!updatedApps.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindAppsUpdated(updatedApps);
}
});
}
}
public boolean isValidShortcut(ShortcutInfo si) {
switch (mOp) {
case OP_CACHE_UPDATE:
return si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
case OP_SESSION_UPDATE:
return si.isPromise();
default:
return false;
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.LauncherModel.BaseModelUpdateTask;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.MultiHashMap;
import java.util.ArrayList;
/**
* Extension of {@link BaseModelUpdateTask} with some utility methods
*/
public abstract class ExtendedModelTask extends BaseModelUpdateTask {
public void bindUpdatedShortcuts(
ArrayList<ShortcutInfo> updatedShortcuts, UserHandleCompat user) {
bindUpdatedShortcuts(updatedShortcuts, new ArrayList<ShortcutInfo>(), user);
}
public void bindUpdatedShortcuts(
final ArrayList<ShortcutInfo> updatedShortcuts,
final ArrayList<ShortcutInfo> removedShortcuts,
final UserHandleCompat user) {
if (!updatedShortcuts.isEmpty() || !removedShortcuts.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindShortcutsChanged(updatedShortcuts, removedShortcuts, user);
}
});
}
}
public void bindDeepShortcuts(BgDataModel dataModel) {
final MultiHashMap<ComponentKey, String> shortcutMapCopy = dataModel.deepShortcutMap.clone();
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindDeepShortcutMap(shortcutMapCopy);
}
});
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.ComponentName;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.PackageInstallerCompat;
import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
import java.util.HashSet;
/**
* Handles changes due to a sessions updates for a currently installing app.
*/
public class PackageInstallStateChangedTask extends ExtendedModelTask {
private final PackageInstallInfo mInstallInfo;
public PackageInstallStateChangedTask(PackageInstallInfo installInfo) {
mInstallInfo = installInfo;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
if (mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLED) {
// Ignore install success events as they are handled by Package add events.
return;
}
synchronized (dataModel) {
final HashSet<ItemInfo> updates = new HashSet<>();
for (ItemInfo info : dataModel.itemsIdMap) {
if (info instanceof ShortcutInfo) {
ShortcutInfo si = (ShortcutInfo) info;
ComponentName cn = si.getTargetComponent();
if (si.isPromise() && (cn != null)
&& mInstallInfo.packageName.equals(cn.getPackageName())) {
si.setInstallProgress(mInstallInfo.progress);
if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
// Mark this info as broken.
si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE;
}
updates.add(si);
}
}
}
for (LauncherAppWidgetInfo widget : dataModel.appWidgets) {
if (widget.providerName.getPackageName().equals(mInstallInfo.packageName)) {
widget.installProgress = mInstallInfo.progress;
updates.add(widget);
}
}
if (!updates.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindRestoreItemsChange(updates);
}
});
}
}
}
}

View File

@ -0,0 +1,378 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.util.Log;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.AppInfo;
import com.android.launcher3.IconCache;
import com.android.launcher3.InstallShortcutReceiver;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import com.android.launcher3.graphics.LauncherIcons;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.ManagedProfileHeuristic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
/**
* Handles updates due to changes in package manager (app installed/updated/removed)
* or when a user availability changes.
*/
public class PackageUpdatedTask extends ExtendedModelTask {
private static final boolean DEBUG = false;
private static final String TAG = "PackageUpdatedTask";
public static final int OP_NONE = 0;
public static final int OP_ADD = 1;
public static final int OP_UPDATE = 2;
public static final int OP_REMOVE = 3; // uninstalled
public static final int OP_UNAVAILABLE = 4; // external media unmounted
public static final int OP_SUSPEND = 5; // package suspended
public static final int OP_UNSUSPEND = 6; // package unsuspended
public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable
private final int mOp;
private final UserHandleCompat mUser;
private final String[] mPackages;
public PackageUpdatedTask(int op, UserHandleCompat user, String... packages) {
mOp = op;
mUser = user;
mPackages = packages;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
final Context context = app.getContext();
final IconCache iconCache = app.getIconCache();
final String[] packages = mPackages;
final int N = packages.length;
FlagOp flagOp = FlagOp.NO_OP;
final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
switch (mOp) {
case OP_ADD: {
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
iconCache.updateIconsForPkg(packages[i], mUser);
appsList.addPackage(context, packages[i], mUser);
}
ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
if (heuristic != null) {
heuristic.processPackageAdd(mPackages);
}
break;
}
case OP_UPDATE:
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
iconCache.updateIconsForPkg(packages[i], mUser);
appsList.updatePackage(context, packages[i], mUser);
app.getWidgetCache().removePackage(packages[i], mUser);
}
// Since package was just updated, the target must be available now.
flagOp = FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
case OP_REMOVE: {
ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
if (heuristic != null) {
heuristic.processPackageRemoved(mPackages);
}
for (int i = 0; i < N; i++) {
iconCache.removeIconsForPkg(packages[i], mUser);
}
// Fall through
}
case OP_UNAVAILABLE:
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
appsList.removePackage(packages[i], mUser);
app.getWidgetCache().removePackage(packages[i], mUser);
}
flagOp = FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
case OP_SUSPEND:
case OP_UNSUSPEND:
flagOp = mOp == OP_SUSPEND ?
FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED) :
FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_SUSPENDED);
if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N);
appsList.updateDisabledFlags(
ItemInfoMatcher.ofPackages(packageSet, mUser), flagOp);
break;
case OP_USER_AVAILABILITY_CHANGE:
flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser)
? FlagOp.addFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER)
: FlagOp.removeFlag(ShortcutInfo.FLAG_DISABLED_QUIET_USER);
// We want to update all packages for this user.
appsList.updateDisabledFlags(ItemInfoMatcher.ofUser(mUser), flagOp);
break;
}
ArrayList<AppInfo> added = null;
ArrayList<AppInfo> modified = null;
final ArrayList<AppInfo> removedApps = new ArrayList<AppInfo>();
if (appsList.added.size() > 0) {
added = new ArrayList<>(appsList.added);
appsList.added.clear();
}
if (appsList.modified.size() > 0) {
modified = new ArrayList<>(appsList.modified);
appsList.modified.clear();
}
if (appsList.removed.size() > 0) {
removedApps.addAll(appsList.removed);
appsList.removed.clear();
}
final HashMap<ComponentName, AppInfo> addedOrUpdatedApps = new HashMap<>();
if (added != null) {
final ArrayList<AppInfo> addedApps = added;
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindAppsAdded(null, null, null, addedApps);
}
});
for (AppInfo ai : added) {
addedOrUpdatedApps.put(ai.componentName, ai);
}
}
if (modified != null) {
final ArrayList<AppInfo> modifiedFinal = modified;
for (AppInfo ai : modified) {
addedOrUpdatedApps.put(ai.componentName, ai);
}
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindAppsUpdated(modifiedFinal);
}
});
}
// Update shortcut infos
if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
final ArrayList<ShortcutInfo> updatedShortcuts = new ArrayList<>();
final ArrayList<ShortcutInfo> removedShortcuts = new ArrayList<>();
final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>();
synchronized (dataModel) {
for (ItemInfo info : dataModel.itemsIdMap) {
if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
ShortcutInfo si = (ShortcutInfo) info;
boolean infoUpdated = false;
boolean shortcutUpdated = false;
// Update shortcuts which use iconResource.
if ((si.iconResource != null)
&& packageSet.contains(si.iconResource.packageName)) {
Bitmap icon = LauncherIcons.createIconBitmap(
si.iconResource.packageName,
si.iconResource.resourceName, context);
if (icon != null) {
si.setIcon(icon);
si.usingFallbackIcon = false;
infoUpdated = true;
}
}
ComponentName cn = si.getTargetComponent();
if (cn != null && packageSet.contains(cn.getPackageName())) {
AppInfo appInfo = addedOrUpdatedApps.get(cn);
if (si.isPromise()) {
if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
// Auto install icon
PackageManager pm = context.getPackageManager();
ResolveInfo matched = pm.resolveActivity(
new Intent(Intent.ACTION_MAIN)
.setComponent(cn).addCategory(Intent.CATEGORY_LAUNCHER),
PackageManager.MATCH_DEFAULT_ONLY);
if (matched == null) {
// Try to find the best match activity.
Intent intent = pm.getLaunchIntentForPackage(
cn.getPackageName());
if (intent != null) {
cn = intent.getComponent();
appInfo = addedOrUpdatedApps.get(cn);
}
if ((intent == null) || (appInfo == null)) {
removedShortcuts.add(si);
continue;
}
si.promisedIntent = intent;
}
}
si.intent = si.promisedIntent;
si.promisedIntent = null;
si.status = ShortcutInfo.DEFAULT;
infoUpdated = true;
si.updateIcon(iconCache);
}
if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction())
&& si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
si.updateIcon(iconCache);
si.title = Utilities.trim(appInfo.title);
si.contentDescription = appInfo.contentDescription;
infoUpdated = true;
}
int oldDisabledFlags = si.isDisabled;
si.isDisabled = flagOp.apply(si.isDisabled);
if (si.isDisabled != oldDisabledFlags) {
shortcutUpdated = true;
}
}
if (infoUpdated || shortcutUpdated) {
updatedShortcuts.add(si);
}
if (infoUpdated) {
LauncherModel.updateItemInDatabase(context, si);
}
} else if (info instanceof LauncherAppWidgetInfo && mOp == OP_ADD) {
LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
if (mUser.equals(widgetInfo.user)
&& widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
&& packageSet.contains(widgetInfo.providerName.getPackageName())) {
widgetInfo.restoreStatus &=
~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
// adding this flag ensures that launcher shows 'click to setup'
// if the widget has a config activity. In case there is no config
// activity, it will be marked as 'restored' during bind.
widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
widgets.add(widgetInfo);
LauncherModel.updateItemInDatabase(context, widgetInfo);
}
}
}
}
bindUpdatedShortcuts(updatedShortcuts, removedShortcuts, mUser);
if (!removedShortcuts.isEmpty()) {
LauncherModel.deleteItemsFromDatabase(context, removedShortcuts);
}
if (!widgets.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindWidgetsRestored(widgets);
}
});
}
}
final HashSet<String> removedPackages = new HashSet<>();
final HashSet<ComponentName> removedComponents = new HashSet<>();
if (mOp == OP_REMOVE) {
// Mark all packages in the broadcast to be removed
Collections.addAll(removedPackages, packages);
// No need to update the removedComponents as
// removedPackages is a super-set of removedComponents
} else if (mOp == OP_UPDATE) {
// Mark disabled packages in the broadcast to be removed
final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
for (int i=0; i<N; i++) {
if (!launcherApps.isPackageEnabledForProfile(packages[i], mUser)) {
removedPackages.add(packages[i]);
}
}
// Update removedComponents as some components can get removed during package update
for (AppInfo info : removedApps) {
removedComponents.add(info.componentName);
}
}
if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
LauncherModel.deleteItemsFromDatabase(
context, ItemInfoMatcher.ofPackages(removedPackages, mUser));
LauncherModel.deleteItemsFromDatabase(
context, ItemInfoMatcher.ofComponents(removedComponents, mUser));
// Remove any queued items from the install queue
InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
// Call the components-removed callback
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindWorkspaceComponentsRemoved(
removedPackages, removedComponents, mUser);
}
});
}
if (!removedApps.isEmpty()) {
// Remove corresponding apps from All-Apps
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.bindAppInfosRemoved(removedApps);
}
});
}
// Notify launcher of widget update. From marshmallow onwards we use AppWidgetHost to
// get widget update signals.
if (!Utilities.ATLEAST_MARSHMALLOW &&
(mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE)) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
callbacks.notifyWidgetProvidersChanged();
}
});
}
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.Context;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutInfoCompat;
import com.android.launcher3.util.MultiHashMap;
import java.util.ArrayList;
import java.util.List;
/**
* Handles changes due to shortcut manager updates (deep shortcut changes)
*/
public class ShortcutsChangedTask extends ExtendedModelTask {
private final String mPackageName;
private final List<ShortcutInfoCompat> mShortcuts;
private final UserHandleCompat mUser;
private final boolean mUpdateIdMap;
public ShortcutsChangedTask(String packageName, List<ShortcutInfoCompat> shortcuts,
UserHandleCompat user, boolean updateIdMap) {
mPackageName = packageName;
mShortcuts = shortcuts;
mUser = user;
mUpdateIdMap = updateIdMap;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
DeepShortcutManager deepShortcutManager = app.getShortcutManager();
deepShortcutManager.onShortcutsChanged(mShortcuts);
// Find ShortcutInfo's that have changed on the workspace.
final ArrayList<ShortcutInfo> removedShortcutInfos = new ArrayList<>();
MultiHashMap<String, ShortcutInfo> idsToWorkspaceShortcutInfos = new MultiHashMap<>();
for (ItemInfo itemInfo : dataModel.itemsIdMap) {
if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
ShortcutInfo si = (ShortcutInfo) itemInfo;
if (si.getPromisedIntent().getPackage().equals(mPackageName)
&& si.user.equals(mUser)) {
idsToWorkspaceShortcutInfos.addToList(si.getDeepShortcutId(), si);
}
}
}
final Context context = LauncherAppState.getInstance().getContext();
final ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
if (!idsToWorkspaceShortcutInfos.isEmpty()) {
// Update the workspace to reflect the changes to updated shortcuts residing on it.
List<ShortcutInfoCompat> shortcuts = deepShortcutManager.queryForFullDetails(
mPackageName, new ArrayList<>(idsToWorkspaceShortcutInfos.keySet()), mUser);
for (ShortcutInfoCompat fullDetails : shortcuts) {
List<ShortcutInfo> shortcutInfos = idsToWorkspaceShortcutInfos
.remove(fullDetails.getId());
if (!fullDetails.isPinned()) {
// The shortcut was previously pinned but is no longer, so remove it from
// the workspace and our pinned shortcut counts.
// Note that we put this check here, after querying for full details,
// because there's a possible race condition between pinning and
// receiving this callback.
removedShortcutInfos.addAll(shortcutInfos);
continue;
}
for (ShortcutInfo shortcutInfo : shortcutInfos) {
shortcutInfo.updateFromDeepShortcutInfo(fullDetails, context);
updatedShortcutInfos.add(shortcutInfo);
}
}
}
// If there are still entries in idsToWorkspaceShortcutInfos, that means that
// the corresponding shortcuts weren't passed in onShortcutsChanged(). This
// means they were cleared, so we remove and unpin them now.
for (String id : idsToWorkspaceShortcutInfos.keySet()) {
removedShortcutInfos.addAll(idsToWorkspaceShortcutInfos.get(id));
}
bindUpdatedShortcuts(updatedShortcutInfos, removedShortcutInfos, mUser);
if (!removedShortcutInfos.isEmpty()) {
LauncherModel.deleteItemsFromDatabase(context, removedShortcutInfos);
}
if (mUpdateIdMap) {
// Update the deep shortcut map if the list of ids has changed for an activity.
dataModel.updateDeepShortcutMap(mPackageName, mUser, mShortcuts);
bindDeepShortcuts(dataModel);
}
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model;
import android.content.Context;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutInfoCompat;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.ComponentKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* Task to handle changing of lock state of the user
*/
public class UserLockStateChangedTask extends ExtendedModelTask {
private final UserHandleCompat mUser;
public UserLockStateChangedTask(UserHandleCompat user) {
mUser = user;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
Context context = app.getContext();
boolean isUserUnlocked = UserManagerCompat.getInstance(context).isUserUnlocked(mUser);
DeepShortcutManager deepShortcutManager = app.getShortcutManager();
HashMap<ShortcutKey, ShortcutInfoCompat> pinnedShortcuts = new HashMap<>();
if (isUserUnlocked) {
List<ShortcutInfoCompat> shortcuts =
deepShortcutManager.queryForPinnedShortcuts(null, mUser);
if (deepShortcutManager.wasLastCallSuccess()) {
for (ShortcutInfoCompat shortcut : shortcuts) {
pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
}
} else {
// Shortcut manager can fail due to some race condition when the lock state
// changes too frequently. For the purpose of the update,
// consider it as still locked.
isUserUnlocked = false;
}
}
// Update the workspace to reflect the changes to updated shortcuts residing on it.
ArrayList<ShortcutInfo> updatedShortcutInfos = new ArrayList<>();
ArrayList<ShortcutInfo> deletedShortcutInfos = new ArrayList<>();
for (ItemInfo itemInfo : dataModel.itemsIdMap) {
if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& mUser.equals(itemInfo.user)) {
ShortcutInfo si = (ShortcutInfo) itemInfo;
if (isUserUnlocked) {
ShortcutInfoCompat shortcut =
pinnedShortcuts.get(ShortcutKey.fromShortcutInfo(si));
// We couldn't verify the shortcut during loader. If its no longer available
// (probably due to clear data), delete the workspace item as well
if (shortcut == null) {
deletedShortcutInfos.add(si);
continue;
}
si.isDisabled &= ~ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
si.updateFromDeepShortcutInfo(shortcut, context);
} else {
si.isDisabled |= ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
}
updatedShortcutInfos.add(si);
}
}
bindUpdatedShortcuts(updatedShortcutInfos, deletedShortcutInfos, mUser);
if (!deletedShortcutInfos.isEmpty()) {
LauncherModel.deleteItemsFromDatabase(context, deletedShortcutInfos);
}
// Remove shortcut id map for that user
Iterator<ComponentKey> keysIter = dataModel.deepShortcutMap.keySet().iterator();
while (keysIter.hasNext()) {
if (keysIter.next().user.equals(mUser)) {
keysIter.remove();
}
}
if (isUserUnlocked) {
dataModel.updateDeepShortcutMap(
null, mUser, deepShortcutManager.queryForAllShortcuts(mUser));
}
bindDeepShortcuts(dataModel);
}
}

View File

@ -121,7 +121,7 @@ public class ManagedProfileHeuristic {
// getting filled with the managed user apps, when it start with a fresh DB (or after
// a very long time).
if (userAppsExisted && !homescreenApps.isEmpty()) {
mModel.addAndBindAddedWorkspaceItems(mContext, homescreenApps);
mModel.addAndBindAddedWorkspaceItems(homescreenApps);
}
}
@ -175,7 +175,7 @@ public class ManagedProfileHeuristic {
// Add the item to home screen and DB. This also generates an item id synchronously.
ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1);
itemList.add(workFolder);
mModel.addAndBindAddedWorkspaceItems(mContext, itemList);
mModel.addAndBindAddedWorkspaceItems(itemList);
mPrefs.edit().putLong(folderIdKey, workFolder.id).apply();
saveWorkFolderShortcuts(workFolder.id, 0, workFolderApps);
@ -200,7 +200,6 @@ public class ManagedProfileHeuristic {
}
}
/**
* Verifies that entries corresponding to {@param users} exist and removes all invalid entries.
*/

View File

@ -17,11 +17,12 @@ LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator mockito-target-minus-junit4
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_SDK_VERSION := current
LOCAL_MIN_SDK_VERSION := 21
LOCAL_PACKAGE_NAME := Launcher3Tests

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,190 @@
package com.android.launcher3.model;
import android.content.ComponentName;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.util.Pair;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.config.ProviderConfig;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.LongArrayMap;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.Arrays;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link AddWorkspaceItemsTask}
*/
public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
private final ComponentName mComponent1 = new ComponentName("a", "b");
private final ComponentName mComponent2 = new ComponentName("b", "b");
private ArrayList<Long> existingScreens;
private ArrayList<Long> newScreens;
private LongArrayMap<GridOccupancy> screenOccupancy;
@Override
protected void setUp() throws Exception {
super.setUp();
existingScreens = new ArrayList<>();
screenOccupancy = new LongArrayMap<>();
newScreens = new ArrayList<>();
idp.numColumns = 5;
idp.numRows = 5;
}
private <T extends ItemInfo> AddWorkspaceItemsTask newTask(T... items) {
return new AddWorkspaceItemsTask(new ArrayList<>(Arrays.asList(items))) {
@Override
protected void addItemToDatabase(Context context, ItemInfo item,
long screenId, int[] pos) {
item.screenId = screenId;
item.cellX = pos[0];
item.cellY = pos[1];
}
@Override
protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) { }
};
}
public void testFindSpaceForItem_prefers_second() {
// First screen has only one hole of size 1
int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
// Second screen has 2 holes of sizes 3x2 and 2x3
setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
Pair<Long, int[]> spaceFound = newTask()
.findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
assertEquals(2L, (long) spaceFound.first);
assertTrue(screenOccupancy.get(spaceFound.first)
.isRegionVacant(spaceFound.second[0], spaceFound.second[1], 1, 1));
// Find a larger space
spaceFound = newTask()
.findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
assertEquals(2L, (long) spaceFound.first);
assertTrue(screenOccupancy.get(spaceFound.first)
.isRegionVacant(spaceFound.second[0], spaceFound.second[1], 2, 3));
}
public void testFindSpaceForItem_adds_new_screen() throws Exception {
// First screen has 2 holes of sizes 3x2 and 2x3
setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
commitScreensToDb();
when(appState.getContext()).thenReturn(getMockContext());
ArrayList<Long> oldScreens = new ArrayList<>(existingScreens);
Pair<Long, int[]> spaceFound = newTask()
.findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
assertFalse(oldScreens.contains(spaceFound.first));
assertTrue(newScreens.contains(spaceFound.first));
}
public void testAddItem_existing_item_ignored() throws Exception {
ShortcutInfo info = new ShortcutInfo();
info.intent = new Intent().setComponent(mComponent1);
// Setup a screen with a hole
setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
commitScreensToDb();
when(appState.getContext()).thenReturn(getMockContext());
// Nothing was added
assertTrue(executeTaskForTest(newTask(info)).isEmpty());
}
public void testAddItem_some_items_added() throws Exception {
ShortcutInfo info = new ShortcutInfo();
info.intent = new Intent().setComponent(mComponent1);
ShortcutInfo info2 = new ShortcutInfo();
info2.intent = new Intent().setComponent(mComponent2);
// Setup a screen with a hole
setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
commitScreensToDb();
when(appState.getContext()).thenReturn(getMockContext());
executeTaskForTest(newTask(info, info2)).get(0).run();
ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
// only info2 should be added because info was already added to the workspace
// in setupWorkspaceWithHoles()
verify(callbacks).bindAppsAdded(any(ArrayList.class), notAnimated.capture(),
animated.capture(), any(ArrayList.class));
assertTrue(notAnimated.getValue().isEmpty());
assertEquals(1, animated.getValue().size());
assertTrue(animated.getValue().contains(info2));
}
private int setupWorkspaceWithHoles(int startId, long screenId, Rect... holes) {
GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
for (Rect r : holes) {
occupancy.markCells(r, false);
}
existingScreens.add(screenId);
screenOccupancy.append(screenId, occupancy);
for (int x = 0; x < idp.numColumns; x++) {
for (int y = 0; y < idp.numRows; y++) {
if (!occupancy.cells[x][y]) {
continue;
}
ShortcutInfo info = new ShortcutInfo();
info.intent = new Intent().setComponent(mComponent1);
info.id = startId++;
info.screenId = screenId;
info.cellX = x;
info.cellY = y;
info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
bgDataModel.addItem(info, false);
}
}
return startId;
}
private void commitScreensToDb() throws Exception {
LauncherSettings.Settings.call(getMockContentResolver(),
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// Clear the table
ops.add(ContentProviderOperation.newDelete(uri).build());
int count = existingScreens.size();
for (int i = 0; i < count; i++) {
ContentValues v = new ContentValues();
long screenId = existingScreens.get(i);
v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
}
getMockContentResolver().applyBatch(ProviderConfig.AUTHORITY, ops);
}
}

View File

@ -0,0 +1,208 @@
package com.android.launcher3.model;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.support.test.InstrumentationRegistry;
import android.test.ProviderTestCase2;
import com.android.launcher3.AllAppsList;
import com.android.launcher3.AppInfo;
import com.android.launcher3.DeferredHandler;
import com.android.launcher3.IconCache;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.Callbacks;
import com.android.launcher3.LauncherModel.BaseModelUpdateTask;
import com.android.launcher3.compat.LauncherActivityInfoCompat;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.config.ProviderConfig;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.TestLauncherProvider;
import org.mockito.ArgumentCaptor;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Base class for writing tests for Model update tasks.
*/
public class BaseModelUpdateTaskTestCase extends ProviderTestCase2<TestLauncherProvider> {
public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
public Context targetContext;
public UserHandleCompat myUser;
public InvariantDeviceProfile idp;
public LauncherAppState appState;
public MyIconCache iconCache;
public BgDataModel bgDataModel;
public AllAppsList allAppsList;
public Callbacks callbacks;
public BaseModelUpdateTaskTestCase() {
super(TestLauncherProvider.class, ProviderConfig.AUTHORITY);
}
@Override
protected void setUp() throws Exception {
super.setUp();
callbacks = mock(Callbacks.class);
appState = mock(LauncherAppState.class);
myUser = UserHandleCompat.myUserHandle();
bgDataModel = new BgDataModel();
targetContext = InstrumentationRegistry.getTargetContext();
idp = new InvariantDeviceProfile();
iconCache = new MyIconCache(targetContext, idp);
allAppsList = new AllAppsList(iconCache, null);
when(appState.getIconCache()).thenReturn(iconCache);
when(appState.getInvariantDeviceProfile()).thenReturn(idp);
}
/**
* Synchronously executes the task and returns all the UI callbacks posted.
*/
public List<Runnable> executeTaskForTest(BaseModelUpdateTask task) throws Exception {
LauncherModel mockModel = mock(LauncherModel.class);
when(mockModel.getCallback()).thenReturn(callbacks);
Field f = BaseModelUpdateTask.class.getDeclaredField("mModel");
f.setAccessible(true);
f.set(task, mockModel);
DeferredHandler mockHandler = mock(DeferredHandler.class);
f = BaseModelUpdateTask.class.getDeclaredField("mUiHandler");
f.setAccessible(true);
f.set(task, mockHandler);
task.execute(appState, bgDataModel, allAppsList);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
verify(mockHandler, atLeast(0)).post(captor.capture());
return captor.getAllValues();
}
/**
* Initializes mock data for the test.
*/
public void initializeData(String resourceName) throws Exception {
Context myContext = InstrumentationRegistry.getContext();
Resources res = myContext.getResources();
int id = res.getIdentifier(resourceName, "raw", myContext.getPackageName());
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(res.openRawResource(id)))) {
String line;
HashMap<String, Class> classMap = new HashMap<>();
while((line = reader.readLine()) != null) {
line = line.trim();
if (line.startsWith("#") || line.isEmpty()) {
continue;
}
String[] commands = line.split(" ");
switch (commands[0]) {
case "classMap":
classMap.put(commands[1], Class.forName(commands[2]));
break;
case "bgItem":
bgDataModel.addItem(
(ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
break;
case "allApps":
allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1));
break;
}
}
}
}
private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
HashMap<String, Field> cache = fieldCache.get(clazz);
if (cache == null) {
cache = new HashMap<>();
Class c = clazz;
while (c != null) {
for (Field f : c.getDeclaredFields()) {
f.setAccessible(true);
cache.put(f.getName(), f);
}
c = c.getSuperclass();
}
fieldCache.put(clazz, cache);
}
Object item = clazz.newInstance();
for (int i = startIndex; i < fieldDef.length; i++) {
String[] fieldData = fieldDef[i].split("=", 2);
Field f = cache.get(fieldData[0]);
Class type = f.getType();
if (type == int.class || type == long.class) {
f.set(item, Integer.parseInt(fieldData[1]));
} else if (type == CharSequence.class || type == String.class) {
f.set(item, fieldData[1]);
} else if (type == Intent.class) {
if (!fieldData[1].startsWith("#Intent")) {
fieldData[1] = "#Intent;" + fieldData[1] + ";end";
}
f.set(item, Intent.parseUri(fieldData[1], 0));
} else if (type == ComponentName.class) {
f.set(item, ComponentName.unflattenFromString(fieldData[1]));
} else {
throw new Exception("Added parsing logic for "
+ f.getName() + " of type " + f.getType());
}
}
return item;
}
public static class MyIconCache extends IconCache {
private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
public MyIconCache(Context context, InvariantDeviceProfile idp) {
super(context, idp);
}
@Override
protected CacheEntry cacheLocked(ComponentName componentName,
LauncherActivityInfoCompat info, UserHandleCompat user,
boolean usePackageIcon, boolean useLowResIcon) {
CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
if (entry == null) {
entry = new CacheEntry();
entry.icon = getDefaultIcon(user);
}
return entry;
}
public void addCache(ComponentName key, String title) {
CacheEntry entry = new CacheEntry();
entry.icon = newIcon();
entry.title = title;
mCache.put(new ComponentKey(key, UserHandleCompat.myUserHandle()), entry);
}
public Bitmap newIcon() {
return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
}
}
}

View File

@ -0,0 +1,81 @@
package com.android.launcher3.model;
import com.android.launcher3.AppInfo;
import com.android.launcher3.IconCache;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.ShortcutInfo;
import java.util.Arrays;
import java.util.HashSet;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CacheDataUpdatedTask}
*/
public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
private static final String NEW_LABEL_PREFIX = "new-label-";
@Override
protected void setUp() throws Exception {
super.setUp();
initializeData("cache_data_updated_task_data");
// Add dummy entries in the cache to simulate update
for (ItemInfo info : bgDataModel.itemsIdMap) {
iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
}
}
private CacheDataUpdatedTask newTask(int op, String... pkg) {
return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
}
public void testCacheUpdate_update_apps() throws Exception {
executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
// Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
// is not updated
verifyUpdate(1L, 2L);
// Verify that only app1 var updated in allAppsList
assertFalse(allAppsList.data.isEmpty());
for (AppInfo info : allAppsList.data) {
if (info.componentName.getPackageName().equals("app1")) {
assertNotNull(info.iconBitmap);
} else {
assertNull(info.iconBitmap);
}
}
}
public void testSessionUpdate_ignores_normal_apps() throws Exception {
executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
// app1 has no restored shortcuts. Verify that nothing was updated.
verifyUpdate();
}
public void testSessionUpdate_updates_pending_apps() throws Exception {
executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
// app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
// were updated
verifyUpdate(5L, 6L);
}
private void verifyUpdate(Long... idsUpdated) {
HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
IconCache noOpIconCache = mock(IconCache.class);
for (ItemInfo info : bgDataModel.itemsIdMap) {
if (updates.contains(info.id)) {
assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
assertNotNull(((ShortcutInfo) info).getIcon(noOpIconCache));
} else {
assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
assertNull(((ShortcutInfo) info).getIcon(noOpIconCache));
}
}
}
}

View File

@ -0,0 +1,61 @@
package com.android.launcher3.model;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.compat.PackageInstallerCompat;
import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
import java.util.Arrays;
import java.util.HashSet;
/**
* Tests for {@link PackageInstallStateChangedTask}
*/
public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
initializeData("package_install_state_change_task_data");
}
private PackageInstallStateChangedTask newTask(String pkg, int progress) {
PackageInstallInfo installInfo = new PackageInstallInfo(pkg);
installInfo.progress = progress;
installInfo.state = PackageInstallerCompat.STATUS_INSTALLING;
return new PackageInstallStateChangedTask(installInfo);
}
public void testSessionUpdate_ignore_installed() throws Exception {
executeTaskForTest(newTask("app1", 30));
// No shortcuts were updated
verifyProgressUpdate(0);
}
public void testSessionUpdate_shortcuts_updated() throws Exception {
executeTaskForTest(newTask("app3", 30));
verifyProgressUpdate(30, 5L, 6L, 7L);
}
public void testSessionUpdate_widgets_updated() throws Exception {
executeTaskForTest(newTask("app4", 30));
verifyProgressUpdate(30, 8L, 9L);
}
private void verifyProgressUpdate(int progress, Long... idsUpdated) {
HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
for (ItemInfo info : bgDataModel.itemsIdMap) {
if (info instanceof ShortcutInfo) {
assertEquals(updates.contains(info.id) ? progress: 0,
((ShortcutInfo) info).getInstallProgress());
} else {
assertEquals(updates.contains(info.id) ? progress: -1,
((LauncherAppWidgetInfo) info).installProgress);
}
}
}
}