Predictive hotseat prototype

Supports filling hotseat with predicted apps, pinning of predicted apps
and manages replacing predicted apps with user drag.

Bug:142753423
Test:Manual
Change-Id: I224294f9353a64c46d28c22263a72332a79fddf4
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
new file mode 100644
index 0000000..ba44213
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.animation.Animator;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.appprediction.ComponentKeyMapper;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
+ * pinning of predicted apps and manages replacement of predicted apps with user drag.
+ */
+public class HotseatPredictionController implements DragController.DragListener,
+        View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher> {
+
+    private static final String TAG = "PredictiveHotseat";
+    private static final boolean DEBUG = false;
+
+
+    private boolean mDragStarted = false;
+    private PredictionUiStateManager mPredictionUiStateManager;
+    private ArrayList<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
+    private Launcher mLauncher;
+
+    public HotseatPredictionController(Launcher launcher) {
+        mLauncher = launcher;
+        mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(mLauncher);
+        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+            mLauncher.getHotseat().addOnAttachStateChangeListener(this);
+        }
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View view) {
+        mPredictionUiStateManager.setHotseatPredictionController(this);
+        mLauncher.getDragController().addDragListener(this);
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        mPredictionUiStateManager.setHotseatPredictionController(null);
+        mLauncher.getDragController().removeDragListener(this);
+    }
+
+    /**
+     * sets the list of predicted items. gets called from PredictionUiStateManager
+     */
+    public void setPredictedApps(List<ComponentKeyMapper> apps) {
+        mPredictedApps.clear();
+        mPredictedApps.addAll(mapToWorkspaceItemInfo(apps));
+        fillGapsWithPrediction(false);
+    }
+
+    /**
+     * Fills gaps in the hotseat with predictions
+     */
+    public void fillGapsWithPrediction(boolean animate) {
+        if (!FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() || mDragStarted) {
+            return;
+        }
+        removePredictedApps(false);
+        int predictionIndex = 0;
+        ArrayList<ItemInfo> itemInfos = new ArrayList<>();
+        int cellCount = mLauncher.getWallpaperDeviceProfile().inv.numHotseatIcons;
+        for (int rank = 0; rank < cellCount; rank++) {
+            if (mPredictedApps.size() == predictionIndex) {
+                break;
+            }
+            View child = mLauncher.getHotseat().getChildAt(
+                    mLauncher.getHotseat().getCellXFromOrder(rank),
+                    mLauncher.getHotseat().getCellYFromOrder(rank));
+            if (child != null) {
+                // we already have an item there. skip cell
+                continue;
+            }
+            WorkspaceItemInfo predictedItem = mPredictedApps.get(predictionIndex++);
+            preparePredictionInfo(predictedItem, rank);
+            itemInfos.add(predictedItem);
+        }
+        mLauncher.bindItems(itemInfos, animate);
+        for (BubbleTextView icon : getPredictedIcons()) {
+            icon.verifyHighRes();
+            icon.setOnLongClickListener((v) -> {
+                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
+                return true;
+            });
+        }
+    }
+
+    private void pinPrediction(ItemInfo info) {
+        BubbleTextView icon = (BubbleTextView) mLauncher.getHotseat().getChildAt(
+                mLauncher.getHotseat().getCellXFromOrder(info.rank),
+                mLauncher.getHotseat().getCellYFromOrder(info.rank));
+        if (icon == null) {
+            return;
+        }
+        WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+        workspaceItemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+        mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
+                workspaceItemInfo.container, workspaceItemInfo.screenId,
+                workspaceItemInfo.cellX, workspaceItemInfo.cellY);
+        icon.animate().scaleY(0.8f).scaleX(0.8f).setListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                icon.animate().scaleY(1).scaleX(1);
+            }
+        });
+        icon.applyFromWorkspaceItem(workspaceItemInfo);
+        icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+    }
+
+
+    private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
+            List<ComponentKeyMapper> components) {
+        AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
+        if (allAppsStore.getApps().length == 0) {
+            return Collections.emptyList();
+        }
+
+        List<WorkspaceItemInfo> predictedApps = new ArrayList<>();
+        for (ComponentKeyMapper mapper : components) {
+            ItemInfoWithIcon info = mapper.getApp(allAppsStore);
+            if (info instanceof AppInfo) {
+                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
+                predictedApps.add(predictedApp);
+            } else if (info instanceof WorkspaceItemInfo) {
+                predictedApps.add(new WorkspaceItemInfo((WorkspaceItemInfo) info));
+            } else {
+                if (DEBUG) {
+                    Log.e(TAG, "Predicted app not found: " + mapper);
+                }
+            }
+            // Stop at the number of hotseat items
+            if (predictedApps.size() == mLauncher.getDeviceProfile().inv.numHotseatIcons) {
+                break;
+            }
+        }
+        return predictedApps;
+    }
+
+    private List<BubbleTextView> getPredictedIcons() {
+        List<BubbleTextView> icons = new ArrayList<>();
+        ViewGroup vg = mLauncher.getHotseat().getShortcutsAndWidgets();
+        for (int i = 0; i < vg.getChildCount(); i++) {
+            View child = vg.getChildAt(i);
+            if (child instanceof BubbleTextView && child.getTag() instanceof WorkspaceItemInfo
+                    && ((WorkspaceItemInfo) child.getTag()).container
+                    == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+                icons.add((BubbleTextView) child);
+            }
+        }
+        return icons;
+    }
+
+    private void removePredictedApps(boolean animate) {
+        for (BubbleTextView icon : getPredictedIcons()) {
+            if (animate) {
+                icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+                    @Override
+                    public void onAnimationSuccess(Animator animator) {
+                        if (icon.getParent() != null) {
+                            ((ViewGroup) icon.getParent()).removeView(icon);
+                        }
+                    }
+                });
+            } else {
+                if (icon.getParent() != null) {
+                    ((ViewGroup) icon.getParent()).removeView(icon);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+        removePredictedApps(true);
+        mDragStarted = true;
+    }
+
+    @Override
+    public void onDragEnd() {
+        if (!mDragStarted) {
+            return;
+        }
+        mDragStarted = false;
+        fillGapsWithPrediction(true);
+    }
+
+    @Nullable
+    @Override
+    public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
+            ItemInfo itemInfo) {
+        if (!FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) return null;
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            return null;
+        }
+        return new PinPrediction(activity, itemInfo);
+    }
+
+    private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
+        itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+        itemInfo.rank = rank;
+        itemInfo.cellX = rank;
+        itemInfo.cellY =  LauncherAppState.getIDP(mLauncher).numHotseatIcons - rank - 1;
+        itemInfo.screenId = rank;
+    }
+
+    private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
+
+        private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
+            super(R.drawable.ic_pin, R.string.pin_prediction, target,
+                    itemInfo);
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            pinPrediction(mItemInfo);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 1a59770..c50c427 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.HotseatPredictionController;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.ItemInfoWithIcon;
@@ -89,6 +90,8 @@
 
     private AllAppsContainerView mAppsView;
 
