Add recent tasks to Taskbar
- Also adds a divider between Hotseat and Recents
- Dedupes Recents from Hotseat
Test: Open some recent tasks, ensure they are deduped
from Hotseat and also handle < 2 tasks.
Bug: 171917176
Change-Id: Ia782c6ccbcda94cfd844aad04dc3d25a3f072c2b
diff --git a/quickstep/res/layout/taskbar_divider.xml b/quickstep/res/layout/taskbar_divider.xml
new file mode 100644
index 0000000..6e1aa1e
--- /dev/null
+++ b/quickstep/res/layout/taskbar_divider.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<View
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/taskbar_divider_thickness"
+ android:layout_height="@dimen/taskbar_divider_height"
+ android:layout_marginStart="@dimen/taskbar_icon_spacing"
+ android:layout_marginEnd="@dimen/taskbar_icon_spacing"
+ android:background="@color/taskbar_divider" />
\ No newline at end of file
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 3bc8ddc..54730f1 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -27,4 +27,5 @@
<!-- Taskbar -->
<color name="taskbar_background">#101010</color>
+ <color name="taskbar_divider">#C0C0C0</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 39cc0b8..0f40775 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -127,4 +127,6 @@
<dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
<!-- Note that this applies to both sides of all icons, so visible space is double this. -->
<dimen name="taskbar_icon_spacing">14dp</dimen>
+ <dimen name="taskbar_divider_thickness">1dp</dimen>
+ <dimen name="taskbar_divider_height">24dp</dimen>
</resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 7608645..6a74aac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -23,6 +23,8 @@
import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR;
import android.animation.Animator;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.view.Gravity;
@@ -42,8 +44,13 @@
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.quickstep.AnimatedFloat;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.WindowManagerWrapper;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Interfaces with Launcher/WindowManager/SystemUI to determine what to show in TaskbarView.
*/
@@ -60,11 +67,17 @@
private final TaskbarStateHandler mTaskbarStateHandler;
private final TaskbarVisibilityController mTaskbarVisibilityController;
private final TaskbarHotseatController mHotseatController;
+ private final TaskbarRecentsController mRecentsController;
private final TaskbarDragController mDragController;
// Initialized in init().
private WindowManager.LayoutParams mWindowLayoutParams;
+ // Contains all loaded Tasks, not yet deduped from Hotseat items.
+ private List<Task> mLatestLoadedRecentTasks;
+ // Contains all loaded Hotseat items.
+ private ItemInfo[] mLatestLoadedHotseatItems;
+
public TaskbarController(BaseQuickstepLauncher launcher,
TaskbarContainerView taskbarContainerView) {
mLauncher = launcher;
@@ -79,6 +92,8 @@
createTaskbarVisibilityControllerCallbacks());
mHotseatController = new TaskbarHotseatController(mLauncher,
createTaskbarHotseatControllerCallbacks());
+ mRecentsController = new TaskbarRecentsController(mLauncher,
+ createTaskbarRecentsControllerCallbacks());
mDragController = new TaskbarDragController(mLauncher);
}
@@ -101,7 +116,16 @@
return new TaskbarViewCallbacks() {
@Override
public View.OnClickListener getItemOnClickListener() {
- return ItemClickHandler.INSTANCE;
+ return view -> {
+ Object tag = view.getTag();
+ if (tag instanceof Task) {
+ Task task = (Task) tag;
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
+ ActivityOptions.makeBasic());
+ } else {
+ ItemClickHandler.INSTANCE.onClick(view);
+ }
+ };
}
@Override
@@ -116,6 +140,23 @@
@Override
public void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
mTaskbarView.updateHotseatItems(hotseatItemInfos);
+ mLatestLoadedHotseatItems = hotseatItemInfos;
+ dedupeAndUpdateRecentItems();
+ }
+ };
+ }
+
+ private TaskbarRecentsControllerCallbacks createTaskbarRecentsControllerCallbacks() {
+ return new TaskbarRecentsControllerCallbacks() {
+ @Override
+ public void updateRecentItems(ArrayList<Task> recentTasks) {
+ mLatestLoadedRecentTasks = recentTasks;
+ dedupeAndUpdateRecentItems();
+ }
+
+ @Override
+ public void updateRecentTaskAtIndex(int taskIndex, Task task) {
+ mTaskbarView.updateRecentTaskAtIndex(taskIndex, task);
}
};
}
@@ -124,11 +165,13 @@
* Initializes the Taskbar, including adding it to the screen.
*/
public void init() {
- mTaskbarView.init(mHotseatController.getNumHotseatIcons());
+ mTaskbarView.init(mHotseatController.getNumHotseatIcons(),
+ mRecentsController.getNumRecentIcons());
addToWindowManager();
mTaskbarStateHandler.setTaskbarCallbacks(createTaskbarStateHandlerCallbacks());
mTaskbarVisibilityController.init();
mHotseatController.init();
+ mRecentsController.init();
}
private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
@@ -149,6 +192,7 @@
mTaskbarStateHandler.setTaskbarCallbacks(null);
mTaskbarVisibilityController.cleanup();
mHotseatController.cleanup();
+ mRecentsController.cleanup();
}
private void removeFromWindowManager() {
@@ -246,6 +290,52 @@
return mTaskbarView.isDraggingItem();
}
+ private void dedupeAndUpdateRecentItems() {
+ if (mLatestLoadedRecentTasks == null || mLatestLoadedHotseatItems == null) {
+ return;
+ }
+
+ final int numRecentIcons = mRecentsController.getNumRecentIcons();
+
+ // From most recent to least recently opened.
+ List<Task> dedupedTasksInDescendingOrder = new ArrayList<>();
+ for (int i = mLatestLoadedRecentTasks.size() - 1; i >= 0; i--) {
+ Task task = mLatestLoadedRecentTasks.get(i);
+ boolean isTaskInHotseat = false;
+ for (ItemInfo hotseatItem : mLatestLoadedHotseatItems) {
+ if (hotseatItem == null) {
+ continue;
+ }
+ ComponentName hotseatActivity = hotseatItem.getTargetComponent();
+ if (hotseatActivity != null && task.key.sourceComponent.getPackageName()
+ .equals(hotseatActivity.getPackageName())) {
+ isTaskInHotseat = true;
+ break;
+ }
+ }
+ if (!isTaskInHotseat) {
+ dedupedTasksInDescendingOrder.add(task);
+ if (dedupedTasksInDescendingOrder.size() == numRecentIcons) {
+ break;
+ }
+ }
+ }
+
+ // TaskbarView expects an array of all the recent tasks to show, in the order to show them.
+ // So we create an array of the proper size, then fill it in such that the most recent items
+ // are at the end. If there aren't enough elements to fill the array, leave them null.
+ Task[] tasksArray = new Task[numRecentIcons];
+ for (int i = 0; i < tasksArray.length; i++) {
+ Task task = i >= dedupedTasksInDescendingOrder.size()
+ ? null
+ : dedupedTasksInDescendingOrder.get(i);
+ tasksArray[tasksArray.length - 1 - i] = task;
+ }
+
+ mTaskbarView.updateRecentTasks(tasksArray);
+ mRecentsController.loadIconsForTasks(tasksArray);
+ }
+
/**
* @return Whether the given View is in the same window as Taskbar.
*/
@@ -283,4 +373,12 @@
protected interface TaskbarHotseatControllerCallbacks {
void updateHotseatItems(ItemInfo[] hotseatItemInfos);
}
+
+ /**
+ * Contains methods that TaskbarRecentsController can call to interface with TaskbarController.
+ */
+ protected interface TaskbarRecentsControllerCallbacks {
+ void updateRecentItems(ArrayList<Task> recentTasks);
+ void updateRecentTaskAtIndex(int taskIndex, Task task);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 2318ff9..baec899 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -25,6 +25,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Point;
+import android.os.UserHandle;
import android.view.DragEvent;
import android.view.View;
@@ -33,6 +34,7 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ClipDescriptionCompat;
import com.android.systemui.shared.system.LauncherAppsCompat;
@@ -102,6 +104,15 @@
item.getIntent().getComponent(), null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
+ } else if (tag instanceof Task) {
+ Task task = (Task) tag;
+ clipDescription = new ClipDescription(task.titleDescription,
+ new String[] {
+ ClipDescriptionCompat.MIMETYPE_APPLICATION_TASK
+ });
+ intent = new Intent();
+ intent.putExtra(ClipDescriptionCompat.EXTRA_TASK_ID, task.key.id);
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(task.key.userId));
}
if (clipDescription != null && intent != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
new file mode 100644
index 0000000..9d4e000
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.CancellableTask;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.ArrayList;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Recent items.
+ */
+public class TaskbarRecentsController {
+
+ private final int mNumRecentIcons = 2;
+ private final BaseQuickstepLauncher mLauncher;
+ private final TaskbarController.TaskbarRecentsControllerCallbacks mTaskbarCallbacks;
+ private final RecentsModel mRecentsModel;
+
+ private final TaskStackChangeListener mTaskStackChangeListener = new TaskStackChangeListener() {
+ @Override
+ public void onTaskStackChanged() {
+ reloadRecentTasksIfNeeded();
+ }
+ };
+
+ // TODO: add TaskbarVisualsChangedListener as well (for calendar/clock?)
+
+ // Used to keep track of the last requested task list id, so that we do not request to load the
+ // tasks again if we have already requested it and the task list has not changed
+ private int mTaskListChangeId = -1;
+
+ // The current background requests to load the task icons
+ private CancellableTask[] mIconLoadRequests = new CancellableTask[mNumRecentIcons];
+
+ public TaskbarRecentsController(BaseQuickstepLauncher launcher,
+ TaskbarController.TaskbarRecentsControllerCallbacks taskbarCallbacks) {
+ mLauncher = launcher;
+ mTaskbarCallbacks = taskbarCallbacks;
+ mRecentsModel = RecentsModel.INSTANCE.get(mLauncher);
+ }
+
+ protected void init() {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackChangeListener);
+ reloadRecentTasksIfNeeded();
+ }
+
+ protected void cleanup() {
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+ mTaskStackChangeListener);
+ cancelAllPendingIconLoadTasks();
+ }
+
+ private void reloadRecentTasksIfNeeded() {
+ if (!mRecentsModel.isTaskListValid(mTaskListChangeId)) {
+ mTaskListChangeId = mRecentsModel.getTasks(this::onRecentTasksChanged);
+ }
+ }
+
+ private void cancelAllPendingIconLoadTasks() {
+ for (int i = 0; i < mIconLoadRequests.length; i++) {
+ if (mIconLoadRequests[i] != null) {
+ mIconLoadRequests[i].cancel();
+ }
+ mIconLoadRequests[i] = null;
+ }
+ }
+
+ private void onRecentTasksChanged(ArrayList<Task> tasks) {
+ mTaskbarCallbacks.updateRecentItems(tasks);
+ }
+
+ /**
+ * For each Task, loads its icon from the cache in the background, then calls
+ * {@link TaskbarController.TaskbarRecentsControllerCallbacks#updateRecentTaskAtIndex}.
+ */
+ protected void loadIconsForTasks(Task[] tasks) {
+ cancelAllPendingIconLoadTasks();
+ for (int i = 0; i < tasks.length; i++) {
+ Task task = tasks[i];
+ if (task == null) {
+ continue;
+ }
+ final int taskIndex = i;
+ mIconLoadRequests[i] = mRecentsModel.getIconCache().updateIconInBackground(
+ task, updatedTask -> onTaskIconLoaded(task, taskIndex));
+ }
+ }
+
+ private void onTaskIconLoaded(Task task, int taskIndex) {
+ mTaskbarCallbacks.updateRecentTaskAtIndex(taskIndex, task);
+ }
+
+ protected int getNumRecentIcons() {
+ return mNumRecentIcons;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c98f09c..d8f3bb5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -19,6 +19,7 @@
import android.content.res.Resources;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.LayoutInflater;
@@ -35,6 +36,7 @@
import com.android.launcher3.R;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.systemui.shared.recents.model.Task;
/**
* Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
@@ -52,6 +54,9 @@
// Initialized in init().
private int mHotseatStartIndex;
private int mHotseatEndIndex;
+ private View mHotseatRecentsDivider;
+ private int mRecentsStartIndex;
+ private int mRecentsEndIndex;
private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
@@ -89,10 +94,17 @@
mControllerCallbacks = taskbarViewCallbacks;
}
- protected void init(int numHotseatIcons) {
+ protected void init(int numHotseatIcons, int numRecentIcons) {
mHotseatStartIndex = 0;
mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
updateHotseatItems(new ItemInfo[numHotseatIcons]);
+
+ int dividerIndex = mHotseatEndIndex + 1;
+ mHotseatRecentsDivider = addDivider(dividerIndex);
+
+ mRecentsStartIndex = dividerIndex + 1;
+ mRecentsEndIndex = mRecentsStartIndex + numRecentIcons - 1;
+ updateRecentTasks(new Task[numRecentIcons]);
}
protected void cleanup() {
@@ -147,6 +159,93 @@
hotseatView.setOnLongClickListener(null);
}
}
+
+ updateHotseatRecentsDividerVisibility();
+ }
+
+ private View addDivider(int dividerIndex) {
+ View divider = inflate(R.layout.taskbar_divider);
+ addView(divider, dividerIndex);
+ return divider;
+ }
+
+ /**
+ * Inflates/binds the Recents items to show in the Taskbar given their Tasks.
+ */
+ protected void updateRecentTasks(Task[] tasks) {
+ for (int i = 0; i < tasks.length; i++) {
+ Task task = tasks[i];
+ int recentsIndex = mRecentsStartIndex + i;
+ View recentsView = getChildAt(recentsIndex);
+
+ // Inflate empty icon Views.
+ if (recentsView == null) {
+ BubbleTextView btv = (BubbleTextView) inflate(R.layout.taskbar_app_icon);
+ LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
+ lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+ recentsView = btv;
+ addView(recentsView, recentsIndex, lp);
+ }
+
+ // Apply the Task, or hide the view if there is none for a given index.
+ if (recentsView instanceof BubbleTextView && task != null) {
+ applyTaskToBubbleTextView((BubbleTextView) recentsView, task);
+ recentsView.setVisibility(VISIBLE);
+ recentsView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+ recentsView.setOnLongClickListener(
+ mControllerCallbacks.getItemOnLongClickListener());
+ } else {
+ recentsView.setVisibility(GONE);
+ recentsView.setOnClickListener(null);
+ recentsView.setOnLongClickListener(null);
+ }
+ }
+
+ updateHotseatRecentsDividerVisibility();
+ }
+
+ private void applyTaskToBubbleTextView(BubbleTextView btv, Task task) {
+ if (task.icon != null) {
+ Drawable icon = task.icon.getConstantState().newDrawable().mutate();
+ btv.applyIconAndLabel(icon, task.titleDescription);
+ }
+ btv.setTag(task);
+ }
+
+ protected void updateRecentTaskAtIndex(int taskIndex, Task task) {
+ View taskView = getChildAt(mRecentsStartIndex + taskIndex);
+ if (taskView instanceof BubbleTextView) {
+ applyTaskToBubbleTextView((BubbleTextView) taskView, task);
+ }
+ }
+
+ /**
+ * Make the divider VISIBLE between the Hotseat and Recents if there is at least one icon in
+ * each, otherwise make it GONE.
+ */
+ private void updateHotseatRecentsDividerVisibility() {
+ if (mHotseatRecentsDivider == null) {
+ return;
+ }
+
+ boolean hasAtLeastOneHotseatItem = false;
+ for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+ if (getChildAt(i).getVisibility() != GONE) {
+ hasAtLeastOneHotseatItem = true;
+ break;
+ }
+ }
+
+ boolean hasAtLeastOneRecentItem = false;
+ for (int i = mRecentsStartIndex; i <= mRecentsEndIndex; i++) {
+ if (getChildAt(i).getVisibility() != GONE) {
+ hasAtLeastOneRecentItem = true;
+ break;
+ }
+ }
+
+ mHotseatRecentsDivider.setVisibility(hasAtLeastOneHotseatItem && hasAtLeastOneRecentItem
+ ? VISIBLE : GONE);
}
@Override
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 5007ffc..6f446ad 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -359,6 +359,16 @@
}
/**
+ * Directly set the icon and label.
+ */
+ @UiThread
+ public void applyIconAndLabel(Drawable icon, CharSequence label) {
+ setIcon(icon);
+ setText(label);
+ setContentDescription(label);
+ }
+
+ /**
* Overrides the default long press timeout.
*/
public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {