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;
+    }
 }