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