Merge "Implement hotseat prediction client" into ub-launcher3-master

This commit is contained in:
TreeHugger Robot
2019-11-05 19:18:49 +00:00
committed by Android (Google) Code Review
5 changed files with 207 additions and 104 deletions
@@ -15,7 +15,15 @@
*/
package com.android.launcher3;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionManager;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -25,14 +33,16 @@ 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.appprediction.DynamicItemCache;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ComponentKey;
import java.util.ArrayList;
import java.util.Collections;
@@ -43,73 +53,86 @@ import java.util.List;
* pinning of predicted apps and manages replacement of predicted apps with user drag.
*/
public class HotseatPredictionController implements DragController.DragListener,
View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher> {
View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
IconCache.ItemInfoUpdateReceiver {
private static final String TAG = "PredictiveHotseat";
private static final boolean DEBUG = false;
private static final String PREDICTION_CLIENT = "hotseat";
private boolean mDragStarted = false;
private PredictionUiStateManager mPredictionUiStateManager;
private ArrayList<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
private int mHotSeatItemsCount;
private Launcher mLauncher;
private Hotseat mHotseat;
private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
private DynamicItemCache mDynamicItemCache;
private AppPredictor mAppPredictor;
private AllAppsStore mAllAppsStore;
public HotseatPredictionController(Launcher launcher) {
mLauncher = launcher;
mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(mLauncher);
if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
mLauncher.getHotseat().addOnAttachStateChangeListener(this);
}
mHotseat = launcher.getHotseat();
mAllAppsStore = mLauncher.getAppsView().getAppsStore();
mAllAppsStore.addUpdateListener(this);
mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
launcher.getDeviceProfile().inv.addOnChangeListener(this);
mHotseat.addOnAttachStateChangeListener(this);
createPredictor();
}
@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) {
if (mDragStarted) {
return;
}
removePredictedApps(false);
List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
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
ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
View child = mHotseat.getChildAt(
mHotseat.getCellXFromOrder(rank),
mHotseat.getCellYFromOrder(rank));
if (child != null && !isPredictedIcon(child)) {
continue;
}
WorkspaceItemInfo predictedItem = mPredictedApps.get(predictionIndex++);
if (predictedApps.size() <= predictionIndex) {
// Remove predicted apps from the past
if (isPredictedIcon(child)) {
mHotseat.removeView(child);
}
continue;
}
WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
if (isPredictedIcon(child)) {
BubbleTextView icon = (BubbleTextView) child;
icon.applyFromWorkspaceItem(predictedItem);
} else {
newItemsToAdd.add(predictedItem);
}
preparePredictionInfo(predictedItem, rank);
itemInfos.add(predictedItem);
}
mLauncher.bindItems(itemInfos, animate);
mLauncher.bindItems(newItemsToAdd, animate);
for (BubbleTextView icon : getPredictedIcons()) {
icon.verifyHighRes();
icon.setOnLongClickListener((v) -> {
@@ -119,29 +142,73 @@ public class HotseatPredictionController implements DragController.DragListener,
}
}
/**
* Unregisters callbacks and frees resources
*/
public void destroy() {
mAllAppsStore.removeUpdateListener(this);
mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
mHotseat.removeOnAttachStateChangeListener(this);
if (mAppPredictor != null) {
mAppPredictor.destroy();
}
}
private void createPredictor() {
AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class);
if (apm == null) {
return;
}
if (mAppPredictor != null) {
mAppPredictor.destroy();
}
mAppPredictor = apm.createAppPredictionSession(
new AppPredictionContext.Builder(mLauncher)
.setUiSurface(PREDICTION_CLIENT)
.setPredictedTargetCount(mHotSeatItemsCount)
.build());
mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
this::setPredictedApps);
mAppPredictor.requestPredictionUpdate();
}
private void setPredictedApps(List<AppTarget> appTargets) {
mComponentKeyMappers.clear();
for (AppTarget appTarget : appTargets) {
ComponentKey key;
if (appTarget.getShortcutInfo() != null) {
key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
} else {
key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
appTarget.getClassName()), appTarget.getUser());
}
mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
}
updateDependencies();
fillGapsWithPrediction(false);
}
private void updateDependencies() {
mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this,
mHotSeatItemsCount);
}
private void pinPrediction(ItemInfo info) {
BubbleTextView icon = (BubbleTextView) mLauncher.getHotseat().getChildAt(
mLauncher.getHotseat().getCellXFromOrder(info.rank),
mLauncher.getHotseat().getCellYFromOrder(info.rank));
BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
mHotseat.getCellXFromOrder(info.rank),
mHotseat.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,
LauncherSettings.Favorites.CONTAINER_HOTSEAT, 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);
}
});
ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
icon.applyFromWorkspaceItem(workspaceItemInfo);
icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
}
private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
List<ComponentKeyMapper> components) {
AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
@@ -163,7 +230,7 @@ public class HotseatPredictionController implements DragController.DragListener,
}
}
// Stop at the number of hotseat items
if (predictedApps.size() == mLauncher.getDeviceProfile().inv.numHotseatIcons) {
if (predictedApps.size() == mHotSeatItemsCount) {
break;
}
}
@@ -172,12 +239,10 @@ public class HotseatPredictionController implements DragController.DragListener,
private List<BubbleTextView> getPredictedIcons() {
List<BubbleTextView> icons = new ArrayList<>();
ViewGroup vg = mLauncher.getHotseat().getShortcutsAndWidgets();
ViewGroup vg = mHotseat.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) {
if (isPredictedIcon(child)) {
icons.add((BubbleTextView) child);
}
}
@@ -191,13 +256,13 @@ public class HotseatPredictionController implements DragController.DragListener,
@Override
public void onAnimationSuccess(Animator animator) {
if (icon.getParent() != null) {
((ViewGroup) icon.getParent()).removeView(icon);
mHotseat.removeView(icon);
}
}
});
} else {
if (icon.getParent() != null) {
((ViewGroup) icon.getParent()).removeView(icon);
mHotseat.removeView(icon);
}
}
}
@@ -222,7 +287,6 @@ public class HotseatPredictionController implements DragController.DragListener,
@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;
}
@@ -233,10 +297,27 @@ public class HotseatPredictionController implements DragController.DragListener,
itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
itemInfo.rank = rank;
itemInfo.cellX = rank;
itemInfo.cellY = LauncherAppState.getIDP(mLauncher).numHotseatIcons - rank - 1;
itemInfo.cellY = mHotSeatItemsCount - rank - 1;
itemInfo.screenId = rank;
}
@Override
public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
this.mHotSeatItemsCount = profile.numHotseatIcons;
createPredictor();
}
@Override
public void onAppsUpdated() {
updateDependencies();
fillGapsWithPrediction(false);
}
@Override
public void reapplyItemInfo(ItemInfoWithIcon info) {
}
private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
@@ -250,4 +331,10 @@ public class HotseatPredictionController implements DragController.DragListener,
pinPrediction(mItemInfo);
}
}
private static boolean isPredictedIcon(View view) {
return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
&& ((WorkspaceItemInfo) view.getTag()).container
== LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
}
}
@@ -18,6 +18,7 @@ package com.android.launcher3.appprediction;
import static android.content.pm.PackageManager.MATCH_INSTANT;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
import android.content.Context;
import android.content.Intent;
@@ -37,8 +38,10 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import com.android.launcher3.AppInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -72,6 +75,7 @@ public class DynamicItemCache {
private final Handler mUiHandler;
private final InstantAppResolver mInstantAppResolver;
private final Runnable mOnUpdateCallback;
private final IconCache mIconCache;
private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
private final Map<String, InstantAppItemInfo> mInstantApps;
@@ -82,6 +86,7 @@ public class DynamicItemCache {
mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
mInstantAppResolver = InstantAppResolver.newInstance(context);
mOnUpdateCallback = onUpdateCallback;
mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
mShortcuts = new HashMap<>();
mInstantApps = new HashMap<>();
@@ -240,4 +245,35 @@ public class DynamicItemCache {
public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
return mShortcuts.get(key);
}
/**
* requests and caches icons for app targets
*/
public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
List<String> instantAppsToLoad = new ArrayList<>();
List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
int total = componentKeyMappers.size();
for (int i = 0, count = 0; i < total && count < itemCount; i++) {
ComponentKeyMapper mapper = componentKeyMappers.get(i);
// Update instant apps
if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
instantAppsToLoad.add(mapper.getPackage());
count++;
} else if (mapper.getComponentKey() instanceof ShortcutKey) {
shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
count++;
} else {
// Reload high res icon
AppInfo info = (AppInfo) mapper.getApp(appsStore);
if (info != null) {
if (info.usingLowResIcon()) {
mIconCache.updateIconInBackground(callback, info);
}
count++;
}
}
}
cacheItems(shortcutsToLoad, instantAppsToLoad);
}
}
@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,15 +18,12 @@ package com.android.launcher3.appprediction;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
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;
@@ -37,7 +34,6 @@ import com.android.launcher3.LauncherStateManager.StateListener;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.ComponentKey;
@@ -90,8 +86,6 @@ public class PredictionUiStateManager implements StateListener, ItemInfoUpdateRe
private AllAppsContainerView mAppsView;
private HotseatPredictionController mHotseatPredictionController;
private PredictionState mPendingState;
private PredictionState mCurrentState;
@@ -153,10 +147,6 @@ public class PredictionUiStateManager implements StateListener, ItemInfoUpdateRe
updateDependencies(mCurrentState);
}
public void setHotseatPredictionController(HotseatPredictionController controller) {
mHotseatPredictionController = controller;
}
@Override
public void reapplyItemInfo(ItemInfoWithIcon info) { }
@@ -193,9 +183,6 @@ public class PredictionUiStateManager implements StateListener, ItemInfoUpdateRe
mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
.setPredictedApps(mCurrentState.apps);
}
if (mHotseatPredictionController != null) {
mHotseatPredictionController.setPredictedApps(mCurrentState.apps);
}
}
private void updatePredictionStateAfterCallback() {
@@ -260,33 +247,8 @@ public class PredictionUiStateManager implements StateListener, ItemInfoUpdateRe
if (!state.isEnabled || mAppsView == null) {
return;
}
IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
List<String> instantAppsToLoad = new ArrayList<>();
List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
int total = state.apps.size();
for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
ComponentKeyMapper mapper = state.apps.get(i);
// Update instant apps
if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
instantAppsToLoad.add(mapper.getPackage());
count++;
} else if (mapper.getComponentKey() instanceof ShortcutKey) {
shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
count++;
} else {
// Reload high res icon
AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
if (info != null) {
if (info.usingLowResIcon()) {
// TODO: Update icon cache to support null callbacks.
iconCache.updateIconInBackground(this, info);
}
count++;
}
}
}
mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
mMaxIconsPerRow);
}
@Override
@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,6 +31,7 @@ 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.config.FeatureFlags;
import com.android.launcher3.graphics.RotationMode;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
@@ -137,7 +138,9 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHotseatPredictionController = new HotseatPredictionController(this);
if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
mHotseatPredictionController = new HotseatPredictionController(this);
}
}
@Override
@@ -165,8 +168,12 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
@Override
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
return Stream.concat(super.getSupportedShortcuts(),
Stream.of(mHotseatPredictionController));
if (mHotseatPredictionController != null) {
return Stream.concat(super.getSupportedShortcuts(),
Stream.of(mHotseatPredictionController));
} else {
return super.getSupportedShortcuts();
}
}
/**
@@ -187,7 +194,17 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
@Override
public void finishBindingItems(int pageBoundFirst) {
super.finishBindingItems(pageBoundFirst);
mHotseatPredictionController.fillGapsWithPrediction(false);
if (mHotseatPredictionController != null) {
mHotseatPredictionController.fillGapsWithPrediction(false);
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mHotseatPredictionController != null) {
mHotseatPredictionController.destroy();
}
}
@Override
@@ -496,7 +496,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
// Text should be visible everywhere but the hotseat.
Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT;
return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
&& info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
}
public void setTextVisibility(boolean visible) {