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