blob: 5f116767454ebdccfd8053f52c7f31a5773d792b [file] [log] [blame]
/*
* 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.res.Resources;
import android.os.UserHandle;
import android.util.Log;
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.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.R;
import com.android.launcher3.ShortcutInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.GridOccupancy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Task to add auto-created workspace items.
*/
public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
static final String TAG = "AddWorkspaceItemsTask";
private final List<Pair<ItemInfo, Object>> mItemList;
/**
* @param itemList items to add on the workspace
*/
public AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList) {
mItemList = itemList;
}
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
if (mItemList.isEmpty()) {
return;
}
Context context = app.getContext();
final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>();
final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<>();
// Generic shortcut preferred placements
final Map<ComponentName, Pair<Long, int[]>> shortcutPreferredPlacements
= getShortcutPreferredPlacements(context, R.array.shortcut_preferred_placements);
// 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) {
List<ItemInfo> filteredItems = new ArrayList<>();
for (Pair<ItemInfo, Object> entry : mItemList) {
ItemInfo item = entry.first;
if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
// Short-circuit this logic if the icon exists somewhere on the workspace
if (shortcutExists(dataModel, item.getIntent(), item.user)) {
continue;
}
}
if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
if (item instanceof AppInfo) {
item = ((AppInfo) item).makeShortcut();
}
}
if (item != null) {
filteredItems.add(item);
}
}
for (ItemInfo item : filteredItems) {
// Find appropriate space for the item.
Pair<Long, int[]> coords = findSpaceForItem(app, dataModel,
shortcutPreferredPlacements.get(item.getIntent().getComponent()),
workspaceScreens, addedWorkspaceScreensFinal, item.spanX, item.spanY);
long screenId = coords.first;
int[] cordinates = coords.second;
ItemInfo itemInfo;
if (item instanceof ShortcutInfo || item instanceof FolderInfo ||
item instanceof LauncherAppWidgetInfo) {
itemInfo = item;
} else if (item instanceof AppInfo) {
itemInfo = ((AppInfo) item).makeShortcut();
} else {
throw new RuntimeException("Unexpected info type");
}
// Add the shortcut to the db
getModelWriter().addItemToDatabase(itemInfo,
LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId,
cordinates[0], cordinates[1]);
// Save the ShortcutInfo for binding in the workspace
addedItemsFinal.add(itemInfo);
}
}
// Update the workspace screens
updateScreens(context, workspaceScreens);
if (!addedItemsFinal.isEmpty()) {
scheduleCallbackTask(new CallbackTask() {
@Override
public void execute(Callbacks callbacks) {
final ArrayList<ItemInfo> addAnimated = new ArrayList<>();
final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>();
if (!addedItemsFinal.isEmpty()) {
ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1);
long lastScreenId = info.screenId;
for (ItemInfo i : addedItemsFinal) {
if (i.screenId == lastScreenId) {
addAnimated.add(i);
} else {
addNotAnimated.add(i);
}
}
}
callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
addNotAnimated, addAnimated);
}
});
}
}
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, UserHandle user) {
final String compPkgName, intentWithPkg, intentWithoutPkg;
if (intent == null) {
// Skip items with null intents
return true;
}
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.
compPkgName = 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(compPkgName).toUri(0);
intentWithoutPkg = intent.toUri(0);
}
} else {
compPkgName = null;
intentWithPkg = intent.toUri(0);
intentWithoutPkg = intent.toUri(0);
}
boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent);
synchronized (dataModel) {
for (ItemInfo item : dataModel.itemsIdMap) {
if (item instanceof ShortcutInfo) {
ShortcutInfo info = (ShortcutInfo) item;
if (item.getIntent() != null && info.user.equals(user)) {
Intent copyIntent = new Intent(item.getIntent());
copyIntent.setSourceBounds(intent.getSourceBounds());
String s = copyIntent.toUri(0);
if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
return true;
}
// checking for existing promise icon with same package name
if (isLauncherAppTarget
&& info.isPromise()
&& info.hasStatusFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)
&& info.getTargetComponent() != null
&& compPkgName != null
&& compPkgName.equals(info.getTargetComponent().getPackageName())) {
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) {
return findSpaceForItem(app, dataModel, null, workspaceScreens,
addedWorkspaceScreensFinal, 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.
*/
protected Pair<Long, int[]> findSpaceForItem(
LauncherAppState app, BgDataModel dataModel,
Pair<Long, int[]> shortcutPreferredPlacement,
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();
if (shortcutPreferredPlacement != null) {
// First, check the preferred placement.
screenId = shortcutPreferredPlacement.first;
cordinates = shortcutPreferredPlacement.second;
if (screenId < screenCount) {
found = isAvailableIconSpaceInScreen(
app, screenItems.get(screenId), cordinates, spanX, spanY);
}
}
if (!found) {
// 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);
}
/**
* Validate that a specific icon space is available.
* <p>
* Heavily inspired by #findNextAvailableIconSpaceInScreen(ArrayList<>,
* int[], int, int) - re-uses factored out code in
* itemInfosToGridOccupancy().
*
* @return true if the specific icon space is available.
*/
private static boolean isAvailableIconSpaceInScreen(
LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
int[] xy, int spanX, int spanY) {
GridOccupancy occupied = itemInfosToGridOccupancy(app, occupiedPos);
return occupied.isRegionVacant(xy[0], xy[1], spanX, spanY);
}
/*
* Generate GridOccupancy from array of ItemInfo
*
* Factored out from #findNextAvailableIconSpaceInScreen(ArrayList<>, int[], int, int)
* to be re-used by #isAvailableIconSpaceInScreen(ArrayList<>, int[], int, int)
*
* @return GridOccupancy with coordinates from items marked as occupied.
*/
private static GridOccupancy itemInfosToGridOccupancy(
LauncherAppState app, ArrayList<ItemInfo> occupiedPos) {
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;
}
private static boolean findNextAvailableIconSpaceInScreen(
LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
int[] xy, int spanX, int spanY) {
GridOccupancy occupied = itemInfosToGridOccupancy(app, occupiedPos);
return occupied.findVacantCell(xy, spanX, spanY);
}
/**
* Get the shortcut preferred placements from a resource array.
* <p>
* An item (a String) should contain the following information separated by commas:
* <ul>
* <li>the {@link ComponentName}</li>
* <li>the screen id (e.g. 0 for the main screen</li>
* <li>the abscissa (aka cell X)</li>
* <li>the ordinate (aka cell Y)</li>
* </ul>
* @param context the context to read the resources from.
* @param array the string array resource to inflate from.
* @return a map of valid placements found in the resource array.
* @throws Resources.NotFoundException if the string array cannot be loaded from the context resources.
*/
private static Map<ComponentName, Pair<Long, int[]>> getShortcutPreferredPlacements(Context context, int array)
throws Resources.NotFoundException {
final String[] rawPlacements = context.getResources().getStringArray(array);
final Map<ComponentName, Pair<Long, int[]>> placements = new HashMap<>(rawPlacements.length);
for (String rawPlacement : rawPlacements) {
final String[] items = rawPlacement.split(",");
if (items.length != 4) {
Log.e(TAG, "Invalid preferred placement : " + rawPlacement
+ ", 4 comma-separated items were expected");
continue;
}
try {
final ComponentName component = ComponentName.unflattenFromString(items[0]);
final long screenId = Long.valueOf(items[1]);
final int cellX = Integer.parseInt(items[2]);
final int cellY = Integer.parseInt(items[3]);
placements.put(component, new Pair<>(screenId, new int[] {cellX, cellY}));
} catch (NumberFormatException ex) {
Log.e(TAG, "Invalid preferred placement: " + rawPlacement, ex);
}
}
return placements;
}
}