From de7ceba80b895395d42e873386481290f2697ce8 Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Tue, 24 Jan 2023 15:27:00 -0800 Subject: [PATCH] Add the KeyboardQuickSwitchView (2/2) Adding KeyboardQuickSwitchView and associated flows. Test: Manually tested alt-tab and alt-shift-tab in and out of overview on a tablet and phone Bug: 258854035 Change-Id: Ifb48b005067b3a9c66acfd5ecdbae144b359d3be --- ...uick_switch_overview_button_background.xml | 22 + ...oard_quick_switch_task_view_background.xml | 21 + .../keyboard_quick_switch_view_background.xml | 21 + .../keyboard_quick_switch_taskview.xml | 52 ++ .../layout/keyboard_quick_switch_overview.xml | 54 ++ .../layout/keyboard_quick_switch_taskview.xml | 52 ++ .../keyboard_quick_switch_thumbnail.xml | 22 + .../res/layout/keyboard_quick_switch_view.xml | 50 ++ quickstep/res/values-land/dimens.xml | 4 +- quickstep/res/values/dimens.xml | 9 + quickstep/res/values/strings.xml | 8 + quickstep/res/values/styles.xml | 9 +- .../KeyboardQuickSwitchController.java | 192 +++++++ .../taskbar/KeyboardQuickSwitchTaskView.java | 173 ++++++ .../taskbar/KeyboardQuickSwitchView.java | 497 ++++++++++++++++++ .../KeyboardQuickSwitchViewController.java | 200 +++++++ .../taskbar/TaskbarActivityContext.java | 4 +- .../launcher3/taskbar/TaskbarControllers.java | 10 +- .../taskbar/TaskbarUIController.java | 32 ++ .../quickstep/OverviewCommandHelper.java | 22 +- .../quickstep/util/BorderAnimator.java | 16 +- .../launcher3/taskbar/TaskbarBaseTestCase.kt | 2 + 22 files changed, 1464 insertions(+), 8 deletions(-) create mode 100644 quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml create mode 100644 quickstep/res/drawable/keyboard_quick_switch_task_view_background.xml create mode 100644 quickstep/res/drawable/keyboard_quick_switch_view_background.xml create mode 100644 quickstep/res/layout-land/keyboard_quick_switch_taskview.xml create mode 100644 quickstep/res/layout/keyboard_quick_switch_overview.xml create mode 100644 quickstep/res/layout/keyboard_quick_switch_taskview.xml create mode 100644 quickstep/res/layout/keyboard_quick_switch_thumbnail.xml create mode 100644 quickstep/res/layout/keyboard_quick_switch_view.xml create mode 100644 quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java create mode 100644 quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java create mode 100644 quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java create mode 100644 quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java diff --git a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml new file mode 100644 index 0000000000..286a3c4e4f --- /dev/null +++ b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/quickstep/res/drawable/keyboard_quick_switch_task_view_background.xml b/quickstep/res/drawable/keyboard_quick_switch_task_view_background.xml new file mode 100644 index 0000000000..d0aac8c364 --- /dev/null +++ b/quickstep/res/drawable/keyboard_quick_switch_task_view_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/quickstep/res/drawable/keyboard_quick_switch_view_background.xml b/quickstep/res/drawable/keyboard_quick_switch_view_background.xml new file mode 100644 index 0000000000..19aaed46b0 --- /dev/null +++ b/quickstep/res/drawable/keyboard_quick_switch_view_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml new file mode 100644 index 0000000000..18c0e1f444 --- /dev/null +++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_overview.xml b/quickstep/res/layout/keyboard_quick_switch_overview.xml new file mode 100644 index 0000000000..bf21a3e297 --- /dev/null +++ b/quickstep/res/layout/keyboard_quick_switch_overview.xml @@ -0,0 +1,54 @@ + + + + + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml new file mode 100644 index 0000000000..48e62766e2 --- /dev/null +++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml b/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml new file mode 100644 index 0000000000..cd6587cc06 --- /dev/null +++ b/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml @@ -0,0 +1,22 @@ + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml new file mode 100644 index 0000000000..5c20a2db3c --- /dev/null +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/quickstep/res/values-land/dimens.xml b/quickstep/res/values-land/dimens.xml index 30983c4d5a..ee594c8ac6 100644 --- a/quickstep/res/values-land/dimens.xml +++ b/quickstep/res/values-land/dimens.xml @@ -82,4 +82,6 @@ 96dp 24dp - \ No newline at end of file + 205dp + 119dp + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index fb04cc0fae..d5e8351a23 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -331,4 +331,13 @@ 4dp + 104dp + 134dp + 20dp + 56dp + 16dp + 16dp + 2dp + 28dp + 16dp diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index d2f58020a3..01d92d1439 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -282,4 +282,12 @@ Move to top/left Move to bottom/right + + + {count, plural, + =1{Show # more app.} + other{Show # more apps.} + } + + %1$s and %2$s diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml index 6119eb6861..4417407638 100644 --- a/quickstep/res/values/styles.xml +++ b/quickstep/res/values/styles.xml @@ -223,4 +223,11 @@ google-sans-text 14sp - \ No newline at end of file + + + diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java new file mode 100644 index 0000000000..c4962cd016 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -0,0 +1,192 @@ +/* + * 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.taskbar; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.R; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Handles initialization of the {@link KeyboardQuickSwitchViewController}. + */ +public final class KeyboardQuickSwitchController implements + TaskbarControllers.LoggableTaskbarController { + + static final int MAX_TASKS = 6; + + @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks(); + + // Initialized on init + @Nullable private RecentsModel mModel; + + // 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; + // Only empty before the recent tasks list has been loaded the first time + @NonNull private List mTasks = new ArrayList<>(); + private int mNumHiddenTasks = 0; + + // Initialized in init + private TaskbarControllers mControllers; + + @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; + + /** Initialize the controller. */ + public void init(@NonNull TaskbarControllers controllers) { + mControllers = controllers; + mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext); + } + + void onConfigurationChanged(@ActivityInfo.Config int configChanges) { + if (mQuickSwitchViewController == null) { + return; + } + if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD + | ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) { + mQuickSwitchViewController.closeQuickSwitchView(true); + return; + } + int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex(); + onDestroy(); + if (currentFocusedIndex != -1) { + mControllers.taskbarActivityContext.getMainThreadHandler().post( + () -> openQuickSwitchView(currentFocusedIndex)); + } + } + + void openQuickSwitchView() { + openQuickSwitchView(-1); + } + + private void openQuickSwitchView(int currentFocusedIndex) { + if (mQuickSwitchViewController != null) { + return; + } + TaskbarOverlayContext overlayContext = + mControllers.taskbarOverlayController.requestWindow(); + KeyboardQuickSwitchView keyboardQuickSwitchView = + (KeyboardQuickSwitchView) overlayContext.getLayoutInflater() + .inflate( + R.layout.keyboard_quick_switch_view, + overlayContext.getDragLayer(), + /* attachToRoot= */ false); + mQuickSwitchViewController = new KeyboardQuickSwitchViewController( + mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); + + if (mModel.isTaskListValid(mTaskListChangeId)) { + mQuickSwitchViewController.openQuickSwitchView( + mTasks, mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex); + return; + } + mTaskListChangeId = mModel.getTasks((tasks) -> { + // Only store MAX_TASK tasks, from most to least recent + Collections.reverse(tasks); + mTasks = tasks.stream().limit(MAX_TASKS).collect(Collectors.toList()); + mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); + mQuickSwitchViewController.openQuickSwitchView( + mTasks, mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex); + }); + } + + void closeQuickSwitchView() { + if (mQuickSwitchViewController == null) { + return; + } + mQuickSwitchViewController.closeQuickSwitchView(true); + } + + /** + * See {@link TaskbarUIController#launchFocusedTask()} + */ + int launchFocusedTask() { + // Return -1 so that the RecentsView is not incorrectly opened when the user closes the + // quick switch view by tapping the screen. + return mQuickSwitchViewController == null + ? -1 : mQuickSwitchViewController.launchFocusedTask(); + } + + void onDestroy() { + if (mQuickSwitchViewController != null) { + mQuickSwitchViewController.onDestroy(); + } + } + + @Override + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "KeyboardQuickSwitchController:"); + + pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); + pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); + pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); + pw.println(prefix + "\tmTasks=["); + for (GroupTask task : mTasks) { + Task task1 = task.task1; + Task task2 = task.task2; + ComponentName cn1 = task1.getTopComponent(); + ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; + pw.println(prefix + "\t\tt1: (id=" + task1.key.id + + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") + + " t2: (id=" + (task2 != null ? task2.key.id : "-1") + + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" + : "no package)")); + } + pw.println(prefix + "\t]"); + + if (mQuickSwitchViewController != null) { + mQuickSwitchViewController.dumpLogs(prefix + '\t', pw); + } + } + + class ControllerCallbacks { + + int getTaskCount() { + return mNumHiddenTasks == 0 ? mTasks.size() : MAX_TASKS + 1; + } + + @Nullable + GroupTask getTaskAt(int index) { + return index < 0 || index >= mTasks.size() ? null : mTasks.get(index); + } + + void updateThumbnailInBackground(Task task, Consumer callback) { + mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); + } + + void updateTitleInBackground(Task task, Consumer callback) { + mModel.getIconCache().updateIconInBackground(task, callback); + } + + void onCloseComplete() { + mQuickSwitchViewController = null; + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java new file mode 100644 index 0000000000..84129fdde4 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java @@ -0,0 +1,173 @@ +/* + * 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.taskbar; + +import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; + +import android.animation.Animator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.launcher3.R; +import com.android.quickstep.util.BorderAnimator; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import java.util.function.Consumer; + +/** + * A view that displays a recent task during a keyboard quick switch. + */ +public class KeyboardQuickSwitchTaskView extends ConstraintLayout { + + @NonNull private final BorderAnimator mBorderAnimator; + + @Nullable private ImageView mThumbnailView1; + @Nullable private ImageView mThumbnailView2; + + public KeyboardQuickSwitchTaskView(@NonNull Context context) { + this(context, null); + } + + public KeyboardQuickSwitchTaskView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardQuickSwitchTaskView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardQuickSwitchTaskView( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setWillNotDraw(false); + Resources resources = context.getResources(); + mBorderAnimator = new BorderAnimator( + /* borderBoundsBuilder= */ bounds -> bounds.set(0, 0, getWidth(), getHeight()), + /* borderWidthPx= */ resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width), + /* borderRadiusPx= */ resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius), + /* borderColor= */ attrs == null + ? DEFAULT_BORDER_COLOR + : context.getTheme() + .obtainStyledAttributes( + attrs, + R.styleable.TaskView, + defStyleAttr, + defStyleRes) + .getColor( + R.styleable.TaskView_borderColor, + DEFAULT_BORDER_COLOR), + /* invalidateViewCallback= */ KeyboardQuickSwitchTaskView.this::invalidate); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mThumbnailView1 = findViewById(R.id.thumbnail1); + mThumbnailView2 = findViewById(R.id.thumbnail2); + } + + @NonNull + protected Animator getFocusAnimator(boolean focused) { + return mBorderAnimator.buildAnimator(focused); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + mBorderAnimator.drawBorder(canvas); + } + + protected void setThumbnails( + @NonNull Task task1, + @Nullable Task task2, + @Nullable ThumbnailUpdateFunction thumbnailUpdateFunction, + @Nullable TitleUpdateFunction titleUpdateFunction) { + applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction); + applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction); + + if (titleUpdateFunction == null) { + setContentDescription(task2 == null + ? task1.titleDescription + : getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription)); + return; + } + titleUpdateFunction.updateTitleInBackground(task1, t -> + setContentDescription(task1.titleDescription)); + if (task2 == null) { + return; + } + titleUpdateFunction.updateTitleInBackground(task2, t -> + setContentDescription(getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription))); + } + + private void applyThumbnail( + @Nullable ImageView thumbnailView, + @Nullable Task task, + @Nullable ThumbnailUpdateFunction updateFunction) { + if (thumbnailView == null) { + return; + } + if (task == null) { + return; + } + if (updateFunction == null) { + applyThumbnail(thumbnailView, task.thumbnail); + return; + } + updateFunction.updateThumbnailInBackground( + task, thumbnailData -> applyThumbnail(thumbnailView, thumbnailData)); + } + + private void applyThumbnail( + @NonNull ImageView thumbnailView, ThumbnailData thumbnailData) { + Bitmap bm = thumbnailData == null ? null : thumbnailData.thumbnail; + + thumbnailView.setVisibility(VISIBLE); + thumbnailView.setImageBitmap(bm); + } + + protected interface ThumbnailUpdateFunction { + + void updateThumbnailInBackground(Task task, Consumer callback); + } + + protected interface TitleUpdateFunction { + + void updateTitleInBackground(Task task, Consumer callback); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java new file mode 100644 index 0000000000..94d62b2c8f --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -0,0 +1,497 @@ +/* + * 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.taskbar; + +import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; + +import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Rect; +import android.icu.text.MessageFormat; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.animation.Interpolator; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.anim.Interpolators; +import com.android.quickstep.util.GroupTask; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +/** + * View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab + * commands. + */ +public class KeyboardQuickSwitchView extends ConstraintLayout { + + private static final long OUTLINE_ANIMATION_DURATION_MS = 333; + private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f; + private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f; + private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; + + private static final long ALPHA_ANIMATION_DURATION_MS = 83; + private static final long ALPHA_ANIMATION_START_DELAY_MS = 67; + + private static final long CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS = 500; + private static final long CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS = 333; + private static final float CONTENT_START_TRANSLATION_X_DP = 32; + private static final float CONTENT_START_TRANSLATION_Y_DP = 40; + private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED; + private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_DECELERATE; + private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = + Interpolators.EMPHASIZED_ACCELERATE; + + private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83; + private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83; + + private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat( + this::invalidateOutline); + + private HorizontalScrollView mScrollView; + private ConstraintLayout mContent; + + private int mTaskViewHeight; + private int mSpacing; + private int mOutlineRadius; + private boolean mIsRtl; + + @Nullable private AnimatorSet mOpenAnimation; + + @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; + + public KeyboardQuickSwitchView(@NonNull Context context) { + this(context, null); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mScrollView = findViewById(R.id.scroll_view); + mContent = findViewById(R.id.content); + + Resources resources = getResources(); + mTaskViewHeight = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_taskview_height); + mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); + mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); + mIsRtl = Utilities.isRtl(resources); + } + + @NonNull + private KeyboardQuickSwitchTaskView createAndAddTaskView( + int index, + int width, + boolean isFinalView, + boolean updateTasks, + @NonNull LayoutInflater layoutInflater, + @Nullable View previousView, + @NonNull List groupTasks) { + KeyboardQuickSwitchTaskView taskView = (KeyboardQuickSwitchTaskView) layoutInflater.inflate( + R.layout.keyboard_quick_switch_taskview, mContent, false); + taskView.setId(View.generateViewId()); + taskView.setOnClickListener(v -> mViewCallbacks.launchTappedTask(index)); + + LayoutParams lp = new LayoutParams(width, mTaskViewHeight); + // Create a right-to-left ordering of views (or left-to-right in RTL locales) + if (previousView != null) { + lp.endToStart = previousView.getId(); + } else { + lp.endToEnd = PARENT_ID; + } + lp.topToTop = PARENT_ID; + lp.bottomToBottom = PARENT_ID; + // Add spacing between views + lp.setMarginEnd(mSpacing); + if (isFinalView) { + // Add spacing to the start of the final view so that scrolling ends with some padding. + lp.startToStart = PARENT_ID; + lp.setMarginStart(mSpacing); + lp.horizontalBias = 1f; + } + + GroupTask groupTask = groupTasks.get(index); + taskView.setThumbnails( + groupTask.task1, + groupTask.task2, + updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, + updateTasks ? mViewCallbacks::updateTitleInBackground : null); + + mContent.addView(taskView, lp); + return taskView; + } + + private void createAndAddOverviewButton( + int width, + @NonNull LayoutInflater layoutInflater, + @Nullable View previousView, + @NonNull String overflowString) { + KeyboardQuickSwitchTaskView overviewButton = + (KeyboardQuickSwitchTaskView) layoutInflater.inflate( + R.layout.keyboard_quick_switch_overview, this, false); + overviewButton.setOnClickListener(v -> mViewCallbacks.launchTappedTask(MAX_TASKS)); + + overviewButton.findViewById(R.id.text).setText(overflowString); + + ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( + width, mTaskViewHeight); + lp.startToStart = PARENT_ID; + lp.endToStart = previousView.getId(); + lp.topToTop = PARENT_ID; + lp.bottomToBottom = PARENT_ID; + lp.setMarginEnd(mSpacing); + lp.setMarginStart(mSpacing); + + mContent.addView(overviewButton, lp); + } + + protected void applyLoadPlan( + @NonNull Context context, + @NonNull List groupTasks, + int numHiddenTasks, + boolean updateTasks, + int currentFocusIndexOverride, + @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) { + if (groupTasks.isEmpty()) { + // Do not show the quick switch view. + return; + } + mViewCallbacks = viewCallbacks; + Resources resources = context.getResources(); + int width = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_taskview_width); + View previousView = null; + + LayoutInflater layoutInflater = LayoutInflater.from(context); + int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size()); + for (int i = 0; i < tasksToDisplay; i++) { + previousView = createAndAddTaskView( + i, + width, + /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0, + updateTasks, + layoutInflater, + previousView, + groupTasks); + } + + if (numHiddenTasks > 0) { + HashMap args = new HashMap<>(); + args.put("count", numHiddenTasks); + createAndAddOverviewButton( + width, + layoutInflater, + previousView, + new MessageFormat( + resources.getString(R.string.quick_switch_overflow), + Locale.getDefault()).format(args)); + } + + getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + animateOpen(currentFocusIndexOverride); + + getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + + protected Animator getCloseAnimation() { + AnimatorSet closeAnimation = new AnimatorSet(); + + Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(0f); + outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); + outlineAnimation.setInterpolator(CLOSE_OUTLINE_INTERPOLATOR); + closeAnimation.play(outlineAnimation); + + Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0f); + alphaAnimation.setStartDelay(ALPHA_ANIMATION_START_DELAY_MS); + alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); + closeAnimation.play(alphaAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_Y, 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); + closeAnimation.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 1f, 0f); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + closeAnimation.play(contentAlphaAnimation); + + closeAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mOpenAnimation != null) { + mOpenAnimation.cancel(); + } + } + }); + + return closeAnimation; + } + + private void animateOpen(int currentFocusIndexOverride) { + if (mOpenAnimation != null) { + // Restart animation since currentFocusIndexOverride can change the initial scroll. + mOpenAnimation.cancel(); + } + mOpenAnimation = new AnimatorSet(); + + Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f); + outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); + mOpenAnimation.play(outlineAnimation); + + Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0f, 1f); + alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); + mOpenAnimation.play(alphaAnimation); + + Animator translationXAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_X, -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); + translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); + translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); + mOpenAnimation.play(translationXAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + mScrollView, TRANSLATION_Y, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); + mOpenAnimation.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 0f, 1f); + contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + mOpenAnimation.play(contentAlphaAnimation); + + ViewOutlineProvider outlineProvider = getOutlineProvider(); + mOpenAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + setClipToPadding(false); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect( + /* rect= */ new Rect( + /* left= */ 0, + /* top= */ 0, + /* right= */ getWidth(), + /* bottom= */ + (int) (getHeight() * Utilities.mapBoundToRange( + mOutlineAnimationProgress.value, + /* lowerBound= */ 0f, + /* upperBound= */ 1f, + /* toMin= */ OUTLINE_START_HEIGHT_FACTOR, + /* toMax= */ 1f, + OPEN_OUTLINE_INTERPOLATOR))), + /* radius= */ mOutlineRadius * Utilities.mapBoundToRange( + mOutlineAnimationProgress.value, + /* lowerBound= */ 0f, + /* upperBound= */ 1f, + /* toMin= */ OUTLINE_START_RADIUS_FACTOR, + /* toMax= */ 1f, + OPEN_OUTLINE_INTERPOLATOR)); + } + }); + if (currentFocusIndexOverride == -1) { + initializeScroll(/* index= */ 0, /* shouldTruncateTarget= */ false); + } else { + animateFocusMove(-1, currentFocusIndexOverride); + } + mScrollView.setVisibility(VISIBLE); + setVisibility(VISIBLE); + requestFocus(); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + setClipToPadding(true); + setOutlineProvider(outlineProvider); + invalidateOutline(); + mOpenAnimation = null; + } + }); + + mOpenAnimation.start(); + } + + protected void animateFocusMove(int fromIndex, int toIndex) { + KeyboardQuickSwitchTaskView focusedTask = getTaskAt(toIndex); + if (focusedTask == null) { + return; + } + AnimatorSet focusAnimation = new AnimatorSet(); + focusAnimation.play(focusedTask.getFocusAnimator(true)); + + KeyboardQuickSwitchTaskView previouslyFocusedTask = getTaskAt(fromIndex); + if (previouslyFocusedTask != null) { + focusAnimation.play(previouslyFocusedTask.getFocusAnimator(false)); + } + + focusAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + focusedTask.requestAccessibilityFocus(); + if (fromIndex == -1) { + int firstVisibleTaskIndex = toIndex == 0 + ? toIndex + : getTaskAt(toIndex - 1) == null + ? toIndex : toIndex - 1; + // Scroll so that the previous task view is truncated as a visual hint that + // there are more tasks + initializeScroll( + firstVisibleTaskIndex, + /* shouldTruncateTarget= */ firstVisibleTaskIndex != toIndex); + } else if (toIndex > fromIndex || toIndex == 0) { + // Scrolling to next task view + if (mIsRtl) { + scrollRightTo(focusedTask); + } else { + scrollLeftTo(focusedTask); + } + } else { + // Scrolling to previous task view + if (mIsRtl) { + scrollLeftTo(focusedTask); + } else { + scrollRightTo(focusedTask); + } + } + if (mViewCallbacks != null) { + mViewCallbacks.updateCurrentFocusIndex(toIndex); + } + } + }); + + focusAnimation.start(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return (mViewCallbacks != null && mViewCallbacks.onKeyUp(keyCode, event)) + || super.onKeyUp(keyCode, event); + } + + private void initializeScroll(int index, boolean shouldTruncateTarget) { + View task = getTaskAt(index); + if (task == null) { + return; + } + if (mIsRtl) { + scrollRightTo( + task, shouldTruncateTarget, /* smoothScroll= */ false); + } else { + scrollLeftTo( + task, shouldTruncateTarget, /* smoothScroll= */ false); + } + } + + private void scrollRightTo(@NonNull View targetTask) { + scrollRightTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true); + } + + private void scrollRightTo( + @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) { + if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { + return; + } + int scrollTo = targetTask.getLeft() - mSpacing + + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); + // Scroll so that the focused task is to the left of the list + if (smoothScroll) { + mScrollView.smoothScrollTo(scrollTo, 0); + } else { + mScrollView.scrollTo(scrollTo, 0); + } + } + + private void scrollLeftTo(@NonNull View targetTask) { + scrollLeftTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true); + } + + private void scrollLeftTo( + @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) { + if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { + return; + } + int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth() + - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); + // Scroll so that the focused task is to the right of the list + if (smoothScroll) { + mScrollView.smoothScrollTo(scrollTo, 0); + } else { + mScrollView.scrollTo(scrollTo, 0); + } + } + + private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) { + boolean isTargetTruncated = + targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth() + || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX(); + + return isTargetTruncated && !shouldTruncateTarget; + } + + @Nullable + protected KeyboardQuickSwitchTaskView getTaskAt(int index) { + return index < 0 || index >= mContent.getChildCount() + ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java new file mode 100644 index 0000000000..f0f361ed51 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -0,0 +1,200 @@ +/* + * 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.taskbar; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.animation.Animator; +import android.view.KeyEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; +import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer; +import com.android.quickstep.util.GroupTask; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +import java.io.PrintWriter; +import java.util.List; +import java.util.function.Consumer; + +/** + * Handles initialization of the {@link KeyboardQuickSwitchView} and supplies it with the list of + * tasks. + */ +public class KeyboardQuickSwitchViewController { + + @NonNull private final ViewCallbacks mViewCallbacks = new ViewCallbacks(); + @NonNull private final TaskbarControllers mControllers; + @NonNull private final TaskbarOverlayContext mOverlayContext; + @NonNull private final KeyboardQuickSwitchView mKeyboardQuickSwitchView; + @NonNull private final KeyboardQuickSwitchController.ControllerCallbacks mControllerCallbacks; + + @Nullable private Animator mCloseAnimation; + + private int mCurrentFocusIndex = -1; + + protected KeyboardQuickSwitchViewController( + @NonNull TaskbarControllers controllers, + @NonNull TaskbarOverlayContext overlayContext, + @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView, + @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks) { + mControllers = controllers; + mOverlayContext = overlayContext; + mKeyboardQuickSwitchView = keyboardQuickSwitchView; + mControllerCallbacks = controllerCallbacks; + } + + protected int getCurrentFocusedIndex() { + return mCurrentFocusIndex; + } + + protected void openQuickSwitchView( + @NonNull List tasks, + int numHiddenTasks, + boolean updateTasks, + int currentFocusIndexOverride) { + TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer(); + dragLayer.addView(mKeyboardQuickSwitchView); + dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true)); + + mKeyboardQuickSwitchView.applyLoadPlan( + mOverlayContext, + tasks, + numHiddenTasks, + updateTasks, + currentFocusIndexOverride, + mViewCallbacks); + } + + protected void closeQuickSwitchView(boolean animate) { + if (mCloseAnimation != null) { + if (animate) { + // Let currently-running animation finish. + return; + } else { + mCloseAnimation.cancel(); + } + } + if (!animate) { + mCloseAnimation = null; + onCloseComplete(); + return; + } + mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation(); + + mCloseAnimation.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + mCloseAnimation = null; + onCloseComplete(); + } + }); + mCloseAnimation.start(); + } + + /** + * Launched the currently-focused task. + * + * Returns index -1 iff the RecentsView shouldn't be opened. + * + * If the index is not -1, then the {@link com.android.quickstep.views.TaskView} at the returned + * index will be focused. + */ + protected int launchFocusedTask() { + // Launch the second-most recent task if the user quick switches too quickly, if possible. + return launchTaskAt(mCurrentFocusIndex == -1 + ? (mControllerCallbacks.getTaskCount() > 1 ? 1 : 0) : mCurrentFocusIndex); + } + + private int launchTaskAt(int index) { + KeyboardQuickSwitchTaskView taskView = mKeyboardQuickSwitchView.getTaskAt(index); + GroupTask task = mControllerCallbacks.getTaskAt(index); + if (taskView == null || task == null) { + return Math.max(0, index); + } else if (task.task2 == null) { + UI_HELPER_EXECUTOR.execute(() -> + ActivityManagerWrapper.getInstance().startActivityFromRecents( + task.task1.key, + mControllers.taskbarActivityContext.getActivityLaunchOptions( + taskView, null).options)); + } else { + mControllers.uiController.launchSplitTasks(taskView, task); + } + return -1; + } + + private void onCloseComplete() { + mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); + mControllerCallbacks.onCloseComplete(); + } + + protected void onDestroy() { + closeQuickSwitchView(false); + } + + public void dumpLogs(String prefix, PrintWriter pw) { + pw.println(prefix + "KeyboardQuickSwitchViewController:"); + + pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus()); + pw.println(prefix + "\tcloseAnimationRunning=" + (mCloseAnimation != null)); + pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex); + } + + class ViewCallbacks { + + boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode != KeyEvent.KEYCODE_TAB) { + return false; + } + int taskCount = mControllerCallbacks.getTaskCount(); + int toIndex = mCurrentFocusIndex == -1 + // Focus the second-most recent app if possible + ? (taskCount > 1 ? 1 : 0) + : (event.isShiftPressed() + // focus a more recent task or loop back to the opposite end + ? Math.max(0, mCurrentFocusIndex == 0 + ? taskCount - 1 : mCurrentFocusIndex - 1) + // focus a less recent app or loop back to the opposite end + : ((mCurrentFocusIndex + 1) % taskCount)); + + mKeyboardQuickSwitchView.animateFocusMove(mCurrentFocusIndex, toIndex); + + return true; + } + + void updateCurrentFocusIndex(int index) { + mCurrentFocusIndex = index; + } + + void launchTappedTask(int index) { + KeyboardQuickSwitchViewController.this.launchTaskAt(index); + closeQuickSwitchView(true); + } + + void updateThumbnailInBackground(Task task, Consumer callback) { + mControllerCallbacks.updateThumbnailInBackground(task, callback); + } + + void updateTitleInBackground(Task task, Consumer callback) { + mControllerCallbacks.updateTitleInBackground(task, callback); + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index c0c14a31e3..6e746eff58 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.taskbar; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.pm.PackageManager.FEATURE_PC; import static android.os.Trace.TRACE_TAG_APP; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; @@ -236,7 +235,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { isDesktopMode ? new DesktopTaskbarRecentAppsController(this) : TaskbarRecentAppsController.DEFAULT, - new TaskbarEduTooltipController(this)); + new TaskbarEduTooltipController(this), + new KeyboardQuickSwitchController()); } public void init(@NonNull TaskbarSharedState sharedState) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index ea70de45de..931d79f885 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -59,6 +59,7 @@ public class TaskbarControllers { public final TaskbarTranslationController taskbarTranslationController; public final TaskbarOverlayController taskbarOverlayController; public final TaskbarEduTooltipController taskbarEduTooltipController; + public final KeyboardQuickSwitchController keyboardQuickSwitchController; @Nullable private LoggableTaskbarController[] mControllersToLog = null; @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null; @@ -103,7 +104,8 @@ public class TaskbarControllers { VoiceInteractionWindowController voiceInteractionWindowController, TaskbarTranslationController taskbarTranslationController, TaskbarRecentAppsController taskbarRecentAppsController, - TaskbarEduTooltipController taskbarEduTooltipController) { + TaskbarEduTooltipController taskbarEduTooltipController, + KeyboardQuickSwitchController keyboardQuickSwitchController) { this.taskbarActivityContext = taskbarActivityContext; this.taskbarDragController = taskbarDragController; this.navButtonController = navButtonController; @@ -127,6 +129,7 @@ public class TaskbarControllers { this.taskbarTranslationController = taskbarTranslationController; this.taskbarRecentAppsController = taskbarRecentAppsController; this.taskbarEduTooltipController = taskbarEduTooltipController; + this.keyboardQuickSwitchController = keyboardQuickSwitchController; } /** @@ -159,6 +162,7 @@ public class TaskbarControllers { taskbarRecentAppsController.init(this); taskbarTranslationController.init(this); taskbarEduTooltipController.init(this); + keyboardQuickSwitchController.init(this); mControllersToLog = new LoggableTaskbarController[] { taskbarDragController, navButtonController, navbarButtonsViewController, @@ -167,7 +171,7 @@ public class TaskbarControllers { stashedHandleViewController, taskbarStashController, taskbarEduController, taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController, voiceInteractionWindowController, taskbarTranslationController, - taskbarEduTooltipController + taskbarEduTooltipController, keyboardQuickSwitchController }; mBackgroundRendererControllers = new BackgroundRendererController[] { taskbarDragLayerController, taskbarScrimViewController, @@ -191,6 +195,7 @@ public class TaskbarControllers { public void onConfigurationChanged(@Config int configChanges) { navbarButtonsViewController.onConfigurationChanged(configChanges); taskbarDragLayerController.onConfigurationChanged(); + keyboardQuickSwitchController.onConfigurationChanged(configChanges); } /** @@ -216,6 +221,7 @@ public class TaskbarControllers { taskbarInsetsController.onDestroy(); voiceInteractionWindowController.onDestroy(); taskbarRecentAppsController.onDestroy(); + keyboardQuickSwitchController.onDestroy(); mControllersToLog = null; mBackgroundRendererControllers = null; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index a38838834b..6324715410 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -230,6 +230,38 @@ public class TaskbarUIController { ); } + /** + * Opens the Keyboard Quick Switch View. + * + * This will set the focus to the first task from the right (from the left in RTL) + */ + public void openQuickSwitchView() { + mControllers.keyboardQuickSwitchController.openQuickSwitchView(); + } + + /** + * Closes the Keyboard Quick Switch View. + * + * No-op if the view is already closed + */ + public void closeQuickSwitchView() { + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(); + } + + /** + * Launches the focused task and closes the Keyboard Quick Switch View. + * + * If the overlay or view are closed, or the overview task is focused, then Overview is + * launched. If the overview task is launched, then the first hidden task is focused. + * + * @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched + */ + public int launchFocusedTask() { + int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask(); + mControllers.keyboardQuickSwitchController.closeQuickSwitchView(); + return focusedTaskIndex; + } + /** * Launches the focused task in splitscreen. * diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index b5240fd214..d0fd65f817 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -31,7 +31,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StatefulActivity; +import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.util.RunnableList; import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; import com.android.quickstep.views.RecentsView; @@ -174,8 +177,25 @@ public class OverviewCommandHelper { mOverviewComponentObserver.getActivityInterface(); RecentsView recents = activityInterface.getVisibleRecentsView(); if (recents == null) { + T activity = activityInterface.getCreatedActivity(); + DeviceProfile dp = activity == null ? null : activity.getDeviceProfile(); + TaskbarUIController uiController = activityInterface.getTaskbarController(); + boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() + && uiController != null + && dp != null + && (dp.isTablet || dp.isTwoPanels); + if (cmd.type == TYPE_HIDE) { - // already hidden + if (!allowQuickSwitch) { + return true; + } + mTaskFocusIndexOverride = uiController.launchFocusedTask(); + if (mTaskFocusIndexOverride == -1) { + return true; + } + } + if (cmd.type == TYPE_KEYBOARD_INPUT && allowQuickSwitch) { + uiController.openQuickSwitchView(); return true; } if (cmd.type == TYPE_HOME) { diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java index 532edb2cf7..1f1c15bdb8 100644 --- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java +++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.java @@ -38,7 +38,8 @@ import com.android.launcher3.anim.Interpolators; * 1. Create an instance in the target view. * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call * {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}. - * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation where appropriate. + * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call + * {@link BorderAnimator#setBorderVisible(boolean)} where appropriate. */ public final class BorderAnimator { @@ -138,6 +139,7 @@ public final class BorderAnimator { /** * Builds the border appearance/disappearance animation. */ + @NonNull public Animator buildAnimator(boolean isAppearing) { mBorderBoundsBuilder.updateBorderBounds(mBorderBounds); mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f); @@ -150,6 +152,18 @@ public final class BorderAnimator { return mRunningBorderAnimation; } + /** + * Immediately shows/hides the border without an animation. + * + * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)} + */ + public void setBorderVisible(boolean visible) { + if (mRunningBorderAnimation != null) { + mRunningBorderAnimation.end(); + } + mBorderAnimationProgress.updateValue(visible ? 1f : 0f); + } + /** * Callback to update the border bounds when building this animation. */ diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt index 8a78d8cb2d..28229a6dbf 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -52,6 +52,7 @@ abstract class TaskbarBaseTestCase { @Mock lateinit var taskbarTranslationController: TaskbarTranslationController @Mock lateinit var taskbarOverlayController: TaskbarOverlayController @Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController + @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController lateinit var mTaskbarControllers: TaskbarControllers @@ -90,6 +91,7 @@ abstract class TaskbarBaseTestCase { taskbarTranslationController, taskbarRecentAppsController, taskbarEduTooltipController, + keyboardQuickSwitchController ) } }