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