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).

Issue: FPIIM-1909
Issue: FP2N-245
Change-Id: I871196fe37dae35172d609fcbf9efd5496a7e4b2
(adapted from commit c01efc9a3c3ff04bcb52924a533211891ed9be5b)
diff --git a/res/values/config.xml b/res/values/config.xml
index a942f02..6157921 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -109,4 +109,8 @@
     <item type="id" name="action_move_screen_forwards" />
     <item type="id" name="action_resize" />
     <item type="id" name="action_deep_shortcuts" />
+
+<!-- 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 3ac9773..b7dae4b 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -28,6 +28,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.net.Uri;
@@ -391,8 +392,30 @@
         runOnWorkerThread(r);
     }
 
-    private static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos,
+    /**
+     * 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(ArrayList<ItemInfo> occupiedPos,
             int[] xy, int spanX, int spanY) {
+        GridOccupancy occupied = itemInfosToGridOccupancy(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(ArrayList<ItemInfo> occupiedPos) {
         LauncherAppState app = LauncherAppState.getInstance();
         InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
 
@@ -402,6 +425,12 @@
                 occupied.markCells(r, true);
             }
         }
+        return occupied;
+    }
+
+    private static boolean findNextAvailableIconSpaceInScreen(ArrayList<ItemInfo> occupiedPos,
+            int[] xy, int spanX, int spanY) {
+        GridOccupancy occupied = itemInfosToGridOccupancy(occupiedPos);
         return occupied.findVacantCell(xy, spanX, spanY);
     }
 
@@ -414,6 +443,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.
@@ -437,12 +480,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) {
@@ -492,6 +548,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.
@@ -505,8 +565,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;
                         int[] cordinates = coords.second;
@@ -3866,4 +3926,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;
+    }
 }