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.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
24 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
26 import static com.android.launcher3.Utilities.getDevicePrefs;
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
115     @WorkerThread
loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts)116     public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
117         // TODO: Implement caching and preloading
118         super.loadItems(ums, pinnedShortcuts);
119 
120         WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(
121                 mApp, ums, pinnedShortcuts, mIDP.numDatabaseAllAppsColumns);
122         mAllAppsState.items.setItems(
123                 mAllAppsState.storage.read(mApp.getContext(), allAppsFactory, ums.allUsers::get));
124         mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items);
125 
126         WorkspaceItemFactory hotseatFactory =
127                 new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numDatabaseHotseatIcons);
128         mHotseatState.items.setItems(
129                 mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get));
130         mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items);
131 
132         // Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
133         mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items);
134         mActive = true;
135     }
136 
137     @Override
workspaceLoadComplete()138     public void workspaceLoadComplete() {
139         super.workspaceLoadComplete();
140         recreatePredictors();
141     }
142 
143     @Override
144     @WorkerThread
modelLoadComplete()145     public void modelLoadComplete() {
146         super.modelLoadComplete();
147 
148         // Log snapshot of the model
149         SharedPreferences prefs = getDevicePrefs(mApp.getContext());
150         long lastSnapshotTimeMillis = prefs.getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
151         // Log snapshot only if previous snapshot was older than a day
152         long now = System.currentTimeMillis();
153         if (now - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
154             if (IS_DEBUG) {
155                 String elapsedTime = formatElapsedTime((now - lastSnapshotTimeMillis) / 1000);
156                 Log.d(TAG, String.format(
157                         "Skipped snapshot logging since previous snapshot was %s old.",
158                         elapsedTime));
159             }
160         } else {
161             IntSparseArrayMap<ItemInfo> itemsIdMap;
162             synchronized (mDataModel) {
163                 itemsIdMap = mDataModel.itemsIdMap.clone();
164             }
165             InstanceId instanceId = new InstanceIdSequence().newInstanceId();
166             for (ItemInfo info : itemsIdMap) {
167                 FolderInfo parent = getContainer(info, itemsIdMap);
168                 StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
169             }
170             additionalSnapshotEvents(instanceId);
171             prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply();
172         }
173 
174         // Only register for launcher snapshot logging if this is the primary ModelDelegate
175         // instance, as there will be additional instances that may be destroyed at any time.
176         if (mIsPrimaryInstance) {
177             registerSnapshotLoggingCallback();
178         }
179     }
180 
additionalSnapshotEvents(InstanceId snapshotInstanceId)181     protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
182 
183     /**
184      * Registers a callback to log launcher workspace layout using Statsd pulled atom.
185      */
registerSnapshotLoggingCallback()186     protected void registerSnapshotLoggingCallback() {
187         if (mStatsManager == null) {
188             Log.d(TAG, "Failed to get StatsManager");
189         }
190 
191         try {
192             mStatsManager.setPullAtomCallback(
193                     SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT,
194                     null /* PullAtomMetadata */,
195                     MODEL_EXECUTOR,
196                     (i, eventList) -> {
197                         InstanceId instanceId = new InstanceIdSequence().newInstanceId();
198                         IntSparseArrayMap<ItemInfo> itemsIdMap;
199                         synchronized (mDataModel) {
200                             itemsIdMap = mDataModel.itemsIdMap.clone();
201                         }
202 
203                         for (ItemInfo info : itemsIdMap) {
204                             FolderInfo parent = getContainer(info, itemsIdMap);
205                             LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
206                             Log.d(TAG, itemInfo.toString());
207                             StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
208                                     instanceId);
209                             eventList.add(statsEvent);
210                         }
211                         Log.d(TAG,
212                                 String.format(
213                                         "Successfully logged %d workspace items with instanceId=%d",
214                                         itemsIdMap.size(), instanceId.getId()));
215                         additionalSnapshotEvents(instanceId);
216                         SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId);
217                         return StatsManager.PULL_SUCCESS;
218                     }
219             );
220             Log.d(TAG, "Successfully registered for launcher snapshot logging!");
221         } catch (RuntimeException e) {
222             Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager",
223                     e);
224         }
225     }
226 
getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap)227     private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) {
228         if (info.container > 0) {
229             ItemInfo containerInfo = itemsIdMap.get(info.container);
230 
231             if (!(containerInfo instanceof FolderInfo)) {
232                 Log.e(TAG, String.format(
233                         "Item info: %s found with invalid container: %s",
234                         info,
235                         containerInfo));
236             } else {
237                 return (FolderInfo) containerInfo;
238             }
239         }
240         return null;
241     }
242 
243     @Override
validateData()244     public void validateData() {
245         super.validateData();
246         if (mAllAppsState.predictor != null) {
247             mAllAppsState.predictor.requestPredictionUpdate();
248         }
249         if (mWidgetsRecommendationState.predictor != null) {
250             mWidgetsRecommendationState.predictor.requestPredictionUpdate();
251         }
252     }
253 
254     @Override
destroy()255     public void destroy() {
256         super.destroy();
257         mActive = false;
258         StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
259         if (mIsPrimaryInstance) {
260             mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT);
261         }
262         destroyPredictors();
263     }
264 
destroyPredictors()265     private void destroyPredictors() {
266         mAllAppsState.destroyPredictor();
267         mHotseatState.destroyPredictor();
268         mWidgetsRecommendationState.destroyPredictor();
269     }
270 
271     @WorkerThread
recreatePredictors()272     private void recreatePredictors() {
273         destroyPredictors();
274         if (!mActive) {
275             return;
276         }
277         Context context = mApp.getContext();
278         AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
279         if (apm == null) {
280             return;
281         }
282 
283         registerPredictor(mAllAppsState, apm.createAppPredictionSession(
284                 new AppPredictionContext.Builder(context)
285                         .setUiSurface("home")
286                         .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns)
287                         .build()));
288 
289         // TODO: get bundle
290         registerPredictor(mHotseatState, apm.createAppPredictionSession(
291                 new AppPredictionContext.Builder(context)
292                         .setUiSurface("hotseat")
293                         .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons)
294                         .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
295                         .build()));
296 
297         registerWidgetsPredictor(apm.createAppPredictionSession(
298                 new AppPredictionContext.Builder(context)
299                         .setUiSurface("widgets")
300                         .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel))
301                         .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
302                         .build()));
303     }
304 
registerPredictor(PredictorState state, AppPredictor predictor)305     private void registerPredictor(PredictorState state, AppPredictor predictor) {
306         state.predictor = predictor;
307         state.predictor.registerPredictionUpdates(
308                 MODEL_EXECUTOR, t -> handleUpdate(state, t));
309         state.predictor.requestPredictionUpdate();
310     }
311 
handleUpdate(PredictorState state, List<AppTarget> targets)312     private void handleUpdate(PredictorState state, List<AppTarget> targets) {
313         if (state.setTargets(targets)) {
314             // No diff, skip
315             return;
316         }
317         mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
318     }
319 
registerWidgetsPredictor(AppPredictor predictor)320     private void registerWidgetsPredictor(AppPredictor predictor) {
321         mWidgetsRecommendationState.predictor = predictor;
322         mWidgetsRecommendationState.predictor.registerPredictionUpdates(
323                 MODEL_EXECUTOR, targets -> {
324                     if (mWidgetsRecommendationState.setTargets(targets)) {
325                         // No diff, skip
326                         return;
327                     }
328                     mApp.getModel().enqueueModelUpdateTask(
329                             new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
330                 });
331         mWidgetsRecommendationState.predictor.requestPredictionUpdate();
332     }
333 
onAppTargetEvent(AppTargetEvent event, int client)334     private void onAppTargetEvent(AppTargetEvent event, int client) {
335         PredictorState state;
336         switch(client) {
337             case CONTAINER_PREDICTION:
338                 state = mAllAppsState;
339                 break;
340             case CONTAINER_WIDGETS_PREDICTION:
341                 state = mWidgetsRecommendationState;
342                 break;
343             case CONTAINER_HOTSEAT_PREDICTION:
344             default:
345                 state = mHotseatState;
346                 break;
347         }
348         if (state.predictor != null) {
349             state.predictor.notifyAppTargetEvent(event);
350             Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction()
351                     + " launchLocation=" + event.getLaunchLocation());
352         }
353     }
354 
getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel)355     private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) {
356         Bundle bundle = new Bundle();
357         ArrayList<AppTargetEvent> widgetEvents =
358                 dataModel.getAllWorkspaceItems().stream()
359                         .filter(PredictionHelper::isTrackedForWidgetPrediction)
360                         .map(item -> {
361                             AppTarget target = getAppTargetFromItemInfo(context, item);
362                             if (target == null) return null;
363                             return wrapAppTargetWithItemLocation(
364                                     target, AppTargetEvent.ACTION_PIN, item);
365                         })
366                         .filter(Objects::nonNull)
367                         .collect(toCollection(ArrayList::new));
368         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents);
369         return bundle;
370     }
371 
372     static class PredictorState {
373 
374         public final FixedContainerItems items;
375         public final PersistedItemArray<ItemInfo> storage;
376         public AppPredictor predictor;
377 
378         private List<AppTarget> mLastTargets;
379 
PredictorState(int container, String storageName)380         PredictorState(int container, String storageName) {
381             items = new FixedContainerItems(container);
382             storage = new PersistedItemArray<>(storageName);
383             mLastTargets = Collections.emptyList();
384         }
385 
destroyPredictor()386         public void destroyPredictor() {
387             if (predictor != null) {
388                 predictor.destroy();
389                 predictor = null;
390             }
391         }
392 
393         /**
394          * Sets the new targets and returns true if it was the same as before.
395          */
setTargets(List<AppTarget> newTargets)396         boolean setTargets(List<AppTarget> newTargets) {
397             List<AppTarget> oldTargets = mLastTargets;
398             mLastTargets = newTargets;
399 
400             int size = oldTargets.size();
401             return size == newTargets.size() && IntStream.range(0, size)
402                     .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i)));
403         }
404     }
405 
406     /**
407      * Compares two targets for the properties which we care about
408      */
areAppTargetsSame(AppTarget t1, AppTarget t2)409     private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) {
410         if (!Objects.equals(t1.getPackageName(), t2.getPackageName())
411                 || !Objects.equals(t1.getUser(), t2.getUser())
412                 || !Objects.equals(t1.getClassName(), t2.getClassName())) {
413             return false;
414         }
415 
416         ShortcutInfo s1 = t1.getShortcutInfo();
417         ShortcutInfo s2 = t2.getShortcutInfo();
418         if (s1 != null) {
419             if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) {
420                 return false;
421             }
422         } else if (s2 != null) {
423             return false;
424         }
425         return true;
426     }
427 
428     private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
429 
430         private final LauncherAppState mAppState;
431         private final UserManagerState mUMS;
432         private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
433         private final int mMaxCount;
434 
435         private int mReadCount = 0;
436 
WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount)437         protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
438                 Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount) {
439             mAppState = appState;
440             mUMS = ums;
441             mPinnedShortcuts = pinnedShortcuts;
442             mMaxCount = maxCount;
443         }
444 
445         @Nullable
446         @Override
createInfo(int itemType, UserHandle user, Intent intent)447         public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) {
448             if (mReadCount >= mMaxCount) {
449                 return null;
450             }
451             switch (itemType) {
452                 case ITEM_TYPE_APPLICATION: {
453                     LauncherActivityInfo lai = mAppState.getContext()
454                             .getSystemService(LauncherApps.class)
455                             .resolveActivity(intent, user);
456                     if (lai == null) {
457                         return null;
458                     }
459                     AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
460                     mAppState.getIconCache().getTitleAndIcon(info, lai, false);
461                     mReadCount++;
462                     return info.makeWorkspaceItem();
463                 }
464                 case ITEM_TYPE_DEEP_SHORTCUT: {
465                     ShortcutKey key = ShortcutKey.fromIntent(intent, user);
466                     if (key == null) {
467                         return null;
468                     }
469                     ShortcutInfo si = mPinnedShortcuts.get(key);
470                     if (si == null) {
471                         return null;
472                     }
473                     WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
474                     mAppState.getIconCache().getShortcutIcon(wii, si);
475                     mReadCount++;
476                     return wii;
477                 }
478             }
479             return null;
480         }
481     }
482 }
483