+    private HotseatPredictionController mHotseatPredictionController;
+
     private PredictionState mPendingState;
     private PredictionState mCurrentState;
 
@@ -150,6 +153,10 @@
         updateDependencies(mCurrentState);
     }
 
+    public void setHotseatPredictionController(HotseatPredictionController controller) {
+        mHotseatPredictionController = controller;
+    }
+
     @Override
     public void reapplyItemInfo(ItemInfoWithIcon info) { }
 
@@ -186,6 +193,9 @@
             mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
                     .setPredictedApps(mCurrentState.apps);
         }
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.setPredictedApps(mCurrentState.apps);
+        }
     }
 
     private void updatePredictionStateAfterCallback() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4e73a79..a6fcf44 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -18,20 +18,21 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-import static com.android.quickstep.util.NavBarPosition.ROTATION_LANDSCAPE;
-import static com.android.quickstep.util.NavBarPosition.ROTATION_SEASCAPE;
 
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.Gravity;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.HotseatPredictionController;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
@@ -50,17 +51,16 @@
 import com.android.quickstep.views.RecentsView;
 
 import java.util.ArrayList;
+import java.util.stream.Stream;
 
 public class QuickstepLauncher extends BaseQuickstepLauncher {
 
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
-
     /**
      * Reusable command for applying the shelf height on the background thread.
      */
     public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
             SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
-
     public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
         @Override
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -87,7 +87,6 @@
             }
         }
     };
-
     public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
         @Override
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -133,6 +132,13 @@
                     | horizontalGravity | verticalGravity;
         }
     };
+    private HotseatPredictionController mHotseatPredictionController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mHotseatPredictionController = new HotseatPredictionController(this);
+    }
 
     @Override
     protected RotationMode getFakeRotationMode(DeviceProfile dp) {
@@ -157,6 +163,12 @@
         }
     }
 
+    @Override
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.concat(super.getSupportedShortcuts(),
+                Stream.of(mHotseatPredictionController));
+    }
+
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
      */
@@ -173,6 +185,12 @@
     }
 
     @Override
+    public void finishBindingItems(int pageBoundFirst) {
+        super.finishBindingItems(pageBoundFirst);
+        mHotseatPredictionController.fillGapsWithPrediction(false);
+    }
+
+    @Override
     public TouchController[] createTouchControllers() {
         Mode mode = SysUINavigationMode.getMode(this);
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d9c2e8..dec8939 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -102,9 +102,11 @@
     <string name="app_info_drop_target_label">App info</string>
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
-
     <!-- Label for install dismiss prediction. -->
     <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+    <!-- Label for pinning predicted app. -->
+    <string name="pin_prediction" translatable="false">Pin Prediction</string>
+
 
     <!-- Permissions: -->
     <skip />
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index c509680..ec307db 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -127,6 +127,7 @@
         public static final int CONTAINER_DESKTOP = -100;
         public static final int CONTAINER_HOTSEAT = -101;
         public static final int CONTAINER_PREDICTION = -102;
+        public static final int CONTAINER_HOTSEAT_PREDICTION = -103;
 
         static final String containerToString(int container) {
             switch (container) {
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index ea2d4d0..0b9d602 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -39,7 +39,8 @@
     default void addInScreenFromBind(View child, ItemInfo info) {
         int x = info.cellX;
         int y = info.cellY;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             int screenId = info.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
@@ -83,7 +84,8 @@
         }
 
         final CellLayout layout;
-        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             layout = getHotseat();
 
             // Hide folder title in the hotseat
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index d7543ab..5689846 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -124,6 +124,9 @@
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
             "Allow Launcher to handle nav bar gestures while Assistant is running over it");
 
+    public static final TogglableFlag ENABLE_HYBRID_HOTSEAT = new TogglableFlag(
+            "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
+
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {