mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 02:38:20 +00:00
Merge "App Pairs: Implement save, inflate, launch, and delete" into udc-qpr-dev
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -140,6 +140,7 @@ public interface TaskShortcutFactory {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dismissTaskMenuView(mTarget);
|
||||
((RecentsView) mTarget.getOverviewPanel())
|
||||
.getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
30
res/layout/app_pair_icon.xml
Normal file
30
res/layout/app_pair_icon.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
102
src/com/android/launcher3/apppairs/AppPairIcon.java
Normal file
102
src/com/android/launcher3/apppairs/AppPairIcon.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user