blob: eed493d4cf631f997824fe70ae4a0b46f707f89a [file] [log] [blame]
/*
* Copyright (C) 2019 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.model;
import static android.app.prediction.AppTargetEvent.ACTION_DISMISS;
import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
import static android.app.prediction.AppTargetEvent.ACTION_PIN;
import static android.app.prediction.AppTargetEvent.ACTION_UNPIN;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.annotation.TargetApi;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
import com.android.launcher3.logger.LauncherAtom.FolderContainer;
import com.android.launcher3.logger.LauncherAtom.HotseatContainer;
import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer;
import com.android.launcher3.logging.StatsLogManager.EventEnum;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
import java.util.Locale;
import java.util.Optional;
import java.util.function.ObjIntConsumer;
import java.util.function.Predicate;
/**
* Utility class to track stats log and emit corresponding app events
*/
@TargetApi(Build.VERSION_CODES.R)
public class AppEventProducer implements StatsLogConsumer {
private static final int MSG_LAUNCH = 0;
private final Context mContext;
private final Handler mMessageHandler;
private final ObjIntConsumer<AppTargetEvent> mCallback;
private LauncherAtom.ItemInfo mLastDragItem;
public AppEventProducer(Context context, ObjIntConsumer<AppTargetEvent> callback) {
mContext = context;
mMessageHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleMessage);
mCallback = callback;
}
@WorkerThread
private boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_LAUNCH: {
mCallback.accept((AppTargetEvent) msg.obj, msg.arg1);
return true;
}
}
return false;
}
@AnyThread
private void sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId, int targetPredictor) {
sendEvent(toAppTarget(atomInfo), atomInfo, eventId, targetPredictor);
}
@AnyThread
private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId,
int targetPredictor) {
if (target != null) {
AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
.setLaunchLocation(getContainer(locationInfo))
.build();
mMessageHandler.obtainMessage(MSG_LAUNCH, targetPredictor, 0, event).sendToTarget();
}
}
@Override
public void consume(EventEnum event, LauncherAtom.ItemInfo atomInfo) {
if (event == LAUNCHER_APP_LAUNCH_TAP
|| event == LAUNCHER_TASK_LAUNCH_SWIPE_DOWN
|| event == LAUNCHER_TASK_LAUNCH_TAP
|| event == LAUNCHER_QUICKSWITCH_RIGHT
|| event == LAUNCHER_QUICKSWITCH_LEFT) {
sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION);
} else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST) {
sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_PREDICTION);
} else if (event == LAUNCHER_ITEM_DRAG_STARTED) {
mLastDragItem = atomInfo;
} else if (event == LAUNCHER_ITEM_DROP_COMPLETED) {
if (mLastDragItem == null) {
return;
}
if (isTrackedForHotseatPrediction(atomInfo)) {
sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
}
if (isTrackedForHotseatPrediction(mLastDragItem)) {
sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
}
mLastDragItem = null;
} else if (event == LAUNCHER_ITEM_DROP_FOLDER_CREATED) {
if (isTrackedForHotseatPrediction(atomInfo)) {
sendEvent(createTempFolderTarget(), atomInfo, ACTION_PIN,
CONTAINER_HOTSEAT_PREDICTION);
sendEvent(atomInfo, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
}
} else if (event == LAUNCHER_FOLDER_CONVERTED_TO_ICON) {
if (isTrackedForHotseatPrediction(atomInfo)) {
sendEvent(createTempFolderTarget(), atomInfo, ACTION_UNPIN,
CONTAINER_HOTSEAT_PREDICTION);
sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
}
} else if (event == LAUNCHER_ITEM_DROPPED_ON_REMOVE) {
if (mLastDragItem != null && isTrackedForHotseatPrediction(mLastDragItem)) {
sendEvent(mLastDragItem, ACTION_UNPIN, CONTAINER_HOTSEAT_PREDICTION);
}
} else if (event == LAUNCHER_HOTSEAT_PREDICTION_PINNED) {
if (isTrackedForHotseatPrediction(atomInfo)) {
sendEvent(atomInfo, ACTION_PIN, CONTAINER_HOTSEAT_PREDICTION);
}
}
}
@Nullable
private AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
UserHandle userHandle = Process.myUserHandle();
if (info.getIsWork()) {
userHandle = UserCache.INSTANCE.get(mContext).getUserProfiles().stream()
.filter(((Predicate<UserHandle>) userHandle::equals).negate())
.findAny()
.orElse(null);
}
if (userHandle == null) {
return null;
}
ComponentName cn = null;
ShortcutInfo shortcutInfo = null;
String id = null;
switch (info.getItemCase()) {
case APPLICATION: {
LauncherAtom.Application app = info.getApplication();
if ((cn = parseNullable(app.getComponentName())) != null) {
id = "app:" + cn.getPackageName();
}
break;
}
case SHORTCUT: {
LauncherAtom.Shortcut si = info.getShortcut();
if (!TextUtils.isEmpty(si.getShortcutId())
&& (cn = parseNullable(si.getShortcutName())) != null) {
Optional<ShortcutInfo> opt = new ShortcutRequest(mContext,
userHandle).forPackage(cn.getPackageName(), si.getShortcutId()).query(
ShortcutRequest.ALL).stream().findFirst();
if (opt.isPresent()) {
shortcutInfo = opt.get();
} else {
return null;
}
id = "shortcut:" + si.getShortcutId();
}
break;
}
case WIDGET: {
LauncherAtom.Widget widget = info.getWidget();
if ((cn = parseNullable(widget.getComponentName())) != null) {
id = "widget:" + cn.getPackageName();
}
break;
}
case TASK: {
LauncherAtom.Task task = info.getTask();
if ((cn = parseNullable(task.getComponentName())) != null) {
id = "app:" + cn.getPackageName();
}
break;
}
case FOLDER_ICON:
return createTempFolderTarget();
}
if (id != null && cn != null) {
if (shortcutInfo != null) {
return new AppTarget.Builder(new AppTargetId(id), shortcutInfo).build();
}
return new AppTarget.Builder(new AppTargetId(id), cn.getPackageName(), userHandle)
.setClassName(cn.getClassName())
.build();
}
return null;
}
private AppTarget createTempFolderTarget() {
return new AppTarget.Builder(new AppTargetId("folder:" + SystemClock.uptimeMillis()),
mContext.getPackageName(), Process.myUserHandle())
.build();
}
private String getContainer(LauncherAtom.ItemInfo info) {
ContainerInfo ci = info.getContainerInfo();
switch (ci.getContainerCase()) {
case WORKSPACE: {
// In case the item type is not widgets, the spaceX and spanY default to 1.
int spanX = info.getWidget().getSpanX();
int spanY = info.getWidget().getSpanY();
return getWorkspaceContainerString(ci.getWorkspace(), spanX, spanY);
}
case HOTSEAT: {
return getHotseatContainerString(ci.getHotseat());
}
case TASK_SWITCHER_CONTAINER: {
return "task-switcher";
}
case ALL_APPS_CONTAINER: {
return "all-apps";
}
case SEARCH_RESULT_CONTAINER: {
return "search-results";
}
case PREDICTED_HOTSEAT_CONTAINER: {
return "predictions/hotseat";
}
case PREDICTION_CONTAINER: {
return "predictions";
}
case SHORTCUTS_CONTAINER: {
return "deep-shortcuts";
}
case FOLDER: {
FolderContainer fc = ci.getFolder();
switch (fc.getParentContainerCase()) {
case WORKSPACE:
return "folder/" + getWorkspaceContainerString(fc.getWorkspace(), 1, 1);
case HOTSEAT:
return "folder/" + getHotseatContainerString(fc.getHotseat());
}
return "folder";
}
}
return "";
}
private static String getWorkspaceContainerString(WorkspaceContainer wc, int spanX, int spanY) {
return String.format(Locale.ENGLISH, "workspace/%d/[%d,%d]/[%d,%d]",
wc.getPageIndex(), wc.getGridX(), wc.getGridY(), spanX, spanY);
}
private static String getHotseatContainerString(HotseatContainer hc) {
return String.format(Locale.ENGLISH, "hotseat/%1$d/[%1$d,0]/[1,1]", hc.getIndex());
}
private static ComponentName parseNullable(String componentNameString) {
return TextUtils.isEmpty(componentNameString)
? null : ComponentName.unflattenFromString(componentNameString);
}
/**
* Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
*/
private static boolean isTrackedForHotseatPrediction(LauncherAtom.ItemInfo info) {
ContainerInfo ci = info.getContainerInfo();
switch (ci.getContainerCase()) {
case HOTSEAT:
return true;
case WORKSPACE:
return ci.getWorkspace().getPageIndex() == 0;
default:
return false;
}
}
}