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 android.app.PendingIntent; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.os.SystemClock; 25 import android.support.annotation.Nullable; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewParent; 29 30 import com.android.launcher3.DropTarget; 31 import com.android.launcher3.ItemInfo; 32 import com.android.launcher3.R; 33 import com.android.launcher3.Utilities; 34 import com.android.launcher3.config.FeatureFlags; 35 import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 36 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 37 import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent; 38 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 39 import com.android.launcher3.util.LogConfig; 40 41 import java.util.Locale; 42 import java.util.UUID; 43 44 import static com.android.launcher3.logging.LoggerUtils.newCommandAction; 45 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; 46 import static com.android.launcher3.logging.LoggerUtils.newDropTarget; 47 import static com.android.launcher3.logging.LoggerUtils.newItemTarget; 48 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; 49 import static com.android.launcher3.logging.LoggerUtils.newTarget; 50 import static com.android.launcher3.logging.LoggerUtils.newTouchAction; 51 52 /** 53 * Manages the creation of {@link LauncherEvent}. 54 * To debug this class, execute following command before side loading a new apk. 55 * 56 * $ adb shell setprop log.tag.UserEvent VERBOSE 57 */ 58 public class UserEventDispatcher { 59 60 private final static int MAXIMUM_VIEW_HIERARCHY_LEVEL = 5; 61 62 private static final String TAG = "UserEvent"; 63 private static final boolean IS_VERBOSE = 64 FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.USEREVENT); 65 private static final String UUID_STORAGE = "uuid"; 66 67 public static UserEventDispatcher newInstance(Context context, boolean isInLandscapeMode, 68 boolean isInMultiWindowMode) { 69 SharedPreferences sharedPrefs = Utilities.getDevicePrefs(context); 70 String uuidStr = sharedPrefs.getString(UUID_STORAGE, null); 71 if (uuidStr == null) { 72 uuidStr = UUID.randomUUID().toString(); 73 sharedPrefs.edit().putString(UUID_STORAGE, uuidStr).apply(); 74 } 75 UserEventDispatcher ued = Utilities.getOverrideObject(UserEventDispatcher.class, 76 context.getApplicationContext(), R.string.user_event_dispatcher_class); 77 ued.mIsInLandscapeMode = isInLandscapeMode; 78 ued.mIsInMultiWindowMode = isInMultiWindowMode; 79 ued.mUuidStr = uuidStr; 80 return ued; 81 } 82 83 /** 84 * Implemented by containers to provide a container source for a given child. 85 */ 86 public interface LogContainerProvider { 87 88 /** 89 * Copies data from the source to the destination proto. 90 * 91 * @param v source of the data 92 * @param info source of the data 93 * @param target dest of the data 94 * @param targetParent dest of the data 95 */ 96 void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent); 97 } 98 99 /** 100 * Recursively finds the parent of the given child which implements IconLogInfoProvider 101 */ 102 public static LogContainerProvider getLaunchProviderRecursive(@Nullable View v) { 103 ViewParent parent; 104 if (v != null) { 105 parent = v.getParent(); 106 } else { 107 return null; 108 } 109 110 // Optimization to only check up to 5 parents. 111 int count = MAXIMUM_VIEW_HIERARCHY_LEVEL; 112 while (parent != null && count-- > 0) { 113 if (parent instanceof LogContainerProvider) { 114 return (LogContainerProvider) parent; 115 } else { 116 parent = parent.getParent(); 117 } 118 } 119 return null; 120 } 121 122 private long mElapsedContainerMillis; 123 private long mElapsedSessionMillis; 124 private long mActionDurationMillis; 125 private boolean mIsInMultiWindowMode; 126 private boolean mIsInLandscapeMode; 127 private String mUuidStr; 128 129 // APP_ICON SHORTCUT WIDGET 130 // -------------------------------------------------------------- 131 // packageNameHash required optional required 132 // componentNameHash required required 133 // intentHash required 134 // -------------------------------------------------------------- 135 136 /** 137 * Fills in the container data on the given event if the given view is not null. 138 * @return whether container data was added. 139 */ 140 protected boolean fillInLogContainerData(LauncherEvent event, @Nullable View v) { 141 // Fill in grid(x,y), pageIndex of the child and container type of the parent 142 LogContainerProvider provider = getLaunchProviderRecursive(v); 143 if (v == null || !(v.getTag() instanceof ItemInfo) || provider == null) { 144 return false; 145 } 146 ItemInfo itemInfo = (ItemInfo) v.getTag(); 147 provider.fillInLogContainerData(v, itemInfo, event.srcTarget[0], event.srcTarget[1]); 148 return true; 149 } 150 151 public void logAppLaunch(View v, Intent intent) { 152 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP), 153 newItemTarget(v), newTarget(Target.Type.CONTAINER)); 154 155 if (fillInLogContainerData(event, v)) { 156 fillIntentInfo(event.srcTarget[0], intent); 157 } 158 dispatchUserEvent(event, intent); 159 } 160 161 protected void fillIntentInfo(Target target, Intent intent) { 162 target.intentHash = intent.hashCode(); 163 ComponentName cn = intent.getComponent(); 164 if (cn != null) { 165 target.packageNameHash = (mUuidStr + cn.getPackageName()).hashCode(); 166 target.componentHash = (mUuidStr + cn.flattenToString()).hashCode(); 167 } 168 } 169 170 public void logNotificationLaunch(View v, PendingIntent intent) { 171 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP), 172 newItemTarget(v), newTarget(Target.Type.CONTAINER)); 173 if (fillInLogContainerData(event, v)) { 174 event.srcTarget[0].packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode(); 175 } 176 dispatchUserEvent(event, null); 177 } 178 179 public void logActionCommand(int command, int containerType) { 180 logActionCommand(command, containerType, 0); 181 } 182 183 public void logActionCommand(int command, int containerType, int pageIndex) { 184 LauncherEvent event = newLauncherEvent( 185 newCommandAction(command), newContainerTarget(containerType)); 186 event.srcTarget[0].pageIndex = pageIndex; 187 dispatchUserEvent(event, null); 188 } 189 190 /** 191 * TODO: Make this function work when a container view is passed as the 2nd param. 192 */ 193 public void logActionCommand(int command, View itemView, int containerType) { 194 LauncherEvent event = newLauncherEvent(newCommandAction(command), 195 newItemTarget(itemView), newTarget(Target.Type.CONTAINER)); 196 197 if (fillInLogContainerData(event, itemView)) { 198 // TODO: Remove the following two lines once fillInLogContainerData can take in a 199 // container view. 200 event.srcTarget[0].type = Target.Type.CONTAINER; 201 event.srcTarget[0].containerType = containerType; 202 } 203 dispatchUserEvent(event, null); 204 } 205 206 public void logActionOnControl(int action, int controlType) { 207 logActionOnControl(action, controlType, null); 208 } 209 210 public void logActionOnControl(int action, int controlType, @Nullable View controlInContainer) { 211 final LauncherEvent event = controlInContainer == null 212 ? newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL)) 213 : newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL), 214 newTarget(Target.Type.CONTAINER)); 215 event.srcTarget[0].controlType = controlType; 216 fillInLogContainerData(event, controlInContainer); 217 dispatchUserEvent(event, null); 218 } 219 220 public void logActionTapOutside(Target target) { 221 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Type.TOUCH), 222 target); 223 event.action.isOutside = true; 224 dispatchUserEvent(event, null); 225 } 226 227 public void logActionOnContainer(int action, int dir, int containerType) { 228 logActionOnContainer(action, dir, containerType, 0); 229 } 230 231 public void logActionOnContainer(int action, int dir, int containerType, int pageIndex) { 232 LauncherEvent event = newLauncherEvent(newTouchAction(action), 233 newContainerTarget(containerType)); 234 event.action.dir = dir; 235 event.srcTarget[0].pageIndex = pageIndex; 236 dispatchUserEvent(event, null); 237 } 238 239 public void logActionOnItem(int action, int dir, int itemType) { 240 Target itemTarget = newTarget(Target.Type.ITEM); 241 itemTarget.itemType = itemType; 242 LauncherEvent event = newLauncherEvent(newTouchAction(action), itemTarget); 243 event.action.dir = dir; 244 dispatchUserEvent(event, null); 245 } 246 247 public void logDeepShortcutsOpen(View icon) { 248 LogContainerProvider provider = getLaunchProviderRecursive(icon); 249 if (icon == null || !(icon.getTag() instanceof ItemInfo)) { 250 return; 251 } 252 ItemInfo info = (ItemInfo) icon.getTag(); 253 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.LONGPRESS), 254 newItemTarget(info), newTarget(Target.Type.CONTAINER)); 255 provider.fillInLogContainerData(icon, info, event.srcTarget[0], event.srcTarget[1]); 256 dispatchUserEvent(event, null); 257 258 resetElapsedContainerMillis(); 259 } 260 261 /* Currently we are only interested in whether this event happens or not and don't 262 * care about which screen moves to where. */ 263 public void logOverviewReorder() { 264 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), 265 newContainerTarget(ContainerType.WORKSPACE), 266 newContainerTarget(ContainerType.OVERVIEW)); 267 dispatchUserEvent(event, null); 268 } 269 270 public void logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView) { 271 LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), 272 newItemTarget(dragObj.originalDragInfo), newTarget(Target.Type.CONTAINER)); 273 event.destTarget = new Target[] { 274 newItemTarget(dragObj.originalDragInfo), newDropTarget(dropTargetAsView) 275 }; 276 277 dragObj.dragSource.fillInLogContainerData(null, dragObj.originalDragInfo, 278 event.srcTarget[0], event.srcTarget[1]); 279 280 if (dropTargetAsView instanceof LogContainerProvider) { 281 ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(null, 282 dragObj.dragInfo, event.destTarget[0], event.destTarget[1]); 283 284 } 285 event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis; 286 dispatchUserEvent(event, null); 287 } 288 289 /** 290 * Currently logs following containers: workspace, allapps, widget tray. 291 */ 292 public final void resetElapsedContainerMillis() { 293 mElapsedContainerMillis = SystemClock.uptimeMillis(); 294 } 295 296 public final void resetElapsedSessionMillis() { 297 mElapsedSessionMillis = SystemClock.uptimeMillis(); 298 mElapsedContainerMillis = SystemClock.uptimeMillis(); 299 } 300 301 public final void resetActionDurationMillis() { 302 mActionDurationMillis = SystemClock.uptimeMillis(); 303 } 304 305 public void dispatchUserEvent(LauncherEvent ev, Intent intent) { 306 ev.isInLandscapeMode = mIsInLandscapeMode; 307 ev.isInMultiWindowMode = mIsInMultiWindowMode; 308 ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis; 309 ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis; 310 311 if (!IS_VERBOSE) { 312 return; 313 } 314 String log = "action:" + LoggerUtils.getActionStr(ev.action); 315 if (ev.srcTarget != null && ev.srcTarget.length > 0) { 316 log += "\n Source " + getTargetsStr(ev.srcTarget); 317 } 318 if (ev.destTarget != null && ev.destTarget.length > 0) { 319 log += "\n Destination " + getTargetsStr(ev.destTarget); 320 } 321 log += String.format(Locale.US, 322 "\n Elapsed container %d ms session %d ms action %d ms", 323 ev.elapsedContainerMillis, 324 ev.elapsedSessionMillis, 325 ev.actionDurationMillis); 326 log += "\n isInLandscapeMode " + ev.isInLandscapeMode; 327 log += "\n isInMultiWindowMode " + ev.isInMultiWindowMode; 328 Log.d(TAG, log); 329 } 330 331 private static String getTargetsStr(Target[] targets) { 332 String result = "child:" + LoggerUtils.getTargetStr(targets[0]); 333 for (int i = 1; i < targets.length; i++) { 334 result += "\tparent:" + LoggerUtils.getTargetStr(targets[i]); 335 } 336 return result; 337 } 338 } 339