/*
* 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.newControlTarget;
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.ControlType;
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.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
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.ComponentKey;
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.
*/
public 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;
// APP_ICON SHORTCUT WIDGET
// --------------------------------------------------------------
// packageNameHash required optional required
// componentNameHash required required
// intentHash required
// --------------------------------------------------------------
@Deprecated
public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
Target itemTarget = newItemTarget(v, mInstantAppResolver);
Action action = newTouchAction(Action.Touch.TAP);
ArrayList targets = makeTargetsList(itemTarget);
if (fillLogContainer(v, itemTarget, targets)) {
onFillInLogContainerData((ItemInfo) v.getTag(), itemTarget, targets);
fillIntentInfo(itemTarget, intent, userHandle);
}
LauncherEvent event = newLauncherEvent(action, targets);
dispatchUserEvent(event, intent);
mAppOrTaskLaunch = true;
}
/**
* Dummy method.
*/
public void logActionTip(int actionType, int viewType) {
}
@Deprecated
public void logTaskLaunchOrDismiss(int action, int direction, int taskIndex,
ComponentKey componentKey) {
LauncherEvent event = newLauncherEvent(newTouchAction(action), // TAP or SWIPE or FLING
newTarget(Target.Type.ITEM));
if (action == Action.Touch.SWIPE || action == Action.Touch.FLING) {
// Direction DOWN means the task was launched, UP means it was dismissed.
event.action.dir = direction;
}
event.srcTarget[0].itemType = ItemType.TASK;
event.srcTarget[0].pageIndex = taskIndex;
fillComponentInfo(event.srcTarget[0], componentKey.componentName);
dispatchUserEvent(event, null);
mAppOrTaskLaunch = true;
}
protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) {
target.intentHash = intent.hashCode();
target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle());
fillComponentInfo(target, intent.getComponent());
}
private void fillComponentInfo(Target target, ComponentName cn) {
if (cn != null) {
target.packageNameHash = (mUuidStr + cn.getPackageName()).hashCode();
target.componentHash = (mUuidStr + cn.flattenToString()).hashCode();
}
}
public void logNotificationLaunch(View v, PendingIntent intent) {
LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
Target itemTarget = newItemTarget(v, mInstantAppResolver);
ArrayList targets = makeTargetsList(itemTarget);
if (fillLogContainer(v, itemTarget, targets)) {
itemTarget.packageNameHash = (mUuidStr + intent.getCreatorPackage()).hashCode();
}
dispatchUserEvent(event, null);
}
public void logActionCommand(int command, Target srcTarget) {
logActionCommand(command, srcTarget, null);
}
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);
}
/**
* TODO: Make this function work when a container view is passed as the 2nd param.
*/
public void logActionCommand(int command, View itemView, int srcContainerType) {
LauncherEvent event = newLauncherEvent(newCommandAction(command),
newItemTarget(itemView, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
Target itemTarget = newItemTarget(itemView, mInstantAppResolver);
ArrayList targets = makeTargetsList(itemTarget);
if (fillLogContainer(itemView, itemTarget, targets)) {
// TODO: Remove the following two lines once fillInLogContainerData can take in a
// container view.
itemTarget.type = Target.Type.CONTAINER;
itemTarget.containerType = srcContainerType;
}
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);
resetElapsedContainerMillis("state changed");
}
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);
resetElapsedContainerMillis("deep shortcut open");
}
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 void logActionBack(boolean completed, int downX, int downY, boolean isButton,
boolean gestureSwipeLeft, int containerType) {
int actionTouch = isButton ? Action.Touch.TAP : Action.Touch.SWIPE;
Action action = newCommandAction(actionTouch);
action.command = Action.Command.BACK;
action.dir = isButton ? Action.Direction.NONE :
gestureSwipeLeft ? Action.Direction.LEFT : Action.Direction.RIGHT;
Target target = newControlTarget(isButton ? ControlType.BACK_BUTTON :
ControlType.BACK_GESTURE);
target.spanX = downX;
target.spanY = downY;
target.cardinality = completed ? 1 : 0;
LauncherEvent event = newLauncherEvent(action, target, newContainerTarget(containerType));
dispatchUserEvent(event, null);
}
/**
* Currently logs following containers: workspace, allapps, widget tray.
*/
public final void resetElapsedContainerMillis(String reason) {
mElapsedContainerMillis = SystemClock.uptimeMillis();
if (!IS_VERBOSE) {
return;
}
Log.d(TAG, "resetElapsedContainerMillis reason=" + reason);
}
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;
}
}