Merge "Filter recents view instances by package name" into tm-qpr-dev

This commit is contained in:
Ikram Gabiyev
2022-12-29 01:17:42 +00:00
committed by Android (Google) Code Review
13 changed files with 410 additions and 9 deletions

View File

@@ -28,6 +28,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/show_windows"
android:layout_height="@dimen/recents_filter_icon_size"
android:layout_width="@dimen/recents_filter_icon_size"
android:layout_gravity="end"
android:visibility="gone"
android:tint="@color/recents_filter_icon"
android:contentDescription="@string/recents_filter_icon_desc"
android:importantForAccessibility="no"
android:src="@drawable/ic_select_windows" />
<com.android.quickstep.views.IconView
android:id="@+id/icon"
android:layout_width="@dimen/task_thumbnail_icon_size"

View File

@@ -38,6 +38,28 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/show_windows"
android:layout_height="@dimen/recents_filter_icon_size"
android:layout_width="@dimen/recents_filter_icon_size"
android:layout_gravity="start"
android:visibility="gone"
android:tint="@color/recents_filter_icon"
android:contentDescription="@string/recents_filter_icon_desc"
android:importantForAccessibility="no"
android:src="@drawable/ic_select_windows" />
<ImageView
android:id="@+id/show_windows_right"
android:layout_height="@dimen/recents_filter_icon_size"
android:layout_width="@dimen/recents_filter_icon_size"
android:layout_gravity="end"
android:visibility="gone"
android:tint="@color/recents_filter_icon"
android:contentDescription="@string/recents_filter_icon_desc"
android:importantForAccessibility="no"
android:src="@drawable/ic_select_windows" />
<com.android.quickstep.views.IconView
android:id="@+id/icon"
android:layout_width="@dimen/task_thumbnail_icon_size"

View File

@@ -76,4 +76,7 @@
<color name="all_set_page_background">#FFFFFFFF</color>
<!-- Recents overview -->
<color name="recents_filter_icon">#333333</color>
</resources>

View File

@@ -306,6 +306,9 @@
<dimen name="taskbar_button_margin_6_5">75dp</dimen>
<dimen name="taskbar_button_margin_default">48dp</dimen>
<!-- Recents overview -->
<dimen name="recents_filter_icon_size">30dp</dimen>
<!-- Launcher splash screen -->
<!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
<!-- starting_surface_exit_animation_window_shift_length -->

View File

@@ -36,6 +36,13 @@
<!-- Recents: Title of a button that clears the task list, i.e. closes all tasks. [CHAR LIMIT=30] -->
<string name="recents_clear_all">Clear all</string>
<!-- Recents: Title of a button that goes back from displaying tasks filtered by package name to displaying all tasks [CHAR LIMIT=30] -->
<string name="recents_back" translatable="false">Back</string>
<!-- TODO: b/260610444. Content description of filtering icons needs to be updated -->
<!-- Recents: Content description for the icon on top of taskviews to initiate filtering -->
<string name="recents_filter_icon_desc" translatable="false">Click to show only this app\'s tasks</string>
<!-- Accessibility title for the list of recent apps [CHAR_LIMIT=none] -->
<string name="accessibility_recent_apps">Recent apps</string>

View File

@@ -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<ArrayList<GroupTask>> callback) {
Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> 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<GroupTask> result = copyOf(mResultsUi);
// and filter GroupTasks
ArrayList<GroupTask> result = mResultsUi.stream().filter(filter)
.map(GroupTask::copy)
.collect(Collectors.toCollection(ArrayList<GroupTask>::new));
mMainThreadExecutor.post(() -> {
callback.accept(result);
});
@@ -156,7 +162,11 @@ public class RecentTasksList {
mLoadingTasksInBackground = false;
mResultsUi = loadResult;
if (callback != null) {
ArrayList<GroupTask> result = copyOf(mResultsUi);
// filter the tasks if needed before passing them into the callback
ArrayList<GroupTask> result = mResultsUi.stream().filter(filter)
.map(GroupTask::copy)
.collect(Collectors.toCollection(ArrayList<GroupTask>::new));
callback.accept(result);
}
});

View File

@@ -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}.
*
* <p> 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<GroupTask> 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<String, Integer> 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<GroupTask> groupTaskList) {
mInstanceCountMap = getInstanceCountMap(groupTaskList);
}
/**
* Returns the map of package names to their count in the most recent list of tasks.
*/
@Nullable
public Map<String, Integer> 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<GroupTask> 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<String, Integer> getInstanceCountMap(List<GroupTask> groupTasks) {
Map<String, Integer> 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<String, Integer> map, String pkgName) {
if (!map.containsKey(pkgName)) {
map.put(pkgName, 0);
}
map.put(pkgName, map.get(pkgName) + 1);
}
}

