From 448e0ade10ac32ae3365d0c689289dd0a0d091db Mon Sep 17 00:00:00 2001 From: Ikram Gabiyev Date: Thu, 20 Oct 2022 06:56:25 +0000 Subject: [PATCH] Filter recents view instances by package name Filter instances of GroupTasks based on package name as a part of support for multi-instance Add a feature flag to toggle multi-instance features See the video below for how to use the demo. Note: some extra UI elements were added since video was recorded, but the filtering process is the same. http://recall/-/da585DRwKRZK3S2xxcQrSm/gW9HZnbCvGyH1DQiVizOW2 See go/multi-instance for more info about the feature Bug: 253520408 Test: manually tested the instance filtering Change-Id: I19c947ca353699096388b9fbbdca6d75cb0041a7 --- quickstep/res/layout/task.xml | 11 ++ quickstep/res/layout/task_grouped.xml | 22 +++ quickstep/res/values/colors.xml | 3 + quickstep/res/values/dimens.xml | 3 + quickstep/res/values/strings.xml | 7 + .../android/quickstep/RecentTasksList.java | 16 +- .../android/quickstep/RecentsFilterState.java | 176 ++++++++++++++++++ .../com/android/quickstep/RecentsModel.java | 23 ++- .../quickstep/views/GroupedTaskView.java | 17 +- .../android/quickstep/views/RecentsView.java | 67 ++++++- .../com/android/quickstep/views/TaskView.java | 44 +++++ res/drawable/ic_select_windows.xml | 26 +++ .../launcher3/config/FeatureFlags.java | 4 + 13 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/RecentsFilterState.java create mode 100644 res/drawable/ic_select_windows.xml diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml index 7e5b85c41d..02a439d5b0 100644 --- a/quickstep/res/layout/task.xml +++ b/quickstep/res/layout/task.xml @@ -28,6 +28,17 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + + + + + #FFFFFFFF + + #333333 + \ No newline at end of file diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index ad77768263..403d6bd486 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -306,6 +306,9 @@ 75dp 48dp + + 30dp + diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 801ba2683f..e691522d5e 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -36,6 +36,13 @@ Clear all + + Back + + + + Click to show only this app\'s tasks + Recent apps diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index d46565bd17..b33ceca838 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -45,6 +45,8 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * Manages the recent task list from the system, caching it as necessary. @@ -129,14 +131,18 @@ public class RecentTasksList { * @return The change id of the current task list */ public synchronized int getTasks(boolean loadKeysOnly, - Consumer> callback) { + Consumer> callback, Predicate filter) { final int requestLoadId = mChangeId; if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) { // The list is up to date, send the callback on the next frame, // so that requestID can be returned first. if (callback != null) { // Copy synchronously as the changeId might change by next frame - ArrayList result = copyOf(mResultsUi); + // and filter GroupTasks + ArrayList result = mResultsUi.stream().filter(filter) + .map(GroupTask::copy) + .collect(Collectors.toCollection(ArrayList::new)); + mMainThreadExecutor.post(() -> { callback.accept(result); }); @@ -156,7 +162,11 @@ public class RecentTasksList { mLoadingTasksInBackground = false; mResultsUi = loadResult; if (callback != null) { - ArrayList result = copyOf(mResultsUi); + // filter the tasks if needed before passing them into the callback + ArrayList result = mResultsUi.stream().filter(filter) + .map(GroupTask::copy) + .collect(Collectors.toCollection(ArrayList::new)); + callback.accept(result); } }); diff --git a/quickstep/src/com/android/quickstep/RecentsFilterState.java b/quickstep/src/com/android/quickstep/RecentsFilterState.java new file mode 100644 index 0000000000..ff6951d33f --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsFilterState.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 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.quickstep; + +import androidx.annotation.Nullable; + +import com.android.quickstep.util.GroupTask; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Keeps track of the state of {@code RecentsView}. + * + *

