1 /* 2 * Copyright (C) 2012 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 17 package com.android.launcher3.logging; 18 19 import static com.android.launcher3.logging.LoggerUtils.newAction; 20 import static com.android.launcher3.logging.LoggerUtils.newCommandAction; 21 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; 22 import static com.android.launcher3.logging.LoggerUtils.newControlTarget; 23 import static com.android.launcher3.logging.LoggerUtils.newDropTarget; 24 import static com.android.launcher3.logging.LoggerUtils.newItemTarget; 25 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; 26 import static com.android.launcher3.logging.LoggerUtils.newTarget; 27 import static com.android.launcher3.logging.LoggerUtils.newTouchAction; 28 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; 29 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; 30 import static com.android.launcher3.userevent.nano.LauncherLogProto.TipType; 31 32 import static java.util.Optional.ofNullable; 33 34 import android.app.PendingIntent; 35 import android.content.ComponentName; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.SharedPreferences; 39 import android.os.Process; 40 import android.os.SystemClock; 41 import android.os.UserHandle; 42 import android.util.Log; 43 import android.view.View; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 48 import com.android.launcher3.DropTarget; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider; 52 import com.android.launcher3.model.data.ItemInfo; 53 import com.android.launcher3.userevent.LauncherLogProto; 54 import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 55 import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent; 56 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 57 import com.android.launcher3.util.ComponentKey; 58 import com.android.launcher3.util.InstantAppResolver; 59 import com.android.launcher3.util.LogConfig; 60 import com.android.launcher3.util.ResourceBasedOverride; 61 62 import com.google.protobuf.InvalidProtocolBufferException; 63 import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 64 import com.google.protobuf.nano.MessageNano; 65 66 import java.util.ArrayList; 67 import java.util.UUID; 68 69 /** 70 * Manages the creation of {@link LauncherEvent}. 71 * To debug this class, execute following command before side loading a new apk. 72 * <p> 73 * $ adb shell setprop log.tag.UserEvent VERBOSE 74 */ 75 public class UserEventDispatcher implements ResourceBasedOverride { 76 77 private static final String TAG = "UserEvent"; 78 private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.USEREVENT); 79 private static final String UUID_STORAGE = "uuid"; 80 81 /** 82 * A factory method for UserEventDispatcher 83 */ newInstance(Context context)84 public static UserEventDispatcher newInstance(Context context) { 85 SharedPreferences sharedPrefs = Utilities.getDevicePrefs(context); 86 String uuidStr = sharedPrefs.getString(UUID_STORAGE, null); 87 if (uuidStr == null) { 88 uuidStr = UUID.randomUUID().toString(); 89 sharedPrefs.edit().putString(UUID_STORAGE, uuidStr).apply(); 90 } 91 UserEventDispatcher ued = Overrides.getObject(UserEventDispatcher.class, 92 context.getApplicationContext(), R.string.user_event_dispatcher_class); 93 ued.mUuidStr = uuidStr; 94 ued.mInstantAppResolver = InstantAppResolver.newInstance(context); 95 return ued; 96 } 97 98 99 /** 100 * Fills in the container data on the given event if the given view is not null. 101 * 102 * @return whether container data was added. 103 */ fillLogContainer(@ullable View v, Target child, @Nullable ArrayList<Target> targets)104 public boolean fillLogContainer(@Nullable View v, Target child, 105 @Nullable ArrayList<Target> targets) { 106 LogContainerProvider firstParent = StatsLogUtils.getLaunchProviderRecursive(v); 107 if (v == null || !(v.getTag() instanceof ItemInfo) || firstParent == null) { 108 return false; 109 } 110 final ItemInfo itemInfo = (ItemInfo) v.getTag(); 111 firstParent.fillInLogContainerData(itemInfo, child, targets); 112 return true; 113 } 114 onFillInLogContainerData(@onNull ItemInfo itemInfo, @NonNull Target target, @NonNull ArrayList<Target> targets)115 protected void onFillInLogContainerData(@NonNull ItemInfo itemInfo, @NonNull Target target, 116 @NonNull ArrayList<Target> targets) { 117 } 118 119 private boolean mSessionStarted; 120 private long mElapsedContainerMillis; 121 private long mElapsedSessionMillis; 122 private long mActionDurationMillis; 123 private String mUuidStr; 124 protected InstantAppResolver mInstantAppResolver; 125 private boolean mAppOrTaskLaunch; 126 private boolean mPreviousHomeGesture; 127 128 // APP_ICON SHORTCUT WIDGET 129 // -------------------------------------------------------------- 130 // packageNameHash required optional required 131 // componentNameHash required required 132 // intentHash required 133 // -------------------------------------------------------------- 134 135 @Deprecated logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle)136 public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) { 137 Target itemTarget = newItemTarget(v, mInstantAppResolver); 138 Action action = newTouchAction(Action.Touch.TAP); 139 ArrayList<Target> targets = makeTargetsList(itemTarget); 140 if (fillLogContainer(v, itemTarget, targets)) { 141 onFillInLogContainerData((ItemInfo) v.getTag(), itemTarget, targets); 142 fillIntentInfo(itemTarget, intent, userHandle); 143 } 144 LauncherEvent event = newLauncherEvent(action, targets); 145 dispatchUserEvent(event, intent); 146 mAppOrTaskLaunch = true; 147 } 148 149 /** 150 * Dummy method. 151 */ logActionTip(int actionType, int viewType)152 public void logActionTip(int actionType, int viewType) { 153 } 154 155 @Deprecated logTaskLaunchOrDismiss(int action, int direction, int taskIndex, ComponentKey componentKey)156 public void logTaskLaunchOrDismiss(int action, int direction, int taskIndex, 157 ComponentKey componentKey) { 158 LauncherEvent event = newLauncherEvent(newTouchAction(action), // TAP or SWIPE or FLING 159 newTarget(Target.Type.ITEM)); 160 if (action == Action.Touch.SWIPE || action == Action.Touch.FLING) { 161 // Direction DOWN means the task was launched, UP means it was dismissed. 162 event.action.dir = direction; 163 } 164 event.srcTarget[0].itemType = ItemType.TASK; 165 event.srcTarget[0].pageIndex = taskIndex; 166 fillComponentInfo(event.srcTarget[0], componentKey.componentName); 167 dispatchUserEvent(event, null); 168 mAppOrTaskLaunch = true; 169 } 170 fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle)171 protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) { 172 target.intentHash = intent.hashCode(); 173 target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle()); 174 fillComponentInfo(target, intent.getComponent()); 175 } 176 fillComponentInfo(Target target, ComponentName cn)177 private void fillComponentInfo(Target target, ComponentName cn) { 178 if (cn != null) { 179 target.packageNameHash = (mUuidStr + cn.getPackageName()).hashCode(); 180 target.componentHash = (mUuidStr + cn.flattenToString()).hashCode(); 181 } 182 } 183 logNotificationLaunch(View v, PendingIntent intent)184 public void logNotificationLaunch(View v, PendingIntent intent) { 185 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP), 186 newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER)); 187 Target itemTarget = newItemTarget(v, mInstantAppResolver); 188 ArrayList<Target> targets = makeTargetsList(itemTarget); 189 190 if (fillLogContainer(v, itemTarget, targets)) { 191 itemTarget.packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode(); 192 } 193 dispatchUserEvent(event, null); 194 } 195 logActionCommand(int command, Target srcTarget)196 public void logActionCommand(int command, Target srcTarget) { 197 logActionCommand(command, srcTarget, null); 198 } 199 logActionCommand(int command, int srcContainerType, int dstContainerType)200 public void logActionCommand(int command, int srcContainerType, int dstContainerType) { 201 logActionCommand(command, newContainerTarget(srcContainerType), 202 dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null); 203 } 204 logActionCommand(int command, int srcContainerType, int dstContainerType, int pageIndex)205 public void logActionCommand(int command, int srcContainerType, int dstContainerType, 206 int pageIndex) { 207 Target srcTarget = newContainerTarget(srcContainerType); 208 srcTarget.pageIndex = pageIndex; 209 logActionCommand(command, srcTarget, 210 dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null); 211 } 212 logActionCommand(int command, Target srcTarget, Target dstTarget)213 public void logActionCommand(int command, Target srcTarget, Target dstTarget) { 214 LauncherEvent event = newLauncherEvent(newCommandAction(command), srcTarget); 215 if (command == Action.Command.STOP) { 216 if (mAppOrTaskLaunch || !mSessionStarted) { 217 mSessionStarted = false; 218 return; 219 } 220 } 221 222 if (dstTarget != null) { 223 event.destTarget = new Target[1]; 224 event.destTarget[0] = dstTarget; 225 event.action.isStateChange = true; 226 } 227 dispatchUserEvent(event, null); 228 } 229 230 /** 231 * TODO: Make this function work when a container view is passed as the 2nd param. 232 */ logActionCommand(int command, View itemView, int srcContainerType)233 public void logActionCommand(int command, View itemView, int srcContainerType) { 234 LauncherEvent event = newLauncherEvent(newCommandAction(command), 235 newItemTarget(itemView, mInstantAppResolver), newTarget(Target.Type.CONTAINER)); 236 237 Target itemTarget = newItemTarget(itemView, mInstantAppResolver); 238 ArrayList<Target> targets = makeTargetsList(itemTarget); 239 240 if (fillLogContainer(itemView, itemTarget, targets)) { 241 // TODO: Remove the following two lines once fillInLogContainerData can take in a 242 // container view. 243 itemTarget.type = Target.Type.CONTAINER; 244 itemTarget.containerType = srcContainerType; 245 } 246 dispatchUserEvent(event, null); 247 } 248 logActionOnControl(int action, int controlType)249 public void logActionOnControl(int action, int controlType) { 250 logActionOnControl(action, controlType, null); 251 } 252 logActionOnControl(int action, int controlType, int parentContainerType)253 public void logActionOnControl(int action, int controlType, int parentContainerType) { 254 logActionOnControl(action, controlType, null, parentContainerType); 255 } 256 257 /** 258 * Logs control action with proper parent hierarchy 259 */ logActionOnControl(int actionType, int controlType, @Nullable View controlInContainer, int... parentTypes)260 public void logActionOnControl(int actionType, int controlType, 261 @Nullable View controlInContainer, int... parentTypes) { 262 Target control = newTarget(Target.Type.CONTROL); 263 control.controlType = controlType; 264 Action action = newAction(actionType); 265 266 ArrayList<Target> targets = makeTargetsList(control); 267 if (controlInContainer != null) { 268 fillLogContainer(controlInContainer, control, targets); 269 } 270 for (int parentContainerType : parentTypes) { 271 if (parentContainerType < 0) continue; 272 targets.add(newContainerTarget(parentContainerType)); 273 } 274 LauncherEvent event = newLauncherEvent(action, targets); 275 if (actionType == Action.Touch.DRAGDROP) { 276 event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis; 277 } 278 dispatchUserEvent(event, null); 279 } 280 logActionTapOutside(Target target)281 public void logActionTapOutside(Target target) { 282 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Type.TOUCH), 283 target); 284 event.action.isOutside = true; 285 dispatchUserEvent(event, null); 286 } 287 logActionBounceTip(int containerType)288 public void logActionBounceTip(int containerType) { 289 LauncherEvent event = newLauncherEvent(newAction(Action.Type.TIP), 290 newContainerTarget(containerType)); 291 event.srcTarget[0].tipType = TipType.BOUNCE; 292 dispatchUserEvent(event, null); 293 } 294 logActionOnContainer(int action, int dir, int containerType)295 public void logActionOnContainer(int action, int dir, int containerType) { 296 logActionOnContainer(action, dir, containerType, 0); 297 } 298 logActionOnContainer(int action, int dir, int containerType, int pageIndex)299 public void logActionOnContainer(int action, int dir, int containerType, int pageIndex) { 300 LauncherEvent event = newLauncherEvent(newTouchAction(action), 301 newContainerTarget(containerType)); 302 event.action.dir = dir; 303 event.srcTarget[0].pageIndex = pageIndex; 304 dispatchUserEvent(event, null); 305 } 306 307 /** 308 * Used primarily for swipe up and down when state changes when swipe up happens from the 309 * navbar bezel, the {@param srcChildContainerType} is NAVBAR and 310 * {@param srcParentContainerType} is either one of the two 311 * (1) WORKSPACE: if the launcher is the foreground activity 312 * (2) APP: if another app was the foreground activity 313 */ logStateChangeAction(int action, int dir, int downX, int downY, int srcChildTargetType, int srcParentContainerType, int dstContainerType, int pageIndex)314 public void logStateChangeAction(int action, int dir, int downX, int downY, 315 int srcChildTargetType, int srcParentContainerType, int dstContainerType, 316 int pageIndex) { 317 LauncherEvent event; 318 if (srcChildTargetType == ItemType.TASK) { 319 event = newLauncherEvent(newTouchAction(action), 320 newItemTarget(srcChildTargetType), 321 newContainerTarget(srcParentContainerType)); 322 } else { 323 event = newLauncherEvent(newTouchAction(action), 324 newContainerTarget(srcChildTargetType), 325 newContainerTarget(srcParentContainerType)); 326 } 327 event.destTarget = new Target[1]; 328 event.destTarget[0] = newContainerTarget(dstContainerType); 329 event.action.dir = dir; 330 event.action.isStateChange = true; 331 event.srcTarget[0].pageIndex = pageIndex; 332 event.srcTarget[0].spanX = downX; 333 event.srcTarget[0].spanY = downY; 334 dispatchUserEvent(event, null); 335 resetElapsedContainerMillis("state changed"); 336 } 337 logActionOnItem(int action, int dir, int itemType)338 public void logActionOnItem(int action, int dir, int itemType) { 339 logActionOnItem(action, dir, itemType, null, null); 340 } 341 342 /** 343 * Creates new {@link LauncherEvent} of ITEM target type with input arguments and dispatches it. 344 * 345 * @param touchAction ENUM value of {@link LauncherLogProto.Action.Touch} Action 346 * @param dir ENUM value of {@link LauncherLogProto.Action.Direction} Action 347 * @param itemType ENUM value of {@link LauncherLogProto.ItemType} 348 * @param gridX Nullable X coordinate of item's position on the workspace grid 349 * @param gridY Nullable Y coordinate of item's position on the workspace grid 350 */ logActionOnItem(int touchAction, int dir, int itemType, @Nullable Integer gridX, @Nullable Integer gridY)351 public void logActionOnItem(int touchAction, int dir, int itemType, 352 @Nullable Integer gridX, @Nullable Integer gridY) { 353 Target itemTarget = newTarget(Target.Type.ITEM); 354 itemTarget.itemType = itemType; 355 ofNullable(gridX).ifPresent(value -> itemTarget.gridX = value); 356 ofNullable(gridY).ifPresent(value -> itemTarget.gridY = value); 357 LauncherEvent event = newLauncherEvent(newTouchAction(touchAction), itemTarget); 358 event.action.dir = dir; 359 dispatchUserEvent(event, null); 360 } 361 362 /** 363 * Logs proto lite version of LauncherEvent object to clearcut. 364 */ logLauncherEvent( com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent)365 public void logLauncherEvent( 366 com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent) { 367 368 if (mPreviousHomeGesture) { 369 mPreviousHomeGesture = false; 370 } 371 mAppOrTaskLaunch = false; 372 launcherEvent.toBuilder() 373 .setElapsedContainerMillis(SystemClock.uptimeMillis() - mElapsedContainerMillis) 374 .setElapsedSessionMillis( 375 SystemClock.uptimeMillis() - mElapsedSessionMillis).build(); 376 try { 377 dispatchUserEvent(LauncherEvent.parseFrom(launcherEvent.toByteArray()), null); 378 } catch (InvalidProtocolBufferNanoException e) { 379 throw new RuntimeException("Cannot convert LauncherEvent from Lite to Nano version."); 380 } 381 } 382 logDeepShortcutsOpen(View icon)383 public void logDeepShortcutsOpen(View icon) { 384 ItemInfo info = (ItemInfo) icon.getTag(); 385 Target child = newItemTarget(info, mInstantAppResolver); 386 ArrayList<Target> targets = makeTargetsList(child); 387 fillLogContainer(icon, child, targets); 388 dispatchUserEvent(newLauncherEvent(newTouchAction(Action.Touch.TAP), targets), null); 389 resetElapsedContainerMillis("deep shortcut open"); 390 } 391 logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView)392 public void logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView) { 393 Target srcChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver); 394 ArrayList<Target> srcTargets = makeTargetsList(srcChild); 395 396 397 Target destChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver); 398 ArrayList<Target> destTargets = makeTargetsList(destChild); 399 400 dragObj.dragSource.fillInLogContainerData(dragObj.originalDragInfo, srcChild, srcTargets); 401 if (dropTargetAsView instanceof LogContainerProvider) { 402 ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(dragObj.dragInfo, 403 destChild, destTargets); 404 } 405 else { 406 destTargets.add(newDropTarget(dropTargetAsView)); 407 } 408 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), srcTargets); 409 Target[] destTargetsArray = new Target[destTargets.size()]; 410 destTargets.toArray(destTargetsArray); 411 event.destTarget = destTargetsArray; 412 413 event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis; 414 dispatchUserEvent(event, null); 415 } 416 logActionBack(boolean completed, int downX, int downY, boolean isButton, boolean gestureSwipeLeft, int containerType)417 public void logActionBack(boolean completed, int downX, int downY, boolean isButton, 418 boolean gestureSwipeLeft, int containerType) { 419 int actionTouch = isButton ? Action.Touch.TAP : Action.Touch.SWIPE; 420 Action action = newCommandAction(actionTouch); 421 action.command = Action.Command.BACK; 422 action.dir = isButton ? Action.Direction.NONE : 423 gestureSwipeLeft ? Action.Direction.LEFT : Action.Direction.RIGHT; 424 Target target = newControlTarget(isButton ? ControlType.BACK_BUTTON : 425 ControlType.BACK_GESTURE); 426 target.spanX = downX; 427 target.spanY = downY; 428 target.cardinality = completed ? 1 : 0; 429 LauncherEvent event = newLauncherEvent(action, target, newContainerTarget(containerType)); 430 431 dispatchUserEvent(event, null); 432 } 433 434 /** 435 * Currently logs following containers: workspace, allapps, widget tray. 436 */ resetElapsedContainerMillis(String reason)437 public final void resetElapsedContainerMillis(String reason) { 438 mElapsedContainerMillis = SystemClock.uptimeMillis(); 439 if (!IS_VERBOSE) { 440 return; 441 } 442 Log.d(TAG, "resetElapsedContainerMillis reason=" + reason); 443 444 } 445 startSession()446 public final void startSession() { 447 mSessionStarted = true; 448 mElapsedSessionMillis = SystemClock.uptimeMillis(); 449 mElapsedContainerMillis = SystemClock.uptimeMillis(); 450 } 451 setPreviousHomeGesture(boolean homeGesture)452 public final void setPreviousHomeGesture(boolean homeGesture) { 453 mPreviousHomeGesture = homeGesture; 454 } 455 isPreviousHomeGesture()456 public final boolean isPreviousHomeGesture() { 457 return mPreviousHomeGesture; 458 } 459 resetActionDurationMillis()460 public final void resetActionDurationMillis() { 461 mActionDurationMillis = SystemClock.uptimeMillis(); 462 } 463 dispatchUserEvent(LauncherEvent ev, Intent intent)464 public void dispatchUserEvent(LauncherEvent ev, Intent intent) { 465 if (mPreviousHomeGesture) { 466 mPreviousHomeGesture = false; 467 } 468 mAppOrTaskLaunch = false; 469 ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis; 470 ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis; 471 if (!IS_VERBOSE) { 472 return; 473 } 474 LauncherLogProto.LauncherEvent liteLauncherEvent; 475 try { 476 liteLauncherEvent = 477 LauncherLogProto.LauncherEvent.parseFrom(MessageNano.toByteArray(ev)); 478 } catch (InvalidProtocolBufferException e) { 479 throw new RuntimeException("Cannot parse LauncherEvent from Nano to Lite version"); 480 } 481 Log.d(TAG, liteLauncherEvent.toString()); 482 } 483 484 /** 485 * Constructs an ArrayList with targets 486 */ makeTargetsList(Target... targets)487 public static ArrayList<Target> makeTargetsList(Target... targets) { 488 ArrayList<Target> result = new ArrayList<>(); 489 for (Target target : targets) { 490 result.add(target); 491 } 492 return result; 493 } 494 } 495