View File

@@ -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<ArrayList<GroupTask>> 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<ArrayList<GroupTask>> callback, Predicate<GroupTask> 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<Boolean> callback) {
public void isTaskRemoved(int taskId, Consumer<Boolean> callback, Predicate<GroupTask> 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

View File

@@ -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);

View File

@@ -165,6 +165,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;
@@ -581,7 +582,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
if (taskRemoved) {
dismissTask(taskId);
}
});
}, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter()));
}
}));
}
@@ -722,6 +723,9 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
@Nullable
private TaskLaunchListener mTaskLaunchListener;
// keeps track of the state of the filter for tasks in recents view
private final RecentsFilterState mFilterState = new RecentsFilterState();
public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
BaseActivityInterface sizeStrategy) {
super(context, attrs, defStyleAttr);
@@ -785,6 +789,55 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
mTintingColor = getForegroundScrimDimColor(context);
// if multi-instance feature is enabled
if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
// invalidate the current list of tasks if filter changes
mFilterState.setOnFilterUpdatedListener(this::invalidateTaskList);
}
// make sure filter is turned off by default
mFilterState.setFilterBy(null);
}
/** Get the state of the filter */
public RecentsFilterState getFilterState() {
return mFilterState;
}
/**
* Toggles the filter and reloads the recents view if needed.
*
* @param packageName package name to filter by if the filter is being turned on;
* should be null if filter is being turned off
*/
public void setAndApplyFilter(@Nullable String packageName) {
mFilterState.setFilterBy(packageName);
updateClearAllFunction();
reloadIfNeeded();
}
/**
* Updates the "Clear All" button and its function depending on the recents view state.
*
* TODO: add a different button for going back to overview. Present solution is for demo only.
*/
public void updateClearAllFunction() {
if (mFilterState.isFiltered()) {
mClearAllButton.setText(R.string.recents_back);
mClearAllButton.setOnClickListener((view) -> {
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() {
@@ -1547,6 +1600,9 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
Task stagedTaskToBeRemovedFromGrid =
mSplitSelectSource != null ? mSplitSelectSource.alreadyRunningTask : null;
// update the map of instance counts
mFilterState.updateInstanceCountMap(taskGroups);
// Add views as children based on whether it's grouped or single task. Looping through
// taskGroups backwards populates the thumbnail grid from least recent to most recent.
for (int i = taskGroups.size() - 1; i >= 0; i--) {
@@ -1581,6 +1637,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
groupTask.mSplitBounds);
} else if (taskView instanceof DesktopTaskView) {
@@ -1589,6 +1646,11 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
} else {
taskView.bind(groupTask.task1, mOrientationState);
}
// enables instance filtering if the feature flag for it is on
if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
taskView.setUpShowAllInstancesListener();
}
}
if (!taskGroups.isEmpty()) {
@@ -2258,7 +2320,8 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
*/
public void reloadIfNeeded() {
if (!mModel.isTaskListValid(mTaskListChangeId)) {
mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
.getFilter(mFilterState.getPackageNameToFilter()));
}
}

View File

@@ -526,6 +526,50 @@ public class TaskView extends FrameLayout implements Reusable {
setOrientationState(orientedState);
}
/**
* Sets up an on-click listener and the visibility for show_windows icon on top of the task.
*/
public void setUpShowAllInstancesListener() {
String taskPackageName = mTaskIdAttributeContainer[0].mTask.key.getPackageName();
// icon of the top/left task
View showWindowsView = findViewById(R.id.show_windows);
updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName));
}
/**
* Returns a callback that updates the state of the filter and the recents overview
*
* @param taskPackageName package name of the task to filter by
*/
@Nullable
protected View.OnClickListener getFilterUpdateCallback(String taskPackageName) {
View.OnClickListener cb = (view) -> {
// 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;
}

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,44Q5.8,44 4.9,43.1Q4,42.2 4,41V21.65Q4,20.45 4.9,19.55Q5.8,18.65 7,18.65H12.75V7Q12.75,5.8 13.65,4.9Q14.55,4 15.75,4H41Q42.2,4 43.1,4.9Q44,5.8 44,7V26.35Q44,27.55 43.1,28.45Q42.2,29.35 41,29.35H35.3V41Q35.3,42.2 34.4,43.1Q33.5,44 32.3,44ZM7,41H32.3Q32.3,41 32.3,41Q32.3,41 32.3,41V24.65H7V41Q7,41 7,41Q7,41 7,41ZM35.3,26.35H41Q41,26.35 41,26.35Q41,26.35 41,26.35V10H15.75V18.65H31.6Q33.2,18.65 34.25,19.7Q35.3,20.75 35.3,22.35Z"/>
</vector>

View File

@@ -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) {