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