• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.app.prediction.AppTargetEvent.ACTION_DISMISS;
19 import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
20 import static android.app.prediction.AppTargetEvent.ACTION_PIN;
21 import static android.app.prediction.AppTargetEvent.ACTION_UNDISMISS;
22 import static android.app.prediction.AppTargetEvent.ACTION_UNPIN;
23 
24 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
25 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
26 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
27 import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO;
30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
32 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED;
33 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
34 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE;
35 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
36 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED;
37 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
38 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
39 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
40 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
41 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
42 import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction;
43 import static com.android.launcher3.model.PredictionHelper.isTrackedForWidgetPrediction;
44 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
45 
46 import android.annotation.TargetApi;
47 import android.app.prediction.AppTarget;
48 import android.app.prediction.AppTargetEvent;
49 import android.app.prediction.AppTargetId;
50 import android.content.ComponentName;
51 import android.content.Context;
52 import android.content.pm.ShortcutInfo;
53 import android.os.Build;
54 import android.os.Handler;
55 import android.os.Message;
56 import android.os.Process;
57 import android.os.SystemClock;
58 import android.os.UserHandle;
59 import android.text.TextUtils;
60 
61 import androidx.annotation.AnyThread;
62 import androidx.annotation.Nullable;
63 import androidx.annotation.WorkerThread;
64 
65 import com.android.launcher3.Utilities;
66 import com.android.launcher3.logger.LauncherAtom;
67 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
68 import com.android.launcher3.logger.LauncherAtom.FolderContainer;
69 import com.android.launcher3.logger.LauncherAtom.HotseatContainer;
70 import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer;
71 import com.android.launcher3.logging.StatsLogManager.EventEnum;
72 import com.android.launcher3.pm.UserCache;
73 import com.android.launcher3.shortcuts.ShortcutRequest;
74 import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
75 
76 import java.util.Locale;
77 import java.util.Optional;
78 import java.util.function.ObjIntConsumer;
79 import java.util.function.Predicate;
80 
81 /**
82  * Utility class to track stats log and emit corresponding app events
83  */
84 @TargetApi(Build.VERSION_CODES.R)
85 public class AppEventProducer implements StatsLogConsumer {
86 
87     private static final int MSG_LAUNCH = 0;
88 
89     private final Context mContext;
90     private final Handler mMessageHandler;
91     private final ObjIntConsumer<AppTargetEvent> mCallback;
92 
93     private LauncherAtom.ItemInfo mLastDragItem;
94 
AppEventProducer(Context context, ObjIntConsumer<AppTargetEvent> callback)95     public AppEventProducer(Context context, ObjIntConsumer<AppTargetEvent> callback) {
96         mContext = context;
97         mMessageHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleMessage);
98         mCallback = callback;
99     }
100 
101     @WorkerThread
handleMessage(Message msg)102     private boolean handleMessage(Message msg) {
103         switch (msg.what) {
104             case MSG_LAUNCH: {
105                 mCallback.accept((AppTargetEvent) msg.obj, msg.arg1);
106                 return true;
107             }
108         }
109         return false;
110     }
111 
112     @AnyThread
sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId, int targetPredictor)113     private void sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId, int targetPredictor) {
114         sendEvent(toAppTarget(atomInfo), atomInfo, eventId, targetPredictor);
115     }
116 
117     @AnyThread
sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId, int targetPredictor)118     private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId,
119             int targetPredictor) {
120         // TODO: remove the running test check when b/231648228 is fixed.
121         if (target != null && !Utilities.isRunningInTestHarness()) {
122             AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
123                     .setLaunchLocation(getContainer(locationInfo))
124                     .build();
125             mMessageHandler.obtainMessage(MSG_LAUNCH, targetPredictor, 0, event).sendToTarget();
126         }
127     }
128 
129     @Override
consume(EventEnum event, LauncherAtom.ItemInfo atomInfo)130     public void consume(EventEnum event, LauncherAtom.ItemInfo atomInfo) {
131         if (event == LAUNCHER_APP_LAUNCH_TAP
132                 || event == LAUNCHER_TASK_LAUNCH_SWIPE_DOWN
133                 || event == LAUNCHER_TASK_LAUNCH_TAP
134                 || event == LAUNCHER_QUICKSWITCH_RIGHT
135                 || event == LAUNCHER_QUICKSWITCH_LEFT) {
136             sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION);
137         } else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST) {
138             sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_PREDICTION);
139         } else if (event == LAUNCHER_ITEM_DRAG_STARTED) {
140             mLastDragItem = atomInfo;
141         } else if (event == LAUNCHER_ITEM_DROP_COMPLETED) {
142             if (mLastDragItem == null) {
143                 return;
144             }
145             if (isTrackedForHotseatPrediction(mLastDragItem)) {
146                 sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
147             }
148             if (isTrackedForHotseatPrediction(atomInfo)) {
149                 sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
150             }
151             if (isTrackedForWidgetPrediction(atomInfo)) {
152                 sendEvent(atomInfo, ACTION_PIN, CONTAINER_WIDGETS_PREDICTION);
153             }
154             mLastDragItem = null;
155         } else if (event == LAUNCHER_ITEM_DROP_FOLDER_CREATED) {
156             if (isTrackedForHotseatPrediction(atomInfo)) {
157                 sendEvent(createTempFolderTarget(), atomInfo, ACTION_PIN,
158                         CONTAINER_HOTSEAT_PREDICTION);
159                 sendEvent(atomInfo, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
160             }
161         } else if (event == LAUNCHER_FOLDER_CONVERTED_TO_ICON) {
162             if (isTrackedForHotseatPrediction(atomInfo)) {
163                 sendEvent(createTempFolderTarget(), atomInfo, ACTION_UNPIN,
164                         CONTAINER_HOTSEAT_PREDICTION);
165                 sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
166             }
167         } else if (event == LAUNCHER_ITEM_DROPPED_ON_REMOVE) {
168             if (mLastDragItem != null && isTrackedForHotseatPrediction(mLastDragItem)) {
169                 sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
170             }
171             if (mLastDragItem != null && isTrackedForWidgetPrediction(mLastDragItem)) {
172                 sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_WIDGETS_PREDICTION);
173             }
174         } else if (event == LAUNCHER_HOTSEAT_PREDICTION_PINNED) {
175             if (isTrackedForHotseatPrediction(atomInfo)) {
176                 sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
177             }
178         } else if (event == LAUNCHER_ONRESUME) {
179             AppTarget target = new AppTarget.Builder(new AppTargetId("launcher:launcher"),
180                     mContext.getPackageName(), Process.myUserHandle())
181                     .build();
182             sendEvent(target, atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION);
183         } else if (event == LAUNCHER_DISMISS_PREDICTION_UNDO) {
184             sendEvent(atomInfo, ACTION_UNDISMISS, CONTAINER_HOTSEAT_PREDICTION);
185         }
186     }
187 
188     @Nullable
toAppTarget(LauncherAtom.ItemInfo info)189     private AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
190         UserHandle userHandle = Process.myUserHandle();
191         if (info.getIsWork()) {
192             userHandle = UserCache.INSTANCE.get(mContext).getUserProfiles().stream()
193                     .filter(((Predicate<UserHandle>) userHandle::equals).negate())
194                     .findAny()
195                     .orElse(null);
196         }
197         if (userHandle == null) {
198             return null;
199         }
200         ComponentName cn = null;
201         ShortcutInfo shortcutInfo = null;
202         String id = null;
203 
204         switch (info.getItemCase()) {
205             case APPLICATION: {
206                 LauncherAtom.Application app = info.getApplication();
207                 if ((cn = parseNullable(app.getComponentName())) != null) {
208                     id = "app:" + cn.getPackageName();
209                 }
210                 break;
211             }
212             case SHORTCUT: {
213                 LauncherAtom.Shortcut si = info.getShortcut();
214                 if (!TextUtils.isEmpty(si.getShortcutId())
215                         && (cn = parseNullable(si.getShortcutName())) != null) {
216                     Optional<ShortcutInfo> opt = new ShortcutRequest(mContext,
217                             userHandle).forPackage(cn.getPackageName(), si.getShortcutId()).query(
218                             ShortcutRequest.ALL).stream().findFirst();
219                     if (opt.isPresent()) {
220                         shortcutInfo = opt.get();
221                     } else {
222                         return null;
223                     }
224                     id = "shortcut:" + si.getShortcutId();
225                 }
226                 break;
227             }
228             case WIDGET: {
229                 LauncherAtom.Widget widget = info.getWidget();
230                 if ((cn = parseNullable(widget.getComponentName())) != null) {
231                     id = "widget:" + cn.getPackageName();
232                 }
233                 break;
234             }
235             case TASK: {
236                 LauncherAtom.Task task = info.getTask();
237                 if ((cn = parseNullable(task.getComponentName())) != null) {
238                     id = "app:" + cn.getPackageName();
239                 }
240                 break;
241             }
242             case FOLDER_ICON:
243                 return createTempFolderTarget();
244         }
245         if (id != null && cn != null) {
246             if (shortcutInfo != null) {
247                 return new AppTarget.Builder(new AppTargetId(id), shortcutInfo).build();
248             }
249             return new AppTarget.Builder(new AppTargetId(id), cn.getPackageName(), userHandle)
250                     .setClassName(cn.getClassName())
251                     .build();
252         }
253         return null;
254     }
255 
256 
createTempFolderTarget()257     private AppTarget createTempFolderTarget() {
258         return new AppTarget.Builder(new AppTargetId("folder:" + SystemClock.uptimeMillis()),
259                 mContext.getPackageName(), Process.myUserHandle())
260                 .build();
261     }
262 
getContainer(LauncherAtom.ItemInfo info)263     private String getContainer(LauncherAtom.ItemInfo info) {
264         ContainerInfo ci = info.getContainerInfo();
265         switch (ci.getContainerCase()) {
266             case WORKSPACE: {
267                 // In case the item type is not widgets, the spaceX and spanY default to 1.
268                 int spanX = info.getWidget().getSpanX();
269                 int spanY = info.getWidget().getSpanY();
270                 return getWorkspaceContainerString(ci.getWorkspace(), spanX, spanY);
271             }
272             case HOTSEAT: {
273                 return getHotseatContainerString(ci.getHotseat());
274             }
275             case TASK_SWITCHER_CONTAINER: {
276                 return "task-switcher";
277             }
278             case ALL_APPS_CONTAINER: {
279                 return "all-apps";
280             }
281             case PREDICTED_HOTSEAT_CONTAINER: {
282                 return "predictions/hotseat";
283             }
284             case PREDICTION_CONTAINER: {
285                 return "predictions";
286             }
287             case SHORTCUTS_CONTAINER: {
288                 return "deep-shortcuts";
289             }
290             case FOLDER: {
291                 FolderContainer fc = ci.getFolder();
292                 switch (fc.getParentContainerCase()) {
293                     case WORKSPACE:
294                         return "folder/" + getWorkspaceContainerString(fc.getWorkspace(), 1, 1);
295                     case HOTSEAT:
296                         return "folder/" + getHotseatContainerString(fc.getHotseat());
297                 }
298                 return "folder";
299             }
300             case SEARCH_RESULT_CONTAINER:
301                 return "search-results";
302             case EXTENDED_CONTAINERS: {
303                 if (ci.getExtendedContainers().getContainerCase()
304                         == DEVICE_SEARCH_RESULT_CONTAINER) {
305                     return "search-results";
306                 }
307             }
308             default: // fall out
309         }
310         return "";
311     }
312 
getWorkspaceContainerString(WorkspaceContainer wc, int spanX, int spanY)313     private static String getWorkspaceContainerString(WorkspaceContainer wc, int spanX, int spanY) {
314         return String.format(Locale.ENGLISH, "workspace/%d/[%d,%d]/[%d,%d]",
315                 wc.getPageIndex(), wc.getGridX(), wc.getGridY(), spanX, spanY);
316     }
317 
getHotseatContainerString(HotseatContainer hc)318     private static String getHotseatContainerString(HotseatContainer hc) {
319         return String.format(Locale.ENGLISH, "hotseat/%1$d/[%1$d,0]/[1,1]", hc.getIndex());
320     }
321 
parseNullable(String componentNameString)322     private static ComponentName parseNullable(String componentNameString) {
323         return TextUtils.isEmpty(componentNameString)
324                 ? null : ComponentName.unflattenFromString(componentNameString);
325     }
326 }
327