newOrder = update.bubbleKeysInOrder.stream()
+ .map(mBubbles::get).filter(Objects::nonNull).toList();
+ if (!newOrder.isEmpty()) {
+ mBubbleBarViewController.reorderBubbles(newOrder);
+ }
+ }
+ if (update.suppressedBubbleKey != null) {
+ // TODO: (b/273316505) handle suppression
+ }
+ if (update.unsuppressedBubbleKey != null) {
+ // TODO: (b/273316505) handle suppression
+ }
+ if (update.selectedBubbleKey != null) {
+ if (mSelectedBubble != null
+ && !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
+ BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey);
+ if (newlySelected != null) {
+ bubbleToSelect = newlySelected;
+ } else {
+ Log.w(TAG, "trying to select bubble that doesn't exist:"
+ + update.selectedBubbleKey);
+ }
+ }
+ }
+ if (bubbleToSelect != null) {
+ setSelectedBubble(bubbleToSelect);
+ }
+ if (update.expandedChanged) {
+ if (update.expanded != mBubbleBarViewController.isExpanded()) {
+ mBubbleBarViewController.setExpandedFromSysui(update.expanded);
+ } else {
+ Log.w(TAG, "expansion was changed but is the same");
+ }
+ }
+ }
+
+ /**
+ * Sets the bubble that should be selected. This notifies the views, it does not notify
+ * WMShell that the selection has changed, that should go through
+ * {@link SystemUiProxy#showBubble}.
+ */
+ public void setSelectedBubble(BubbleBarBubble b) {
+ if (!Objects.equals(b, mSelectedBubble)) {
+ if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey());
+ mSelectedBubble = b;
+ mBubbleBarViewController.updateSelectedBubble(mSelectedBubble);
+ }
+ }
+
+ /**
+ * Returns the selected bubble or null if no bubble is selected.
+ */
+ @Nullable
+ public String getSelectedBubbleKey() {
+ if (mSelectedBubble != null) {
+ return mSelectedBubble.getKey();
+ }
+ return null;
+ }
+
+ //
+ // Loading data for the bubbles
+ //
+
+ @Nullable
+ private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) {
+ String appName;
+ Bitmap badgeBitmap;
+ Bitmap bubbleBitmap;
+ Path dotPath;
+ int dotColor;
+
+ boolean isImportantConvo = false; // TODO: (b/269671451) needs to be added to BubbleInfo
+
+ ShortcutRequest.QueryResult result = new ShortcutRequest(context,
+ new UserHandle(b.getUserId()))
+ .forPackage(b.getPackageName(), b.getShortcutId())
+ .query(FLAG_MATCH_DYNAMIC
+ | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
+ | FLAG_MATCH_CACHED
+ | FLAG_GET_PERSONS_DATA);
+
+ ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
+ if (shortcutInfo == null) {
+ Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
+ + " with shortcutId: " + b.getShortcutId());
+ }
+
+ ApplicationInfo appInfo;
+ try {
+ appInfo = mLauncherApps.getApplicationInfo(
+ b.getPackageName(),
+ 0,
+ new UserHandle(b.getUserId()));
+ } catch (PackageManager.NameNotFoundException e) {
+ // If we can't find package... don't think we should show the bubble.
+ Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
+ return null;
+ }
+ if (appInfo == null) {
+ Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
+ return null;
+ }
+ PackageManager pm = context.getPackageManager();
+ appName = String.valueOf(appInfo.loadLabel(pm));
+ Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
+ Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
+
+ // Badged bubble image
+ Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
+ b.getIcon());
+ if (bubbleDrawable == null) {
+ // Default to app icon
+ bubbleDrawable = appIcon;
+ }
+
+ BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
+ badgeBitmap = badgeBitmapInfo.icon;
+
+ float[] bubbleBitmapScale = new float[1];
+ bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
+
+ // Dot color & placement
+ Path iconPath = PathParser.createPathFromPathData(
+ context.getResources().getString(
+ com.android.internal.R.string.config_icon_mask));
+ Matrix matrix = new Matrix();
+ float scale = bubbleBitmapScale[0];
+ float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
+ matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+ radius /* pivot y */);
+ iconPath.transform(matrix);
+ dotPath = iconPath;
+ dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
+ Color.WHITE, WHITE_SCRIM_ALPHA);
+
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ BubbleView bubbleView = (BubbleView) inflater.inflate(
+ R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
+
+ BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
+ badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+ bubbleView.setBubble(bubble);
+ return bubble;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
new file mode 100644
index 0000000000..07de3b8c15
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -0,0 +1,304 @@
+/*
+ * 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.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/**
+ * The view that holds all the bubble views. Modifying this view should happen through
+ * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates,
+ * selection) should happen through {@link BubbleBarController} which is the source of truth
+ * for state information about the bubbles.
+ *
+ * The bubble bar has a couple of visual states:
+ * - stashed as a handle
+ * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it
+ * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row
+ * with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble
+ * view above the bar.
+ *
+ * The bubble bar has some behavior related to taskbar:
+ * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed"
+ * state)
+ * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its
+ * "expanded" state)
+ * - When bubble bar is in its "expanded" state, taskbar becomes stashed
+ *
+ * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally
+ * the bubble bar and stashed handle are not shown on lockscreen.
+ *
+ * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead
+ * the bubbles are shown fully by WMShell in their floating mode.
+ */
+public class BubbleBarView extends FrameLayout {
+
+ private static final String TAG = BubbleBarView.class.getSimpleName();
+
+ // TODO: (b/273594744) calculate the amount of space we have and base the max on that
+ // if it's smaller than 5.
+ private static final int MAX_BUBBLES = 5;
+
+ private final TaskbarActivityContext mActivityContext;
+ private final BubbleBarBackground mBubbleBarBackground;
+
+ // The current bounds of all the bubble bar.
+ private final Rect mBubbleBarBounds = new Rect();
+ // The amount the bubbles overlap when they are stacked in the bubble bar
+ private final float mIconOverlapAmount;
+ // The spacing between the bubbles when they are expanded in the bubble bar
+ private final float mIconSpacing;
+ // The size of a bubble in the bar
+ private final float mIconSize;
+ // The elevation of the bubbles within the bar
+ private final float mBubbleElevation;
+
+ // Whether the bar is expanded (i.e. the bubble activity is being displayed).
+ private boolean mIsBarExpanded = false;
+ // The currently selected bubble view.
+ private BubbleView mSelectedBubbleView;
+ // The click listener when the bubble bar is collapsed.
+ private View.OnClickListener mOnClickListener;
+
+ private final Rect mTempRect = new Rect();
+
+ // We don't reorder the bubbles when they are expanded as it could be jarring for the user
+ // this runnable will be populated with any reordering of the bubbles that should be applied
+ // once they are collapsed.
+ @Nullable
+ private Runnable mReorderRunnable;
+
+ public BubbleBarView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mActivityContext = ActivityContext.lookupContext(context);
+
+ mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
+ mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+ mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
+ setClipToPadding(false);
+
+ mBubbleBarBackground = new BubbleBarBackground(mActivityContext,
+ getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
+ setBackgroundDrawable(mBubbleBarBackground);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mBubbleBarBounds.left = left;
+ mBubbleBarBounds.top = top;
+ mBubbleBarBounds.right = right;
+ mBubbleBarBounds.bottom = bottom;
+
+ // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
+ setPivotX(getWidth());
+ setPivotY(getHeight());
+
+ // Position the views
+ updateChildrenRenderNodeProperties();
+ }
+
+ /**
+ * Returns the bounds of the bubble bar.
+ */
+ public Rect getBubbleBarBounds() {
+ return mBubbleBarBounds;
+ }
+
+ // TODO: (b/273592694) animate it
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() + 1 > MAX_BUBBLES) {
+ removeViewInLayout(getChildAt(getChildCount() - 1));
+ }
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Updates the z order, positions, and badge visibility of the bubble views in the bar based
+ * on the expanded state.
+ */
+ // TODO: (b/273592694) animate it
+ private void updateChildrenRenderNodeProperties() {
+ int bubbleCount = getChildCount();
+ final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+ for (int i = 0; i < bubbleCount; i++) {
+ BubbleView bv = (BubbleView) getChildAt(i);
+ bv.setTranslationY(ty);
+ if (mIsBarExpanded) {
+ final float tx = i * (mIconSize + mIconSpacing);
+ bv.setTranslationX(tx);
+ bv.setZ(0);
+ bv.showBadge();
+ } else {
+ bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
+ bv.setTranslationX(i * mIconOverlapAmount);
+ if (i > 0) {
+ bv.hideBadge();
+ } else {
+ bv.showBadge();
+ }
+ }
+ }
+ }
+
+ /**
+ * Reorders the views to match the provided list.
+ */
+ public void reorder(List viewOrder) {
+ if (isExpanded()) {
+ mReorderRunnable = () -> doReorder(viewOrder);
+ } else {
+ doReorder(viewOrder);
+ }
+ }
+
+ // TODO: (b/273592694) animate it
+ private void doReorder(List viewOrder) {
+ if (!isExpanded()) {
+ for (int i = 0; i < viewOrder.size(); i++) {
+ View child = viewOrder.get(i);
+ if (child != null) {
+ removeViewInLayout(child);
+ addViewInLayout(child, i, child.getLayoutParams());
+ }
+ }
+ updateChildrenRenderNodeProperties();
+ }
+ }
+
+ /**
+ * Sets which bubble view should be shown as selected.
+ */
+ // TODO: (b/273592694) animate it
+ public void setSelectedBubble(BubbleView view) {
+ mSelectedBubbleView = view;
+ updateArrowForSelected();
+ invalidate();
+ }
+
+ private void updateArrowForSelected() {
+ if (mSelectedBubbleView == null) {
+ Log.w(TAG, "trying to update selection arrow without a selected view!");
+ return;
+ }
+ final int index = indexOfChild(mSelectedBubbleView);
+ // Find the center of the bubble when it's expanded, set the arrow position to it.
+ final float tx = getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+ mBubbleBarBackground.setArrowPosition(tx);
+ }
+
+ @Override
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ setOrUnsetClickListener();
+ }
+
+ /**
+ * The click listener used for the bubble view gets added / removed depending on whether
+ * the bar is expanded or collapsed, this updates whether the listener is set based on state.
+ */
+ private void setOrUnsetClickListener() {
+ super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener);
+ }
+
+ /**
+ * Sets whether the bubble bar is expanded or collapsed.
+ */
+ // TODO: (b/273592694) animate it
+ public void setExpanded(boolean isBarExpanded) {
+ if (mIsBarExpanded != isBarExpanded) {
+ mIsBarExpanded = isBarExpanded;
+ updateArrowForSelected();
+ setOrUnsetClickListener();
+ if (!isBarExpanded && mReorderRunnable != null) {
+ mReorderRunnable.run();
+ mReorderRunnable = null;
+ }
+ mBubbleBarBackground.showArrow(mIsBarExpanded);
+ requestLayout(); // trigger layout to reposition views & update size for expansion
+ }
+ }
+
+ /**
+ * Returns whether the bubble bar is expanded.
+ */
+ public boolean isExpanded() {
+ return mIsBarExpanded;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int childCount = getChildCount();
+ final float iconWidth = mIsBarExpanded
+ ? (childCount * (mIconSize + mIconSpacing))
+ : mIconSize + ((childCount - 1) * mIconOverlapAmount);
+ final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd();
+ setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ measureChild(child, (int) mIconSize, (int) mIconSize);
+ }
+ }
+
+ /**
+ * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar
+ * touch bounds.
+ */
+ public boolean isEventOverAnyItem(MotionEvent ev) {
+ if (getVisibility() == View.VISIBLE) {
+ getBoundsOnScreen(mTempRect);
+ return mTempRect.contains((int) ev.getX(), (int) ev.getY());
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!mIsBarExpanded) {
+ // When the bar is collapsed, all taps on it should expand it.
+ return true;
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
new file mode 100644
index 0000000000..0afc2cb4fe
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -0,0 +1,297 @@
+/*
+ * 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.bubbles;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.MultiPropertyFactory;
+import com.android.launcher3.util.MultiValueAlpha;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
+ * responding to changes in bubble state provided by BubbleBarController.
+ */
+public class BubbleBarViewController {
+
+ private static final String TAG = BubbleBarViewController.class.getSimpleName();
+
+ private final TaskbarActivityContext mActivity;
+ private final BubbleBarView mBarView;
+ private final int mIconSize;
+
+ // Initialized in init.
+ private BubbleStashController mBubbleStashController;
+ private BubbleBarController mBubbleBarController;
+ private View.OnClickListener mBubbleClickListener;
+ private View.OnClickListener mBubbleBarClickListener;
+
+ // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
+ private final MultiValueAlpha mBubbleBarAlpha;
+ private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
+ private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
+ this::updateTranslationY);
+
+ // Modified when swipe up is happening on the bubble bar or task bar.
+ private float mBubbleBarSwipeUpTranslationY;
+
+ // Whether the bar is hidden for a sysui state.
+ private boolean mHiddenForSysui;
+ // Whether the bar is hidden because there are no bubbles.
+ private boolean mHiddenForNoBubbles;
+
+ public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
+ mActivity = activity;
+ mBarView = barView;
+ mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
+ mBubbleBarAlpha.setUpdateVisibility(true);
+ mIconSize = activity.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ }
+
+ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+ mBubbleStashController = bubbleControllers.bubbleStashController;
+ mBubbleBarController = bubbleControllers.bubbleBarController;
+
+ mActivity.addOnDeviceProfileChangeListener(dp ->
+ mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight
+ );
+ mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight;
+ mBubbleBarScale.updateValue(1f);
+ mBubbleClickListener = v -> onBubbleClicked(v);
+ mBubbleBarClickListener = v -> setExpanded(true);
+ mBarView.setOnClickListener(mBubbleBarClickListener);
+ // TODO: when barView layout changes tell taskbarInsetsController the insets have changed.
+ }
+
+ private void onBubbleClicked(View v) {
+ BubbleBarBubble bubble = ((BubbleView) v).getBubble();
+ if (bubble == null) {
+ Log.e(TAG, "bubble click listener, bubble was null");
+ }
+ final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
+ if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
+ // Tapping the currently selected bubble while expanded collapses the view.
+ setExpanded(false);
+ mBubbleStashController.stashBubbleBar();
+ } else {
+ mBubbleBarController.setSelectedBubble(bubble);
+ // TODO: Tell SysUi to show the expanded view for this bubble.
+ }
+ }
+
+ //
+ // The below animators are exposed to BubbleStashController so it can manage the stashing
+ // animation.
+ //
+
+ public MultiPropertyFactory getBubbleBarAlpha() {
+ return mBubbleBarAlpha;
+ }
+
+ public AnimatedFloat getBubbleBarScale() {
+ return mBubbleBarScale;
+ }
+
+ public AnimatedFloat getBubbleBarTranslationY() {
+ return mBubbleBarTranslationY;
+ }
+
+ /**
+ * Whether the bubble bar is visible or not.
+ */
+ public boolean isBubbleBarVisible() {
+ return mBarView.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * The bounds of the bubble bar.
+ */
+ public Rect getBubbleBarBounds() {
+ return mBarView.getBubbleBarBounds();
+ }
+
+ /**
+ * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
+ * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
+ */
+ public boolean isExpanded() {
+ return mBarView.isExpanded();
+ }
+
+ /**
+ * Whether the motion event is within the bounds of the bubble bar.
+ */
+ public boolean isEventOverAnyItem(MotionEvent ev) {
+ return mBarView.isEventOverAnyItem(ev);
+ }
+
+ //
+ // Visibility of the bubble bar
+ //
+
+ /**
+ * Returns whether the bubble bar is hidden because there are no bubbles.
+ */
+ public boolean isHiddenForNoBubbles() {
+ return mHiddenForNoBubbles;
+ }
+
+ /**
+ * Sets whether the bubble bar should be hidden because there are no bubbles.
+ */
+ public void setHiddenForBubbles(boolean hidden) {
+ if (mHiddenForNoBubbles != hidden) {
+ mHiddenForNoBubbles = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ /**
+ * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
+ */
+ public void setHiddenForSysui(boolean hidden) {
+ if (mHiddenForSysui != hidden) {
+ mHiddenForSysui = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ // TODO: (b/273592694) animate it
+ private void updateVisibilityForStateChange() {
+ if (!mHiddenForSysui && !mBubbleStashController.isStashed() && !mHiddenForNoBubbles) {
+ mBarView.setVisibility(VISIBLE);
+ } else {
+ mBarView.setVisibility(INVISIBLE);
+ }
+ }
+
+ //
+ // Modifying view related properties.
+ //
+
+ /**
+ * Sets the translation of the bubble bar during the swipe up gesture.
+ */
+ public void setTranslationYForSwipe(float transY) {
+ mBubbleBarSwipeUpTranslationY = transY;
+ updateTranslationY();
+ }
+
+ private void updateTranslationY() {
+ mBarView.setTranslationY(mBubbleBarTranslationY.value
+ + mBubbleBarSwipeUpTranslationY);
+ }
+
+ /**
+ * Applies scale properties for the entire bubble bar.
+ */
+ private void updateScale() {
+ float scale = mBubbleBarScale.value;
+ mBarView.setScaleX(scale);
+ mBarView.setScaleY(scale);
+ }
+
+ //
+ // Manipulating the specific bubble views in the bar
+ //
+
+ /**
+ * Removes the provided bubble from the bubble bar.
+ */
+ public void removeBubble(BubbleBarBubble b) {
+ if (b != null) {
+ mBarView.removeView(b.getView());
+ } else {
+ Log.w(TAG, "removeBubble, bubble was null!");
+ }
+ }
+
+ /**
+ * Adds the provided bubble to the bubble bar.
+ */
+ public void addBubble(BubbleBarBubble b) {
+ if (b != null) {
+ mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
+ b.getView().setOnClickListener(mBubbleClickListener);
+ } else {
+ Log.w(TAG, "addBubble, bubble was null!");
+ }
+ }
+
+ /**
+ * Reorders the bubbles based on the provided list.
+ */
+ public void reorderBubbles(List newOrder) {
+ List viewList = newOrder.stream().filter(Objects::nonNull)
+ .map(BubbleBarBubble::getView).toList();
+ mBarView.reorder(viewList);
+ }
+
+ /**
+ * Updates the selected bubble.
+ */
+ public void updateSelectedBubble(BubbleBarBubble newlySelected) {
+ mBarView.setSelectedBubble(newlySelected.getView());
+ }
+
+ /**
+ * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
+ * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
+ * showing a selected bubble. This method should ONLY be called from UI events originating
+ * from Launcher.
+ */
+ public void setExpanded(boolean isExpanded) {
+ if (isExpanded != mBarView.isExpanded()) {
+ mBarView.setExpanded(isExpanded);
+ if (!isExpanded) {
+ // TODO: Tell SysUi to collapse the bubble
+ } else {
+ final String selectedKey = mBubbleBarController.getSelectedBubbleKey();
+ if (selectedKey != null) {
+ // TODO: Tell SysUi to show the bubble
+ } else {
+ Log.w(TAG, "trying to expand bubbles when there isn't one selected");
+ }
+ // TODO: Tell taskbar stash controller to stash without bubbles following
+ }
+ }
+ }
+
+ /**
+ * Sets whether the bubble bar should be expanded. This method is used in response to UI events
+ * from SystemUI.
+ */
+ public void setExpandedFromSysui(boolean isExpanded) {
+ if (!isExpanded) {
+ mBubbleStashController.stashBubbleBar();
+ } else {
+ mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
new file mode 100644
index 0000000000..6417f3c585
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -0,0 +1,80 @@
+/*
+ * 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.bubbles;
+
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.RunnableList;
+
+/**
+ * Hosts various bubble controllers to facilitate passing between one another.
+ */
+public class BubbleControllers {
+
+ public final BubbleBarController bubbleBarController;
+ public final BubbleBarViewController bubbleBarViewController;
+ public final BubbleStashController bubbleStashController;
+ public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+
+ private final RunnableList mPostInitRunnables = new RunnableList();
+
+ /**
+ * Want to add a new controller? Don't forget to:
+ * * Call init
+ * * Call onDestroy
+ */
+ public BubbleControllers(
+ BubbleBarController bubbleBarController,
+ BubbleBarViewController bubbleBarViewController,
+ BubbleStashController bubbleStashController,
+ BubbleStashedHandleViewController bubbleStashedHandleViewController) {
+ this.bubbleBarController = bubbleBarController;
+ this.bubbleBarViewController = bubbleBarViewController;
+ this.bubbleStashController = bubbleStashController;
+ this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
+ }
+
+ /**
+ * Initializes all controllers. Note that controllers can now reference each other through this
+ * BubbleControllers instance, but should be careful to only access things that were created
+ * in constructors for now, as some controllers may still be waiting for init().
+ */
+ public void init(TaskbarControllers taskbarControllers) {
+ bubbleBarController.init(taskbarControllers, this);
+ bubbleBarViewController.init(taskbarControllers, this);
+ bubbleStashedHandleViewController.init(taskbarControllers, this);
+ bubbleStashController.init(taskbarControllers, this);
+
+ mPostInitRunnables.executeAllAndDestroy();
+ }
+
+ /**
+ * If all controllers are already initialized, runs the given callback immediately. Otherwise,
+ * queues it to run after calling init() on all controllers. This should likely be used in any
+ * case where one controller is telling another controller to do something inside init().
+ */
+ public void runAfterInit(Runnable runnable) {
+ // If this has been executed in init, it automatically runs adds to it.
+ mPostInitRunnables.add(runnable);
+ }
+
+ /**
+ * Cleans up all controllers.
+ */
+ public void onDestroy() {
+ bubbleStashedHandleViewController.onDestroy();
+ bubbleBarController.onDestroy();
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
new file mode 100644
index 0000000000..0ab53b0c20
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -0,0 +1,277 @@
+/*
+ * 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.bubbles;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.Nullable;
+import android.view.InsetsController;
+
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.StashedHandleViewController;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarStashController;
+import com.android.launcher3.util.MultiPropertyFactory;
+
+/**
+ * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
+ * create a cohesive animation between stashed/unstashed states.
+ */
+public class BubbleStashController {
+
+ private static final String TAG = BubbleStashController.class.getSimpleName();
+
+ /**
+ * How long to stash/unstash.
+ */
+ public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
+
+ /**
+ * The scale bubble bar animates to when being stashed.
+ */
+ private static final float STASHED_BAR_SCALE = 0.5f;
+
+ protected final TaskbarActivityContext mActivity;
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+ private BubbleBarViewController mBarViewController;
+ private BubbleStashedHandleViewController mHandleViewController;
+ private TaskbarStashController mTaskbarStashController;
+
+ private MultiPropertyFactory.MultiProperty mIconAlphaForStash;
+ private AnimatedFloat mIconScaleForStash;
+ private AnimatedFloat mIconTranslationYForStash;
+ private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
+
+ private boolean mRequestedStashState;
+ private boolean mRequestedExpandedState;
+
+ private boolean mIsStashed;
+ private int mStashedHeight;
+ private int mUnstashedHeight;
+ private boolean mBubblesShowingOnHome;
+ private boolean mBubblesShowingOnOverview;
+
+ @Nullable
+ private AnimatorSet mAnimator;
+
+ public BubbleStashController(TaskbarActivityContext activity) {
+ mActivity = activity;
+ }
+
+ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+ mControllers = controllers;
+ mBarViewController = bubbleControllers.bubbleBarViewController;
+ mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
+ mTaskbarStashController = controllers.taskbarStashController;
+
+ mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0);
+ mIconScaleForStash = mBarViewController.getBubbleBarScale();
+ mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY();
+
+ mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
+ StashedHandleViewController.ALPHA_INDEX_STASHED);
+
+ mStashedHeight = mHandleViewController.getStashedHeight();
+ mUnstashedHeight = mHandleViewController.getUnstashedHeight();
+
+ bubbleControllers.runAfterInit(() -> {
+ if (mTaskbarStashController.isStashed()) {
+ stashBubbleBar();
+ } else {
+ showBubbleBar(false /* expandBubbles */);
+ }
+ });
+ }
+
+ /**
+ * Returns the touchable height of the bubble bar based on it's stashed state.
+ */
+ public int getTouchableHeight() {
+ return mIsStashed ? mStashedHeight : mUnstashedHeight;
+ }
+
+ /**
+ * Returns whether the bubble bar is currently stashed.
+ */
+ public boolean isStashed() {
+ return mIsStashed;
+ }
+
+ /**
+ * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
+ */
+ public void setBubblesShowingOnHome(boolean onHome) {
+ if (mBubblesShowingOnHome != onHome) {
+ mBubblesShowingOnHome = onHome;
+ if (mBubblesShowingOnHome) {
+ showBubbleBar(/* expanded= */ false);
+ } else if (!mBarViewController.isExpanded()) {
+ stashBubbleBar();
+ }
+ }
+ }
+
+ /** Whether bubbles are showing on the launcher home page. */
+ public boolean isBubblesShowingOnHome() {
+ return mBubblesShowingOnHome;
+ }
+
+ // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing
+ /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */
+ public void setBubblesShowingOnOverview(boolean onOverview) {
+ if (mBubblesShowingOnOverview != onOverview) {
+ mBubblesShowingOnOverview = onOverview;
+ if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
+ stashBubbleBar();
+ }
+ }
+ }
+
+ /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
+ public void onSysuiLockedStateChange(boolean isSysuiLocked) {
+ if (isSysuiLocked) {
+ // TODO: should the normal path flip mBubblesOnHome / check if this is needed
+ // If we're locked, we're no longer showing on home.
+ mBubblesShowingOnHome = false;
+ mBubblesShowingOnOverview = false;
+ stashBubbleBar();
+ }
+ }
+
+ /**
+ * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the
+ * bar does not stash).
+ */
+ public void stashBubbleBar() {
+ mRequestedStashState = true;
+ mRequestedExpandedState = false;
+ updateStashedAndExpandedState();
+ }
+
+ /**
+ * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}.
+ */
+ public void showBubbleBar(boolean expandBubbles) {
+ mRequestedStashState = false;
+ mRequestedExpandedState = expandBubbles;
+ updateStashedAndExpandedState();
+ }
+
+ private void updateStashedAndExpandedState() {
+ if (mBarViewController.isHiddenForNoBubbles()) {
+ // If there are no bubbles the bar and handle are invisible, nothing to do here.
+ return;
+ }
+ boolean isStashed = mRequestedStashState
+ && !mBubblesShowingOnHome
+ && !mBubblesShowingOnOverview;
+ if (mIsStashed != isStashed) {
+ mIsStashed = isStashed;
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION);
+ mAnimator.start();
+ onIsStashedChanged();
+ }
+ if (mBarViewController.isExpanded() != mRequestedExpandedState) {
+ mBarViewController.setExpanded(mRequestedExpandedState);
+ }
+ }
+
+ /**
+ * Create a stash animation.
+ *
+ * @param isStashed whether it's a stash animation or an unstash animation
+ * @param duration duration of the animation
+ * @return the animation
+ */
+ private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f;
+
+ AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
+ // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
+ AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
+ AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
+
+ final float firstHalfDurationScale;
+ final float secondHalfDurationScale;
+
+ if (isStashed) {
+ firstHalfDurationScale = 0.75f;
+ secondHalfDurationScale = 0.5f;
+
+ fullLengthAnimatorSet.play(mIconTranslationYForStash.animateToValue(stashTranslation));
+
+ firstHalfAnimatorSet.playTogether(
+ mIconAlphaForStash.animateToValue(0),
+ mIconScaleForStash.animateToValue(STASHED_BAR_SCALE));
+ secondHalfAnimatorSet.playTogether(
+ mBubbleStashedHandleAlpha.animateToValue(1));
+ } else {
+ firstHalfDurationScale = 0.5f;
+ secondHalfDurationScale = 0.75f;
+
+ // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
+ final float hotseatTransY = mActivity.getDeviceProfile().getTaskbarOffsetY();
+ final float translationY = mBubblesShowingOnHome ? hotseatTransY : 0;
+ fullLengthAnimatorSet.playTogether(
+ mIconScaleForStash.animateToValue(1),
+ mIconTranslationYForStash.animateToValue(translationY));
+
+ firstHalfAnimatorSet.playTogether(
+ mBubbleStashedHandleAlpha.animateToValue(0)
+ );
+ secondHalfAnimatorSet.playTogether(
+ mIconAlphaForStash.animateToValue(1)
+ );
+ }
+
+ fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed));
+
+ fullLengthAnimatorSet.setDuration(duration);
+ firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
+ secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
+ secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
+
+ animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
+ secondHalfAnimatorSet);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimator = null;
+ mControllers.runAfterInit(() -> {
+ if (isStashed) {
+ mBarViewController.setExpanded(false);
+ }
+ });
+ }
+ });
+ return animatorSet;
+ }
+
+ private void onIsStashedChanged() {
+ mControllers.runAfterInit(() -> {
+ mHandleViewController.onIsStashedChanged();
+ // TODO: when stash changes tell taskbarInsetsController the insets have changed.
+ });
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
new file mode 100644
index 0000000000..2170a5dc5b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -0,0 +1,262 @@
+/*
+ * 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.bubbles;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.RevealOutlineAnimation;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.taskbar.StashedHandleView;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.MultiPropertyFactory;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
+
+/**
+ * Handles properties/data collection, then passes the results to our stashed handle View to render.
+ */
+public class BubbleStashedHandleViewController {
+
+ private final TaskbarActivityContext mActivity;
+ private final StashedHandleView mStashedHandleView;
+ private final MultiValueAlpha mTaskbarStashedHandleAlpha;
+
+ // Initialized in init.
+ private BubbleBarViewController mBarViewController;
+ private BubbleStashController mBubbleStashController;
+ private RegionSamplingHelper mRegionSamplingHelper;
+ private int mBarSize;
+ private int mStashedHandleWidth;
+ private int mStashedHandleHeight;
+
+ // The bounds we want to clip to in the settled state when showing the stashed handle.
+ private final Rect mStashedHandleBounds = new Rect();
+
+ // When the reveal animation is cancelled, we can assume it's about to create a new animation,
+ // which should start off at the same point the cancelled one left off.
+ private float mStartProgressForNextRevealAnim;
+ private boolean mWasLastRevealAnimReversed;
+
+ // XXX: if there are more of these maybe do state flags instead
+ private boolean mHiddenForSysui;
+ private boolean mHiddenForNoBubbles;
+ private boolean mHiddenForHomeButtonDisabled;
+
+ public BubbleStashedHandleViewController(TaskbarActivityContext activity,
+ StashedHandleView stashedHandleView) {
+ mActivity = activity;
+ mStashedHandleView = stashedHandleView;
+ mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1);
+ }
+
+ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+ mBarViewController = bubbleControllers.bubbleBarViewController;
+ mBubbleStashController = bubbleControllers.bubbleStashController;
+
+ Resources resources = mActivity.getResources();
+ mStashedHandleHeight = resources.getDimensionPixelSize(
+ R.dimen.bubblebar_stashed_handle_height);
+ mStashedHandleWidth = resources.getDimensionPixelSize(
+ R.dimen.bubblebar_stashed_handle_width);
+ mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
+
+ final int bottomMargin = resources.getDimensionPixelSize(
+ R.dimen.transient_taskbar_bottom_margin);
+ mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin;
+
+ mTaskbarStashedHandleAlpha.get(0).setValue(0);
+
+ final int stashedTaskbarHeight = resources.getDimensionPixelSize(
+ R.dimen.bubblebar_stashed_size);
+ mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ float stashedHandleRadius = view.getHeight() / 2f;
+ outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius);
+ }
+ });
+
+ mRegionSamplingHelper = new RegionSamplingHelper(mStashedHandleView,
+ new RegionSamplingHelper.SamplingCallback() {
+ @Override
+ public void onRegionDarknessChanged(boolean isRegionDark) {
+ mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */);
+ }
+
+ @Override
+ public Rect getSampledRegion(View sampledView) {
+ return mStashedHandleView.getSampledRegion();
+ }
+ }, Executors.UI_HELPER_EXECUTOR);
+
+ mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
+ // As more bubbles get added, the icon bounds become larger. To ensure a consistent
+ // handle bar position, we pin it to the edge of the screen.
+ Rect bubblebarRect = mBarViewController.getBubbleBarBounds();
+ final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+
+ mStashedHandleBounds.set(
+ bubblebarRect.right - mStashedHandleWidth,
+ stashedCenterY - mStashedHandleHeight / 2,
+ bubblebarRect.right,
+ stashedCenterY + mStashedHandleHeight / 2);
+ mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+
+ view.setPivotX(view.getWidth());
+ view.setPivotY(view.getHeight() - stashedTaskbarHeight / 2f);
+ });
+ }
+
+ public void onDestroy() {
+ mRegionSamplingHelper.stopAndDestroy();
+ mRegionSamplingHelper = null;
+ }
+
+ /**
+ * Returns the height of the stashed handle.
+ */
+ public int getStashedHeight() {
+ return mStashedHandleHeight;
+ }
+
+ /**
+ * Returns the height when the bubble bar is unstashed (so the height of the bubble bar).
+ */
+ public int getUnstashedHeight() {
+ return mBarSize;
+ }
+
+ /**
+ * Called when system ui state changes. Bubbles don't show when the device is locked.
+ */
+ public void setHiddenForSysui(boolean hidden) {
+ if (mHiddenForSysui != hidden) {
+ mHiddenForSysui = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ /**
+ * Called when the handle should be hidden (or shown) because there are no bubbles
+ * (or 1+ bubbles).
+ */
+ public void setHiddenForBubbles(boolean hidden) {
+ if (mHiddenForNoBubbles != hidden) {
+ mHiddenForNoBubbles = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ /**
+ * Called when the home button is enabled / disabled. Bubbles don't show if home is disabled.
+ */
+ // TODO: is this needed for bubbles?
+ public void setIsHomeButtonDisabled(boolean homeDisabled) {
+ mHiddenForHomeButtonDisabled = homeDisabled;
+ updateVisibilityForStateChange();
+ }
+
+ // TODO: (b/273592694) animate it?
+ private void updateVisibilityForStateChange() {
+ if (!mHiddenForSysui && !mHiddenForHomeButtonDisabled && !mHiddenForNoBubbles) {
+ mStashedHandleView.setVisibility(VISIBLE);
+ } else {
+ mStashedHandleView.setVisibility(INVISIBLE);
+ }
+ updateRegionSampling();
+ }
+
+ /**
+ * Called when bubble bar is stash state changes so that updates to the stashed handle color
+ * can be started or stopped.
+ */
+ public void onIsStashedChanged() {
+ updateRegionSampling();
+ }
+
+ private void updateRegionSampling() {
+ boolean handleVisible = mStashedHandleView.getVisibility() == VISIBLE
+ && mBubbleStashController.isStashed();
+ mRegionSamplingHelper.setWindowVisible(handleVisible);
+ if (handleVisible) {
+ mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
+ mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
+ } else {
+ mRegionSamplingHelper.stop();
+ }
+ }
+
+ /**
+ * Sets the translation of the stashed handle during the swipe up gesture.
+ */
+ public void setTranslationYForSwipe(float transY) {
+ mStashedHandleView.setTranslationY(transY);
+ }
+
+ /**
+ * Used by {@link BubbleStashController} to animate the handle when stashing or un stashing.
+ */
+ public MultiPropertyFactory getStashedHandleAlpha() {
+ return mTaskbarStashedHandleAlpha;
+ }
+
+ /**
+ * Creates and returns an Animator that updates the stashed handle shape and size.
+ * When stashed, the shape is a thin rounded pill. When unstashed, the shape morphs into
+ * the size of where the bubble bar icons will be.
+ */
+ public Animator createRevealAnimToIsStashed(boolean isStashed) {
+ Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
+
+ // Account for the full visual height of the bubble bar
+ int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2;
+ bubbleBarBounds.top -= heightDiff;
+ bubbleBarBounds.bottom += heightDiff;
+ float stashedHandleRadius = mStashedHandleView.getHeight() / 2f;
+ final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
+ stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
+
+ boolean isReversed = !isStashed;
+ boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
+ mWasLastRevealAnimReversed = isReversed;
+ if (changingDirection) {
+ mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
+ }
+
+ ValueAnimator revealAnim = handleRevealProvider.createRevealAnimator(mStashedHandleView,
+ isReversed, mStartProgressForNextRevealAnim);
+ revealAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mStartProgressForNextRevealAnim = ((ValueAnimator) animation).getAnimatedFraction();
+ }
+ });
+ return revealAnim;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
new file mode 100644
index 0000000000..e22e63aa36
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -0,0 +1,134 @@
+/*
+ * 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.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ImageView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconNormalizer;
+
+// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
+// TODO: (b/269670235) currently this doesn't show the 'update dot'
+/**
+ * View that displays a bubble icon, along with an app badge on either the left or
+ * right side of the view.
+ */
+public class BubbleView extends ConstraintLayout {
+
+ // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
+ public static final int DEFAULT_PATH_SIZE = 100;
+
+ private final ImageView mBubbleIcon;
+ private final ImageView mAppIcon;
+ private final int mBubbleSize;
+
+ // TODO: (b/273310265) handle RTL
+ private boolean mOnLeft = false;
+
+ private BubbleBarBubble mBubble;
+
+ public BubbleView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ // We manage positioning the badge ourselves
+ setLayoutDirection(LAYOUT_DIRECTION_LTR);
+
+ LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
+
+ mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ mBubbleIcon = findViewById(R.id.icon_view);
+ mAppIcon = findViewById(R.id.app_icon_view);
+
+ setFocusable(true);
+ setClickable(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ BubbleView.this.getOutline(outline);
+ }
+ });
+ }
+
+ private void getOutline(Outline outline) {
+ final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
+ final int inset = (mBubbleSize - normalizedSize) / 2;
+ outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+ }
+
+ /** Sets the bubble being rendered in this view. */
+ void setBubble(BubbleBarBubble bubble) {
+ mBubble = bubble;
+ mBubbleIcon.setImageBitmap(bubble.getIcon());
+ mAppIcon.setImageBitmap(bubble.getBadge());
+ }
+
+ /** Returns the bubble being rendered in this view. */
+ @Nullable
+ BubbleBarBubble getBubble() {
+ return mBubble;
+ }
+
+ /** Shows the app badge on this bubble. */
+ void showBadge() {
+ Bitmap appBadgeBitmap = mBubble.getBadge();
+ if (appBadgeBitmap == null) {
+ mAppIcon.setVisibility(GONE);
+ return;
+ }
+
+ int translationX;
+ if (mOnLeft) {
+ translationX = -(mBubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
+ } else {
+ translationX = 0;
+ }
+
+ mAppIcon.setTranslationX(translationX);
+ mAppIcon.setVisibility(VISIBLE);
+ }
+
+ /** Hides the app badge on this bubble. */
+ void hideBadge() {
+ mAppIcon.setVisibility(GONE);
+ }
+
+ @Override
+ public String toString() {
+ return "BubbleView{" + mBubble + "}";
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 3990dade70..0eef70e270 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -402,8 +402,9 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
.inflate(R.layout.predicted_app_icon, parent, false);
icon.applyFromWorkspaceItem(info);
- icon.setOnClickListener(ItemClickHandler.INSTANCE);
- icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler());
+ Launcher launcher = Launcher.getLauncher(parent.getContext());
+ icon.setOnClickListener(launcher.getItemOnClickListener());
+ icon.setOnFocusChangeListener(launcher.getFocusHandler());
return icon;
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 08d147f7cf..163c36fa24 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -56,6 +56,10 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler {
return RemoteViews.startPendingIntent(hostView, pendingIntent,
remoteResponse.getLaunchOptions(view));
}
+ if (mLauncher.getSplitToWorkspaceController().handleSecondWidgetSelectionForSplit(view,
+ pendingIntent)) {
+ return true;
+ }
Pair options = remoteResponse.getLaunchOptions(view);
ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager()
.getActivityLaunchOptions(hostView);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b2b062344e..d67dbaec09 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -74,6 +74,7 @@ import android.hardware.SensorManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.DisplayManager;
import android.media.permission.SafeCloseable;
+import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.IBinder;
@@ -91,6 +92,7 @@ import android.window.SplashScreen;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import com.android.app.viewcapture.SettingsAwareViewCapture;
import com.android.launcher3.AbstractFloatingView;
@@ -764,6 +766,7 @@ public class QuickstepLauncher extends Launcher {
mActiveOnBackAnimationCallback.onBackStarted(backEvent);
}
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void onBackInvoked() {
// Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
@@ -970,8 +973,8 @@ public class QuickstepLauncher extends Launcher {
return mTaskbarUIController;
}
- public SplitSelectStateController getSplitSelectStateController() {
- return mSplitSelectStateController;
+ public SplitToWorkspaceController getSplitToWorkspaceController() {
+ return mSplitToWorkspaceController;
}
public T getActionsView() {
@@ -1178,6 +1181,11 @@ public class QuickstepLauncher extends Launcher {
}
}
+ @Override
+ public void tryClearAccessibilityFocus(View view) {
+ view.clearAccessibilityFocus();
+ }
+
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java
index 9c59361431..87c836bbac 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagTogglerPrefUi.java
@@ -16,12 +16,10 @@
package com.android.launcher3.uioverrides.flags;
-import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
import static com.android.launcher3.config.FeatureFlags.FlagState.TEAMFOOD;
import static com.android.launcher3.uioverrides.flags.FlagsFactory.TEAMFOOD_FLAG;
import android.content.Context;
-import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Process;
import android.text.Html;
@@ -52,31 +50,27 @@ public final class FlagTogglerPrefUi {
private final PreferenceFragmentCompat mFragment;
private final Context mContext;
- private final SharedPreferences mSharedPreferences;
-
private final PreferenceDataStore mDataStore = new PreferenceDataStore() {
@Override
public void putBoolean(String key, boolean value) {
- mSharedPreferences.edit().putBoolean(key, value).apply();
+ FlagsFactory.getSharedPreferences().edit().putBoolean(key, value).apply();
updateMenu();
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
- return mSharedPreferences.getBoolean(key, defaultValue);
+ return FlagsFactory.getSharedPreferences().getBoolean(key, defaultValue);
}
};
public FlagTogglerPrefUi(PreferenceFragmentCompat fragment) {
mFragment = fragment;
mContext = fragment.getActivity();
- mSharedPreferences = mContext.getSharedPreferences(
- FLAGS_PREF_NAME, Context.MODE_PRIVATE);
}
public void applyTo(PreferenceGroup parent) {
- Set modifiedPrefs = mSharedPreferences.getAll().keySet();
+ Set modifiedPrefs = FlagsFactory.getSharedPreferences().getAll().keySet();
List flags = FlagsFactory.getDebugFlags();
flags.sort((f1, f2) -> {
// Sort first by any prefs that the user has changed, then alphabetically.
@@ -102,7 +96,7 @@ public final class FlagTogglerPrefUi {
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.itemView.setOnLongClickListener(v -> {
- mSharedPreferences.edit().remove(flag.key).apply();
+ FlagsFactory.getSharedPreferences().edit().remove(flag.key).apply();
setChecked(getFlagStateFromSharedPrefs(flag));
updateSummary(this, flag);
updateMenu();
@@ -133,7 +127,7 @@ public final class FlagTogglerPrefUi {
private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
String summary = flag.defaultValue == TEAMFOOD
? "[TEAMFOOD] " : "";
- if (mSharedPreferences.contains(flag.key)) {
+ if (FlagsFactory.getSharedPreferences().contains(flag.key)) {
summary += "[OVERRIDDEN] ";
}
if (!TextUtils.isEmpty(summary)) {
@@ -156,7 +150,7 @@ public final class FlagTogglerPrefUi {
public void onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_apply_flags) {
- mSharedPreferences.edit().commit();
+ FlagsFactory.getSharedPreferences().edit().commit();
Log.e(TAG,
"Killing launcher process " + Process.myPid() + " to apply new flag values");
System.exit(0);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java
index d066abe335..a68e753589 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java
@@ -21,7 +21,6 @@ import static android.app.ActivityThread.currentApplication;
import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
import static com.android.launcher3.config.FeatureFlags.FlagState.DISABLED;
import static com.android.launcher3.config.FeatureFlags.FlagState.ENABLED;
-import static com.android.launcher3.config.FeatureFlags.FlagState.TEAMFOOD;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.content.Context;
@@ -30,6 +29,8 @@ import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.config.FeatureFlags.BooleanFlag;
import com.android.launcher3.config.FeatureFlags.FlagState;
import com.android.launcher3.config.FeatureFlags.IntFlag;
@@ -52,10 +53,11 @@ public class FlagsFactory {
private static final FlagsFactory INSTANCE = new FlagsFactory();
private static final boolean FLAG_AUTO_APPLY_ENABLED = true;
- public static final String FLAGS_PREF_NAME = "featureFlags";
+ private static final String FLAGS_PREF_NAME = "featureFlags";
public static final String NAMESPACE_LAUNCHER = "launcher";
private static final List sDebugFlags = new ArrayList<>();
+ private static SharedPreferences sSharedPreferences;
static final BooleanFlag TEAMFOOD_FLAG = getReleaseFlag(
0, "LAUNCHER_TEAMFOOD", DISABLED, "Enable this flag to opt-in all team food flags");
@@ -93,10 +95,8 @@ public class FlagsFactory {
public static BooleanFlag getDebugFlag(
int bugId, String key, FlagState flagState, String description) {
if (IS_DEBUG_DEVICE) {
- SharedPreferences prefs = currentApplication()
- .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
boolean defaultValue = getEnabledValue(flagState);
- boolean currentValue = prefs.getBoolean(key, defaultValue);
+ boolean currentValue = getSharedPreferences().getBoolean(key, defaultValue);
DebugFlag flag = new DebugFlag(key, description, flagState, currentValue);
sDebugFlags.add(flag);
return flag;
@@ -115,9 +115,7 @@ public class FlagsFactory {
boolean defaultValueInCode = getEnabledValue(flagState);
boolean defaultValue = DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValueInCode);
if (IS_DEBUG_DEVICE) {
- SharedPreferences prefs = currentApplication()
- .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
- boolean currentValue = prefs.getBoolean(key, defaultValue);
+ boolean currentValue = getSharedPreferences().getBoolean(key, defaultValue);
DebugFlag flag = new DeviceFlag(key, description, flagState, currentValue,
defaultValueInCode);
sDebugFlags.add(flag);
@@ -145,6 +143,17 @@ public class FlagsFactory {
}
}
+ /** Returns the SharedPreferences instance backing Debug FeatureFlags. */
+ @NonNull
+ static SharedPreferences getSharedPreferences() {
+ if (sSharedPreferences == null) {
+ sSharedPreferences = currentApplication()
+ .createDeviceProtectedStorageContext()
+ .getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+ }
+ return sSharedPreferences;
+ }
+
/**
* Dumps the current flags state to the print writer
*/
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index f1c4f68e28..80f5558315 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -15,11 +15,15 @@
*/
package com.android.launcher3.uioverrides.touchcontrollers;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+
import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
@@ -106,6 +110,7 @@ public class NoButtonQuickSwitchTouchController implements TouchController,
newCancelListener(this::clearState);
private boolean mNoIntercept;
+ private Boolean mIsTrackpadFourFingerSwipe;
private LauncherState mStartState;
private boolean mIsHomeScreenVisible = true;
@@ -131,7 +136,9 @@ public class NoButtonQuickSwitchTouchController implements TouchController,
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ int action = ev.getActionMasked();
+ if (action == ACTION_DOWN) {
+ mIsTrackpadFourFingerSwipe = null;
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
return false;
@@ -140,6 +147,13 @@ public class NoButtonQuickSwitchTouchController implements TouchController,
// Only detect horizontal swipe for intercept, then we will allow swipe up as well.
mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
false /* ignoreSlopWhenSettling */);
+ } else if (isTrackpadMultiFingerSwipe(ev) && mIsTrackpadFourFingerSwipe == null
+ && action == ACTION_MOVE) {
+ mIsTrackpadFourFingerSwipe = isTrackpadFourFingerSwipe(ev);
+ mNoIntercept = !mIsTrackpadFourFingerSwipe;
+ if (mNoIntercept) {
+ return false;
+ }
}
if (mNoIntercept) {
@@ -162,9 +176,6 @@ public class NoButtonQuickSwitchTouchController implements TouchController,
if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
return false;
}
- if (isTrackpadMultiFingerSwipe(ev)) {
- return false;
- }
int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
return false;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index 683f4ea896..395833ffea 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -21,7 +21,6 @@ import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
-import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN;
import android.graphics.PointF;
@@ -106,8 +105,7 @@ public class StatusBarTouchController implements TouchController {
// Currently input dispatcher will not do touch transfer if there are more than
// one touch pointer. Hence, even if slope passed, only set the slippery flag
// when there is single touch event. (context: InputDispatcher.cpp line 1445)
- if (dy > mTouchSlop && dy > Math.abs(dx) && (isTrackpadMotionEvent(ev)
- || ev.getPointerCount() == 1)) {
+ if (dy > mTouchSlop && dy > Math.abs(dx) && ev.getPointerCount() == 1) {
ev.setAction(ACTION_DOWN);
dispatchTouchEvent(ev);
setWindowSlippery(true);
@@ -161,8 +159,7 @@ public class StatusBarTouchController implements TouchController {
} else {
// For NORMAL state, only listen if the event originated above the navbar height
DeviceProfile dp = mLauncher.getDeviceProfile();
- if (!isTrackpadMotionEvent(ev) && ev.getY() > (mLauncher.getDragLayer().getHeight()
- - dp.getInsets().bottom)) {
+ if (ev.getY() > (mLauncher.getDragLayer().getHeight() - dp.getInsets().bottom)) {
return false;
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 97956701ab..d64347f977 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -105,6 +105,7 @@ import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.tracing.InputConsumerProto;
import com.android.launcher3.tracing.SwipeHandlerProto;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.TraceHelper;
@@ -624,7 +625,7 @@ public abstract class AbsSwipeUpHandler,
mActivityInterface);
});
- notifyGestureStartedAsync();
+ notifyGestureStarted();
}
private void onDeferredActivityLaunch() {
@@ -961,7 +962,7 @@ public abstract class AbsSwipeUpHandler,
}
});
}
- notifyGestureStartedAsync();
+ notifyGestureStarted();
setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
if (mIsTransientTaskbar && !mTaskbarAlreadyOpen && !isLikelyToStartNewTask) {
@@ -994,7 +995,7 @@ public abstract class AbsSwipeUpHandler,
* Notifies the launcher that the swipe gesture has started. This can be called multiple times.
*/
@UiThread
- private void notifyGestureStartedAsync() {
+ private void notifyGestureStarted() {
final T curActivity = mActivity;
if (curActivity != null) {
// Once the gesture starts, we can no longer transition home through the button, so
@@ -2063,8 +2064,8 @@ public abstract class AbsSwipeUpHandler,
mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
mRecentsAnimationTargets));
- // Disable scrolling in RecentsView for trackpad gestures.
- if (!mGestureState.isTrackpadGesture()) {
+ // Disable scrolling in RecentsView for trackpad 3-finger swipe up gesture.
+ if (!mGestureState.isThreeFingerTrackpadGesture()) {
mRecentsViewScrollLinked = true;
}
}
@@ -2168,7 +2169,7 @@ public abstract class AbsSwipeUpHandler,
ActiveGestureLog.INSTANCE.addLog("Unexpected task appeared"
+ " id=" + taskInfo.taskId
+ " pkg=" + taskInfo.baseIntent.getComponent().getPackageName());
- finishRecentsAnimationOnTasksAppeared();
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
} else if (handleTaskAppeared(appearedTaskTargets)) {
Optional taskTargetOptional =
Arrays.stream(appearedTaskTargets)
@@ -2176,17 +2177,22 @@ public abstract class AbsSwipeUpHandler,
targetCompat.taskId == mGestureState.getLastStartedTaskId())
.findFirst();
if (!taskTargetOptional.isPresent()) {
- finishRecentsAnimationOnTasksAppeared();
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
RemoteAnimationTarget taskTarget = taskTargetOptional.get();
TaskView taskView = mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
- finishRecentsAnimationOnTasksAppeared();
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
ViewGroup splashView = mActivity.getDragLayer();
+ final QuickstepLauncher quickstepLauncher = mActivity instanceof QuickstepLauncher
+ ? (QuickstepLauncher) mActivity : null;
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController().pauseBlursOnWindows(true);
+ }
// When revealing the app with launcher splash screen, make the app visible
// and behind the splash view before the splash is animated away.
@@ -2194,7 +2200,7 @@ public abstract class AbsSwipeUpHandler,
new SurfaceTransactionApplier(splashView);
SurfaceTransaction transaction = new SurfaceTransaction();
for (RemoteAnimationTarget target : appearedTaskTargets) {
- transaction.forSurface(target.leash).setAlpha(1).setLayer(-1);
+ transaction.forSurface(target.leash).setAlpha(1).setLayer(-1).setShow();
}
surfaceApplier.scheduleApply(transaction);
@@ -2206,16 +2212,25 @@ public abstract class AbsSwipeUpHandler,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- finishRecentsAnimationOnTasksAppeared();
+ // Hiding launcher which shows the app surface behind, then
+ // finishing recents to the app. After transition finish, showing
+ // the views on launcher again, so it can be visible when next
+ // animation starts.
+ splashView.setAlpha(0);
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController()
+ .pauseBlursOnWindows(false);
+ }
+ finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
}
});
}
}
}
- private void finishRecentsAnimationOnTasksAppeared() {
+ private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
if (mRecentsAnimationController != null) {
- mRecentsAnimationController.finish(false /* toRecents */, null /* onFinishComplete */);
+ mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete);
}
ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 2b0623a67d..02f9f57ec2 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,6 +15,9 @@
*/
package com.android.quickstep;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadThreeFingerSwipe;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
@@ -27,6 +30,7 @@ import android.annotation.Nullable;
import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Build;
+import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import com.android.launcher3.statemanager.BaseState;
@@ -139,8 +143,30 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL
private final BaseActivityInterface mActivityInterface;
private final MultiStateCallback mStateCallback;
private final int mGestureId;
- private boolean mIsTrackpadGesture;
+ public enum TrackpadGestureType {
+ NONE,
+ // Assigned before we know whether it's a 3-finger or 4-finger gesture.
+ MULTI_FINGER,
+ THREE_FINGER,
+ FOUR_FINGER;
+
+ public static TrackpadGestureType getTrackpadGestureType(MotionEvent event) {
+ if (!isTrackpadMultiFingerSwipe(event)) {
+ return TrackpadGestureType.NONE;
+ }
+ if (isTrackpadThreeFingerSwipe(event)) {
+ return TrackpadGestureType.THREE_FINGER;
+ }
+ if (isTrackpadFourFingerSwipe(event)) {
+ return TrackpadGestureType.FOUR_FINGER;
+ }
+
+ return TrackpadGestureType.MULTI_FINGER;
+ }
+ }
+
+ private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
private CachedTaskInfo mRunningTask;
private GestureEndTarget mEndTarget;
private RemoteAnimationTarget mLastAppearedTaskTarget;
@@ -249,17 +275,22 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL
}
/**
- * Sets if the gesture is is from the trackpad.
+ * Sets if the gesture is is from the trackpad, if so, whether 3-finger, or 4-finger
*/
- public void setIsTrackpadGesture(boolean isTrackpadGesture) {
- mIsTrackpadGesture = isTrackpadGesture;
+ public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
+ mTrackpadGestureType = trackpadGestureType;
}
- /**
- * @return if the gesture is from the trackpad.
- */
public boolean isTrackpadGesture() {
- return mIsTrackpadGesture;
+ return mTrackpadGestureType != TrackpadGestureType.NONE;
+ }
+
+ public boolean isThreeFingerTrackpadGesture() {
+ return mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+ }
+
+ public boolean isFourFingerTrackpadGesture() {
+ return mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER;
}
/**
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 3979444499..c18ad5a626 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -141,8 +141,8 @@ public class LauncherBackAnimationController {
@Override
public void onBackCancelled() {
handler.post(() -> {
- resetPositionAnimated();
- mProgressAnimator.reset();
+ mProgressAnimator.onBackCancelled(
+ LauncherBackAnimationController.this::resetPositionAnimated);
});
}
@@ -192,7 +192,7 @@ public class LauncherBackAnimationController {
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {}
+ public void onAnimationCancelled() {}
};
SystemUiProxy.INSTANCE.get(mLauncher).setBackToLauncherCallback(mBackCallback, runner);
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index fbe2778fdb..07db194ff4 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -249,7 +249,7 @@ public class OverviewCommandHelper {
}
GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
- false /* isTrackpadGesture */);
+ GestureState.TrackpadGestureType.NONE);
gestureState.setHandlingAtomicEvent(true);
AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
.newHandler(gestureState, cmd.createTime);
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 5391f4d46c..5df4734eee 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -125,6 +125,13 @@ public class QuickstepTestInformationHandler extends TestInformationHandler {
.getTaskbarAllAppsTopPadding());
}
+ case TestProtocol.REQUEST_TASKBAR_APPS_LIST_SCROLL_Y: {
+ return getTISBinderUIProperty(Bundle::putInt, tisBinder ->
+ tisBinder.getTaskbarManager()
+ .getCurrentActivityContext()
+ .getTaskbarAllAppsScroll());
+ }
+
case TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT:
runOnTISBinder(tisBinder -> {
enableBlockingTimeout(tisBinder, true);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 4adfae5ee8..f8e09e1e88 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -150,10 +150,17 @@ public class RecentsAnimationController {
@UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
- if (mFinishRequested) {
- // If finishing, add to pending finish callbacks, otherwise, if finished, adding to the
- // destroyed RunnableList will just trigger the callback to be called immediately
- mPendingFinishCallbacks.add(callback);
+ finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */);
+ }
+
+ @UiThread
+ public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint,
+ boolean forceFinish) {
+ mPendingFinishCallbacks.add(callback);
+ if (!forceFinish && mFinishRequested) {
+ // If finish has already been requested, then add the callback to the pending list.
+ // If already finished, then adding it to the destroyed RunnableList will just
+ // trigger the callback to be called immediately
return;
}
ActiveGestureLog.INSTANCE.addLog(
@@ -165,15 +172,19 @@ public class RecentsAnimationController {
mFinishRequested = true;
mFinishTargetIsLauncher = toRecents;
mOnFinishedListener.accept(this);
- mPendingFinishCallbacks.add(callback);
- UI_HELPER_EXECUTOR.execute(() -> {
+ Runnable finishCb = () -> {
mController.finish(toRecents, sendUserLeaveHint);
InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
InteractionJankMonitorWrapper.end(
InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
MAIN_EXECUTOR.execute(mPendingFinishCallbacks::executeAllAndDestroy);
- });
+ };
+ if (forceFinish) {
+ finishCb.run();
+ } else {
+ UI_HELPER_EXECUTOR.execute(finishCb);
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 9a23557fdf..d3e4ce5db8 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -98,7 +98,7 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener {
private final ArrayList mOnDestroyActions = new ArrayList<>();
- private @SystemUiStateFlags int mSystemUiStateFlags;
+ private @SystemUiStateFlags int mSystemUiStateFlags = QuickStepContract.SYSUI_STATE_AWAKE;
private NavigationMode mMode = THREE_BUTTONS;
private NavBarPosition mNavBarPosition;
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 512d47e6c7..fdb30cedf3 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -967,7 +967,7 @@ public class SystemUiProxy implements ISystemUiProxy {
IRemoteAnimationRunner runner) {
mBackToLauncherCallback = callback;
mBackToLauncherRunner = runner;
- if (mBackAnimation == null) {
+ if (mBackAnimation == null || mBackToLauncherCallback == null) {
return;
}
try {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index c3c11977d3..c8c629233c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -115,7 +115,7 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn
}
}
// But force-finish it anyways
- finishRunningRecentsAnimation(false /* toHome */);
+ finishRunningRecentsAnimation(false /* toHome */, true /* forceFinish */);
if (mCallbacks != null) {
// If mCallbacks still != null, that means we are getting this startRecentsAnimation()
@@ -291,13 +291,26 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn
* Finishes the running recents animation.
*/
public void finishRunningRecentsAnimation(boolean toHome) {
+ finishRunningRecentsAnimation(toHome, false /* forceFinish */);
+ }
+
+ /**
+ * Finishes the running recents animation.
+ * @param forceFinish will synchronously finish the controller
+ */
+ private void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish) {
if (mController != null) {
ActiveGestureLog.INSTANCE.addLog(
/* event= */ "finishRunningRecentsAnimation", toHome);
mCallbacks.notifyAnimationCanceled();
- Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
- ? mController::finishAnimationToHome
- : mController::finishAnimationToApp);
+ if (forceFinish) {
+ mController.finishController(toHome, null, false /* sendUserLeaveHint */,
+ true /* forceFinish */);
+ } else {
+ Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
+ ? mController::finishAnimationToHome
+ : mController::finishAnimationToApp);
+ }
cleanUpRecentsAnimation();
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index fd7b3434bd..813523888d 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -128,19 +128,19 @@ public interface TaskShortcutFactory {
* A menu item, "Save app pair", that allows the user to preserve the current app combination as
* a single persistent icon on the Home screen, allowing for quick split screen initialization.
*/
- class SaveAppPairSystemShortcut extends SystemShortcut {
-
+ class SaveAppPairSystemShortcut extends SystemShortcut {
private final TaskView mTaskView;
- public SaveAppPairSystemShortcut(BaseDraggingActivity target, TaskView taskView) {
- super(R.drawable.ic_save_app_pair, R.string.save_app_pair, target,
+ public SaveAppPairSystemShortcut(BaseDraggingActivity activity, TaskView taskView) {
+ super(R.drawable.ic_save_app_pair, R.string.save_app_pair, activity,
taskView.getItemInfo(), taskView);
mTaskView = taskView;
}
@Override
public void onClick(View view) {
- // TODO (b/274189428): Call "saveAppPair" function in new AppPairController class
+ ((RecentsView) mTarget.getOverviewPanel())
+ .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index f0d0bdb0ed..499a2601a9 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -243,7 +243,17 @@ public final class TaskViewUtils {
TOUCH_RESPONSE_INTERPOLATOR);
out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
TOUCH_RESPONSE_INTERPOLATOR);
-
+ out.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ final SurfaceTransaction showTransaction = new SurfaceTransaction();
+ for (int i = targets.apps.length - 1; i >= 0; --i) {
+ showTransaction.getTransaction().show(targets.apps[i].leash);
+ }
+ applier.scheduleApply(showTransaction);
+ }
+ });
out.addOnFrameCallback(() -> {
for (RemoteTargetHandle handle : remoteTargetHandles) {
handle.getTaskViewSimulator().apply(handle.getTransformParams());
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1cf682bffc..038c6743dd 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,11 +24,11 @@ import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
-import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
@@ -77,11 +77,11 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.StatefulActivity;
@@ -647,7 +647,7 @@ public class TouchInteractionService extends Service
// onConsumerInactive and wipe the previous gesture state
GestureState prevGestureState = new GestureState(mGestureState);
GestureState newGestureState = createGestureState(mGestureState,
- isTrackpadMultiFingerSwipe(event));
+ getTrackpadGestureType(event));
newGestureState.setSwipeUpStartTimeMs(SystemClock.uptimeMillis());
mConsumer.onConsumerAboutToBeSwitched();
mGestureState = newGestureState;
@@ -656,7 +656,7 @@ public class TouchInteractionService extends Service
} else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()
&& mDeviceState.canTriggerAssistantAction(event)) {
mGestureState = createGestureState(mGestureState,
- isTrackpadMultiFingerSwipe(event));
+ getTrackpadGestureType(event));
// Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
// should not interrupt it. QuickSwitch assumes that interruption can only
// happen if the next gesture is also quick switch.
@@ -678,7 +678,9 @@ public class TouchInteractionService extends Service
if (mUncheckedConsumer != InputConsumer.NO_OP) {
switch (event.getActionMasked()) {
- case ACTION_DOWN, ACTION_UP ->
+ case ACTION_DOWN:
+ // fall through
+ case ACTION_UP:
ActiveGestureLog.INSTANCE.addLog(
/* event= */ "onMotionEvent(" + (int) event.getRawX() + ", "
+ (int) event.getRawY() + "): "
@@ -687,15 +689,18 @@ public class TouchInteractionService extends Service
/* gestureEvent= */ event.getActionMasked() == ACTION_DOWN
? MOTION_DOWN
: MOTION_UP);
- case ACTION_MOVE ->
+ break;
+ case ACTION_MOVE:
ActiveGestureLog.INSTANCE.addLog("onMotionEvent: "
+ MotionEvent.actionToString(event.getActionMasked()) + ","
+ MotionEvent.classificationToString(event.getClassification())
+ ", pointerCount: " + event.getPointerCount(), MOTION_MOVE);
- default ->
+ break;
+ default: {
ActiveGestureLog.INSTANCE.addLog("onMotionEvent: "
+ MotionEvent.actionToString(event.getActionMasked()) + ","
+ MotionEvent.classificationToString(event.getClassification()));
+ }
}
}
@@ -708,9 +713,13 @@ public class TouchInteractionService extends Service
event.setAction(ACTION_CANCEL);
}
- // Skip ACTION_POINTER_DOWN and ACTION_POINTER_UP events from trackpad.
- if (!mGestureState.isTrackpadGesture() || (action != ACTION_POINTER_DOWN
- && action != ACTION_POINTER_UP)) {
+ if (mGestureState.isTrackpadGesture() && (action == ACTION_POINTER_DOWN
+ || action == ACTION_POINTER_UP)) {
+ // Skip ACTION_POINTER_DOWN and ACTION_POINTER_UP events from trackpad.
+ if (action == ACTION_POINTER_DOWN) {
+ mGestureState.setTrackpadGestureType(getTrackpadGestureType(event));
+ }
+ } else {
mUncheckedConsumer.onMotionEvent(event);
}
@@ -744,7 +753,7 @@ public class TouchInteractionService extends Service
}
public GestureState createGestureState(GestureState previousGestureState,
- boolean isTrackpadGesture) {
+ GestureState.TrackpadGestureType trackpadGestureType) {
final GestureState gestureState;
TopTaskTracker.CachedTaskInfo taskInfo;
if (mTaskAnimationManager.isRecentsAnimationRunning()) {
@@ -761,7 +770,7 @@ public class TouchInteractionService extends Service
taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false);
gestureState.updateRunningTask(taskInfo);
}
- gestureState.setIsTrackpadGesture(isTrackpadGesture);
+ gestureState.setTrackpadGestureType(trackpadGestureType);
// Log initial state for the gesture.
ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=")
@@ -835,12 +844,18 @@ public class TouchInteractionService extends Service
// If Taskbar is present, we listen for long press to unstash it.
TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
- if (tac != null && canStartSystemGesture) {
- reasonString.append(NEWLINE_PREFIX)
- .append(reasonPrefix)
- .append(SUBSTRING_PREFIX)
- .append("TaskbarActivityContext != null, using TaskbarStashInputConsumer");
- base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, tac);
+ if (tac != null) {
+ // Present always on large screen or on small screen w/ flag
+ DeviceProfile dp = tac.getDeviceProfile();
+ boolean useTaskbarConsumer = dp.isTaskbarPresent && !TaskbarManager.isPhoneMode(dp);
+ if (canStartSystemGesture && useTaskbarConsumer) {
+ reasonString.append(NEWLINE_PREFIX)
+ .append(reasonPrefix)
+ .append(SUBSTRING_PREFIX)
+ .append("TaskbarActivityContext != null, "
+ + "using TaskbarStashInputConsumer");
+ base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, tac);
+ }
}
if (mDeviceState.isBubblesExpanded()) {
@@ -860,7 +875,6 @@ public class TouchInteractionService extends Service
}
if (ENABLE_TRACKPAD_GESTURE.get() && mGestureState.isTrackpadGesture()
- && mGestureState.getActivityInterface().isResumed()
&& !previousGestureState.isRecentsAnimationRunning()) {
reasonString = newCompoundString(reasonPrefix)
.append(SUBSTRING_PREFIX)
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 601533d7e6..2dcbbb907a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -203,8 +203,24 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
}
int edgeFlags = ev.getEdgeFlags();
ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
- // Disable scrolling in RecentsView for trackpad gestures.
- if (!mGestureState.isTrackpadGesture()) {
+
+ if (mGestureState.isTrackpadGesture()) {
+ // Disable scrolling in RecentsView for 3-finger trackpad gesture. We don't know if a
+ // trackpad motion event is 3-finger or 4-finger with the U API until ACTION_MOVE (we
+ // skip ACTION_POINTER_UP events in TouchInteractionService), so in order to make sure
+ // that RecentsView always get a closed sequence of motion events and yet disable
+ // 3-finger scroll, we do the following (1) always dispatch ACTION_DOWN and ACTION_UP
+ // trackpad multi-finger motion events. (2) only dispatch 4-finger ACTION_MOVE motion
+ // events.
+ switch (ev.getActionMasked()) {
+ case ACTION_MOVE -> {
+ if (mGestureState.isFourFingerTrackpadGesture()) {
+ mRecentsViewDispatcher.dispatchEvent(ev);
+ }
+ }
+ default -> mRecentsViewDispatcher.dispatchEvent(ev);
+ }
+ } else {
mRecentsViewDispatcher.dispatchEvent(ev);
}
ev.setEdgeFlags(edgeFlags);
@@ -294,8 +310,14 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
boolean haveNotPassedSlopOnContinuedGesture =
!mPassedSlopOnThisGesture && mPassedPilferInputSlop;
double degrees = Math.toDegrees(Math.atan(upDist / horizontalDist));
- boolean isLikelyToStartNewTask = haveNotPassedSlopOnContinuedGesture
- || degrees <= OVERVIEW_MIN_DEGREES;
+
+ // Regarding degrees >= -OVERVIEW_MIN_DEGREES - Trackpad gestures can start anywhere
+ // on the screen, allowing downward swipes. We want to impose the same angle in that
+ // scenario.
+ boolean swipeWithinQuickSwitchRange = degrees <= OVERVIEW_MIN_DEGREES
+ && (!mGestureState.isTrackpadGesture() || degrees >= -OVERVIEW_MIN_DEGREES);
+ boolean isLikelyToStartNewTask =
+ haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange;
if (!mPassedPilferInputSlop) {
if (passedSlop) {
@@ -306,9 +328,12 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC
// Do not allow quick switch for trackpad 3-finger gestures
// TODO(b/261815244): might need to impose stronger conditions for the swipe
// angle
- boolean noQuickSwitchForTrackpadGesture = mGestureState.isTrackpadGesture()
- && isLikelyToStartNewTask;
- if (isHorizontalSwipeWhenDisabled || noQuickSwitchForTrackpadGesture) {
+ boolean noQuickSwitchForThreeFingerGesture = isLikelyToStartNewTask
+ && mGestureState.isThreeFingerTrackpadGesture();
+ boolean noQuickstepForFourFingerGesture = !isLikelyToStartNewTask
+ && mGestureState.isFourFingerTrackpadGesture();
+ if (isHorizontalSwipeWhenDisabled || noQuickSwitchForThreeFingerGesture
+ || noQuickstepForFourFingerGesture) {
forceCancelGesture(ev);
break;
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java
index f3d2a60b15..898aa8640d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/StatusBarInputConsumer.java
@@ -33,6 +33,7 @@ public class StatusBarInputConsumer extends DelegateInputConsumer {
private final SystemUiProxy mSystemUiProxy;
private final float mTouchSlop;
private final PointF mDown = new PointF();
+ private boolean mHasPassedTouchSlop;
public StatusBarInputConsumer(Context context, InputConsumer delegate,
InputMonitorCompat inputMonitor) {
@@ -53,13 +54,21 @@ public class StatusBarInputConsumer extends DelegateInputConsumer {
mDelegate.onMotionEvent(ev);
switch (ev.getActionMasked()) {
- case ACTION_DOWN -> mDown.set(ev.getX(), ev.getY());
+ case ACTION_DOWN -> {
+ mDown.set(ev.getX(), ev.getY());
+ mHasPassedTouchSlop = false;
+ }
case ACTION_MOVE -> {
- float displacementY = ev.getY() - mDown.y;
- if (displacementY > mTouchSlop) {
- setActive(ev);
- ev.setAction(ACTION_DOWN);
- dispatchTouchEvent(ev);
+ if (!mHasPassedTouchSlop) {
+ float displacementY = ev.getY() - mDown.y;
+ if (Math.abs(displacementY) > mTouchSlop) {
+ mHasPassedTouchSlop = true;
+ if (displacementY > 0) {
+ setActive(ev);
+ ev.setAction(ACTION_DOWN);
+ dispatchTouchEvent(ev);
+ }
+ }
}
}
}
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 79971de0e8..8274a51ff7 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -26,6 +26,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
@@ -63,11 +64,13 @@ import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.util.Executors;
import com.android.quickstep.GestureState;
import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.LottieAnimationColorUtils;
import com.android.quickstep.util.TISBindHelper;
import com.airbnb.lottie.LottieAnimationView;
import java.net.URISyntaxException;
+import java.util.Map;
/**
* A page shows after SUW flow to hint users to swipe up from the bottom of the screen to go home
@@ -82,6 +85,9 @@ public class AllSetActivity extends Activity {
private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight";
private static final String EXTRA_DEVICE_NAME = "suwDeviceName";
+ private static final String LOTTIE_PRIMARY_COLOR_TOKEN = ".primary";
+ private static final String LOTTIE_TERTIARY_COLOR_TOKEN = ".tertiary";
+
private static final float HINT_BOTTOM_FACTOR = 1 - .94f;
private static final int MAX_SWIPE_DURATION = 350;
@@ -114,7 +120,8 @@ public class AllSetActivity extends Activity {
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- int mode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ Resources resources = getResources();
+ int mode = resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES;
Intent intent = getIntent();
int accentColor = intent.getIntExtra(
@@ -126,7 +133,7 @@ public class AllSetActivity extends Activity {
mBackground = new BgDrawable(this);
findViewById(R.id.root_view).setBackground(mBackground);
mContentView = findViewById(R.id.content_view);
- mSwipeUpShift = getResources().getDimension(R.dimen.allset_swipe_up_shift);
+ mSwipeUpShift = resources.getDimension(R.dimen.allset_swipe_up_shift);
TextView subtitle = findViewById(R.id.subtitle);
String suwDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
@@ -188,8 +195,15 @@ public class AllSetActivity extends Activity {
// There's a bug in the currently used external Lottie library (v5.2.0), and it doesn't load
// the correct animation from the raw resources when configuration changes, so we need to
// manually load the resource and pass it to Lottie.
- mAnimatedBackground.setAnimation(getResources().openRawResource(R.raw.all_set_page_bg),
+ mAnimatedBackground.setAnimation(resources.openRawResource(R.raw.all_set_page_bg),
null);
+
+ LottieAnimationColorUtils.updateColors(
+ mAnimatedBackground,
+ Map.of(LOTTIE_PRIMARY_COLOR_TOKEN, R.color.all_set_bg_primary,
+ LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
+ getTheme());
+
startBackgroundAnimation();
}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 6f50e3e4a5..67a0756646 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -233,13 +233,19 @@ abstract class TutorialController implements BackGestureAttemptCallback,
@LayoutRes
protected int getMockHotseatResId() {
- return mTutorialFragment.isLargeScreen()
- ? (mTutorialFragment.isFoldable()
+ if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
+ return mTutorialFragment.isLargeScreen()
+ ? mTutorialFragment.isFoldable()
+ ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat
+ : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat
+ : R.layout.redesigned_gesture_tutorial_mock_hotseat;
+ } else {
+ return mTutorialFragment.isLargeScreen()
+ ? mTutorialFragment.isFoldable()
? R.layout.gesture_tutorial_foldable_mock_hotseat
- : R.layout.gesture_tutorial_tablet_mock_hotseat)
- : (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
- ? R.layout.redesigned_gesture_tutorial_mock_hotseat
- : R.layout.gesture_tutorial_mock_hotseat);
+ : R.layout.gesture_tutorial_tablet_mock_hotseat
+ : R.layout.gesture_tutorial_mock_hotseat;
+ }
}
@LayoutRes
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
new file mode 100644
index 0000000000..cbde257003
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 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.quickstep.util;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.Context;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
+ */
+public class AppPairsController {
+
+ private static final int POINT_THREE_RATIO = 0;
+ private static final int POINT_FIVE_RATIO = 1;
+ private static final int POINT_SEVEN_RATIO = 2;
+ /**
+ * Used to calculate {@link #complement(int)}
+ */
+ private static final int FULL_RATIO = 2;
+
+ private static final int LEFT_TOP = 0;
+ private static final int RIGHT_BOTTOM = 1 << 2;
+
+ // TODO (jeremysim b/274189428): Support saving different ratios in future.
+ public int DEFAULT_RATIO = POINT_FIVE_RATIO;
+
+ private final Context mContext;
+ private final SplitSelectStateController mSplitSelectStateController;
+ public AppPairsController(Context context,
+ SplitSelectStateController splitSelectStateController) {
+ mContext = context;
+ mSplitSelectStateController = splitSelectStateController;
+ }
+
+ /**
+ * Creates a new app pair ItemInfo and adds it to the workspace
+ */
+ public void saveAppPair(TaskView taskView) {
+ TaskView.TaskIdAttributeContainer[] attributes = taskView.getTaskIdAttributeContainers();
+ WorkspaceItemInfo app1 = attributes[0].getItemInfo().clone();
+ WorkspaceItemInfo app2 = attributes[1].getItemInfo().clone();
+ app1.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ app2.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ app1.rank = DEFAULT_RATIO + LEFT_TOP;
+ app2.rank = complement(DEFAULT_RATIO) + RIGHT_BOTTOM;
+ FolderInfo newAppPair = FolderInfo.createAppPair(app1, app2);
+ // TODO (jeremysim b/274189428): Generate default title here.
+ newAppPair.title = "App pair 1";
+
+ IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+ MODEL_EXECUTOR.execute(() -> {
+ newAppPair.contents.forEach(member -> {
+ member.title = "";
+ member.bitmap = iconCache.getDefaultIcon(newAppPair.user);
+ iconCache.getTitleAndIcon(member, member.usingLowResIcon());
+ });
+ MAIN_EXECUTOR.execute(() -> {
+ LauncherAccessibilityDelegate delegate =
+ Launcher.getLauncher(mContext).getAccessibilityDelegate();
+ if (delegate != null) {
+ MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
+ }
+ });
+ });
+
+ }
+
+ /**
+ * Used to calculate the "opposite" side of the split ratio, so we can know how big the split
+ * apps are supposed to be. This math works because POINT_THREE_RATIO is internally represented
+ * by 0, POINT_FIVE_RATIO is represented by 1, and POINT_SEVEN_RATIO is represented by 2. There
+ * are no other supported ratios for now.
+ */
+ private int complement(int ratio1) {
+ int ratio2 = FULL_RATIO - ratio1;
+ return ratio2;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
index cecf58d105..b5c582a3e0 100644
--- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java
+++ b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
@@ -74,6 +74,10 @@ public class BaseDepthController {
// Hints that there is potentially content behind Launcher and that we shouldn't optimize by
// marking the launcher surface as opaque. Only used in certain Launcher states.
private boolean mHasContentBehindLauncher;
+
+ /** Pause applying depth and blur, can be used when something behind the Launcher. */
+ protected boolean mPauseBlurs;
+
/**
* Last blur value, in pixels, that was applied.
* For debugging purposes.
@@ -104,6 +108,13 @@ public class BaseDepthController {
mHasContentBehindLauncher = hasContentBehindLauncher;
}
+ public void pauseBlursOnWindows(boolean pause) {
+ if (pause != mPauseBlurs) {
+ mPauseBlurs = pause;
+ applyDepthAndBlur();
+ }
+ }
+
protected void applyDepthAndBlur() {
float depth = mDepth;
IBinder windowToken = mLauncher.getRootView().getWindowToken();
@@ -121,9 +132,9 @@ public class BaseDepthController {
return;
}
boolean hasOpaqueBg = mLauncher.getScrimView().isFullyOpaque();
- boolean isSurfaceOpaque = !mHasContentBehindLauncher && hasOpaqueBg;
+ boolean isSurfaceOpaque = mPauseBlurs || (!mHasContentBehindLauncher && hasOpaqueBg);
- mCurrentBlur = !mCrossWindowBlursEnabled || hasOpaqueBg
+ mCurrentBlur = !mCrossWindowBlursEnabled || hasOpaqueBg || mPauseBlurs
? 0 : (int) (depth * mMaxBlurRadius);
SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()
.setBackgroundBlurRadius(mSurface, mCurrentBlur)
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
index 1f1c15bdb8..c43fb27f05 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
@@ -16,17 +16,18 @@
package com.android.quickstep.util;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.annotation.ColorInt;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.view.View;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
-import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.Interpolators;
@@ -35,7 +36,9 @@ import com.android.launcher3.anim.Interpolators;
* Utility class for drawing a rounded-rect border around a view.
*
* To use this class:
- * 1. Create an instance in the target view.
+ * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
+ * provided border bounds. If the border will not be visible outside of those bounds, then a
+ * {@link ViewScaleTargetProvider} must be provided in the constructor.
* 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 or call
@@ -56,12 +59,13 @@ public final class BorderAnimator {
@Px private final int mBorderWidthPx;
@Px private final int mBorderRadiusPx;
@NonNull private final Runnable mInvalidateViewCallback;
+ @Nullable private final ViewScaleTargetProvider mViewScaleTargetProvider;
private final long mAppearanceDurationMs;
private final long mDisappearanceDurationMs;
@NonNull private final Interpolator mInterpolator;
@NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private int mAlignmentAdjustment;
+ private float mAlignmentAdjustment;
@Nullable private Animator mRunningBorderAnimation;
@@ -76,6 +80,22 @@ public final class BorderAnimator {
borderRadiusPx,
borderColor,
invalidateViewCallback,
+ /* viewScaleTargetProvider= */ null);
+ }
+
+ public BorderAnimator(
+ @NonNull BorderBoundsBuilder borderBoundsBuilder,
+ int borderWidthPx,
+ int borderRadiusPx,
+ @ColorInt int borderColor,
+ @NonNull Runnable invalidateViewCallback,
+ @Nullable ViewScaleTargetProvider viewScaleTargetProvider) {
+ this(borderBoundsBuilder,
+ borderWidthPx,
+ borderRadiusPx,
+ borderColor,
+ invalidateViewCallback,
+ viewScaleTargetProvider,
DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
DEFAULT_INTERPOLATOR);
@@ -87,6 +107,7 @@ public final class BorderAnimator {
int borderRadiusPx,
@ColorInt int borderColor,
@NonNull Runnable invalidateViewCallback,
+ @Nullable ViewScaleTargetProvider viewScaleTargetProvider,
long appearanceDurationMs,
long disappearanceDurationMs,
@NonNull Interpolator interpolator) {
@@ -94,6 +115,7 @@ public final class BorderAnimator {
mBorderWidthPx = borderWidthPx;
mBorderRadiusPx = borderRadiusPx;
mInvalidateViewCallback = invalidateViewCallback;
+ mViewScaleTargetProvider = viewScaleTargetProvider;
mAppearanceDurationMs = appearanceDurationMs;
mDisappearanceDurationMs = disappearanceDurationMs;
mInterpolator = interpolator;
@@ -106,16 +128,14 @@ public final class BorderAnimator {
private void updateOutline() {
float interpolatedProgress = mInterpolator.getInterpolation(
mBorderAnimationProgress.value);
- mAlignmentAdjustment = (int) Utilities.mapBoundToRange(
- mBorderAnimationProgress.value,
- /* lowerBound= */ 0f,
- /* upperBound= */ 1f,
- /* toMin= */ 0f,
- /* toMax= */ (float) (mBorderWidthPx / 2f),
- mInterpolator);
+ float borderWidth = mBorderWidthPx * interpolatedProgress;
+ // Outset the border by half the width to create an outwards-growth animation
+ mAlignmentAdjustment = (-borderWidth / 2f)
+ // Inset the border if we are scaling the container up
+ + (mViewScaleTargetProvider == null ? 0 : mBorderWidthPx);
mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
- mBorderPaint.setStrokeWidth(Math.round(mBorderWidthPx * interpolatedProgress));
+ mBorderPaint.setStrokeWidth(borderWidth);
mInvalidateViewCallback.run();
}
@@ -126,13 +146,16 @@ public final class BorderAnimator {
* calling super.
*/
public void drawBorder(Canvas canvas) {
+ // Increase the radius if we are scaling the container up
+ float radiusAdjustment = mViewScaleTargetProvider == null
+ ? -mAlignmentAdjustment : mAlignmentAdjustment;
canvas.drawRoundRect(
/* left= */ mBorderBounds.left + mAlignmentAdjustment,
/* top= */ mBorderBounds.top + mAlignmentAdjustment,
/* right= */ mBorderBounds.right - mAlignmentAdjustment,
/* bottom= */ mBorderBounds.bottom - mAlignmentAdjustment,
- /* rx= */ mBorderRadiusPx - mAlignmentAdjustment,
- /* ry= */ mBorderRadiusPx - mAlignmentAdjustment,
+ /* rx= */ mBorderRadiusPx + radiusAdjustment,
+ /* ry= */ mBorderRadiusPx + radiusAdjustment,
/* paint= */ mBorderPaint);
}
@@ -146,22 +169,81 @@ public final class BorderAnimator {
mRunningBorderAnimation.setDuration(
isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
+ mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setViewScales();
+ }
+ });
mRunningBorderAnimation.addListener(
- AnimatorListeners.forEndCallback(() -> mRunningBorderAnimation = null));
+ AnimatorListeners.forEndCallback(() -> {
+ mRunningBorderAnimation = null;
+ if (isAppearing) {
+ return;
+ }
+ resetViewScales();
+ }));
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();
}
+ mBorderBoundsBuilder.updateBorderBounds(mBorderBounds);
+ if (visible) {
+ setViewScales();
+ }
mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
+ if (!visible) {
+ resetViewScales();
+ }
+ }
+
+ private void setViewScales() {
+ if (mViewScaleTargetProvider == null) {
+ return;
+ }
+ View container = mViewScaleTargetProvider.getContainerView();
+ float width = container.getWidth();
+ float height = container.getHeight();
+ // scale up just enough to make room for the border
+ float scaleX = 1f + ((2 * mBorderWidthPx) / width);
+ float scaleY = 1f + ((2 * mBorderWidthPx) / height);
+
+ container.setPivotX(width / 2);
+ container.setPivotY(height / 2);
+ container.setScaleX(scaleX);
+ container.setScaleY(scaleY);
+
+ View contentView = mViewScaleTargetProvider.getContentView();
+ contentView.setPivotX(contentView.getWidth() / 2f);
+ contentView.setPivotY(contentView.getHeight() / 2f);
+ contentView.setScaleX(1f / scaleX);
+ contentView.setScaleY(1f / scaleY);
+ }
+
+ private void resetViewScales() {
+ if (mViewScaleTargetProvider == null) {
+ return;
+ }
+ View container = mViewScaleTargetProvider.getContainerView();
+ container.setPivotX(container.getWidth());
+ container.setPivotY(container.getHeight());
+ container.setScaleX(1f);
+ container.setScaleY(1f);
+
+ View contentView = mViewScaleTargetProvider.getContentView();
+ contentView.setPivotX(contentView.getWidth() / 2f);
+ contentView.setPivotY(contentView.getHeight() / 2f);
+ contentView.setScaleX(1f);
+ contentView.setScaleY(1f);
}
/**
@@ -174,4 +256,25 @@ public final class BorderAnimator {
*/
void updateBorderBounds(Rect rect);
}
+
+ /**
+ * Provider for scaling target views for the beginning and end of this animation.
+ */
+ public interface ViewScaleTargetProvider {
+
+ /**
+ * Returns the content view's container. This view will be scaled up to make room for the
+ * border.
+ */
+ @NonNull
+ View getContainerView();
+
+ /**
+ * Returns the content view. This view will be scaled down reciprocally to the container's
+ * up-scaling to maintain its original size. This should be the view containing all of the
+ * content being surrounded by the border.
+ */
+ @NonNull
+ View getContentView();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/LottieAnimationColorUtils.java b/quickstep/src/com/android/quickstep/util/LottieAnimationColorUtils.java
new file mode 100644
index 0000000000..f98b04bf54
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/LottieAnimationColorUtils.java
@@ -0,0 +1,90 @@
+/*
+ * 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.quickstep.util;
+
+import static com.airbnb.lottie.LottieProperty.COLOR_FILTER;
+
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.model.KeyPath;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/** Utility class for programmatically updating Lottie animation tokenized colors. */
+public final class LottieAnimationColorUtils {
+
+ private LottieAnimationColorUtils() {}
+
+ /**
+ * Updates the given Lottie animation's tokenized colors according to the given mapping.
+ *
+ * Use this method signature only when {@code tokenToColorCodeMap} maps to packed ARBG color
+ * integers.
+ *
+ * @param animationView {@link LottieAnimationView} whose animation's colors need to be updated
+ * @param tokenToColorCodeMap A mapping from the color tokens used in the Lottie file used in
+ * {@code animationView} to packed ARBG color integers.
+ */
+ public static void updateColors(
+ @NonNull LottieAnimationView animationView,
+ @NonNull Map tokenToColorCodeMap) {
+ updateColors(animationView, tokenToColorCodeMap, null);
+ }
+
+ /**
+ * Updates the given Lottie animation's tokenized colors according to the given mapping.
+ *
+ * Use this method signature with a non-null theme only when {@code tokenToColorCodeMap} maps
+ * to color resource references.
+ *
+ * @param animationView {@link LottieAnimationView} whose animation's colors need to be updated
+ * @param tokenToColorCodeMap A mapping from the color tokens used in the Lottie file used in
+ * {@code animationView} to packed ARBG color integers or color
+ * resource references.
+ * @param theme {@link Theme} to be used when resolving color resource references. {@code null}
+ * iff {@code tokenToColorCodeMap} maps to packed ARBG color integers.
+ */
+ public static void updateColors(
+ @NonNull LottieAnimationView animationView,
+ @NonNull Map tokenToColorCodeMap,
+ @Nullable Theme theme) {
+ Resources resources = animationView.getResources();
+ final Map tokenToColorMap = theme == null
+ // tokenToColorCodeMap maps directly to ARBG values
+ ? tokenToColorCodeMap
+ // tokenToColorCodeMap maps to color references, build a mapping to resolved colors
+ : tokenToColorCodeMap.keySet().stream().collect(Collectors.toMap(
+ Function.identity(),
+ token -> resources.getColor(tokenToColorCodeMap.get(token), theme)));
+
+ animationView.addLottieOnCompositionLoadedListener(
+ composition -> tokenToColorMap.forEach(
+ (token, color) -> animationView.addValueCallback(
+ new KeyPath("**", token, "**"),
+ COLOR_FILTER,
+ frameInfo -> new PorterDuffColorFilter(
+ color, PorterDuff.Mode.SRC_ATOP))));
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index d44d7f637d..8b21115f0f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -84,6 +84,7 @@ public class SplitSelectStateController {
private final Handler mHandler;
private final RecentsModel mRecentTasksModel;
private final SplitAnimationController mSplitAnimationController;
+ private final AppPairsController mAppPairsController;
private StatsLogManager mStatsLogManager;
private final SystemUiProxy mSystemUiProxy;
private final StateManager mStateManager;
@@ -97,8 +98,15 @@ public class SplitSelectStateController {
private UserHandle mInitialUser;
private int mInitialTaskId = INVALID_TASK_ID;
/** {@link #mSecondTaskIntent} and {@link #mSecondUser} (the user of the Intent) are set
- * together when split is confirmed with an Intent. */
+ * together when split is confirmed with an Intent. Either this or {@link #mSecondPendingIntent}
+ * will be set, but not both
+ */
private Intent mSecondTaskIntent;
+ /**
+ * Set when split is confirmed via a widget. Either this or {@link #mSecondTaskIntent} will be
+ * set, but not both
+ */
+ private PendingIntent mSecondPendingIntent;
private UserHandle mSecondUser;
private int mSecondTaskId = INVALID_TASK_ID;
private boolean mRecentsAnimationRunning;
@@ -128,6 +136,7 @@ public class SplitSelectStateController {
mDepthController = depthController;
mRecentTasksModel = recentsModel;
mSplitAnimationController = new SplitAnimationController(this);
+ mAppPairsController = new AppPairsController(context, this);
}
/**
@@ -246,6 +255,16 @@ public class SplitSelectStateController {
mSecondUser = user;
}
+ /**
+ * To be called as soon as user selects the second app (even if animations aren't complete)
+ * Sets {@link #mSecondUser} from that of the pendingIntent
+ * @param pendingIntent The second PendingIntent that will be launched.
+ */
+ public void setSecondTask(PendingIntent pendingIntent) {
+ mSecondPendingIntent = pendingIntent;
+ mSecondUser = pendingIntent.getCreatorUserHandle();
+ }
+
/**
* To be called when we want to launch split pairs from an existing GroupedTaskView.
*/
@@ -290,17 +309,18 @@ public class SplitSelectStateController {
if (freezeTaskList) {
options1.setFreezeRecentTasksReordering();
}
+ boolean hasSecondaryPendingIntent = mSecondPendingIntent != null;
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback);
final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
ActivityThread.currentActivityThread().getApplicationThread(),
"LaunchSplitPair");
- if (intent1 == null && intent2 == null) {
+ if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
- } else if (intent2 == null) {
+ } else if (intent2 == null && !hasSecondaryPendingIntent) {
launchIntentOrShortcut(intent1, mInitialUser, options1, taskId2, stagePosition,
splitRatio, remoteTransition, shellInstanceId);
} else if (intent1 == null) {
@@ -310,7 +330,9 @@ public class SplitSelectStateController {
} else {
mSystemUiProxy.startIntents(getPendingIntent(intent1, mInitialUser),
getShortcutInfo(intent1, mInitialUser), options1.toBundle(),
- getPendingIntent(intent2, mSecondUser),
+ hasSecondaryPendingIntent
+ ? mSecondPendingIntent
+ : getPendingIntent(intent2, mSecondUser),
getShortcutInfo(intent2, mSecondUser), null /* options2 */,
stagePosition, splitRatio, remoteTransition, shellInstanceId);
}
@@ -321,11 +343,11 @@ public class SplitSelectStateController {
animationRunner, 300, 150,
ActivityThread.currentActivityThread().getApplicationThread());
- if (intent1 == null && intent2 == null) {
+ if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) {
mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
shellInstanceId);
- } else if (intent2 == null) {
+ } else if (intent2 == null && !hasSecondaryPendingIntent) {
launchIntentOrShortcutLegacy(intent1, mInitialUser, options1, taskId2,
stagePosition, splitRatio, adapter, shellInstanceId);
} else if (intent1 == null) {
@@ -336,7 +358,9 @@ public class SplitSelectStateController {
mSystemUiProxy.startIntentsWithLegacyTransition(
getPendingIntent(intent1, mInitialUser),
getShortcutInfo(intent1, mInitialUser), options1.toBundle(),
- getPendingIntent(intent2, mSecondUser),
+ hasSecondaryPendingIntent
+ ? mSecondPendingIntent
+ : getPendingIntent(intent2, mSecondUser),
getShortcutInfo(intent2, mSecondUser), null /* options2 */, stagePosition,
splitRatio, adapter, shellInstanceId);
}
@@ -374,7 +398,22 @@ public class SplitSelectStateController {
}
}
+ /**
+ * We treat launching by intents as grouped in two ways,
+ * If {@param intent} represents the first app, we always convert the intent to pending intent
+ * It it represents second app, either the second intent OR mSecondPendingIntent will be used
+ * convert second intent to a pendingIntent OR return mSecondPendingIntent as is
+ */
private PendingIntent getPendingIntent(Intent intent, UserHandle user) {
+ boolean isParamFirstIntent = intent != null && intent == mInitialTaskIntent;
+ if (!isParamFirstIntent && mSecondPendingIntent != null) {
+ // Because mSecondPendingIntent and mSecondTaskIntent can't both be set, we know we need
+ // to be using mSecondPendingIntent
+ return mSecondPendingIntent;
+ }
+
+ // intent param must either be mInitialTaskIntent or mSecondTaskIntent, convert either to
+ // a new PendingIntent
return intent == null ? null : (user != null
? PendingIntent.getActivityAsUser(mContext, 0, intent,
FLAG_MUTABLE | FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, null /* options */, user)
@@ -517,7 +556,7 @@ public class SplitSelectStateController {
}
@Override
- public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ public void onAnimationCancelled() {
postAsyncCallback(mHandler, () -> {
if (mSuccessCallback != null) {
// Launching legacy tasks while recents animation is running will always cause
@@ -546,6 +585,7 @@ public class SplitSelectStateController {
mSplitEvent = null;
mAnimateCurrentTaskDismissal = false;
mDismissingFromSplitPair = false;
+ mSecondPendingIntent = null;
}
/**
@@ -577,7 +617,8 @@ public class SplitSelectStateController {
}
private boolean isSecondTaskIntentSet() {
- return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
+ return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null
+ || mSecondPendingIntent != null);
}
public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
@@ -587,4 +628,8 @@ public class SplitSelectStateController {
public FloatingTaskView getFirstFloatingTaskView() {
return mFirstFloatingTaskView;
}
+
+ public AppPairsController getAppPairsController() {
+ return mAppPairsController;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index dd10c2da5d..148a45a386 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -21,9 +21,15 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_WORKSP
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.view.View;
@@ -55,14 +61,38 @@ public class SplitToWorkspaceController {
R.dimen.multi_window_task_divider_size) / 2;
}
+ /**
+ * Handles widget selection from staged split.
+ * @param view Original widget view
+ * @param pendingIntent Provided by widget via InteractionHandler
+ * @return {@code true} if we can attempt launch the widget into split, {@code false} otherwise
+ * to allow launcher to handle the click
+ */
+ public boolean handleSecondWidgetSelectionForSplit(View view, PendingIntent pendingIntent) {
+ if (shouldIgnoreSecondSplitLaunch()) {
+ return false;
+ }
+
+ // Convert original widgetView into bitmap to use for animation
+ // TODO(b/276361926) get the icon for this widget via PackageManager?
+ int width = view.getWidth();
+ int height = view.getHeight();
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+
+ mController.setSecondTask(pendingIntent);
+
+ startWorkspaceAnimation(view, bitmap, null /*icon*/);
+ return true;
+ }
+
/**
* Handles second app selection from stage split. If the item can't be opened in split or
* it's not in stage split state, we pass it onto Launcher's default item click handler.
*/
public boolean handleSecondAppSelectionForSplit(View view) {
- if ((!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
- && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get())
- || !mController.isSplitSelectActive()) {
+ if (shouldIgnoreSecondSplitLaunch()) {
return false;
}
Object tag = view.getTag();
@@ -86,6 +116,12 @@ public class SplitToWorkspaceController {
mController.setSecondTask(intent, user);
+ startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
+ return true;
+ }
+
+ private void startWorkspaceAnimation(@NonNull View view, @Nullable Bitmap bitmap,
+ @Nullable Drawable icon) {
boolean isTablet = mLauncher.getDeviceProfile().isTablet;
SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet);
PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration());
@@ -107,8 +143,7 @@ public class SplitToWorkspaceController {
false /* fadeWithThumbnail */, true /* isStagedTask */);
FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher,
- view, null /* thumbnail */, bitmapInfo.newIcon(mLauncher),
- secondTaskStartingBounds);
+ view, bitmap, icon, secondTaskStartingBounds);
secondFloatingTaskView.setAlpha(1);
secondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
@@ -138,6 +173,11 @@ public class SplitToWorkspaceController {
}
});
pendingAnimation.buildAnim().start();
- return true;
+ }
+
+ private boolean shouldIgnoreSecondSplitLaunch() {
+ return (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
+ && !ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get())
+ || !mController.isSplitSelectActive();
}
}
diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
index 7ab285dfa7..441f88d115 100644
--- a/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
+++ b/quickstep/src/com/android/quickstep/util/SurfaceTransaction.java
@@ -106,6 +106,15 @@ public class SurfaceTransaction {
mTransaction.setShadowRadius(mSurface, radius);
return this;
}
+
+ /**
+ * Requests to show the given surface.
+ * @return this Builder
+ */
+ public SurfaceProperties setShow() {
+ mTransaction.show(mSurface);
+ return this;
+ }
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index cf6ee2d2e5..f0afa69983 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1192,6 +1192,17 @@ public abstract class RecentsView= 0; --i) {
+ showTransaction.getTransaction().show(apps[i].leash);
+ }
+ surfaceApplier.scheduleApply(showTransaction);
+ }
+ });
anim.play(appAnimator);
anim.addListener(new AnimatorListenerAdapter() {
@Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index 428bd95159..8ee0fbb671 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -77,10 +77,10 @@ class TaskMenuViewWithArrow : ArrowPopup {
shouldScaleArrow = true
mIsArrowRotated = true
// This synchronizes the arrow and menu to open at the same time
- OPEN_CHILD_FADE_START_DELAY = OPEN_FADE_START_DELAY
- OPEN_CHILD_FADE_DURATION = OPEN_FADE_DURATION
- CLOSE_FADE_START_DELAY = CLOSE_CHILD_FADE_START_DELAY
- CLOSE_FADE_DURATION = CLOSE_CHILD_FADE_DURATION
+ mOpenChildFadeStartDelay = mOpenFadeStartDelay
+ mOpenChildFadeDuration = mOpenFadeDuration
+ mCloseFadeStartDelay = mCloseChildFadeStartDelay
+ mCloseFadeDuration = mCloseChildFadeDuration
}
private var alignedOptionIndex: Int = 0
@@ -213,7 +213,7 @@ class TaskMenuViewWithArrow : ArrowPopup {
scrim?.let {
anim.play(
ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha)
- .setDuration(OPEN_DURATION.toLong())
+ .setDuration(mOpenDuration.toLong())
)
}
}
@@ -222,7 +222,7 @@ class TaskMenuViewWithArrow : ArrowPopup {
scrim?.let {
anim.play(
ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f)
- .setDuration(CLOSE_DURATION.toLong())
+ .setDuration(mCloseDuration.toLong())
)
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 42589ced5d..53660b5040 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -581,6 +581,7 @@ public class TaskView extends FrameLayout implements Reusable {
mIconView, STAGE_POSITION_UNDEFINED);
mSnapshotView.bind(task);
setOrientationState(orientedState);
+ mDigitalWellBeingToast.initialize(mTask);
}
/**
@@ -984,10 +985,7 @@ public class TaskView extends FrameLayout implements Reusable {
}
if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
mIconLoadRequest = iconCache.updateIconInBackground(mTask,
- (task) -> {
- setIcon(mIconView, task.icon);
- mDigitalWellBeingToast.initialize(mTask);
- });
+ (task) -> setIcon(mIconView, task.icon));
}
} else {
if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
index 3bf4ad38c3..20466ad844 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
@@ -53,6 +53,7 @@ abstract class TaskbarBaseTestCase {
@Mock lateinit var taskbarOverlayController: TaskbarOverlayController
@Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
@Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
+ @Mock lateinit var taskbarPinningController: TaskbarDividerPopupController
lateinit var taskbarControllers: TaskbarControllers
@@ -91,7 +92,8 @@ abstract class TaskbarBaseTestCase {
taskbarSpringOnStashController,
taskbarRecentAppsController,
taskbarEduTooltipController,
- keyboardQuickSwitchController
+ keyboardQuickSwitchController,
+ taskbarPinningController,
)
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index 735c5e6fd4..624347131b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -29,6 +29,8 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.tapl.Taskbar;
import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
@@ -49,11 +51,17 @@ public class TaplTestsTaskbar extends AbstractQuickStepTest {
private static final String CALCULATOR_APP_PACKAGE =
resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+ private AutoCloseable mLauncherLayout;
+
@Override
public void setUp() throws Exception {
Assume.assumeTrue(mLauncher.isTablet());
super.setUp();
- mLauncher.useTestWorkspaceLayoutOnReload();
+
+ LauncherLayoutBuilder layoutBuilder = new LauncherLayoutBuilder().atHotseat(0).putApp(
+ "com.google.android.apps.nexuslauncher.tests",
+ "com.android.launcher3.testcomponent.BaseTestingActivity");
+ mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder);
TaplTestsLauncher3.initialize(this);
startAppFast(CALCULATOR_APP_PACKAGE);
@@ -62,9 +70,11 @@ public class TaplTestsTaskbar extends AbstractQuickStepTest {
}
@After
- public void tearDown() {
- mLauncher.useDefaultWorkspaceLayoutOnReload();
+ public void tearDown() throws Exception {
mLauncher.enableBlockTimeout(false);
+ if (mLauncherLayout != null) {
+ mLauncherLayout.close();
+ }
}
@Test
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 512df8e3d0..acfd54c4d5 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -18,6 +18,7 @@
package com.android.quickstep.util
import android.app.ActivityManager
+import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -32,6 +33,8 @@ import com.android.launcher3.statehandlers.DepthController
import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
+import com.android.launcher3.util.mock
import com.android.launcher3.util.withArgCaptor
import com.android.quickstep.RecentsModel
import com.android.quickstep.SystemUiProxy
@@ -59,6 +62,7 @@ class SplitSelectStateControllerTest {
@Mock lateinit var handler: Handler
@Mock lateinit var context: Context
@Mock lateinit var recentsModel: RecentsModel
+ @Mock lateinit var pendingIntent: PendingIntent
lateinit var splitSelectStateController: SplitSelectStateController
@@ -348,6 +352,14 @@ class SplitSelectStateControllerTest {
assertFalse(splitSelectStateController.isSplitSelectActive)
}
+ @Test
+ fun secondPendingIntentSet() {
+ val itemInfo = ItemInfo()
+ splitSelectStateController.setInitialTaskSelect(null, 0, itemInfo, null, 1)
+ splitSelectStateController.setSecondTask(pendingIntent)
+ assertTrue(splitSelectStateController.isBothSplitAppsConfirmed)
+ }
+
// Generate GroupTask with default userId.
private fun generateGroupTask(
task1ComponentName: ComponentName,
diff --git a/res/color/app_title_text_dark.xml b/res/color-night-v31/widget_picker_primary_surface_color_dark.xml
similarity index 84%
rename from res/color/app_title_text_dark.xml
rename to res/color-night-v31/widget_picker_primary_surface_color_dark.xml
index 220d10fc8f..13ceaa09e7 100644
--- a/res/color/app_title_text_dark.xml
+++ b/res/color-night-v31/widget_picker_primary_surface_color_dark.xml
@@ -13,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-
-
+
+
diff --git a/res/color-night-v31/widget_picker_secondary_surface_color_dark.xml b/res/color-night-v31/widget_picker_secondary_surface_color_dark.xml
new file mode 100644
index 0000000000..89e9f81b99
--- /dev/null
+++ b/res/color-night-v31/widget_picker_secondary_surface_color_dark.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/res/color-night-v31/widget_picker_tab_background_unselected_dark.xml b/res/color-night-v31/widget_picker_tab_background_unselected_dark.xml
new file mode 100644
index 0000000000..ddd7b0a683
--- /dev/null
+++ b/res/color-night-v31/widget_picker_tab_background_unselected_dark.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/res/color-v31/widget_picker_primary_surface_color_light.xml b/res/color-v31/widget_picker_primary_surface_color_light.xml
new file mode 100644
index 0000000000..02dffdddf7
--- /dev/null
+++ b/res/color-v31/widget_picker_primary_surface_color_light.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/res/color-v31/widget_picker_secondary_surface_color_light.xml b/res/color-v31/widget_picker_secondary_surface_color_light.xml
new file mode 100644
index 0000000000..cd052681e9
--- /dev/null
+++ b/res/color-v31/widget_picker_secondary_surface_color_light.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/res/color/widget_picker_tab_text.xml b/res/color/widget_picker_tab_text.xml
new file mode 100644
index 0000000000..0e0fc38316
--- /dev/null
+++ b/res/color/widget_picker_tab_text.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/color/widgets_picker_scrim.xml b/res/color/widgets_picker_scrim.xml
index 5d5130086a..1cf97f61f7 100644
--- a/res/color/widgets_picker_scrim.xml
+++ b/res/color/widgets_picker_scrim.xml
@@ -18,5 +18,5 @@
*/
-->
-
+
diff --git a/res/drawable/bg_widgets_content.xml b/res/drawable/bg_widgets_content.xml
index b0b699ba22..1528a9787a 100644
--- a/res/drawable/bg_widgets_content.xml
+++ b/res/drawable/bg_widgets_content.xml
@@ -24,7 +24,7 @@
-
-
+
@@ -32,7 +32,7 @@
-
-
+
@@ -42,7 +42,7 @@
-
-
+
@@ -52,7 +52,7 @@
-
-
+
diff --git a/res/drawable/bg_widgets_full_sheet.xml b/res/drawable/bg_widgets_full_sheet.xml
index dfcd354ce7..66a1d40507 100644
--- a/res/drawable/bg_widgets_full_sheet.xml
+++ b/res/drawable/bg_widgets_full_sheet.xml
@@ -16,7 +16,7 @@
-
+
diff --git a/res/drawable/bg_widgets_header_states.xml b/res/drawable/bg_widgets_header_states.xml
index f45a7ababf..d03778d0c6 100644
--- a/res/drawable/bg_widgets_header_states.xml
+++ b/res/drawable/bg_widgets_header_states.xml
@@ -24,7 +24,7 @@
-
-
+
-
-
+
-
-
+
-
-
+
-
-
+
-
-
+
-
+
diff --git a/res/drawable/bg_widgets_header_two_pane.xml b/res/drawable/bg_widgets_header_two_pane.xml
index e1408cc235..ca3feef1f0 100644
--- a/res/drawable/bg_widgets_header_two_pane.xml
+++ b/res/drawable/bg_widgets_header_two_pane.xml
@@ -20,7 +20,7 @@
android:paddingTop="@dimen/widget_list_header_view_vertical_padding"
android:paddingBottom="@dimen/widget_list_header_view_vertical_padding" >
-
+ android:drawable="@drawable/bg_widgets_header_states_two_pane" />
+
diff --git a/res/drawable/bg_widgets_searchbox.xml b/res/drawable/bg_widgets_searchbox.xml
index dc6d868bd7..b09f0cd1b6 100644
--- a/res/drawable/bg_widgets_searchbox.xml
+++ b/res/drawable/bg_widgets_searchbox.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
-
+
\ No newline at end of file
diff --git a/res/drawable/bottom_rounded_popup_ripple.xml b/res/drawable/bottom_rounded_popup_ripple.xml
new file mode 100644
index 0000000000..739833a47c
--- /dev/null
+++ b/res/drawable/bottom_rounded_popup_ripple.xml
@@ -0,0 +1,27 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/drop_target_frame.xml b/res/drawable/drop_target_frame.xml
index 9f04103510..0a73b3f05a 100644
--- a/res/drawable/drop_target_frame.xml
+++ b/res/drawable/drop_target_frame.xml
@@ -17,6 +17,6 @@
-
+
\ No newline at end of file
diff --git a/res/drawable/drop_target_frame_hover.xml b/res/drawable/drop_target_frame_hover.xml
index ddf3a4d799..6acffd1542 100644
--- a/res/drawable/drop_target_frame_hover.xml
+++ b/res/drawable/drop_target_frame_hover.xml
@@ -17,5 +17,5 @@
-
+
\ No newline at end of file
diff --git a/res/drawable/ic_allapps_search.xml b/res/drawable/ic_allapps_search.xml
index dbed824358..0c3ab78575 100644
--- a/res/drawable/ic_allapps_search.xml
+++ b/res/drawable/ic_allapps_search.xml
@@ -20,6 +20,6 @@
android:viewportWidth="24.0"
android:autoMirrored="true">
diff --git a/res/drawable/ic_note_taking_widget_category.xml b/res/drawable/ic_note_taking_widget_category.xml
new file mode 100644
index 0000000000..2b5915736f
--- /dev/null
+++ b/res/drawable/ic_note_taking_widget_category.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/ic_touch.xml b/res/drawable/ic_touch.xml
new file mode 100644
index 0000000000..ea0e05c547
--- /dev/null
+++ b/res/drawable/ic_touch.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/res/drawable/ic_visibility.xml b/res/drawable/ic_visibility.xml
new file mode 100644
index 0000000000..864de2ec18
--- /dev/null
+++ b/res/drawable/ic_visibility.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/res/drawable/taskbar_divider_bg.xml b/res/drawable/taskbar_divider_bg.xml
new file mode 100644
index 0000000000..a8c2ae7032
--- /dev/null
+++ b/res/drawable/taskbar_divider_bg.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/res/drawable/top_rounded_popup_ripple.xml b/res/drawable/top_rounded_popup_ripple.xml
new file mode 100644
index 0000000000..7468480192
--- /dev/null
+++ b/res/drawable/top_rounded_popup_ripple.xml
@@ -0,0 +1,27 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/widget_picker_collapse_handle.xml b/res/drawable/widget_picker_collapse_handle.xml
new file mode 100644
index 0000000000..f79663d00e
--- /dev/null
+++ b/res/drawable/widget_picker_collapse_handle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/res/drawable/widget_picker_tabs_background.xml b/res/drawable/widget_picker_tabs_background.xml
new file mode 100644
index 0000000000..a874dd8b90
--- /dev/null
+++ b/res/drawable/widget_picker_tabs_background.xml
@@ -0,0 +1,48 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/widget_suggestions.xml b/res/drawable/widget_suggestions.xml
index b090a68a43..61e42434ba 100644
--- a/res/drawable/widget_suggestions.xml
+++ b/res/drawable/widget_suggestions.xml
@@ -18,7 +18,7 @@ Copyright (C) 2023 The Android Open Source Project
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
- android:tint="@color/widget_picker_background_selected"
+ android:tint="?attr/widgetPickerSuggestionsIconColor"
android:gravity="center"
>
-
+
-
-
+
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index e31bf7affe..9ec346a261 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -34,7 +34,7 @@
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_margin"
android:layout_centerHorizontal="true"
- android:background="@drawable/bg_rounded_corner_bottom_sheet_handle"/>
+ android:background="@drawable/widget_picker_collapse_handle"/>
@@ -89,7 +89,7 @@
android:gravity="center_horizontal"
android:orientation="horizontal"
android:paddingVertical="8dp"
- android:background="?android:attr/colorBackground"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
style="@style/TextHeadline"
launcher:layout_sticky="true">
@@ -100,9 +100,9 @@
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
- android:background="@drawable/all_apps_tabs_background"
+ android:background="@drawable/widget_picker_tabs_background"
android:text="@string/widgets_full_sheet_personal_tab"
- android:textColor="@color/all_apps_tab_text"
+ android:textColor="@color/widget_picker_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
@@ -113,9 +113,9 @@
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
- android:background="@drawable/all_apps_tabs_background"
+ android:background="@drawable/widget_picker_tabs_background"
android:text="@string/widgets_full_sheet_work_tab"
- android:textColor="@color/all_apps_tab_text"
+ android:textColor="@color/widget_picker_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 887f00cbd0..25bbad41fa 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -41,7 +41,7 @@
android:gravity="center_horizontal"
android:textSize="24sp"
android:layout_marginTop="24dp"
- android:textColor="?android:attr/textColorSecondary"
+ android:textColor="?attr/widgetPickerTitleColor"
android:text="@string/widget_button_text"/>
diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml
index 6d44865ef0..a615c5e025 100644
--- a/res/layout/widgets_search_bar.xml
+++ b/res/layout/widgets_search_bar.xml
@@ -25,7 +25,7 @@
android:imeOptions="actionSearch"
android:importantForAutofill="no"
android:textColor="?android:attr/textColorPrimary"
- android:textColorHint="?android:attr/textColorSecondary"/>
+ android:textColorHint="?attr/widgetPickerSearchTextColor"/>
+ android:background="@drawable/widget_picker_collapse_handle"/>
@@ -87,7 +87,7 @@
android:orientation="horizontal"
android:paddingVertical="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
- android:background="?android:attr/colorBackground"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
style="@style/TextHeadline"
launcher:layout_sticky="true">
@@ -98,9 +98,9 @@
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
- android:background="@drawable/all_apps_tabs_background"
+ android:background="@drawable/widget_picker_tabs_background"
android:text="@string/widgets_full_sheet_personal_tab"
- android:textColor="@color/all_apps_tab_text"
+ android:textColor="@color/widget_picker_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
@@ -111,9 +111,9 @@
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
- android:background="@drawable/all_apps_tabs_background"
+ android:background="@drawable/widget_picker_tabs_background"
android:text="@string/widgets_full_sheet_work_tab"
- android:textColor="@color/all_apps_tab_text"
+ android:textColor="@color/widget_picker_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index 8f2a25e409..f8d72e89d4 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -43,7 +43,7 @@
android:id="@+id/search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?android:attr/colorBackground"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
android:clipToPadding="false"
android:elevation="0.1dp"
android:paddingBottom="8dp"
@@ -61,7 +61,7 @@
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
android:paddingBottom="16dp"
android:orientation="horizontal"
- android:background="?android:attr/colorBackground"
+ android:background="?attr/widgetPickerPrimarySurfaceColor"
launcher:layout_sticky="true">