• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.model;
17 
18 import static android.text.format.DateUtils.DAY_IN_MILLIS;
19 import static android.text.format.DateUtils.formatElapsedTime;
20 
21 import static com.android.launcher3.LauncherPrefs.getDevicePrefs;
22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
24 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
26 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
27 import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle;
28 import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo;
29 import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation;
30 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
31 
32 import static java.util.stream.Collectors.toCollection;
33 
34 import android.app.StatsManager;
35 import android.app.prediction.AppPredictionContext;
36 import android.app.prediction.AppPredictionManager;
37 import android.app.prediction.AppPredictor;
38 import android.app.prediction.AppTarget;
39 import android.app.prediction.AppTargetEvent;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.SharedPreferences;
43 import android.content.pm.LauncherActivityInfo;
44 import android.content.pm.LauncherApps;
45 import android.content.pm.ShortcutInfo;
46 import android.os.Bundle;
47 import android.os.UserHandle;
48 import android.util.Log;
49 import android.util.StatsEvent;
50 
51 import androidx.annotation.Nullable;
52 import androidx.annotation.WorkerThread;
53 
54 import com.android.launcher3.InvariantDeviceProfile;
55 import com.android.launcher3.LauncherAppState;
56 import com.android.launcher3.logger.LauncherAtom;
57 import com.android.launcher3.logging.InstanceId;
58 import com.android.launcher3.logging.InstanceIdSequence;
59 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
60 import com.android.launcher3.model.data.AppInfo;
61 import com.android.launcher3.model.data.FolderInfo;
62 import com.android.launcher3.model.data.ItemInfo;
63 import com.android.launcher3.model.data.WorkspaceItemInfo;
64 import com.android.launcher3.shortcuts.ShortcutKey;
65 import com.android.launcher3.util.IntSparseArrayMap;
66 import com.android.launcher3.util.PersistedItemArray;
67 import com.android.quickstep.logging.SettingsChangeLogger;
68 import com.android.quickstep.logging.StatsLogCompatManager;
69 import com.android.systemui.shared.system.SysUiStatsLog;
70 
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Objects;
76 import java.util.stream.IntStream;
77 
78 /**
79  * Model delegate which loads prediction items
80  */
81 public class QuickstepModelDelegate extends ModelDelegate {
82 
83     public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
84     private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS";
85     private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
86     private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
87 
88     private static final boolean IS_DEBUG = false;
89     private static final String TAG = "QuickstepModelDelegate";
90 
91     private final PredictorState mAllAppsState =
92             new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
93     private final PredictorState mHotseatState =
94             new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
95     private final PredictorState mWidgetsRecommendationState =
96             new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");
97 
98     private final InvariantDeviceProfile mIDP;
99     private final AppEventProducer mAppEventProducer;
100     private final StatsManager mStatsManager;
101     private final Context mContext;
102 
103     protected boolean mActive = false;
104 
QuickstepModelDelegate(Context context)105     public QuickstepModelDelegate(Context context) {
106         mContext = context;
107         mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
108 
109         mIDP = InvariantDeviceProfile.INSTANCE.get(context);
110         StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
111         mStatsManager = context.getSystemService(StatsManager.class);
112     }
113 
114     @Override
loadHotseatItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts)115     public void loadHotseatItems(UserManagerState ums,
116             Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
117         // TODO: Implement caching and preloading
118         super.loadHotseatItems(ums, pinnedShortcuts);
119 
120         WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts,
121                 mIDP.numDatabaseHotseatIcons, mHotseatState.containerId);
122         FixedContainerItems hotseatItems = new FixedContainerItems(mHotseatState.containerId,
123                 mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get));
124         mDataModel.extraItems.put(mHotseatState.containerId, hotseatItems);
125     }
126 
127     @Override
loadAllAppsItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts)128     public void loadAllAppsItems(UserManagerState ums,
129             Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
130         // TODO: Implement caching and preloading
131         super.loadAllAppsItems(ums, pinnedShortcuts);
132 
133         WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts,
134                 mIDP.numDatabaseAllAppsColumns, mAllAppsState.containerId);
135         FixedContainerItems allAppsPredictionItems = new FixedContainerItems(
136                 mAllAppsState.containerId, mAllAppsState.storage.read(mApp.getContext(),
137                 allAppsFactory, ums.allUsers::get));
138         mDataModel.extraItems.put(mAllAppsState.containerId, allAppsPredictionItems);
139     }
140 
141     @Override
loadWidgetsRecommendationItems()142     public void loadWidgetsRecommendationItems() {
143         // TODO: Implement caching and preloading
144         super.loadWidgetsRecommendationItems();
145 
146         // Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
147         mDataModel.extraItems.put(mWidgetsRecommendationState.containerId,
148                 new FixedContainerItems(mWidgetsRecommendationState.containerId,
149                         new ArrayList<>()));
150     }
151 
152     @Override
markActive()153     public void markActive() {
154         super.markActive();
155         mActive = true;
156     }
157 
158     @Override
workspaceLoadComplete()159     public void workspaceLoadComplete() {
160         super.workspaceLoadComplete();
161         recreatePredictors();
162     }
163 
164     @Override
165     @WorkerThread
modelLoadComplete()166     public void modelLoadComplete() {
167         super.modelLoadComplete();
168 
169         // Log snapshot of the model
170         SharedPreferences prefs = getDevicePrefs(mApp.getContext());
171         long lastSnapshotTimeMillis = prefs.getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
172         // Log snapshot only if previous snapshot was older than a day
173         long now = System.currentTimeMillis();
174         if (now - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
175             if (IS_DEBUG) {
176                 String elapsedTime = formatElapsedTime((now - lastSnapshotTimeMillis) / 1000);
177                 Log.d(TAG, String.format(
178                         "Skipped snapshot logging since previous snapshot was %s old.",
179                         elapsedTime));
180             }
181         } else {
182             IntSparseArrayMap<ItemInfo> itemsIdMap;
183             synchronized (mDataModel) {
184                 itemsIdMap = mDataModel.itemsIdMap.clone();
185             }
186             InstanceId instanceId = new InstanceIdSequence().newInstanceId();
187             for (ItemInfo info : itemsIdMap) {
188                 FolderInfo parent = getContainer(info, itemsIdMap);
189                 StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
190             }
191             additionalSnapshotEvents(instanceId);
192             prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply();
193         }
194 
195         // Only register for launcher snapshot logging if this is the primary ModelDelegate
196         // instance, as there will be additional instances that may be destroyed at any time.
197         if (mIsPrimaryInstance) {
198             registerSnapshotLoggingCallback();
199         }
200     }
201 
additionalSnapshotEvents(InstanceId snapshotInstanceId)202     protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
203 
204     /**
205      * Registers a callback to log launcher workspace layout using Statsd pulled atom.
206      */
registerSnapshotLoggingCallback()207     protected void registerSnapshotLoggingCallback() {
208         if (mStatsManager == null) {
209             Log.d(TAG, "Failed to get StatsManager");
210         }
211 
212         try {
213             mStatsManager.setPullAtomCallback(
214                     SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT,
215                     null /* PullAtomMetadata */,
216                     MODEL_EXECUTOR,
217                     (i, eventList) -> {
218                         InstanceId instanceId = new InstanceIdSequence().newInstanceId();
219                         IntSparseArrayMap<ItemInfo> itemsIdMap;
220                         synchronized (mDataModel) {
221                             itemsIdMap = mDataModel.itemsIdMap.clone();
222                         }
223 
224                         for (ItemInfo info : itemsIdMap) {
225                             FolderInfo parent = getContainer(info, itemsIdMap);
226                             LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
227                             Log.d(TAG, itemInfo.toString());
228                             StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
229                                     instanceId);
230                             eventList.add(statsEvent);
231                         }
232                         Log.d(TAG,
233                                 String.format(
234                                         "Successfully logged %d workspace items with instanceId=%d",
235                                         itemsIdMap.size(), instanceId.getId()));
236                         additionalSnapshotEvents(instanceId);
237                         SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId);
238                         return StatsManager.PULL_SUCCESS;
239                     }
240             );
241             Log.d(TAG, "Successfully registered for launcher snapshot logging!");
242         } catch (RuntimeException e) {
243             Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager",
244                     e);
245         }
246     }
247 
getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap)248     private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) {
249         if (info.container > 0) {
250             ItemInfo containerInfo = itemsIdMap.get(info.container);
251 
252             if (!(containerInfo instanceof FolderInfo)) {
253                 Log.e(TAG, String.format(
254                         "Item info: %s found with invalid container: %s",
255                         info,
256                         containerInfo));
257             }
258             // Allow crash to help debug b/173838775
259             return (FolderInfo) containerInfo;
260         }
261         return null;
262     }
263 
264     @Override
validateData()265     public void validateData() {
266         super.validateData();
267         if (mAllAppsState.predictor != null) {
268             mAllAppsState.predictor.requestPredictionUpdate();
269         }
270         if (mWidgetsRecommendationState.predictor != null) {
271             mWidgetsRecommendationState.predictor.requestPredictionUpdate();
272         }
273     }
274 
275     @Override
destroy()276     public void destroy() {
277         super.destroy();
278         mActive = false;
279         StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
280         if (mIsPrimaryInstance) {
281             mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT);
282         }
283         destroyPredictors();
284     }
285 
destroyPredictors()286     private void destroyPredictors() {
287         mAllAppsState.destroyPredictor();
288         mHotseatState.destroyPredictor();
289         mWidgetsRecommendationState.destroyPredictor();
290     }
291 
292     @WorkerThread
recreatePredictors()293     private void recreatePredictors() {
294         destroyPredictors();
295         if (!mActive) {
296             return;
297         }
298         Context context = mApp.getContext();
299         AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
300         if (apm == null) {
301             return;
302         }
303 
304         registerPredictor(mAllAppsState, apm.createAppPredictionSession(
305                 new AppPredictionContext.Builder(context)
306                         .setUiSurface("home")
307                         .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns)
308                         .build()));
309 
310         // TODO: get bundle
311         registerPredictor(mHotseatState, apm.createAppPredictionSession(
312                 new AppPredictionContext.Builder(context)
313                         .setUiSurface("hotseat")
314                         .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
315                         .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
316                         .build()));
317 
318         registerWidgetsPredictor(apm.createAppPredictionSession(
319                 new AppPredictionContext.Builder(context)
320                         .setUiSurface("widgets")
321                         .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel))
322                         .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
323                         .build()));
324     }
325 
registerPredictor(PredictorState state, AppPredictor predictor)326     private void registerPredictor(PredictorState state, AppPredictor predictor) {
327         state.setTargets(Collections.emptyList());
328         state.predictor = predictor;
329         state.predictor.registerPredictionUpdates(
330                 MODEL_EXECUTOR, t -> handleUpdate(state, t));
331         state.predictor.requestPredictionUpdate();
332     }
333 
handleUpdate(PredictorState state, List<AppTarget> targets)334     private void handleUpdate(PredictorState state, List<AppTarget> targets) {
335         if (state.setTargets(targets)) {
336             // No diff, skip
337             return;
338         }
339         mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
340     }
341 
registerWidgetsPredictor(AppPredictor predictor)342     private void registerWidgetsPredictor(AppPredictor predictor) {
343         mWidgetsRecommendationState.predictor = predictor;
344         mWidgetsRecommendationState.predictor.registerPredictionUpdates(
345                 MODEL_EXECUTOR, targets -> {
346                     if (mWidgetsRecommendationState.setTargets(targets)) {
347                         // No diff, skip
348                         return;
349                     }
350                     mApp.getModel().enqueueModelUpdateTask(
351                             new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
352                 });
353         mWidgetsRecommendationState.predictor.requestPredictionUpdate();
354     }
355 
onAppTargetEvent(AppTargetEvent event, int client)356     private void onAppTargetEvent(AppTargetEvent event, int client) {
357         PredictorState state;
358         switch(client) {
359             case CONTAINER_PREDICTION:
360                 state = mAllAppsState;
361                 break;
362             case CONTAINER_WIDGETS_PREDICTION:
363                 state = mWidgetsRecommendationState;
364                 break;
365             case CONTAINER_HOTSEAT_PREDICTION:
366             default:
367                 state = mHotseatState;
368                 break;
369         }
370         if (state.predictor != null) {
371             state.predictor.notifyAppTargetEvent(event);
372             Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction()
373                     + " launchLocation=" + event.getLaunchLocation());
374         }
375     }
376 
getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel)377     private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) {
378         Bundle bundle = new Bundle();
379         ArrayList<AppTargetEvent> widgetEvents =
380                 dataModel.getAllWorkspaceItems().stream()
381                         .filter(PredictionHelper::isTrackedForWidgetPrediction)
382                         .map(item -> {
383                             AppTarget target = getAppTargetFromItemInfo(context, item);
384                             if (target == null) return null;
385                             return wrapAppTargetWithItemLocation(
386                                     target, AppTargetEvent.ACTION_PIN, item);
387                         })
388                         .filter(Objects::nonNull)
389                         .collect(toCollection(ArrayList::new));
390         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents);
391         return bundle;
392     }
393 
394     static class PredictorState {
395 
396         public final int containerId;
397         public final PersistedItemArray<ItemInfo> storage;
398         public AppPredictor predictor;
399 
400         private List<AppTarget> mLastTargets;
401 
PredictorState(int containerId, String storageName)402         PredictorState(int containerId, String storageName) {
403             this.containerId = containerId;
404             storage = new PersistedItemArray<>(storageName);
405             mLastTargets = Collections.emptyList();
406         }
407 
destroyPredictor()408         public void destroyPredictor() {
409             if (predictor != null) {
410                 predictor.destroy();
411                 predictor = null;
412             }
413         }
414 
415         /**
416          * Sets the new targets and returns true if it was the same as before.
417          */
setTargets(List<AppTarget> newTargets)418         boolean setTargets(List<AppTarget> newTargets) {
419             List<AppTarget> oldTargets = mLastTargets;
420             mLastTargets = newTargets;
421 
422             int size = oldTargets.size();
423             return size == newTargets.size() && IntStream.range(0, size)
424                     .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i)));
425         }
426     }
427 
428     /**
429      * Compares two targets for the properties which we care about
430      */
areAppTargetsSame(AppTarget t1, AppTarget t2)431     private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) {
432         if (!Objects.equals(t1.getPackageName(), t2.getPackageName())
433                 || !Objects.equals(t1.getUser(), t2.getUser())
434                 || !Objects.equals(t1.getClassName(), t2.getClassName())) {
435             return false;
436         }
437 
438         ShortcutInfo s1 = t1.getShortcutInfo();
439         ShortcutInfo s2 = t2.getShortcutInfo();
440         if (s1 != null) {
441             if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) {
442                 return false;
443             }
444         } else if (s2 != null) {
445             return false;
446         }
447         return true;
448     }
449 
450     private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
451 
452         private final LauncherAppState mAppState;
453         private final UserManagerState mUMS;
454         private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
455         private final int mMaxCount;
456         private final int mContainer;
457 
458         private int mReadCount = 0;
459 
WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount, int container)460         protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
461                 Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount, int container) {
462             mAppState = appState;
463             mUMS = ums;
464             mPinnedShortcuts = pinnedShortcuts;
465             mMaxCount = maxCount;
466             mContainer = container;
467         }
468 
469         @Nullable
470         @Override
createInfo(int itemType, UserHandle user, Intent intent)471         public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) {
472             if (mReadCount >= mMaxCount) {
473                 return null;
474             }
475             switch (itemType) {
476                 case ITEM_TYPE_APPLICATION: {
477                     LauncherActivityInfo lai = mAppState.getContext()
478                             .getSystemService(LauncherApps.class)
479                             .resolveActivity(intent, user);
480                     if (lai == null) {
481                         return null;
482                     }
483                     AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
484                     info.container = mContainer;
485                     mAppState.getIconCache().getTitleAndIcon(info, lai, false);
486                     mReadCount++;
487                     return info.makeWorkspaceItem(mAppState.getContext());
488                 }
489                 case ITEM_TYPE_DEEP_SHORTCUT: {
490                     ShortcutKey key = ShortcutKey.fromIntent(intent, user);
491                     if (key == null) {
492                         return null;
493                     }
494                     ShortcutInfo si = mPinnedShortcuts.get(key);
495                     if (si == null) {
496                         return null;
497                     }
498                     WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
499                     wii.container = mContainer;
500                     mAppState.getIconCache().getShortcutIcon(wii, si);
501                     mReadCount++;
502                     return wii;
503                 }
504             }
505             return null;
506         }
507     }
508 }
509