/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.logging; import static com.android.launcher3.logging.LoggerUtils.newAction; import static com.android.launcher3.logging.LoggerUtils.newCommandAction; import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; import static com.android.launcher3.logging.LoggerUtils.newDropTarget; import static com.android.launcher3.logging.LoggerUtils.newItemTarget; import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; import static com.android.launcher3.logging.LoggerUtils.newTarget; import static com.android.launcher3.logging.LoggerUtils.newTouchAction; import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; import static com.android.launcher3.userevent.nano.LauncherLogProto.TipType; import static java.util.Optional.ofNullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.SystemClock; import android.util.Log; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.DropTarget; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.userevent.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.LogConfig; import com.android.launcher3.util.ResourceBasedOverride; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; import java.util.ArrayList; import java.util.UUID; /** * Manages the creation of {@link LauncherEvent}. * To debug this class, execute following command before side loading a new apk. *

* $ adb shell setprop log.tag.UserEvent VERBOSE */ public class UserEventDispatcher implements ResourceBasedOverride { private static final String TAG = "UserEvent"; private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.USEREVENT); private static final String UUID_STORAGE = "uuid"; /** * A factory method for UserEventDispatcher */ public static UserEventDispatcher newInstance(Context context) { SharedPreferences sharedPrefs = Utilities.getDevicePrefs(context); String uuidStr = sharedPrefs.getString(UUID_STORAGE, null); if (uuidStr == null) { uuidStr = UUID.randomUUID().toString(); sharedPrefs.edit().putString(UUID_STORAGE, uuidStr).apply(); } UserEventDispatcher ued = Overrides.getObject(UserEventDispatcher.class, context.getApplicationContext(), R.string.user_event_dispatcher_class); ued.mUuidStr = uuidStr; ued.mInstantAppResolver = InstantAppResolver.newInstance(context); return ued; } /** * Fills in the container data on the given event if the given view is not null. * * @return whether container data was added. */ private boolean fillLogContainer(@Nullable View v, Target child, @Nullable ArrayList targets) { LogContainerProvider firstParent = StatsLogUtils.getLaunchProviderRecursive(v); if (v == null || !(v.getTag() instanceof ItemInfo) || firstParent == null) { return false; } final ItemInfo itemInfo = (ItemInfo) v.getTag(); firstParent.fillInLogContainerData(itemInfo, child, targets); return true; } protected void onFillInLogContainerData(@NonNull ItemInfo itemInfo, @NonNull Target target, @NonNull ArrayList targets) { } private boolean mSessionStarted; private long mElapsedContainerMillis; private long mElapsedSessionMillis; private long mActionDurationMillis; private String mUuidStr; protected InstantAppResolver mInstantAppResolver; private boolean mAppOrTaskLaunch; private boolean mPreviousHomeGesture; private void fillComponentInfo(Target target, ComponentName cn) { if (cn != null) { target.packageNameHash = (mUuidStr + cn.getPackageName()).hashCode(); target.componentHash = (mUuidStr + cn.flattenToString()).hashCode(); } } public void logActionCommand(int command, int srcContainerType, int dstContainerType) { logActionCommand(command, newContainerTarget(srcContainerType), dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null); } public void logActionCommand(int command, int srcContainerType, int dstContainerType, int pageIndex) { Target srcTarget = newContainerTarget(srcContainerType); srcTarget.pageIndex = pageIndex; logActionCommand(command, srcTarget, dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null); } public void logActionCommand(int command, Target srcTarget, Target dstTarget) { LauncherEvent event = newLauncherEvent(newCommandAction(command), srcTarget); if (command == Action.Command.STOP) { if (mAppOrTaskLaunch || !mSessionStarted) { mSessionStarted = false; return; } } if (dstTarget != null) { event.destTarget = new Target[1]; event.destTarget[0] = dstTarget; event.action.isStateChange = true; } dispatchUserEvent(event, null); } public void logActionOnControl(int action, int controlType) { logActionOnControl(action, controlType, null); } public void logActionOnControl(int action, int controlType, int parentContainerType) { logActionOnControl(action, controlType, null, parentContainerType); } /** * Logs control action with proper parent hierarchy */ public void logActionOnControl(int actionType, int controlType, @Nullable View controlInContainer, int... parentTypes) { Target control = newTarget(Target.Type.CONTROL); control.controlType = controlType; Action action = newAction(actionType); ArrayList targets = makeTargetsList(control); if (controlInContainer != null) { fillLogContainer(controlInContainer, control, targets); } for (int parentContainerType : parentTypes) { if (parentContainerType < 0) continue; targets.add(newContainerTarget(parentContainerType)); } LauncherEvent event = newLauncherEvent(action, targets); if (actionType == Action.Touch.DRAGDROP) { event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis; } dispatchUserEvent(event, null); } public void logActionTapOutside(Target target) { LauncherEvent event = newLauncherEvent(newTouchAction(Action.Type.TOUCH), target); event.action.isOutside = true; dispatchUserEvent(event, null); } public void logActionBounceTip(int containerType) { LauncherEvent event = newLauncherEvent(newAction(Action.Type.TIP), newContainerTarget(containerType)); event.srcTarget[0].tipType = TipType.BOUNCE; dispatchUserEvent(event, null); } public void logActionOnContainer(int action, int dir, int containerType) { logActionOnContainer(action, dir, containerType, 0); } public void logActionOnContainer(int action, int dir, int containerType, int pageIndex) { LauncherEvent event = newLauncherEvent(newTouchAction(action), newContainerTarget(containerType)); event.action.dir = dir; event.srcTarget[0].pageIndex = pageIndex; dispatchUserEvent(event, null); } /** * Used primarily for swipe up and down when state changes when swipe up happens from the * navbar bezel, the {@param srcChildContainerType} is NAVBAR and * {@param srcParentContainerType} is either one of the two * (1) WORKSPACE: if the launcher is the foreground activity * (2) APP: if another app was the foreground activity */ public void logStateChangeAction(int action, int dir, int downX, int downY, int srcChildTargetType, int srcParentContainerType, int dstContainerType, int pageIndex) { LauncherEvent event; if (srcChildTargetType == ItemType.TASK) { event = newLauncherEvent(newTouchAction(action), newItemTarget(srcChildTargetType), newContainerTarget(srcParentContainerType)); } else { event = newLauncherEvent(newTouchAction(action), newContainerTarget(srcChildTargetType), newContainerTarget(srcParentContainerType)); } event.destTarget = new Target[1]; event.destTarget[0] = newContainerTarget(dstContainerType); event.action.dir = dir; event.action.isStateChange = true; event.srcTarget[0].pageIndex = pageIndex; event.srcTarget[0].spanX = downX; event.srcTarget[0].spanY = downY; dispatchUserEvent(event, null); } public void logActionOnItem(int action, int dir, int itemType) { logActionOnItem(action, dir, itemType, null, null); } /** * Creates new {@link LauncherEvent} of ITEM target type with input arguments and dispatches it. * * @param touchAction ENUM value of {@link LauncherLogProto.Action.Touch} Action * @param dir ENUM value of {@link LauncherLogProto.Action.Direction} Action * @param itemType ENUM value of {@link LauncherLogProto.ItemType} * @param gridX Nullable X coordinate of item's position on the workspace grid * @param gridY Nullable Y coordinate of item's position on the workspace grid */ public void logActionOnItem(int touchAction, int dir, int itemType, @Nullable Integer gridX, @Nullable Integer gridY) { Target itemTarget = newTarget(Target.Type.ITEM); itemTarget.itemType = itemType; ofNullable(gridX).ifPresent(value -> itemTarget.gridX = value); ofNullable(gridY).ifPresent(value -> itemTarget.gridY = value); LauncherEvent event = newLauncherEvent(newTouchAction(touchAction), itemTarget); event.action.dir = dir; dispatchUserEvent(event, null); } /** * Logs proto lite version of LauncherEvent object to clearcut. */ public void logLauncherEvent( com.android.launcher3.userevent.LauncherLogProto.LauncherEvent launcherEvent) { if (mPreviousHomeGesture) { mPreviousHomeGesture = false; } mAppOrTaskLaunch = false; launcherEvent.toBuilder() .setElapsedContainerMillis(SystemClock.uptimeMillis() - mElapsedContainerMillis) .setElapsedSessionMillis( SystemClock.uptimeMillis() - mElapsedSessionMillis).build(); try { dispatchUserEvent(LauncherEvent.parseFrom(launcherEvent.toByteArray()), null); } catch (InvalidProtocolBufferNanoException e) { throw new RuntimeException("Cannot convert LauncherEvent from Lite to Nano version."); } } public void logDeepShortcutsOpen(View icon) { ItemInfo info = (ItemInfo) icon.getTag(); Target child = newItemTarget(info, mInstantAppResolver); ArrayList targets = makeTargetsList(child); fillLogContainer(icon, child, targets); dispatchUserEvent(newLauncherEvent(newTouchAction(Action.Touch.TAP), targets), null); } public void logDragNDrop(DropTarget.DragObject dragObj, View dropTargetAsView) { Target srcChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver); ArrayList srcTargets = makeTargetsList(srcChild); Target destChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver); ArrayList destTargets = makeTargetsList(destChild); //dragObj.dragSource.fillInLogContainerData(dragObj.originalDragInfo, srcChild, srcTargets); if (dropTargetAsView instanceof LogContainerProvider) { ((LogContainerProvider) dropTargetAsView).fillInLogContainerData(dragObj.dragInfo, destChild, destTargets); } else { destTargets.add(newDropTarget(dropTargetAsView)); } LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP), srcTargets); Target[] destTargetsArray = new Target[destTargets.size()]; destTargets.toArray(destTargetsArray); event.destTarget = destTargetsArray; event.actionDurationMillis = SystemClock.uptimeMillis() - mActionDurationMillis; dispatchUserEvent(event, null); } public final void startSession() { mSessionStarted = true; mElapsedSessionMillis = SystemClock.uptimeMillis(); mElapsedContainerMillis = SystemClock.uptimeMillis(); } public final void setPreviousHomeGesture(boolean homeGesture) { mPreviousHomeGesture = homeGesture; } public final boolean isPreviousHomeGesture() { return mPreviousHomeGesture; } public final void resetActionDurationMillis() { mActionDurationMillis = SystemClock.uptimeMillis(); } public void dispatchUserEvent(LauncherEvent ev, Intent intent) { if (mPreviousHomeGesture) { mPreviousHomeGesture = false; } mAppOrTaskLaunch = false; ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis; ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis; if (!IS_VERBOSE) { return; } LauncherLogProto.LauncherEvent liteLauncherEvent; try { liteLauncherEvent = LauncherLogProto.LauncherEvent.parseFrom(MessageNano.toByteArray(ev)); } catch (InvalidProtocolBufferException e) { throw new RuntimeException("Cannot parse LauncherEvent from Nano to Lite version"); } Log.d(TAG, liteLauncherEvent.toString()); } /** * Constructs an ArrayList with targets */ public static ArrayList makeTargetsList(Target... targets) { ArrayList result = new ArrayList<>(); for (Target target : targets) { result.add(target); } return result; } }