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