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