diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index cb8d1c4bba..e9f35345ca 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -43,6 +43,7 @@ import com.android.launcher3.DropTarget; import com.android.launcher3.Hotseat; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherState; import com.android.launcher3.R; @@ -56,6 +57,7 @@ import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.FileLog; +import com.android.launcher3.model.PredictionModel; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -69,6 +71,7 @@ import com.android.launcher3.uioverrides.PredictedAppIcon; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntArray; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -93,8 +96,6 @@ public class HotseatPredictionController implements DragController.DragListener, //TODO: replace this with AppTargetEvent.ACTION_UNPIN (b/144119543) private static final int APPTARGET_ACTION_UNPIN = 4; - private static final String PREDICTED_ITEMS_CACHE_KEY = "predicted_item_keys"; - private static final String APP_LOCATION_HOTSEAT = "hotseat"; private static final String APP_LOCATION_WORKSPACE = "workspace"; @@ -115,11 +116,13 @@ public class HotseatPredictionController implements DragController.DragListener, private DynamicItemCache mDynamicItemCache; + private final PredictionModel mPredictionModel; private AppPredictor mAppPredictor; private AllAppsStore mAllAppsStore; private AnimatorSet mIconRemoveAnimators; private boolean mUIUpdatePaused = false; - private boolean mRequiresCacheUpdate = false; + private boolean mRequiresCacheUpdate = true; + private boolean mIsCacheEmpty; private HotseatEduController mHotseatEduController; @@ -138,15 +141,16 @@ public class HotseatPredictionController implements DragController.DragListener, mLauncher = launcher; mHotseat = launcher.getHotseat(); mAllAppsStore = mLauncher.getAppsView().getAppsStore(); + mPredictionModel = LauncherAppState.INSTANCE.get(launcher).getPredictionModel(); mAllAppsStore.addUpdateListener(this); mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction); mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons; launcher.getDeviceProfile().inv.addOnChangeListener(this); mHotseat.addOnAttachStateChangeListener(this); + mIsCacheEmpty = mPredictionModel.getPredictionComponentKeys().isEmpty(); if (mHotseat.isAttachedToWindow()) { onViewAttachedToWindow(mHotseat); } - showCachedItems(); } /** @@ -185,6 +189,11 @@ public class HotseatPredictionController implements DragController.DragListener, return; } List predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers); + if (mComponentKeyMappers.isEmpty() != predictedApps.isEmpty()) { + // Safely ignore update as AppsList is not ready yet. This will called again once + // apps are ready (HotseatPredictionController#onAppsUpdated) + return; + } int predictionIndex = 0; ArrayList newItems = new ArrayList<>(); // make sure predicted icon removal and filling predictions don't step on each other @@ -305,14 +314,23 @@ public class HotseatPredictionController implements DragController.DragListener, mAppPredictor.requestPredictionUpdate(); } - private void showCachedItems() { - ArrayList componentKeys = getCachedComponentKeys(); + /** + * Create WorkspaceItemInfo objects and binds PredictedAppIcon views for cached predicted items. + */ + public void showCachedItems(List apps, IntArray ranks) { + int count = Math.min(ranks.size(), apps.size()); + List items = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + WorkspaceItemInfo item = new WorkspaceItemInfo(apps.get(i)); + preparePredictionInfo(item, ranks.get(i)); + items.add(item); + } mComponentKeyMappers.clear(); - for (ComponentKey key : componentKeys) { + for (ComponentKey key : mPredictionModel.getPredictionComponentKeys()) { mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache)); } updateDependencies(); - fillGapsWithPrediction(); + bindItems(items, false, null); } private Bundle getAppPredictionContextExtra() { @@ -390,41 +408,20 @@ public class HotseatPredictionController implements DragController.DragListener, predictionLog.append("]"); if (Utilities.IS_DEBUG_DEVICE) FileLog.d(TAG, predictionLog.toString()); updateDependencies(); - fillGapsWithPrediction(); + fillGapsWithPrediction(); if (!isEduSeen() && mHotseatEduController != null) { mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers)); } - // should invalidate cache if AiAi sends empty list of AppTargets - if (appTargets.isEmpty()) { - mRequiresCacheUpdate = true; - } - cachePredictionComponentKeys(componentKeys); + cachePredictionComponentKeysIfNecessary(componentKeys); } - private void cachePredictionComponentKeys(ArrayList componentKeys) { - if (!mRequiresCacheUpdate) return; - StringBuilder builder = new StringBuilder(); - for (ComponentKey componentKey : componentKeys) { - builder.append(componentKey); - builder.append("\n"); - } - mLauncher.getDevicePrefs().edit().putString(PREDICTED_ITEMS_CACHE_KEY, - builder.toString()).apply(); + private void cachePredictionComponentKeysIfNecessary(ArrayList componentKeys) { + if (!mRequiresCacheUpdate && componentKeys.isEmpty() == mIsCacheEmpty) return; + mPredictionModel.cachePredictionComponentKeys(componentKeys); + mIsCacheEmpty = componentKeys.isEmpty(); mRequiresCacheUpdate = false; } - private ArrayList getCachedComponentKeys() { - String cachedBlob = mLauncher.getDevicePrefs().getString(PREDICTED_ITEMS_CACHE_KEY, ""); - ArrayList results = new ArrayList<>(); - for (String line : cachedBlob.split("\n")) { - ComponentKey key = ComponentKey.fromString(line); - if (key != null) { - results.add(key); - } - } - return results; - } - private void updateDependencies() { mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this, mHotSeatItemsCount); 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 a2d9e17534..eaf9f61964 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 @@ -44,6 +44,7 @@ import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.Folder; import com.android.launcher3.hybridhotseat.HotseatPredictionController; +import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; @@ -58,6 +59,7 @@ import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchContro import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.UiThreadHelper.AsyncCommand; @@ -70,6 +72,7 @@ import com.android.quickstep.views.TaskView; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; public class QuickstepLauncher extends BaseQuickstepLauncher { @@ -177,6 +180,14 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { } } + @Override + public void bindPredictedItems(List appInfos, IntArray ranks) { + super.bindPredictedItems(appInfos, ranks); + if (mHotseatPredictionController != null) { + mHotseatPredictionController.showCachedItems(appInfos, ranks); + } + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index c75bd95690..1d9c0c3aa0 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -2225,6 +2225,9 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, workspace.requestLayout(); } + @Override + public void bindPredictedItems(List appInfos, IntArray ranks) { } + /** * Add the views for a widget to the workspace. */ diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 2f38037892..14e604d9ed 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -33,6 +33,7 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.IconCache; import com.android.launcher3.icons.IconProvider; import com.android.launcher3.icons.LauncherIcons; +import com.android.launcher3.model.PredictionModel; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.InstallSessionTracker; @@ -57,6 +58,7 @@ public class LauncherAppState { private final IconCache mIconCache; private final WidgetPreviewLoader mWidgetCache; private final InvariantDeviceProfile mInvariantDeviceProfile; + private final PredictionModel mPredictionModel; private SecureSettingsObserver mNotificationDotsObserver; private InstallSessionTracker mInstallSessionTracker; @@ -127,6 +129,7 @@ public class LauncherAppState { mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName); mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache); mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext)); + mPredictionModel = new PredictionModel(mContext); } protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { @@ -182,6 +185,10 @@ public class LauncherAppState { return mModel; } + public PredictionModel getPredictionModel() { + return mPredictionModel; + } + public WidgetPreviewLoader getWidgetCache() { return mWidgetCache; } diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java index c98be5691f..1465100b83 100644 --- a/src/com/android/launcher3/model/BaseLoaderResults.java +++ b/src/com/android/launcher3/model/BaseLoaderResults.java @@ -17,6 +17,7 @@ package com.android.launcher3.model; import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; +import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks; import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially; import android.util.Log; @@ -196,6 +197,10 @@ public abstract class BaseLoaderResults { // Load items on the current page. bindWorkspaceItems(currentWorkspaceItems, mainExecutor); bindAppWidgets(currentAppWidgets, mainExecutor); + + // Locate available spots for prediction using currentWorkspaceItems + IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons); + bindPredictedItems(gaps, mainExecutor); // In case of validFirstPage, only bind the first screen, and defer binding the // remaining screens after first onDraw (and an optional the fade animation whichever // happens later). @@ -247,6 +252,11 @@ public abstract class BaseLoaderResults { } } + private void bindPredictedItems(IntArray ranks, final Executor executor) { + executeCallbacksTask( + c -> c.bindPredictedItems(mBgDataModel.cachedPredictedItems, ranks), executor); + } + protected void executeCallbacksTask(CallbackTask task, Executor executor) { executor.execute(() -> { if (mMyBindingId != mBgDataModel.lastBindId) { diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index f79a9d14a7..2522a498e6 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -92,6 +92,11 @@ public class BgDataModel { */ public final Map pinnedShortcutCounts = new HashMap<>(); + /** + * List of all cached predicted items visible on home screen + */ + public final ArrayList cachedPredictedItems = new ArrayList<>(); + /** * True if the launcher has permission to access deep shortcuts. */ @@ -366,5 +371,10 @@ public class BgDataModel { void bindDeepShortcutMap(HashMap deepShortcutMap); void bindAllApplications(AppInfo[] apps); + + /** + * Binds predicted appInfos at at available prediction slots. + */ + void bindPredictedItems(List appInfos, IntArray ranks); } } diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 4c02837fd1..90aaf4443b 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -179,6 +179,7 @@ public class LoaderTask implements Runnable { try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) { List allShortcuts = new ArrayList<>(); loadWorkspace(allShortcuts); + loadCachedPredictions(); logger.addSplit("loadWorkspace"); verifyNotStopped(); @@ -849,6 +850,23 @@ public class LoaderTask implements Runnable { } } + private List loadCachedPredictions() { + List componentKeys = mApp.getPredictionModel().getPredictionComponentKeys(); + List results = new ArrayList<>(); + if (componentKeys == null) return results; + List l; + mBgDataModel.cachedPredictedItems.clear(); + for (ComponentKey key : componentKeys) { + l = mLauncherApps.getActivityList(key.componentName.getPackageName(), key.user); + if (l.size() == 0) continue; + boolean quietMode = mUserManager.isQuietModeEnabled(key.user); + AppInfo info = new AppInfo(l.get(0), key.user, quietMode); + mBgDataModel.cachedPredictedItems.add(info); + mIconCache.getTitleAndIcon(info, false); + } + return results; + } + private List loadAllApps() { final List profiles = mUserCache.getUserProfiles(); List allActivityList = new ArrayList<>(); @@ -880,6 +898,14 @@ public class LoaderTask implements Runnable { PackageInstallInfo.fromInstallingState(info)); } } + for (AppInfo item : mBgDataModel.cachedPredictedItems) { + List l = mLauncherApps.getActivityList( + item.componentName.getPackageName(), item.user); + for (LauncherActivityInfo info : l) { + boolean quietMode = mUserManager.isQuietModeEnabled(item.user); + mBgAllAppsList.add(new AppInfo(info, item.user, quietMode), info); + } + } mBgAllAppsList.getAndResetChangeFlag(); return allActivityList; diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java index ef7e828ff1..4efeba5b0f 100644 --- a/src/com/android/launcher3/model/ModelUtils.java +++ b/src/com/android/launcher3/model/ModelUtils.java @@ -19,11 +19,14 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherSettings; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import java.util.List; +import java.util.stream.IntStream; /** * Utils class for {@link com.android.launcher3.LauncherModel}. @@ -109,4 +112,17 @@ public class ModelUtils { } }); } + + /** + * Iterates though current workspace items and returns available hotseat ranks for prediction. + */ + public static IntArray getMissingHotseatRanks(List items, int len) { + IntSet seen = new IntSet(); + items.stream().filter( + info -> info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) + .forEach(i -> seen.add(i.screenId)); + IntArray result = new IntArray(len); + IntStream.range(0, len).filter(i -> !seen.contains(i)).forEach(result::add); + return result; + } } diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java new file mode 100644 index 0000000000..6aa41eb760 --- /dev/null +++ b/src/com/android/launcher3/model/PredictionModel.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 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.model; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.UserHandle; + +import com.android.launcher3.Utilities; +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Model helper for app predictions in workspace + */ +public class PredictionModel { + private static final String CACHED_ITEMS_KEY = "predicted_item_keys"; + private static final int MAX_CACHE_ITEMS = 5; + + private final Context mContext; + private final SharedPreferences mDevicePrefs; + private ArrayList mCachedComponentKeys; + + public PredictionModel(Context context) { + mContext = context; + mDevicePrefs = Utilities.getDevicePrefs(mContext); + } + + /** + * Formats and stores a list of component key in device preferences. + */ + public void cachePredictionComponentKeys(List componentKeys) { + StringBuilder builder = new StringBuilder(); + int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS); + for (int i = 0; i < count; i++) { + builder.append(componentKeys.get(i)); + builder.append("\n"); + } + mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply(); + mCachedComponentKeys = null; + } + + /** + * parses and returns ComponentKeys saved by + * {@link PredictionModel#cachePredictionComponentKeys(List)} + */ + public List getPredictionComponentKeys() { + if (mCachedComponentKeys == null) { + mCachedComponentKeys = new ArrayList<>(); + + String cachedBlob = mDevicePrefs.getString(CACHED_ITEMS_KEY, ""); + for (String line : cachedBlob.split("\n")) { + ComponentKey key = ComponentKey.fromString(line); + if (key != null) { + mCachedComponentKeys.add(key); + } + } + } + return mCachedComponentKeys; + } + + /** + * Remove uninstalled applications from model + */ + public void removePackage(String pkgName, UserHandle user, ArrayList ids) { + for (int i = ids.size() - 1; i >= 0; i--) { + AppInfo info = ids.get(i); + if (info.user.equals(user) && pkgName.equals(info.componentName.getPackageName())) { + ids.remove(i); + } + } + cachePredictionComponentKeys(getPredictionComponentKeys().stream() + .filter(cn -> !(cn.user.equals(user) && cn.componentName.getPackageName().equals( + pkgName))).collect(Collectors.toList())); + } +} diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index dd6fc49426..4a15af1d44 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -195,6 +195,9 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity @Override public void bindItems(List shortcuts, boolean forceAnimateIcons) { } + @Override + public void bindPredictedItems(List appInfos, IntArray ranks) { } + @Override public void bindScreens(IntArray orderedScreenIds) { }