Allow app shortcuts to have a preferred placement
Look for a preferred placement when somebody asks to add a new shortcut.
The preferred placements are part of the resources configuration (config.xml).
FPIIM-1909
Change-Id: I871196fe37dae35172d609fcbf9efd5496a7e4b2
diff --git a/res/values/config.xml b/res/values/config.xml
index 93c6d14..aaf848f 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -91,4 +91,8 @@
<item type="id" name="action_move_screen_backwards" />
<item type="id" name="action_move_screen_forwards" />
<item type="id" name="action_resize" />
+
+<!-- Shortcut preferred placements. An item denotes a component (component name) and its
+ placement on a screen (screen and coordinates). These elements have to be comma-separated. -->
+ <string-array name="shortcut_preferred_placements" translatable="false" />
</resources>
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index b5922c6..2ef5e4f 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -31,6 +31,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
@@ -75,6 +76,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -403,6 +405,44 @@
runOnWorkerThread(r);
}
+ /**
+ * Validate that a specific icon space is available.
+ * <p>
+ * Heavily inspired by #findNextAvailableIconSpaceInScreen(ArrayList<>,
+ * int[], int, int) - a blunt copy-paste of the first part to build up the
+ * occupied positions.
+ *
+ * @return true if the specific icon space is available.
+ */
+ private static boolean isAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos,
+ int[] xy, int spanX, int spanY) {
+ LauncherAppState app = LauncherAppState.getInstance();
+ InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
+ final int xCount = (int) profile.numColumns;
+ final int yCount = (int) profile.numRows;
+ boolean[][] occupied = new boolean[xCount][yCount];
+ boolean available = true;
+ if (occupiedPos != null) {
+ for (ItemInfo r : occupiedPos) {
+ int right = r.cellX + r.spanX;
+ int bottom = r.cellY + r.spanY;
+ for (int x = r.cellX; 0 <= x && x < right && x < xCount; x++) {
+ for (int y = r.cellY; 0 <= y && y < bottom && y < yCount; y++) {
+ occupied[x][y] = true;
+ }
+ }
+ }
+ }
+ int right = xy[0] + spanX;
+ int bottom = xy[1] + spanY;
+ for (int x = xy[0]; available && 0 <= x && x < right && x < xCount; x++) {
+ for (int y = xy[1]; available && 0 <= y && y < bottom && y < yCount; y++) {
+ available = !occupied[x][y];
+ }
+ }
+ return available;
+ }
+
private static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos,
int[] xy, int spanX, int spanY) {
LauncherAppState app = LauncherAppState.getInstance();
@@ -433,6 +473,20 @@
ArrayList<Long> workspaceScreens,
ArrayList<Long> addedWorkspaceScreensFinal,
int spanX, int spanY) {
+ return findSpaceForItem(context, 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.
+ */
+ @Thunk Pair<Long, int[]> findSpaceForItem(
+ Context context,
+ 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.
@@ -456,12 +510,25 @@
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 (shortcutPreferredPlacement != null) {
+ // First, check the preferred placement.
+ screenId = shortcutPreferredPlacement.first;
+ cordinates = shortcutPreferredPlacement.second;
+ if (screenId < screenCount) {
+ found = isAvailableIconSpaceInScreen(
+ screenItems.get(screenId), cordinates, spanX, spanY);
+ }
+ }
+
+ if (!found) {
+ // Then, 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) {
@@ -509,6 +576,10 @@
final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
+ // 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.
@@ -522,8 +593,8 @@
}
}
- // Find appropriate space for the item.
Pair<Long, int[]> coords = findSpaceForItem(context,
+ shortcutPreferredPlacements.get(item.getIntent().getComponent()),
workspaceScreens, addedWorkspaceScreensFinal,
1, 1);
long screenId = coords.first;
@@ -3728,4 +3799,47 @@
public static Looper getWorkerLooper() {
return sWorkerThread.getLooper();
}
+
+ /**
+ * 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;
+ }
}