Merge "App Pairs: Implement save, inflate, launch, and delete" into udc-qpr-dev

This commit is contained in:
Jeremy Sim
2023-08-08 00:56:04 +00:00
committed by Android (Google) Code Review
16 changed files with 523 additions and 85 deletions

View File

@@ -119,6 +119,7 @@ import com.android.systemui.unfold.updates.RotationChangeProvider;
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Optional;
/**
@@ -997,9 +998,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
if (recents == null) {
return;
}
recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
info.getComponentKey(),
foundTask -> {
recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(info.getComponentKey()),
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView =
recents.getTaskViewByTaskId(foundTask.key.id);

View File

@@ -40,8 +40,11 @@ import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Stream;
/**
@@ -204,9 +207,10 @@ public class TaskbarUIController {
return;
}
recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback(
splitSelectSource.itemInfo.getComponentKey(),
foundTask -> {
recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
splitSelectSource.alreadyRunningTaskId = foundTask == null
? INVALID_TASK_ID
: foundTask.key.id;
@@ -221,9 +225,10 @@ public class TaskbarUIController {
*/
public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
RecentsView recents = getRecentsView();
recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
info.getComponentKey(),
foundTask -> {
recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(info.getComponentKey()),
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
// TODO (b/266482558): This additional null check is needed because there

View File

@@ -117,6 +117,7 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.statehandlers.DepthController;
@@ -173,6 +174,7 @@ import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
import com.android.systemui.unfold.UnfoldSharedComponent;
@@ -618,9 +620,10 @@ public class QuickstepLauncher extends Launcher {
RecentsView recentsView = getOverviewPanel();
// Check if there is already an instance of this app running, if so, initiate the split
// using that.
mSplitSelectStateController.findLastActiveTaskAndRunCallback(
splitSelectSource.itemInfo.getComponentKey(),
foundTask -> {
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
boolean taskWasFound = foundTask != null;
splitSelectSource.alreadyRunningTaskId = taskWasFound
? foundTask.key.id
@@ -1326,6 +1329,13 @@ public class QuickstepLauncher extends Launcher {
: groupTask.mSplitBounds.leftTaskPercent);
}
/**
* Launches two apps as an app pair.
*/
public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
}
public boolean canStartHomeSafely() {
OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();

View File

@@ -140,6 +140,7 @@ public interface TaskShortcutFactory {
@Override
public void onClick(View view) {
dismissTaskMenuView(mTarget);
((RecentsView) mTarget.getOverviewPanel())
.getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
}

View File

@@ -17,19 +17,30 @@
package com.android.quickstep.util;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import java.util.Arrays;
/**
* Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -52,10 +63,13 @@ public class AppPairsController {
private final Context mContext;
private final SplitSelectStateController mSplitSelectStateController;
private final StatsLogManager mStatsLogManager;
public AppPairsController(Context context,
SplitSelectStateController splitSelectStateController) {
SplitSelectStateController splitSelectStateController,
StatsLogManager statsLogManager) {
mContext = context;
mSplitSelectStateController = splitSelectStateController;
mStatsLogManager = statsLogManager;
}
/**
@@ -84,11 +98,51 @@ public class AppPairsController {
LauncherAccessibilityDelegate delegate =
Launcher.getLauncher(mContext).getAccessibilityDelegate();
if (delegate != null) {
MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
delegate.addToWorkspace(newAppPair, true);
mStatsLogManager.logger().withItemInfo(newAppPair)
.log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE);
}
});
});
}
/**
* 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.
*/
public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Arrays.asList(app1Key, app2Key),
foundTasks -> {
@Nullable Task foundTask1 = foundTasks.get(0);
Intent task1Intent;
int task1Id;
if (foundTask1 != null) {
task1Id = foundTask1.key.id;
task1Intent = null;
} else {
task1Id = ActivityTaskManager.INVALID_TASK_ID;
task1Intent = app1.intent;
}
mSplitSelectStateController.setInitialTaskSelect(task1Intent,
SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
app1,
LAUNCHER_APP_PAIR_LAUNCH,
task1Id);
@Nullable Task foundTask2 = foundTasks.get(1);
if (foundTask2 != null) {
mSplitSelectStateController.setSecondTask(foundTask2);
} else {
mSplitSelectStateController.setSecondTask(
app2.intent, app2.user);
}
mSplitSelectStateController.launchSplitTasks();
});
}
/**

View File

@@ -78,6 +78,7 @@ import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -126,7 +127,7 @@ public class SplitSelectStateController {
mDepthController = depthController;
mRecentTasksModel = recentsModel;
mSplitAnimationController = new SplitAnimationController(this);
mAppPairsController = new AppPairsController(context, this);
mAppPairsController = new AppPairsController(context, this, statsLogManager);
mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
}
@@ -153,37 +154,46 @@ public class SplitSelectStateController {
}
/**
* Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task
* matching a given ComponentName. Then uses that Task (which could be null) with the given
* callback.
* Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in
* RecentsModel. If found, the Task will be the most recently-interacted-with instance of that
* Task. Then runs the given callback on that List.
* <p>
* Used in various task-switching or splitscreen operations when we need to check if there is a
* currently running Task of a certain type and use the most recent one.
*/
public void findLastActiveTaskAndRunCallback(
@Nullable ComponentKey componentKey, Consumer<Task> callback) {
public void findLastActiveTasksAndRunCallback(
@Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
if (componentKey == null) {
callback.accept(null);
if (componentKeys == null || componentKeys.isEmpty()) {
callback.accept(Collections.emptyList());
return;
}
Task lastActiveTask = null;
// Loop through tasks in reverse, since they are ordered with most-recent tasks last.
for (int i = taskGroups.size() - 1; i >= 0; i--) {
GroupTask groupTask = taskGroups.get(i);
Task task1 = groupTask.task1;
if (isInstanceOfComponent(task1, componentKey)) {
lastActiveTask = task1;
break;
}
Task task2 = groupTask.task2;
if (isInstanceOfComponent(task2, componentKey)) {
lastActiveTask = task2;
break;
List<Task> lastActiveTasks = new ArrayList<>();
// For each key we are looking for, add to lastActiveTasks with the corresponding Task
// (or null if not found).
for (ComponentKey key : componentKeys) {
Task lastActiveTask = null;
// Loop through tasks in reverse, since they are ordered with most-recent tasks last
for (int i = taskGroups.size() - 1; i >= 0; i--) {
GroupTask groupTask = taskGroups.get(i);
Task task1 = groupTask.task1;
// Don't add duplicate Tasks
if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
lastActiveTask = task1;
break;
}
Task task2 = groupTask.task2;
if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
lastActiveTask = task2;
break;
}
}
lastActiveTasks.add(lastActiveTask);
}
callback.accept(lastActiveTask);
callback.accept(lastActiveTasks);
});
}
@@ -226,7 +236,7 @@ public class SplitSelectStateController {
* To be called when the both split tasks are ready to be launched. Call after launcher side
* animations are complete.
*/
public void launchSplitTasks(Consumer<Boolean> callback) {
public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
@@ -238,6 +248,14 @@ public class SplitSelectStateController {
.log(mSplitSelectDataHolder.getSplitEvent());
}
/**
* A version of {@link #launchTasks(Consumer, boolean, float, InstanceId)} with no success
* callback.
*/
public void launchSplitTasks() {
launchSplitTasks(null);
}
/**
* To be called as soon as user selects the second task (even if animations aren't complete)
* @param task The second task that will be launched.
@@ -271,8 +289,8 @@ public class SplitSelectStateController {
* create a split instance, null for cases that bring existing instaces to the
* foreground (quickswitch, launching previous pairs from overview)
*/
public void launchTasks(Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
@Nullable InstanceId shellInstanceId) {
public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
float splitRatio, @Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
final ActivityOptions options1 = ActivityOptions.makeBasic();
@@ -457,7 +475,7 @@ public class SplitSelectStateController {
}
private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
Consumer<Boolean> callback, String transitionName) {
@Nullable Consumer<Boolean> callback, String transitionName) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
return new RemoteTransition(animationRunner,
@@ -465,7 +483,7 @@ public class SplitSelectStateController {
}
private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
Consumer<Boolean> callback) {
@Nullable Consumer<Boolean> callback) {
final RemoteSplitLaunchAnimationRunner animationRunner =
new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
return new RemoteAnimationAdapter(animationRunner, 300, 150,
@@ -514,7 +532,7 @@ public class SplitSelectStateController {
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> callback) {
@Nullable Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
@@ -563,7 +581,7 @@ public class SplitSelectStateController {
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
Consumer<Boolean> successCallback) {
@Nullable Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;

View File

@@ -37,6 +37,7 @@ import com.android.launcher3.util.withArgCaptor
import com.android.quickstep.RecentsModel
import com.android.quickstep.SystemUiProxy
import com.android.systemui.shared.recents.model.Task
import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -48,7 +49,6 @@ import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.function.Consumer
@RunWith(AndroidJUnit4::class)
class SplitSelectStateControllerTest {
@@ -67,6 +67,9 @@ class SplitSelectStateControllerTest {
private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
private var taskIdCounter = 0
private fun getUniqueId(): Int { return ++taskIdCounter }
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -100,15 +103,15 @@ class SplitSelectStateControllerTest {
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTaskAndRunCallback
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
nonMatchingComponent,
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonMatchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -139,27 +142,27 @@ class SplitSelectStateControllerTest {
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTaskAndRunCallback
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<Task> {
Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
it.key.baseIntent.component.packageName,
it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it.key.baseIntent.component.className,
it[0].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it, groupTask1.task1)
assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
matchingComponent,
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -190,15 +193,15 @@ class SplitSelectStateControllerTest {
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTaskAndRunCallback
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
nonPrimaryUserComponent,
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -231,28 +234,28 @@ class SplitSelectStateControllerTest {
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTaskAndRunCallback
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<Task> {
Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
it.key.baseIntent.component.packageName,
it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it.key.baseIntent.component.className,
it[0].key.baseIntent.component?.className,
matchingClass
)
assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier)
assertEquals(it, groupTask1.task1)
assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
nonPrimaryUserComponent,
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -283,27 +286,200 @@ class SplitSelectStateControllerTest {
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTaskAndRunCallback
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<Task> {
Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
it.key.baseIntent.component.packageName,
it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it.key.baseIntent.component.className,
it[0].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it, groupTask2.task2)
assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
matchingComponent,
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
}
// Send our mocked tasks
consumer.accept(tasks)
}
@Test
fun activeTasks_multipleSearchShouldFindTask() {
val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
val matchingPackage = "hotdog"
val matchingClass = "juice"
val matchingComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
generateGroupTask(
ComponentName("hotdog", "pie"),
ComponentName("pumpkin", "pie")
)
val groupTask2 =
generateGroupTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass)
)
val tasks: ArrayList<GroupTask> = ArrayList()
tasks.add(groupTask2)
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<List<Task>> {
assertEquals("Expected array length 2", 2, it.size)
assertNull("No tasks should have matched", it[0] /*task*/)
assertEquals(
"ComponentName package mismatched",
it[1].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it[1].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it[1], groupTask2.task2)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonMatchingComponent, matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
}
// Send our mocked tasks
consumer.accept(tasks)
}
@Test
fun activeTasks_multipleSearchShouldNotFindSameTaskTwice() {
val matchingPackage = "hotdog"
val matchingClass = "juice"
val matchingComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
generateGroupTask(
ComponentName("hotdog", "pie"),
ComponentName("pumpkin", "pie")
)
val groupTask2 =
generateGroupTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass)
)
val tasks: ArrayList<GroupTask> = ArrayList()
tasks.add(groupTask2)
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<List<Task>> {
assertEquals("Expected array length 2", 2, it.size)
assertEquals(
"ComponentName package mismatched",
it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it[0].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it[0], groupTask2.task2)
assertNull("No tasks should have matched", it[1] /*task*/)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent, matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
}
// Send our mocked tasks
consumer.accept(tasks)
}
@Test
fun activeTasks_multipleSearchShouldFindDifferentInstancesOfSameTask() {
val matchingPackage = "hotdog"
val matchingClass = "juice"
val matchingComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
generateGroupTask(
ComponentName(matchingPackage, matchingClass),
ComponentName("pumpkin", "pie")
)
val groupTask2 =
generateGroupTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass)
)
val tasks: ArrayList<GroupTask> = ArrayList()
tasks.add(groupTask2)
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
// #findLastActiveTasksAndRunCallback
val taskConsumer =
Consumer<List<Task>> {
assertEquals("Expected array length 2", 2, it.size)
assertEquals(
"ComponentName package mismatched",
it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it[0].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it[0], groupTask1.task1)
assertEquals(
"ComponentName package mismatched",
it[1].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
it[1].key.baseIntent.component?.className,
matchingClass
)
assertEquals(it[1], groupTask2.task2)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent, matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -366,6 +542,7 @@ class SplitSelectStateControllerTest {
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
var intent = Intent()
intent.component = task1ComponentName
taskInfo.baseIntent = intent
@@ -373,6 +550,7 @@ class SplitSelectStateControllerTest {
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
intent = Intent()
intent.component = task2ComponentName
taskInfo.baseIntent = intent
@@ -393,6 +571,7 @@ class SplitSelectStateControllerTest {
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
// Apply custom userHandle1
taskInfo.userId = userHandle1.identifier
var intent = Intent()
@@ -401,6 +580,7 @@ class SplitSelectStateControllerTest {
task1.key = Task.TaskKey(taskInfo)
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
// Apply custom userHandle2
taskInfo.userId = userHandle2.identifier
intent = Intent()

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 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.
-->
<com.android.launcher3.apppairs.AppPairIcon
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:focusable="true" >
<com.android.launcher3.views.DoubleShadowBubbleTextView
style="@style/BaseIcon.Workspace"
android:id="@+id/app_pair_icon_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:layout_gravity="top" />
</com.android.launcher3.apppairs.AppPairIcon>

View File

@@ -145,6 +145,7 @@ import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.BaseSearchConfig;
import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
@@ -2451,9 +2452,9 @@ public class Launcher extends StatefulActivity<LauncherState>
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
FolderInfo info = (FolderInfo) item;
// TODO (jeremysim b/274189428): Create app pair icon
view = null;
view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -3395,4 +3396,12 @@ public class Launcher extends StatefulActivity<LauncherState>
public View.OnLongClickListener getAllAppsItemLongClickListener() {
return ItemLongClickListener.INSTANCE_ALL_APPS;
}
/**
* Handles an app pair launch; overridden in
* {@link com.android.launcher3.uioverrides.QuickstepLauncher}
*/
public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
// Overridden
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2023 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.apppairs;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.views.ActivityContext;
import java.util.Collections;
import java.util.Comparator;
/**
* A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
*/
public class AppPairIcon extends FrameLayout implements DraggableView {
private ActivityContext mActivity;
private BubbleTextView mAppPairName;
private FolderInfo mInfo;
public AppPairIcon(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AppPairIcon(Context context) {
super(context);
}
/**
* Builds an AppPairIcon to be added to the Launcher
*/
public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
@Nullable ViewGroup group, FolderInfo appPairInfo) {
LayoutInflater inflater = (group != null)
? LayoutInflater.from(group.getContext())
: activity.getLayoutInflater();
AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
// Sort contents, so that left-hand app comes first
Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank));
icon.setClipToPadding(false);
icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
// TODO (jeremysim b/274189428): Replace this placeholder icon
WorkspaceItemInfo placeholder = new WorkspaceItemInfo();
placeholder.newIcon(icon.getContext());
icon.mAppPairName.applyFromWorkspaceItem(placeholder);
icon.mAppPairName.setText(appPairInfo.title);
icon.setTag(appPairInfo);
icon.setOnClickListener(activity.getItemOnClickListener());
icon.mInfo = appPairInfo;
icon.mActivity = activity;
icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
return icon;
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;
}
@Override
public void getWorkspaceVisualDragBounds(Rect bounds) {
mAppPairName.getIconBounds(bounds);
}
public FolderInfo getInfo() {
return mInfo;
}
}

View File

@@ -51,6 +51,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.TextClock;
import androidx.annotation.NonNull;
@@ -70,6 +71,7 @@ import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.config.FeatureFlags;
@@ -358,12 +360,13 @@ public class LauncherPreviewRenderer extends ContextWrapper
addInScreenFromBind(icon, info);
}
private void inflateAndAddFolder(FolderInfo info) {
private void inflateAndAddCollectionIcon(FolderInfo info) {
CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP
? mWorkspaceScreens.get(info.screenId)
: mHotseat;
FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
info);
FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info)
: AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info);
addInScreenFromBind(folderIcon, info);
}
@@ -467,7 +470,8 @@ public class LauncherPreviewRenderer extends ContextWrapper
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case Favorites.ITEM_TYPE_FOLDER:
inflateAndAddFolder((FolderInfo) itemInfo);
case Favorites.ITEM_TYPE_APP_PAIR:
inflateAndAddCollectionIcon((FolderInfo) itemInfo);
break;
default:
break;

View File

@@ -641,11 +641,17 @@ public class StatsLogManager implements ResourceBasedOverride {
@UiEvent(doc = "User has swiped upwards from the gesture handle to show transient taskbar.")
LAUNCHER_TRANSIENT_TASKBAR_SHOW(1331),
@UiEvent(doc = "User has clicked an app pair and launched directly into split screen.")
LAUNCHER_APP_PAIR_LAUNCH(1374),
@UiEvent(doc = "User saved an app pair.")
LAUNCHER_APP_PAIR_SAVE(1456),
@UiEvent(doc = "App launched through pending intent")
LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394),
;
LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394)
// ADD MORE
;
private final int mId;

View File

@@ -690,9 +690,11 @@ public class LoaderTask implements Runnable {
break;
case Favorites.ITEM_TYPE_FOLDER:
case Favorites.ITEM_TYPE_APP_PAIR:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
folderInfo.itemType = c.itemType;
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(c.mTitleIndex);
folderInfo.spanX = 1;

View File

@@ -489,6 +489,7 @@ public class ModelWriter {
case Favorites.ITEM_TYPE_APPLICATION:
case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
case Favorites.ITEM_TYPE_FOLDER:
case Favorites.ITEM_TYPE_APP_PAIR:
if (!mBgDataModel.workspaceItems.contains(modelItem)) {
mBgDataModel.workspaceItems.add(modelItem);
}

View File

@@ -119,8 +119,8 @@ public class FolderInfo extends ItemInfo {
public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
FolderInfo newAppPair = new FolderInfo();
newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
newAppPair.contents.add(app1);
newAppPair.contents.add(app2);
newAppPair.add(app1, /* animate */ false);
newAppPair.add(app2, /* animate */ false);
return newAppPair;
}

View File

@@ -42,6 +42,7 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.InstanceId;
@@ -95,6 +96,8 @@ public class ItemClickHandler {
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
} else if (v instanceof AppPairIcon) {
onClickAppPairIcon(v);
}
} else if (tag instanceof AppInfo) {
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
@@ -122,6 +125,17 @@ public class ItemClickHandler {
}
}
/**
* Event handler for an app pair icon click.
*
* @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
*/
private static void onClickAppPairIcon(View v) {
Launcher launcher = Launcher.getLauncher(v.getContext());
FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
}
/**
* Event handler for the app widget view which has not fully restored.
*/