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