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