More specifically, used for keeping track of the state of filters applied on tasks + * in {@code RecentsView} for multi-instance management. + */ +public class RecentsFilterState { + // the minimum number of tasks per package present to allow filtering + public static final int MIN_FILTERING_TASK_COUNT = 2; + + // default filter that returns true for any input + public static final Predicate DEFAULT_FILTER = (groupTask -> true); + + // the package name to filter recent tasks by + @Nullable + private String mPackageNameToFilter = null; + + // the callback that gets executed upon filter change + @Nullable + private Runnable mOnFilterUpdatedListener = null; + + // map maintaining the count for each unique base activity package name currently in the recents + @Nullable + private Map mInstanceCountMap; + + /** + * Returns {@code true} if {@code RecentsView} filters tasks by some package name. + */ + public boolean isFiltered() { + return mPackageNameToFilter != null; + } + + /** + * Returns the package name that tasks are filtered by. + */ + @Nullable + public String getPackageNameToFilter() { + return mPackageNameToFilter; + } + + + /** + * Sets a listener on any changes to the filter. + * + * @param callback listener to be executed upon filter updates + */ + public void setOnFilterUpdatedListener(@Nullable Runnable callback) { + mOnFilterUpdatedListener = callback; + } + + /** + * Updates the filter such that tasks are filtered by a certain package name. + * + * @param packageName package name of the base activity to filter tasks by; + * if null, filter is turned off + */ + public void setFilterBy(@Nullable String packageName) { + if (Objects.equals(packageName, mPackageNameToFilter)) { + return; + } + + mPackageNameToFilter = packageName; + + if (mOnFilterUpdatedListener != null) { + mOnFilterUpdatedListener.run(); + } + } + + /** + * Updates the map of package names to their count in the most recent list of tasks. + * + * @param groupTaskList the list of tasks that map update is be based on + */ + public void updateInstanceCountMap(List groupTaskList) { + mInstanceCountMap = getInstanceCountMap(groupTaskList); + } + + /** + * Returns the map of package names to their count in the most recent list of tasks. + */ + @Nullable + public Map getInstanceCountMap() { + return mInstanceCountMap; + } + + /** + * Returns a predicate for filtering out GroupTasks by package name. + * + * @param packageName package name to filter GroupTasks by + * if null, Predicate always returns true. + */ + public static Predicate getFilter(@Nullable String packageName) { + if (packageName == null) { + return DEFAULT_FILTER; + } + + return (groupTask) -> (groupTask.task2 != null + && groupTask.task2.key.getPackageName().equals(packageName)) + || groupTask.task1.key.getPackageName().equals(packageName); + } + + /** + * Returns a map of package names to their frequencies in a list of GroupTasks. + * + * @param groupTasks the list to go through to create the map + */ + public static Map getInstanceCountMap(List groupTasks) { + Map instanceCountMap = new HashMap<>(); + + for (GroupTask groupTask : groupTasks) { + final String firstTaskPkgName = groupTask.task1.key.getPackageName(); + final String secondTaskPkgName = + groupTask.task2 == null ? null : groupTask.task2.key.getPackageName(); + + // increment the instance count for the first task's base activity package name + incrementOrAddIfNotExists(instanceCountMap, firstTaskPkgName); + + // check if second task is non existent + if (secondTaskPkgName != null) { + // increment the instance count for the second task's base activity package name + incrementOrAddIfNotExists(instanceCountMap, secondTaskPkgName); + } + } + + return instanceCountMap; + } + + /** + * Returns true if tasks of provided package name should show filter UI. + * + * @param taskPackageName package name of the task in question + */ + public boolean shouldShowFilterUI(String taskPackageName) { + // number of occurrences in recents overview with the package name of this task + int instanceCount = getInstanceCountMap().get(taskPackageName); + + // if the number of occurrences isn't enough make sure tasks can't be filtered by + // the package name of this task + return !(isFiltered() || instanceCount < MIN_FILTERING_TASK_COUNT); + } + + private static void incrementOrAddIfNotExists(Map map, String pkgName) { + if (!map.containsKey(pkgName)) { + map.put(pkgName, 0); + } + map.put(pkgName, map.get(pkgName) + 1); + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index 3d6da8e726..913f08f41d 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -50,6 +50,7 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.function.Predicate; /** * Singleton class to load and manage recents model. @@ -104,7 +105,22 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * @return the request id associated with this call. */ public int getTasks(Consumer> callback) { - return mTaskList.getTasks(false /* loadKeysOnly */, callback); + return mTaskList.getTasks(false /* loadKeysOnly */, callback, + RecentsFilterState.DEFAULT_FILTER); + } + + + /** + * Fetches the list of recent tasks, based on a filter + * + * @param callback The callback to receive the task plan once its complete or null. This is + * always called on the UI thread. + * @param filter Returns true if a GroupTask should be included into the list passed into + * callback. + * @return the request id associated with this call. + */ + public int getTasks(Consumer> callback, Predicate filter) { + return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter); } /** @@ -126,8 +142,9 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener * Checks if a task has been removed or not. * * @param callback Receives true if task is removed, false otherwise + * @param filter Returns true if GroupTask should be in the list of considerations */ - public void isTaskRemoved(int taskId, Consumer callback) { + public void isTaskRemoved(int taskId, Consumer callback, Predicate filter) { mTaskList.getTasks(true /* loadKeysOnly */, (taskGroups) -> { for (GroupTask group : taskGroups) { if (group.containsTask(taskId)) { @@ -136,7 +153,7 @@ public class RecentsModel implements IconChangeListener, TaskStackChangeListener } } callback.accept(true); - }); + }, filter); } @Override diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index 2cada0afcc..3f7d677970 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -2,7 +2,6 @@ package com.android.quickstep.views; import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; import android.content.Context; import android.graphics.PointF; @@ -102,6 +101,22 @@ public class GroupedTaskView extends TaskView { PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT); } + /** + * Sets up an on-click listener and the visibility for show_windows icon on top of each task. + */ + @Override + public void setUpShowAllInstancesListener() { + // sets up the listener for the left/top task + super.setUpShowAllInstancesListener(); + + // right/bottom task's base package name + String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName(); + + // icon of the right/bottom task + View showWindowsView = findViewById(R.id.show_windows_right); + updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)); + } + @Override public void onTaskListVisibilityChanged(boolean visible, int changes) { super.onTaskListVisibilityChanged(visible, changes); diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 3a2841e7a8..971eda50c5 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -164,6 +164,7 @@ import com.android.quickstep.BaseActivityInterface; import com.android.quickstep.GestureState; import com.android.quickstep.RecentsAnimationController; import com.android.quickstep.RecentsAnimationTargets; +import com.android.quickstep.RecentsFilterState; import com.android.quickstep.RecentsModel; import com.android.quickstep.RemoteAnimationTargets; import com.android.quickstep.RemoteTargetGluer; @@ -580,7 +581,7 @@ public abstract class RecentsView { + this.setAndApplyFilter(null); + }); + } else { + mClearAllButton.setText(R.string.recents_clear_all); + mClearAllButton.setOnClickListener(this::dismissAllTasks); + } + } + + /** + * Invalidates the list of tasks so that an update occurs to the list of tasks if requested. + */ + private void invalidateTaskList() { + mTaskListChangeId = -1; } public OverScroller getScroller() { @@ -1546,6 +1599,9 @@ public abstract class RecentsView= 0; i--) { @@ -1580,6 +1636,7 @@ public abstract class RecentsView { + // update and apply a new filter + getRecentsView().setAndApplyFilter(taskPackageName); + }; + + if (!getRecentsView().getFilterState().shouldShowFilterUI(taskPackageName)) { + cb = null; + } + return cb; + } + + /** + * Sets the correct visibility and callback on the provided filterView based on whether + * the callback is null or not + */ + protected void updateFilterCallback(@NonNull View filterView, + @Nullable View.OnClickListener callback) { + if (callback == null) { + filterView.setVisibility(GONE); + } else { + filterView.setVisibility(VISIBLE); + } + + filterView.setOnClickListener(callback); + } + public TaskIdAttributeContainer[] getTaskIdAttributeContainers() { return mTaskIdAttributeContainer; } diff --git a/res/drawable/ic_select_windows.xml b/res/drawable/ic_select_windows.xml new file mode 100644 index 0000000000..cba0fde614 --- /dev/null +++ b/res/drawable/ic_select_windows.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index b46e43f63b..082f6a1a95 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -377,6 +377,10 @@ public final class FeatureFlags { "ENABLE_TASKBAR_EDU_TOOLTIP", false, "Enable the tooltip version of the Taskbar education flow."); + public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag( + "ENABLE_MULTI_INSTANCE", false, + "Enables creation and filtering of multiple task instances in overview"); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) {