| /* |
| * 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. |
| * <p> |
| * $ 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<Target> 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<Target> 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<Target> 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<Target> 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<Target> srcTargets = makeTargetsList(srcChild); |
| |
| |
| Target destChild = newItemTarget(dragObj.originalDragInfo, mInstantAppResolver); |
| ArrayList<Target> 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<Target> makeTargetsList(Target... targets) { |
| ArrayList<Target> result = new ArrayList<>(); |
| for (Target target : targets) { |
| result.add(target); |
| } |
| return result; |
| } |
| } |