Add CUJ Jank interactions for App Pair saving and launching

* Added finishCallback consumer to LauncherAccessibilityDelegate
to inform when adding an item to workspace was completed.
* The logic seemed to be dependent on the parameter
"focusForAccessibility", but all callers of that are currently
passing in true

Bug: 328646540
Test: https://paste.googleplex.com/6232597136408576
Newly added CUJs showing up when playing w/ device

Change-Id: Ia4944f8d23634bb92296938ea2d07a6babf6f77c
This commit is contained in:
Vinit Nayak
2024-03-13 18:18:56 -07:00
parent 7d5531757b
commit 422a634a24
7 changed files with 81 additions and 29 deletions
@@ -1253,7 +1253,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
if (findExactPairMatch) {
// We did not find the app pair we were looking for, so launch one.
recents.getSplitSelectController().getAppPairsController().launchAppPair(
(AppPairIcon) launchingIconView);
(AppPairIcon) launchingIconView, -1 /*cuj*/);
} else {
startItemInfoActivity(itemInfos.get(0));
}
@@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEAS
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
import static com.android.launcher3.Flags.enablePredictiveBackGesture;
import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
@@ -1347,7 +1348,8 @@ public class QuickstepLauncher extends Launcher {
* Launches two apps as an app pair.
*/
public void launchAppPair(AppPairIcon appPairIcon) {
mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon);
mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE);
}
public boolean canStartHomeSafely() {
@@ -19,6 +19,7 @@ package com.android.quickstep.util;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -40,6 +41,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.jank.Cuj;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
@@ -62,6 +64,7 @@ import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import java.util.Arrays;
@@ -112,6 +115,7 @@ public class AppPairsController {
* well on trampoline apps).
*/
public void saveAppPair(GroupedTaskView gtv) {
InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
TaskView.TaskIdAttributeContainer[] attributes = gtv.getTaskIdAttributeContainers();
WorkspaceItemInfo recentsInfo1 = attributes[0].getItemInfo();
WorkspaceItemInfo recentsInfo2 = attributes[1].getItemInfo();
@@ -168,7 +172,13 @@ public class AppPairsController {
LauncherAccessibilityDelegate delegate =
Launcher.getLauncher(mContext).getAccessibilityDelegate();
if (delegate != null) {
delegate.addToWorkspace(newAppPair, true);
delegate.addToWorkspace(newAppPair, true, (success) -> {
if (success) {
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
} else {
InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
}
});
mStatsLogManager.logger().withItemInfo(newAppPair)
.log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE);
}
@@ -179,12 +189,18 @@ public class AppPairsController {
/**
* Launches an app pair by searching the RecentsModel for running instances of each app, and
* staging either those running instances or launching the apps as new Intents.
*
* @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank
* monitoring
*/
public void launchAppPair(AppPairIcon appPairIcon) {
public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0);
WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1);
ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
mSplitSelectStateController.setLaunchingCuj(cuj);
InteractionJankMonitorWrapper.begin(appPairIcon, cuj);
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Arrays.asList(app1Key, app2Key),
false /* findExactPairMatch */,
@@ -343,7 +359,8 @@ public class AppPairsController {
&& !lastActiveTasksOfAppPair.contains(runningTaskId2)) {
// Neither A nor B are on screen, so just launch a new app pair
// normally.
launchAppPair(launchingIconView);
launchAppPair(launchingIconView,
CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR);
} else {
// Exactly one app (A or B) is on-screen, so we have to launch the other
// on the appropriate side.
@@ -388,7 +405,8 @@ public class AppPairsController {
if (!task1IsOnScreen && !task2IsOnScreen) {
// Neither App A nor App B are on-screen, launch the app pair normally.
launchAppPair(launchingIconView);
launchAppPair(launchingIconView,
CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR);
} else {
// Either A or B is on-screen, so launch the other on the appropriate
// side.
@@ -104,6 +104,7 @@ import com.android.quickstep.views.SplitInstructionsView;
import com.android.systemui.animation.RemoteAnimationRunnerCompat;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.splitscreen.ISplitSelectListener;
@@ -151,6 +152,12 @@ public class SplitSelectStateController {
/** True when the first selected split app is being launched in fullscreen. */
private boolean mLaunchingFirstAppFullscreen;
/**
* Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be
* set for all launches.
*/
private int mLaunchCuj = -1;
private FloatingTaskView mFirstFloatingTaskView;
private SplitInstructionsView mSplitInstructionsView;
@@ -707,6 +714,10 @@ public class SplitSelectStateController {
return mSplitAnimationController;
}
public void setLaunchingCuj(int launchCuj) {
mLaunchCuj = launchCuj;
}
/**
* Requires Shell Transitions
*/
@@ -850,6 +861,11 @@ public class SplitSelectStateController {
mSplitInstructionsView = null;
mLaunchingFirstAppFullscreen = false;
if (mLaunchCuj != -1) {
InteractionJankMonitorWrapper.end(mLaunchCuj);
}
mLaunchCuj = -1;
if (mSessionInstanceIds != null) {
mStatsLogManager.logger()
.withInstanceId(mSessionInstanceIds.second)
@@ -105,7 +105,7 @@ class AppPairsControllerTest {
whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo)
whenever(mockTask1.getKey()).thenReturn(mockTaskKey1)
whenever(mockTask2.getKey()).thenReturn(mockTaskKey2)
doNothing().whenever(spyAppPairsController).launchAppPair(any())
doNothing().whenever(spyAppPairsController).launchAppPair(any(), any())
doNothing()
.whenever(spyAppPairsController)
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
@@ -210,7 +210,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchAppPair and launchToSide were never called
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, never())
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}
@@ -234,7 +234,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
}
@@ -258,7 +258,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
}
@@ -282,7 +282,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
}
@@ -306,7 +306,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
}
@@ -330,7 +330,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchAppPair was called
verify(spyAppPairsController, times(1)).launchAppPair(any())
verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
verify(spyAppPairsController, never())
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}
@@ -354,7 +354,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
}
@@ -378,7 +378,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchToSide was called with the correct arguments
verify(spyAppPairsController, never()).launchAppPair(any())
verify(spyAppPairsController, never()).launchAppPair(any(), any())
verify(spyAppPairsController, times(1))
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
}
@@ -402,7 +402,7 @@ class AppPairsControllerTest {
callback.accept(arrayOf(mockTask1, mockTask2))
// Verify that launchAppPair was called
verify(spyAppPairsController, times(1)).launchAppPair(any())
verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
verify(spyAppPairsController, never())
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}
@@ -22,6 +22,8 @@ import android.view.KeyEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.Nullable;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.ButtonDropTarget;
import com.android.launcher3.CellLayout;
@@ -58,6 +60,7 @@ import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Launcher> {
@@ -173,7 +176,7 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
} else if (action == MOVE) {
return beginAccessibleDrag(host, item, fromKeyboard);
} else if (action == ADD_TO_WORKSPACE) {
return addToWorkspace(item, true);
return addToWorkspace(item, true /*accessibility*/, null /*finishCallback*/);
} else if (action == MOVE_TO_WORKSPACE) {
return moveToWorkspace(item);
} else if (action == RESIZE) {
@@ -384,13 +387,19 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
* @param item item to be added
* @param accessibility true if the first item to be added to the workspace
* should be focused for accessibility.
* @param finishCallback Callback which will be run after this item has been added
* and the view has been transitioned to the workspace, or on failure.
*
* @return true if the item could be successfully added
*/
public boolean addToWorkspace(ItemInfo item, boolean accessibility) {
public boolean addToWorkspace(ItemInfo item, boolean accessibility,
@Nullable Consumer<Boolean> finishCallback) {
final int[] coordinates = new int[2];
final int screenId = findSpaceOnWorkspace(item, coordinates);
if (screenId == -1) {
if (finishCallback != null) {
finishCallback.accept(false /*success*/);
}
return false;
}
mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
@@ -400,7 +409,7 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
LauncherSettings.Favorites.CONTAINER_DESKTOP,
screenId, coordinates[0], coordinates[1]);
bindItem(item, accessibility);
bindItem(item, accessibility, finishCallback);
announceConfirmation(R.string.item_added_to_workspace);
} else if (item instanceof PendingAddItemInfo) {
PendingAddItemInfo info = (PendingAddItemInfo) item;
@@ -415,7 +424,7 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
mContext.getModelWriter().addItemToDatabase(info,
LauncherSettings.Favorites.CONTAINER_DESKTOP,
screenId, coordinates[0], coordinates[1]);
bindItem(info, accessibility);
bindItem(info, accessibility, finishCallback);
} else if (item instanceof FolderInfo fi) {
Workspace<?> workspace = mContext.getWorkspace();
workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
@@ -425,23 +434,30 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Lau
fi.contents.forEach(member -> {
mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1);
});
bindItem(fi, accessibility);
bindItem(fi, accessibility, finishCallback);
}
}));
return true;
}
private void bindItem(ItemInfo item, boolean focusForAccessibility) {
private void bindItem(ItemInfo item, boolean focusForAccessibility,
@Nullable Consumer<Boolean> finishCallback) {
View view = mContext.getItemInflater().inflateItem(item, mContext.getModelWriter());
if (view == null) {
if (finishCallback != null) {
finishCallback.accept(false /*success*/);
}
return;
}
AnimatorSet anim = null;
if (focusForAccessibility) {
anim = new AnimatorSet();
anim.addListener(forEndCallback(
() -> view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
}
AnimatorSet anim = new AnimatorSet();
anim.addListener(forEndCallback((success) -> {
if (focusForAccessibility) {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
if (finishCallback != null) {
finishCallback.accept(success);
}
}));
mContext.bindInflatedItems(Collections.singletonList(Pair.create(item, view)), anim);
}
@@ -168,7 +168,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP);
handleClose(true);
Launcher.getLauncher(mActivityContext).getAccessibilityDelegate()
.addToWorkspace(info, /*accessibility=*/ false);
.addToWorkspace(info, /*accessibility=*/ false, /*finishCallback=*/ null);
}
@Override