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