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