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
17package com.android.launcher3.logging;
18
19import android.app.PendingIntent;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.os.SystemClock;
25import android.support.annotation.Nullable;
26import android.util.Log;
27import android.view.View;
28import android.view.ViewParent;
29
30import com.android.launcher3.DropTarget;
31import com.android.launcher3.ItemInfo;
32import com.android.launcher3.R;
33import com.android.launcher3.Utilities;
34import com.android.launcher3.config.FeatureFlags;
35import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
36import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
37import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent;
38import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
39import com.android.launcher3.util.LogConfig;
40
41import java.util.Locale;
42import java.util.UUID;
43
44import static com.android.launcher3.logging.LoggerUtils.newCommandAction;
45import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
46import static com.android.launcher3.logging.LoggerUtils.newDropTarget;
47import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
48import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
49import static com.android.launcher3.logging.LoggerUtils.newTarget;
50import 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 */
58public 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