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