Merge "Refactor OverviewComponentObserver for connected displays" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index cc746eb..c564594 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -636,3 +636,20 @@
description: "Enables gesture navigation handling on connected displays"
bug: "382130680"
}
+
+flag {
+ name: "enable_taskbar_behind_shade"
+ namespace: "lse_desktop_experience"
+ description: "Keeps taskbar behind notification shade when its pulled down"
+ bug: "343194358"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_scalability_for_desktop_experience"
+ namespace: "launcher"
+ description: "Enable more grid scale options on the launcher for desktop experience"
+ bug: "375491272"
+}
diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml
new file mode 100644
index 0000000..1592055
--- /dev/null
+++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@color/materialColorPrimary"
+ android:alpha="0.38"/>
+ <item android:color="@color/materialColorPrimary"/>
+</selector>
\ No newline at end of file
diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml
new file mode 100644
index 0000000..051c18f
--- /dev/null
+++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true"
+ android:state_pressed="true"
+ android:color="@color/materialColorOnPrimary"
+ android:alpha="0.15"/>
+ <item android:state_enabled="true"
+ android:state_hovered="true"
+ android:color="@color/materialColorOnPrimary"
+ android:alpha="0.11" />
+ <item android:color="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml
new file mode 100644
index 0000000..74df84b
--- /dev/null
+++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@color/materialColorOnPrimary"
+ android:alpha="0.38"/>
+ <item android:color="@color/materialColorOnPrimary"/>
+</selector>
diff --git a/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml b/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml
new file mode 100644
index 0000000..7067f13
--- /dev/null
+++ b/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/keyboard_quick_switch_scroll_button_bg" />
+ <corners android:radius="@dimen/keyboard_quick_switch_scroll_button_corner_radius" />
+</shape>
diff --git a/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml b/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml
new file mode 100644
index 0000000..dd63f54
--- /dev/null
+++ b/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <solid android:color="@color/keyboard_quick_switch_scroll_button_fg" />
+ <corners android:radius="@dimen/keyboard_quick_switch_scroll_button_corner_radius" />
+</shape>
diff --git a/quickstep/res/drawable/ic_chevron_end.xml b/quickstep/res/drawable/ic_chevron_end.xml
new file mode 100644
index 0000000..9ca4f3a
--- /dev/null
+++ b/quickstep/res/drawable/ic_chevron_end.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z" />
+</vector>
diff --git a/quickstep/res/drawable/ic_chevron_start.xml b/quickstep/res/drawable/ic_chevron_start.xml
new file mode 100644
index 0000000..913da02
--- /dev/null
+++ b/quickstep/res/drawable/ic_chevron_start.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M560,720L320,480L560,240L616,296L432,480L616,664L560,720Z" />
+</vector>
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 345b97c..885bdb9 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -67,6 +67,24 @@
</androidx.constraintlayout.widget.ConstraintLayout>
+ <ImageButton
+ android:id="@+id/scroll_button_start"
+ android:src="@drawable/ic_chevron_start"
+ android:contentDescription="@string/quick_switch_scroll_arrow_left"
+ android:background="@drawable/bg_keyboard_quick_switch_scroll_button"
+ android:foreground="@drawable/fg_keyboard_quick_switch_scroll_button"
+ android:tint="@color/keyboard_quick_switch_scroll_button_icon"
+ android:layout_width="@dimen/keyboard_quick_switch_scroll_button_width"
+ android:layout_height="@dimen/keyboard_quick_switch_scroll_button_height"
+ android:paddingHorizontal="@dimen/keyboard_quick_switch_scroll_button_horizontal_padding"
+ android:paddingVertical="@dimen/keyboard_quick_switch_scroll_button_vertical_padding"
+ android:layout_marginStart="@dimen/keyboard_quick_switch_view_spacing"
+ android:visibility="gone"
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
<HorizontalScrollView
android:id="@+id/scroll_view"
android:layout_width="wrap_content"
@@ -76,9 +94,12 @@
android:alpha="0"
android:visibility="gone"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constrainedWidth="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent">
+ app:layout_constraintStart_toEndOf="@id/scroll_button_start"
+ app:layout_constraintEnd_toStartOf="@id/scroll_button_end">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
@@ -89,4 +110,22 @@
</HorizontalScrollView>
+ <ImageButton
+ android:id="@+id/scroll_button_end"
+ android:src="@drawable/ic_chevron_end"
+ android:contentDescription="@string/quick_switch_scroll_arrow_right"
+ android:background="@drawable/bg_keyboard_quick_switch_scroll_button"
+ android:foreground="@drawable/fg_keyboard_quick_switch_scroll_button"
+ android:tint="@color/keyboard_quick_switch_scroll_button_icon"
+ android:layout_width="@dimen/keyboard_quick_switch_scroll_button_width"
+ android:layout_height="@dimen/keyboard_quick_switch_scroll_button_height"
+ android:paddingHorizontal="@dimen/keyboard_quick_switch_scroll_button_horizontal_padding"
+ android:paddingVertical="@dimen/keyboard_quick_switch_scroll_button_vertical_padding"
+ android:layout_marginEnd="@dimen/keyboard_quick_switch_view_spacing"
+ android:visibility="gone"
+
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
</com.android.launcher3.taskbar.KeyboardQuickSwitchView>
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index d699cdf..e69fa4d 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -56,6 +56,7 @@
<!-- Accessibility actions -->
<item type="id" name="action_move_to_top_or_left" />
<item type="id" name="action_move_to_bottom_or_right" />
+ <item type="id" name="action_create_application_bubble" />
<!-- The max scale for the wallpaper when it's zoomed in -->
<item name="config_wallpaperMaxScale" format="float" type="dimen">
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 52ebdae..6196be4 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -530,6 +530,11 @@
<dimen name="keyboard_quick_switch_text_button_radius">360dp</dimen>
<dimen name="keyboard_quick_switch_text_button_horizontal_padding">16dp</dimen>
<dimen name="keyboard_quick_switch_text_button_fade_edge_length">20dp</dimen>
+ <dimen name="keyboard_quick_switch_scroll_button_width">36dp</dimen>
+ <dimen name="keyboard_quick_switch_scroll_button_height">56dp</dimen>
+ <dimen name="keyboard_quick_switch_scroll_button_horizontal_padding">12dp</dimen>
+ <dimen name="keyboard_quick_switch_scroll_button_vertical_padding">32dp</dimen>
+ <dimen name="keyboard_quick_switch_scroll_button_corner_radius">18dp</dimen>
<!-- Digital Wellbeing -->
<dimen name="digital_wellbeing_toast_height">48dp</dimen>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 8e70a2b..65f4b3c 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -322,6 +322,8 @@
<string name="move_drop_target_top_or_left">Move to top/left</string>
<!-- Label for moving drop target to the bottom or right side of the screen, depending on orientation (from the Taskbar only). -->
<string name="move_drop_target_bottom_or_right">Move to bottom/right</string>
+ <!-- Label for creating an application bubble (from the Taskbar only). -->
+ <string name="open_app_as_a_bubble">Open app as a bubble</string>
<!-- Label for quick switch tile showing how many more apps are available. The number will be displayed above this text. [CHAR LIMIT=NONE] -->
<string name="quick_switch_overflow">{count, plural,
@@ -335,6 +337,13 @@
<!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
<string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
+ <!-- Accessibility label for an arrow button within quick switch UI that scrolls the quick switch content left
+ TODO(b/397975686): Make these translatable when verified by UX. -->
+ <string name="quick_switch_scroll_arrow_left" translatable="false">Scroll left</string>
+ <!-- Accessibility label for an arrow button within quick switch UI that scrolls the quick switch content right
+ TODO(b/397975686): Make these translatable when verified by UX. -->
+ <string name="quick_switch_scroll_arrow_right" translatable="false">Scroll right</string>
+
<!-- Strings for bubble bar -->
<!-- Fallback name for a bubble if it does have a title [CHAR_LIMIT=none] -->
<string name="bubble_bar_bubble_fallback_description">Bubble</string>
@@ -366,4 +375,9 @@
<string name="header_default_app_title">App title</string>
<!-- Content description for the header close button. [CHAR LIMIT=NONE] -->
<string name="header_close_icon_description">Close button</string>
+
+ <!-- Label for pinning an item to the taskbar. [CHAR_LIMIT=20] -->
+ <string name="pin_to_taskbar">Pin to taskbar</string>
+ <!-- Label for unpinning an item from the taskbar. [CHAR_LIMIT=20] -->
+ <string name="unpin_from_taskbar">Unpin from taskbar</string>
</resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 4b4d68d..336ef48 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -30,10 +30,12 @@
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.HorizontalScrollView;
+import android.widget.ImageButton;
import android.widget.TextView;
import android.window.OnBackInvokedDispatcher;
import android.window.WindowOnBackInvokedDispatcher;
@@ -45,6 +47,7 @@
import com.android.app.animation.Interpolators;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
@@ -102,8 +105,12 @@
private HorizontalScrollView mScrollView;
private ConstraintLayout mContent;
- private int mTaskViewWidth;
- private int mTaskViewHeight;
+ private boolean mSupportsScrollArrows = false;
+ private ImageButton mStartScrollArrow;
+ private ImageButton mEndScrollArrow;
+
+ private int mTaskViewBorderWidth;
+ private int mTaskViewRadius;
private int mSpacing;
private int mSmallSpacing;
private int mOutlineRadius;
@@ -112,11 +119,13 @@
private int mOverviewTaskIndex = -1;
private int mDesktopTaskIndex = -1;
- @Nullable private AnimatorSet mOpenAnimation;
+ @Nullable
+ private AnimatorSet mOpenAnimation;
private boolean mIsBackCallbackRegistered = false;
- @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
+ @Nullable
+ private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
public KeyboardQuickSwitchView(@NonNull Context context) {
this(context, null);
@@ -152,18 +161,35 @@
mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane);
mScrollView = findViewById(R.id.scroll_view);
mContent = findViewById(R.id.content);
+ mStartScrollArrow = findViewById(R.id.scroll_button_start);
+ mEndScrollArrow = findViewById(R.id.scroll_button_end);
+
+ setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
Resources resources = getResources();
- mTaskViewWidth = resources.getDimensionPixelSize(
- R.dimen.keyboard_quick_switch_taskview_width);
- mTaskViewHeight = resources.getDimensionPixelSize(
- R.dimen.keyboard_quick_switch_taskview_height);
mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing);
mSmallSpacing = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_view_small_spacing);
mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
+ mTaskViewBorderWidth = resources.getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_border_width);
+ mTaskViewRadius = resources.getDimensionPixelSize(
+ R.dimen.keyboard_quick_switch_task_view_radius);
+
mIsRtl = Utilities.isRtl(resources);
+ if (Flags.taskbarOverflow()) {
+ initializeScrollArrows();
+
+ if (mIsRtl) {
+ mStartScrollArrow.setContentDescription(
+ resources.getString(R.string.quick_switch_scroll_arrow_right));
+ mEndScrollArrow.setContentDescription(
+ resources.getString(R.string.quick_switch_scroll_arrow_left));
+ }
+ }
+
+
TypefaceUtils.setTypeface(
mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE);
@@ -331,6 +357,78 @@
});
}
+ private void initializeScrollArrows() {
+ mSupportsScrollArrows = true;
+
+ mStartScrollArrow.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mIsRtl) {
+ runScrollCommand(false, () -> {
+ mScrollView.smoothScrollBy(mScrollView.getWidth(), 0);
+ });
+ } else {
+ runScrollCommand(false, () -> {
+ mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0);
+ });
+ }
+ }
+ });
+
+ mEndScrollArrow.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mIsRtl) {
+ runScrollCommand(false, () -> {
+ mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0);
+ });
+ } else {
+ runScrollCommand(false, () -> {
+ mScrollView.smoothScrollBy(mScrollView.getWidth(), 0);
+ });
+ }
+ }
+ });
+
+ // Add listeners to disable arrow buttons when the scroll view cannot be further scrolled in
+ // the associated direction.
+ mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() {
+ @Override
+ public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX,
+ int oldScrollY) {
+ updateArrowButtonsEnabledState();
+ }
+ });
+
+ // Update scroll view outline to clip its contents with rounded corners.
+ mScrollView.setClipToOutline(true);
+ mScrollView.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int spacingWithoutBorder = mSpacing - mTaskViewBorderWidth;
+ outline.setRoundRect(spacingWithoutBorder,
+ spacingWithoutBorder, view.getWidth() - spacingWithoutBorder,
+ view.getHeight() - spacingWithoutBorder,
+ mTaskViewRadius);
+ }
+ });
+ }
+
+ private void updateArrowButtonsEnabledState() {
+ if (!mDisplayingRecentTasks) {
+ return;
+ }
+
+ int scrollX = mScrollView.getScrollX();
+ if (mIsRtl) {
+ mEndScrollArrow.setEnabled(scrollX > 0);
+ mStartScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth());
+ } else {
+ mStartScrollArrow.setEnabled(scrollX > 0);
+ mEndScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth());
+ }
+ }
+
int getOverviewTaskIndex() {
return mOverviewTaskIndex;
}
@@ -346,6 +444,21 @@
mViewCallbacks = null;
}
+ private void animateDisplayedContentForClose(View view, AnimatorSet animator) {
+ Animator translationYAnimation = ObjectAnimator.ofFloat(
+ view,
+ TRANSLATION_Y,
+ 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
+ translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
+ translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
+ animator.play(translationYAnimation);
+
+ Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 1f, 0f);
+ contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
+ animator.play(contentAlphaAnimation);
+
+ }
+
protected Animator getCloseAnimation() {
AnimatorSet closeAnimation = new AnimatorSet();
@@ -360,17 +473,11 @@
closeAnimation.play(alphaAnimation);
View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane;
- Animator translationYAnimation = ObjectAnimator.ofFloat(
- displayedContent,
- TRANSLATION_Y,
- 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
- translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
- translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
- closeAnimation.play(translationYAnimation);
-
- Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 1f, 0f);
- contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
- closeAnimation.play(contentAlphaAnimation);
+ animateDisplayedContentForClose(displayedContent, closeAnimation);
+ if (mSupportsScrollArrows) {
+ animateDisplayedContentForClose(mStartScrollArrow, closeAnimation);
+ animateDisplayedContentForClose(mEndScrollArrow, closeAnimation);
+ }
closeAnimation.addListener(new AnimatorListenerAdapter() {
@Override
@@ -385,6 +492,31 @@
return closeAnimation;
}
+ private void animateDisplayedContentForOpen(View view, AnimatorSet animator) {
+ Animator translationXAnimation = ObjectAnimator.ofFloat(
+ view,
+ TRANSLATION_X,
+ -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
+ translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
+ translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
+ animator.play(translationXAnimation);
+
+ Animator translationYAnimation = ObjectAnimator.ofFloat(
+ view,
+ TRANSLATION_Y,
+ -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
+ translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
+ translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
+ animator.play(translationYAnimation);
+
+ view.setAlpha(0.0f);
+ Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 0f,
+ 1f);
+ contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
+ contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
+ animator.play(contentAlphaAnimation);
+ }
+
protected void animateOpen(int currentFocusIndexOverride) {
if (mOpenAnimation != null) {
// Restart animation since currentFocusIndexOverride can change the initial scroll.
@@ -407,26 +539,12 @@
mOpenAnimation.play(alphaAnimation);
View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane;
- Animator translationXAnimation = ObjectAnimator.ofFloat(
- displayedContent,
- TRANSLATION_X,
- -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
- translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
- translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
- mOpenAnimation.play(translationXAnimation);
+ animateDisplayedContentForOpen(displayedContent, mOpenAnimation);
+ if (mSupportsScrollArrows) {
+ animateDisplayedContentForOpen(mStartScrollArrow, mOpenAnimation);
+ animateDisplayedContentForOpen(mEndScrollArrow, mOpenAnimation);
+ }
- Animator translationYAnimation = ObjectAnimator.ofFloat(
- displayedContent,
- TRANSLATION_Y,
- -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
- translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
- translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
- mOpenAnimation.play(translationYAnimation);
-
- Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 0f, 1f);
- contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
- contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
- mOpenAnimation.play(contentAlphaAnimation);
ViewOutlineProvider outlineProvider = getOutlineProvider();
mOpenAnimation.addListener(new AnimatorListenerAdapter() {
@@ -461,6 +579,27 @@
OPEN_OUTLINE_INTERPOLATOR));
}
});
+
+ if (mSupportsScrollArrows) {
+ mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (mScrollView.getWidth() == 0) {
+ return;
+ }
+
+ if (mContent.getWidth() > mScrollView.getWidth()) {
+ mStartScrollArrow.setVisibility(VISIBLE);
+ mEndScrollArrow.setVisibility(VISIBLE);
+ updateArrowButtonsEnabledState();
+ }
+ mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+ }
+ });
+ }
+
animateFocusMove(-1, Math.min(
getTaskCount() - 1,
currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
diff --git a/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt
new file mode 100644
index 0000000..b9a211d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/PinToTaskbarShortcut.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar
+
+import android.content.Context
+import android.view.View
+import com.android.launcher3.R
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.views.ActivityContext
+
+/**
+ * A single menu item shortcut to allow users to pin an item to the taskbar and unpin an item from
+ * the taskbar.
+ */
+class PinToTaskbarShortcut<T>(target: T, itemInfo: ItemInfo?, originalView: View, isPin: Boolean) :
+ SystemShortcut<T>(
+ if (isPin) R.drawable.ic_pin else R.drawable.ic_unpin,
+ if (isPin) R.string.pin_to_taskbar else R.string.unpin_from_taskbar,
+ target,
+ itemInfo,
+ originalView,
+ ) where T : Context?, T : ActivityContext? {
+
+ override fun onClick(v: View?) {
+ // TODO(b/375648361): Pin/Unpin the item here.
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index a1620a1..cfbddbd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -81,6 +81,7 @@
import android.widget.Toast;
import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
+import android.window.DesktopModeFlags.DesktopModeFlag;
import android.window.RemoteTransition;
import androidx.annotation.NonNull;
@@ -94,6 +95,7 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.BubbleTextView.RunningAppState;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
@@ -197,6 +199,9 @@
private static final String WINDOW_TITLE = "Taskbar";
+ private static final DesktopModeFlag ENABLE_TASKBAR_BEHIND_SHADE = new DesktopModeFlag(
+ Flags::enableTaskbarBehindShade, false);
+
private final @Nullable Context mNavigationBarPanelContext;
private final TaskbarDragLayer mDragLayer;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 10eb64a..b510e7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -191,6 +191,8 @@
private boolean mIsQsbInline;
+ private RecentsAnimationCallbacks mRecentsAnimationCallbacks;
+
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
new DeviceProfile.OnDeviceProfileChangeListener() {
@Override
@@ -295,6 +297,11 @@
mIsDestroyed = true;
mCanSyncViews = false;
+ if (mRecentsAnimationCallbacks != null) {
+ mRecentsAnimationCallbacks.removeListener(mTaskBarRecentsAnimationListener);
+ mRecentsAnimationCallbacks = null;
+ }
+
mIconAlignment.finishAnimation();
mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
@@ -315,6 +322,7 @@
// If going to overview, stash the task bar
// If going home, align the icons to hotseat
AnimatorSet animatorSet = new AnimatorSet();
+ mRecentsAnimationCallbacks = callbacks;
// Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly.
TaskbarStashController stashController = mControllers.taskbarStashController;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 0fa82ae..d4ad555 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -204,6 +204,7 @@
ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
mContainer.updateItems(hotseatItemInfos, recentTasks);
mControllers.taskbarViewController.updateIconViewsRunningStates();
+ mControllers.taskbarPopupController.setHotseatInfosList(mHotseatItems);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index a9ee584..e81563e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -16,18 +16,20 @@
package com.android.launcher3.taskbar;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
-import static com.android.launcher3.popup.SystemShortcut.PIN_UNPIN_ITEM;
import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.Point;
import android.util.Pair;
+import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
import com.android.launcher3.AbstractFloatingView;
@@ -81,6 +83,8 @@
private TaskbarControllers mControllers;
private boolean mAllowInitialSplitSelection;
private AppInfo[] mAppInfosList;
+ // Saves the ItemInfos in the hotseat without the predicted items.
+ private SparseArray<ItemInfo> mHotseatInfosList;
private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut;
@@ -149,6 +153,14 @@
.filter(Objects::nonNull)
.collect(Collectors.toList());
+ // TODO(b/375648361): Revisit to see if this can be implemented within getSystemShortcuts().
+ if (Flags.enablePinningAppWithContextMenu()) {
+ SystemShortcut shortcut = createPinShortcut(context, item, icon);
+ if (shortcut != null) {
+ systemShortcuts.add(0, shortcut);
+ }
+ }
+
container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
R.layout.popup_container, context.getDragLayer(), false);
container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
@@ -172,9 +184,6 @@
// append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order
// here will reflect in the popup
ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
- if (Flags.enablePinningAppWithContextMenu()) {
- shortcuts.add(PIN_UNPIN_ITEM);
- }
shortcuts.add(APP_INFO);
if (!mControllers.taskbarDesktopModeController
.isInDesktopModeAndNotInOverview(mContext.getDisplayId())) {
@@ -193,6 +202,24 @@
return shortcuts.stream();
}
+ @Nullable
+ private SystemShortcut createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo,
+ BubbleTextView originalView) {
+ // Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut to pin.
+ if (itemInfo.isPredictedItem()) {
+ return null;
+ }
+ if (itemInfo.container == CONTAINER_HOTSEAT) {
+ return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false);
+ }
+ if (mHotseatInfosList.size()
+ < mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) {
+ return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true);
+ }
+
+ return null;
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarPopupController:");
@@ -276,6 +303,10 @@
return index < 0 ? null : mAppInfosList[index];
}
+ public void setHotseatInfosList(SparseArray<ItemInfo> info) {
+ mHotseatInfosList = info;
+ }
+
/**
* Returns a stream of Multi Instance menu options if an app supports it.
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
index 25db960..94cff0b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
@@ -22,6 +22,7 @@
import android.content.Intent;
import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
@@ -38,6 +39,7 @@
import com.android.launcher3.util.ShortcutUtil;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.LogUtils;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import java.util.List;
@@ -50,6 +52,7 @@
public static final int MOVE_TO_TOP_OR_LEFT = R.id.action_move_to_top_or_left;
public static final int MOVE_TO_BOTTOM_OR_RIGHT = R.id.action_move_to_bottom_or_right;
+ public static final int CREATE_APPLICATION_BUBBLE = R.id.action_create_application_bubble;
private final LauncherApps mLauncherApps;
private final StatsLogManager mStatsLogManager;
@@ -67,6 +70,9 @@
MOVE_TO_BOTTOM_OR_RIGHT,
R.string.move_drop_target_bottom_or_right,
KeyEvent.KEYCODE_R));
+ mActions.put(CREATE_APPLICATION_BUBBLE, new LauncherAction(
+ CREATE_APPLICATION_BUBBLE, R.string.open_app_as_a_bubble,
+ KeyEvent.KEYCODE_L));
}
@Override
@@ -76,11 +82,27 @@
}
out.add(mActions.get(MOVE_TO_TOP_OR_LEFT));
out.add(mActions.get(MOVE_TO_BOTTOM_OR_RIGHT));
+ if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+ out.add(mActions.get(CREATE_APPLICATION_BUBBLE));
+ }
}
@Override
protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
- if (item instanceof ItemInfoWithIcon
+ if (action == DEEP_SHORTCUTS) {
+ mContext.showPopupMenuForIcon((BubbleTextView) host);
+ return true;
+ } else if (action == CREATE_APPLICATION_BUBBLE) {
+ if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ && item instanceof WorkspaceItemInfo) {
+ ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo();
+ SystemUiProxy.INSTANCE.get(mContext).showShortcutBubble(shortcutInfo);
+ return true;
+ } else if (item.getIntent() != null && item.getIntent().getPackage() != null) {
+ SystemUiProxy.INSTANCE.get(mContext).showAppBubble(item.getIntent(), item.user);
+ return true;
+ }
+ } else if (item instanceof ItemInfoWithIcon
&& (action == MOVE_TO_TOP_OR_LEFT || action == MOVE_TO_BOTTOM_OR_RIGHT)) {
ItemInfoWithIcon info = (ItemInfoWithIcon) item;
int side = action == MOVE_TO_TOP_OR_LEFT
@@ -112,10 +134,6 @@
instanceIds.first);
}
return true;
- } else if (action == DEEP_SHORTCUTS) {
- mContext.showPopupMenuForIcon((BubbleTextView) host);
-
- return true;
}
return false;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index de8e286..e589c87 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -477,7 +477,7 @@
}
// Recents divider takes priority.
- if (!mAddedDividerForRecents) {
+ if (!mAddedDividerForRecents && !mActivityContext.areDesktopTasksVisible()) {
updateAllAppsDivider();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
index 822ca64..f1ed6c5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarSpecsEvaluator.kt
@@ -26,6 +26,8 @@
numColumns: Int = taskbarActivityContext.deviceProfile.inv.numColumns,
) {
var taskbarIconSize: TaskbarIconSize = getIconSizeByGrid(numColumns, numRows)
+ val numShownHotseatIcons
+ get() = taskbarActivityContext.deviceProfile.numShownHotseatIcons
// TODO(b/341146605) : initialize it to taskbar container in later cl.
private var taskbarContainer: List<TaskbarContainer> = emptyList()
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index aab8ad1..484978c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -48,7 +48,6 @@
import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT;
import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
import static com.android.launcher3.popup.SystemShortcut.INSTALL;
-import static com.android.launcher3.popup.SystemShortcut.PIN_UNPIN_ITEM;
import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
import static com.android.launcher3.popup.SystemShortcut.UNINSTALL_APP;
import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
@@ -475,9 +474,6 @@
List<SystemShortcut.Factory> shortcuts = new ArrayList(Arrays.asList(
APP_INFO, WellbeingModel.SHORTCUT_FACTORY, mHotseatPredictionController));
- if (Flags.enablePinningAppWithContextMenu()) {
- shortcuts.add(0, PIN_UNPIN_ITEM);
- }
shortcuts.addAll(getSplitShortcuts());
shortcuts.add(WIDGETS);
shortcuts.add(INSTALL);
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index c51f659..23b8e36 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -102,6 +102,7 @@
import android.window.TransitionInfo;
import android.window.WindowAnimationState;
+import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -221,6 +222,7 @@
private final Runnable mLauncherOnDestroyCallback = () -> {
ActiveGestureProtoLogProxy.logLauncherDestroyed();
+ mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
mRecentsView = null;
mContainer = null;
mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
@@ -313,7 +315,7 @@
*/
private static final int LOG_NO_OP_PAGE_INDEX = -1;
- protected final TaskAnimationManager mTaskAnimationManager;
+ protected TaskAnimationManager mTaskAnimationManager;
// Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
private RunningWindowAnim[] mRunningWindowAnim;
// Possible second animation running at the same time as mRunningWindowAnim
@@ -1114,9 +1116,6 @@
public void onGestureEnded(float endVelocityPxPerMs, PointF velocityPxPerMs) {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
- Log.d(TAG, "onGestureEnded: mGestureStarted=" + mGestureStarted
- + ", mIsMotionPaused=" + mIsMotionPaused
- + ", flingThresholdPassed=" + (Math.abs(endVelocityPxPerMs) > flingThreshold));
boolean isFling = mGestureStarted && !mIsMotionPaused
&& Math.abs(endVelocityPxPerMs) > flingThreshold;
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -1265,12 +1264,12 @@
dpiFromPx(velocityPxPerMs.x),
dpiFromPx(velocityPxPerMs.y),
Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
+
if (mGestureState.isHandlingAtomicEvent()) {
// Button mode, this is only used to go to recents.
return RECENTS;
}
- Log.d(TAG, "calculateEndTarget: isCancel=" + isCancel + ", isFlingY=" + isFlingY);
GestureEndTarget endTarget;
if (isCancel) {
endTarget = LAST_TASK;
@@ -1280,7 +1279,6 @@
endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
}
- Log.d(TAG, "calculateEndTarget: endTarget(1)=" + endTarget);
if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
return LAST_TASK;
}
@@ -1299,7 +1297,6 @@
return LAST_TASK;
}
}
- Log.d(TAG, "calculateEndTarget: endTarget(2)=" + endTarget);
return endTarget;
}
@@ -1308,12 +1305,9 @@
final boolean willGoToNewTask =
isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
final boolean isSwipeUp = endVelocity < 0;
- Log.d(TAG, "calculateEndTargetForFlingY: willGoToNewTask=" + willGoToNewTask
- + ", isSwipeUp=" + isSwipeUp);
if (!isSwipeUp) {
final boolean isCenteredOnNewTask = mRecentsView != null
&& mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
- Log.d(TAG, "calculateEndTargetForFlingY: isCenteredOnNewTask=" + isCenteredOnNewTask);
return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK;
}
@@ -1326,9 +1320,6 @@
// Fully gestural mode.
final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
- Log.d(TAG, "calculateEndTargetForNonFling: isScrollingToNewTask=" + isScrollingToNewTask
- + ", isFlingX=" + isFlingX
- + ", mIsMotionPaused=" + mIsMotionPaused);
if (isScrollingToNewTask && isFlingX) {
// Flinging towards new task takes precedence over mIsMotionPaused (which only
// checks y-velocity).
@@ -1338,7 +1329,6 @@
} else if (isScrollingToNewTask) {
return NEW_TASK;
}
- Log.d(TAG, "calculateEndTargetForNonFling: mCanSlowSwipeGoHome=" + mCanSlowSwipeGoHome);
return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK;
}
@@ -1382,7 +1372,7 @@
&& mIsTransientTaskbar
&& mContainerInterface.getTaskbarController() != null) {
mContainerInterface.getTaskbarController()
- .setUserIsNotGoingHome(endTarget != GestureState.GestureEndTarget.HOME);
+ .setUserIsNotGoingHome(endTarget != HOME);
}
float endShift = endTarget.isLauncher ? 1 : 0;
@@ -2070,7 +2060,7 @@
* specific edge case: if we switch from A to B, and back to A before B appears, we need to
* start A again to ensure it stays on top.
*/
- @androidx.annotation.CallSuper
+ @CallSuper
protected void onRestartPreviouslyAppearedTask() {
// Finish the controller here, since we won't get onTaskAppeared() for a task that already
// appeared.
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index e7e2074..baabde8 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -258,13 +258,15 @@
Log.d(TAG, "cancelLongPress: " + reason);
}
// Log LPNH abandon latency if we didn't trigger but were still prepared to.
- long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime();
- if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)
- && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) {
- mStatsLogManager.latencyLogger()
- .withInstanceId(new InstanceIdSequence().newInstanceId())
- .withLatency(latencyMs)
- .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
+ if (mCurrentMotionEvent != null && mCurrentDownEvent != null) {
+ long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime();
+ if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)
+ && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) {
+ mStatsLogManager.latencyLogger()
+ .withInstanceId(new InstanceIdSequence().newInstanceId())
+ .withLatency(latencyMs)
+ .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
+ }
}
mGestureState.setIsInExtendedSlopRegion(false);
MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index b83acf0..37359a1 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -84,9 +84,6 @@
}
}
- fun getScrollAdjustment(showAsGrid: Boolean): Int =
- if (showAsGrid) gridTranslationX.toInt() else 0
-
private fun getBorderBounds(bounds: Rect) {
bounds.set(0, 0, width, height)
val outlinePadding =
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
index 8d53552..c20aa11 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
@@ -428,7 +428,6 @@
private const val INDEX_CONTENT_ALPHA = 0
private const val INDEX_COLOR_FILTER_ALPHA = 1
private const val INDEX_MODAL_ALPHA = 2
-
/** Used to hide the app chip for 90:10 flex split. */
private const val INDEX_MINIMUM_RATIO_ALPHA = 3
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 44bf82c..54d5e95 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2507,6 +2507,11 @@
int minDistanceFromScreenStart = Integer.MAX_VALUE;
int minDistanceFromScreenStartIndex = INVALID_PAGE;
for (int i = 0; i < getChildCount(); ++i) {
+ // Do not set the destination page to the AddDesktopButton, which has the same page
+ // scrolls as the first [TaskView] and shouldn't be scrolled to.
+ if (getChildAt(i) instanceof AddDesktopButton) {
+ continue;
+ }
int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
if (distanceFromScreenStart < minDistanceFromScreenStart) {
minDistanceFromScreenStart = distanceFromScreenStart;
@@ -3013,9 +3018,22 @@
startIconFadeInOnGestureComplete();
animateActionsViewIn();
- for (TaskView taskView : getTaskViews()) {
- if (taskView instanceof DesktopTaskView desktopTaskView) {
- desktopTaskView.setRemoteTargetHandles(mRemoteTargetHandles);
+ if (mEnableDrawingLiveTile) {
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.setRemoteTargetHandles(mRemoteTargetHandles);
+ }
+ }
+ TaskView runningTaskView = getRunningTaskView();
+ if (showAsGrid() && enableGridOnlyOverview() && runningTaskView != null) {
+ runActionOnRemoteHandles(remoteTargetHandle -> {
+ TaskViewSimulator taskViewSimulator = remoteTargetHandle.getTaskViewSimulator();
+ // After settling in Overview, recentsScroll will be used to adjust horizontally
+ // location and taskGridTranslationX doesn't needs to be applied.
+ taskViewSimulator.taskGridTranslationX.value = 0;
+ taskViewSimulator.taskGridTranslationY.value =
+ runningTaskView.getGridTranslationY();
+ });
}
}
@@ -3530,19 +3548,6 @@
mAddDesktopButton.setGridTranslationX(translationX);
}
- final TaskView runningTask = getRunningTaskView();
- if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
- runActionOnRemoteHandles(
- remoteTargetHandle -> {
- remoteTargetHandle.getTaskViewSimulator().taskGridTranslationX.value =
- runningTask.getGridTranslationX()
- - runningTask.getNonGridTranslationX();
- remoteTargetHandle.getTaskViewSimulator().taskGridTranslationY.value =
- runningTask.getGridTranslationY();
- }
- );
- }
-
mClearAllButton.setGridTranslationPrimary(
clearAllTotalTranslationX - snappedTaskGridTranslationX);
mClearAllButton.setGridScrollOffset(
@@ -6215,9 +6220,7 @@
private int getFirstViewIndex() {
final View firstView;
- if (mAddDesktopButton != null) {
- firstView = mAddDesktopButton;
- } else if (mShowAsGridLastOnLayout) {
+ if (mShowAsGridLastOnLayout) {
// For grid Overview, it always start if a large tile (focused task or desktop task) if
// they exist, otherwise it start with the first task.
TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView();
@@ -6287,13 +6290,6 @@
outPageScrolls[clearAllIndex] = clearAllScroll;
}
- int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
- if (addDesktopButtonIndex != -1 && addDesktopButtonIndex < outPageScrolls.length) {
- outPageScrolls[addDesktopButtonIndex] =
- newPageScrolls[addDesktopButtonIndex] + mAddDesktopButton.getScrollAdjustment(
- showAsGrid);
- }
-
int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
getTaskViews().forEachWithIndexInParent((index, taskView) -> {
float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
@@ -6308,6 +6304,14 @@
"getPageScrolls - outPageScrolls[" + index + "]: " + outPageScrolls[index]);
}
});
+
+ int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
+ if (addDesktopButtonIndex >= 0 && addDesktopButtonIndex < outPageScrolls.length) {
+ int firstViewIndex = getFirstViewIndex();
+ if (firstViewIndex >= 0 && firstViewIndex < outPageScrolls.length) {
+ outPageScrolls[addDesktopButtonIndex] = outPageScrolls[firstViewIndex];
+ }
+ }
if (DEBUG) {
Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll);
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 276318c..3819772 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -100,7 +100,6 @@
import com.android.systemui.shared.system.ActivityManagerWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -309,8 +308,10 @@
// progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
protected var fullscreenProgress = 0f
set(value) {
- field = Utilities.boundToRange(value, 0f, 1f)
- onFullscreenProgressChanged(field)
+ if (value != field) {
+ field = Utilities.boundToRange(value, 0f, 1f)
+ onFullscreenProgressChanged(field)
+ }
}
// gridProgress 0 = carousel; 1 = 2 row grid.
@@ -490,8 +491,10 @@
// 1 = The TaskView is settled and no longer transitioning
private var settledProgress = 1f
set(value) {
- field = value
- onSettledProgressUpdated(field)
+ if (value != field) {
+ field = value
+ onSettledProgressUpdated(field)
+ }
}
private val settledProgressPropertyFactory =
@@ -511,7 +514,7 @@
private var viewModel: TaskViewModel? = null
private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject()
- private val coroutineScope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.main) }
+ private val coroutineScope: CoroutineScope by RecentsDependencies.inject()
private val coroutineJobs = mutableListOf<Job>()
/**
@@ -740,8 +743,9 @@
// The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in
// onRecycle. So it should be initialized at this point. TaskView Lifecycle:
// `bind` -> `onBind` -> onAttachedToWindow() -> onDetachFromWindow -> onRecycle
- coroutineJobs +=
- coroutineScope.launch { viewModel!!.state.collectLatest(::updateTaskViewState) }
+ coroutineJobs += coroutineScope.launch(dispatcherProvider.main) {
+ viewModel!!.state.collectLatest(::updateTaskViewState)
+ }
}
}
@@ -1687,7 +1691,9 @@
protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) {
taskContainers.forEach {
- it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE)
+ if (!enableOverviewIconMenu()) {
+ it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE)
+ }
it.overlay.setFullscreenProgress(fullscreenProgress)
}
settledProgressFullscreen =
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
index 4b6d5e5..ac1572e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
@@ -21,6 +21,7 @@
import android.view.View
import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION
import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
@@ -39,6 +40,7 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
@@ -54,6 +56,9 @@
private val iconViews: Array<View>
get() = taskbarView.iconViews
+ private val desktopVisibilityController: DesktopVisibilityController
+ get() = DesktopVisibilityController.INSTANCE[context]
+
@Before
fun obtainView() {
taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
@@ -225,4 +230,34 @@
assertThat(expectedViewToRemove in iconViews).isFalse()
}
}
+
+ @Test
+ fun testUpdateItems_desktopMode_hotseatItem_noDivider() {
+ whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true)
+ runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) }
+ assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT)
+ }
+
+ @Test
+ @ForceRtl
+ fun testUpdateItems_rtlAndDesktopMode_hotseatItem_noDivider() {
+ whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true)
+ runOnMainSync { taskbarView.updateItems(createHotseatItems(1), emptyList()) }
+ assertThat(taskbarView).hasIconTypes(HOTSEAT, ALL_APPS)
+ }
+
+ @Test
+ fun testUpdateItems_desktopMode_recentItem_hasDivider() {
+ whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true)
+ runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) }
+ assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, RECENT)
+ }
+
+ @Test
+ @ForceRtl
+ fun testUpdateItems_rtlAndDesktopMode_recentItem_hasDivider() {
+ whenever(desktopVisibilityController.isInDesktopMode(context.displayId)).thenReturn(true)
+ runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(1)) }
+ assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, ALL_APPS)
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 6d53e8e..d96e06e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -26,6 +26,7 @@
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.util.AllModulesMinusWMProxy
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.util.DisplayController
@@ -39,12 +40,14 @@
import dagger.BindsInstance
import dagger.Component
import dagger.Module
+import dagger.Provides
import javax.inject.Inject
import org.junit.rules.ExternalResource
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+import org.mockito.kotlin.spy
/**
* [SandboxApplication] for running Taskbar tests.
@@ -135,6 +138,20 @@
@Binds abstract fun bindDisplayController(controller: DisplayControllerSpy): DisplayController
}
+@Module
+object DesktopVisibilityControllerModule {
+ @JvmStatic
+ @Provides
+ @LauncherAppSingleton
+ fun provideDesktopVisibilityController(
+ @ApplicationContext context: Context,
+ systemUiProxy: SystemUiProxy,
+ lifecycleTracker: DaggerSingletonTracker,
+ ): DesktopVisibilityController {
+ return spy(DesktopVisibilityController(context, systemUiProxy, lifecycleTracker))
+ }
+}
+
@LauncherAppSingleton
@Component(
modules =
@@ -143,6 +160,7 @@
FakePrefsModule::class,
DisplayControllerModule::class,
TaskbarSandboxModule::class,
+ DesktopVisibilityControllerModule::class,
]
)
interface TaskbarSandboxComponent : LauncherAppComponent {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index b3056f5..cfeade8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -311,6 +311,18 @@
}
@Test
+ public void testTouchCancelWithoutTouchDown() {
+ mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL));
+
+ assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
+ assertFalse(mLongPressTriggered.get());
+ verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
+ verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+ verifyNoMoreInteractions(mStatsLogger);
+ verifyNoMoreInteractions(mStatsLatencyLogger);
+ }
+
+ @Test
public void testLongPressAbortedByTouchSlopPassedVertically() {
mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
sleep(MIN_TIME_TO_LOG_ABANDON_MS);
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
index fc757b4..0ccc76b 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractTaplTestsTaskbar.java
@@ -55,7 +55,9 @@
"com.android.launcher3.testcomponent.BaseTestingActivity");
mLauncherLayout = TestUtil.setLauncherDefaultLayout(mTargetContext, layoutBuilder);
AbstractLauncherUiTest.initialize(this);
- startAppFast(CALCULATOR_APP_PACKAGE);
+ if (startCalendarAppDuringSetup()) {
+ startAppFast(CALCULATOR_APP_PACKAGE);
+ }
mLauncher.enableBlockTimeout(true);
mLauncher.showTaskbarIfHidden();
}
@@ -72,8 +74,20 @@
return DisplayController.isTransientTaskbar(context);
}
+ protected boolean startCalendarAppDuringSetup() {
+ return true;
+ }
+
+ protected boolean expectTaskbarIconsMatchHotseat() {
+ return true;
+ }
+
protected Taskbar getTaskbar() {
Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar();
+ if (!expectTaskbarIconsMatchHotseat()) {
+ return taskbar;
+ }
+
List<String> taskbarIconNames = taskbar.getIconNames();
List<String> hotseatIconNames = mLauncher.getHotseatIconNames();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java
index 8fedf5c..b2617dd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsLockedTaskbar.java
@@ -15,14 +15,18 @@
*/
package com.android.quickstep;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.test.InstrumentationRegistry.getTargetContext;
+import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP;
-import android.app.WindowConfiguration;
+import static com.google.common.truth.Truth.assertThat;
+
import android.os.RemoteException;
import android.util.Log;
import android.view.WindowManagerGlobal;
@@ -30,6 +34,7 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.launcher3.tapl.HomeAllApps;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.rule.SetPropRule;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -40,6 +45,7 @@
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExternalResource;
import org.junit.runner.RunWith;
@LargeTest
@@ -51,25 +57,40 @@
public SetPropRule mSetPropRule =
new SetPropRule(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP, "true");
+ // Default-to-desktop feature requires the display to be freeform mode.
+ @Rule
+ public ExternalResource mFreeformDisplayRule = new ExternalResource() {
+ private int mOriginalWindowingMode = WINDOWING_MODE_UNDEFINED;
+
+ @Override
+ protected void before() {
+ mOriginalWindowingMode = setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
+ }
+
+ @Override
+ protected void after() {
+ if (mOriginalWindowingMode != WINDOWING_MODE_UNDEFINED) {
+ setDisplayWindowingMode(mOriginalWindowingMode);
+ }
+ }
+ };
+
@Override
public void setUp() throws Exception {
Assume.assumeTrue(mLauncher.isTablet());
Assume.assumeTrue(Flags.enterDesktopByDefaultOnFreeformDisplays());
Assume.assumeTrue(DesktopModeStatus.canEnterDesktopMode(getTargetContext()));
super.setUp();
-
- // Default-to-desktop feature requires the display to be freeform mode.
- setDisplayWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
}
@Override
- public void tearDown() throws Exception {
- // Reset the display windowing mode to the device default.
- setDisplayWindowingMode(WindowConfiguration.WINDOWING_MODE_UNDEFINED);
+ protected boolean startCalendarAppDuringSetup() {
+ return false;
+ }
- mLauncher.recreateTaskbar();
-
- super.tearDown();
+ @Override
+ protected boolean expectTaskbarIconsMatchHotseat() {
+ return false;
}
@Test
@@ -87,10 +108,31 @@
mLauncher.getLaunchedAppState().assertTaskbarVisible();
}
- private void setDisplayWindowingMode(int windowingMode) {
+ @Test
+ @PortraitLandscape
+ @NavigationModeSwitch
+ @TaskbarModeSwitch(mode = PERSISTENT)
+ public void testDragFromAllAppsToWorspace() {
+ mDevice.pressHome();
+ waitForResumed("Launcher internal state is still Background");
+
+ final HomeAllApps allApps = getTaskbar().openAllAppsOnHome();
+ allApps.freeze();
try {
+ allApps.getAppIcon(TEST_APP_NAME).dragToWorkspace(false, false);
+ assertThat(mLauncher.getWorkspace().getWorkspaceAppIcon(TEST_APP_NAME)).isNotNull();
+ } finally {
+ allApps.unfreeze();
+ }
+ }
+
+ private int setDisplayWindowingMode(int windowingMode) {
+ try {
+ int originalWindowingMode =
+ WindowManagerGlobal.getWindowManagerService().getWindowingMode(DEFAULT_DISPLAY);
WindowManagerGlobal.getWindowManagerService().setWindowingMode(
DEFAULT_DISPLAY, windowingMode);
+ return originalWindowingMode;
} catch (RemoteException e) {
Log.e(TAG, "error setting windowing mode", e);
throw new RuntimeException(e);
diff --git a/res/drawable/ic_unpin.xml b/res/drawable/ic_unpin.xml
new file mode 100644
index 0000000..557b4f9
--- /dev/null
+++ b/res/drawable/ic_unpin.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <clip-path
+ android:pathData="M0,0h24v24h-24z"/>
+ <path
+ android:pathData="M17,3V5H16V13.175L14,11.175V5H10V7.175L7.825,5L7,4.175V3H17ZM12,23L11,22V16H6V14L8,12V10.85L1.4,4.2L2.8,2.8L21.2,21.2L19.75,22.6L13.15,16H13V22L12,23ZM8.85,14H11.15L10.05,12.9L10,12.85L8.85,14Z"
+ android:fillColor="#FF000000"/>
+ </group>
+</vector>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 7aa709d..64f67cd 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -23,6 +23,11 @@
<dimen name="dynamic_grid_left_right_margin">8dp</dimen>
<!-- Minimum amount of next page visible in spring loaded mode -->
<dimen name="dynamic_grid_spring_loaded_min_next_space_visible">48dp</dimen>
+ <item name="aspect_ratio_portrait" format="float" type="dimen">1.0</item>
+ <!-- 1.05 was the constant we found to validate square-ish devices
+ to load portrait orientation specs for responsive grids -->
+ <item name="aspect_ratio_portrait_and_square" format="float" type="dimen">1.05</item>
+ <item name="aspect_ratio_landscape" format="float" type="dimen">10</item>
<dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
<dimen name="cell_layout_padding">10.77dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 56befd6..db87686 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -235,8 +235,6 @@
<string name="pin_prediction">Pin Prediction</string>
<!-- Label for bubbling a launcher item. [CHAR_LIMIT=20] -->
<string name="bubble">Bubble</string>
- <!-- Label for pinning an item to the taskbar. [CHAR_LIMIT=20] -->
- <string name="pin_to_taskbar">Pin to taskbar</string>
<!-- Permissions: -->
<skip />
diff --git a/res/xml/default_workspace_7x3.xml b/res/xml/default_workspace_7x3.xml
new file mode 100644
index 0000000..d17491e
--- /dev/null
+++ b/res/xml/default_workspace_7x3.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 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.
+-->
+<!-- Google-specific version of Launcher3/res/xml/default_workspace.xml -->
+<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+ <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
+ <!-- Dialer Messaging Play Chrome Camera -->
+ <favorite
+ launcher:container="-101"
+ launcher:screen="0"
+ launcher:x="0"
+ launcher:y="0"
+ launcher:className="com.google.android.dialer.extensions.GoogleDialtactsActivity"
+ launcher:packageName="com.google.android.dialer" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="1"
+ launcher:x="1"
+ launcher:y="0"
+ launcher:className="com.google.android.apps.messaging.ui.ConversationListActivity"
+ launcher:packageName="com.google.android.apps.messaging" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="2"
+ launcher:x="2"
+ launcher:y="0"
+ launcher:className="com.android.vending.AssetBrowserActivity"
+ launcher:packageName="com.android.vending" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="3"
+ launcher:x="3"
+ launcher:y="0"
+ launcher:className="com.google.android.apps.chrome.Main"
+ launcher:packageName="com.android.chrome" />
+
+ <!-- Resolve camera intent if GoogleCamera is not available e.g. on emulator -->
+ <resolve
+ launcher:container="-101"
+ launcher:screen="4"
+ launcher:x="4"
+ launcher:y="0" >
+ <favorite
+ launcher:className="com.android.camera.CameraLauncher"
+ launcher:packageName="com.google.android.GoogleCamera" />
+ <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
+ <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
+ </resolve>
+
+ <!-- Bottom row -->
+ <!-- [space] [space] [space] [space] [space] -->
+
+</favorites>
diff --git a/res/xml/default_workspace_8x3.xml b/res/xml/default_workspace_8x3.xml
new file mode 100644
index 0000000..d17491e
--- /dev/null
+++ b/res/xml/default_workspace_8x3.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 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.
+-->
+<!-- Google-specific version of Launcher3/res/xml/default_workspace.xml -->
+<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+ <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
+ <!-- Dialer Messaging Play Chrome Camera -->
+ <favorite
+ launcher:container="-101"
+ launcher:screen="0"
+ launcher:x="0"
+ launcher:y="0"
+ launcher:className="com.google.android.dialer.extensions.GoogleDialtactsActivity"
+ launcher:packageName="com.google.android.dialer" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="1"
+ launcher:x="1"
+ launcher:y="0"
+ launcher:className="com.google.android.apps.messaging.ui.ConversationListActivity"
+ launcher:packageName="com.google.android.apps.messaging" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="2"
+ launcher:x="2"
+ launcher:y="0"
+ launcher:className="com.android.vending.AssetBrowserActivity"
+ launcher:packageName="com.android.vending" />
+
+ <favorite
+ launcher:container="-101"
+ launcher:screen="3"
+ launcher:x="3"
+ launcher:y="0"
+ launcher:className="com.google.android.apps.chrome.Main"
+ launcher:packageName="com.android.chrome" />
+
+ <!-- Resolve camera intent if GoogleCamera is not available e.g. on emulator -->
+ <resolve
+ launcher:container="-101"
+ launcher:screen="4"
+ launcher:x="4"
+ launcher:y="0" >
+ <favorite
+ launcher:className="com.android.camera.CameraLauncher"
+ launcher:packageName="com.google.android.GoogleCamera" />
+ <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
+ <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
+ </resolve>
+
+ <!-- Bottom row -->
+ <!-- [space] [space] [space] [space] [space] -->
+
+</favorites>
diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml
index 1d0dbff..5fac66f 100644
--- a/res/xml/device_profiles.xml
+++ b/res/xml/device_profiles.xml
@@ -207,4 +207,45 @@
</grid-option>
+ <grid-option
+ launcher:name="fixed_landscape_mode"
+ launcher:numFolderRows="2"
+ launcher:numFolderColumns="3"
+ launcher:numFolderRowsLandscape="2"
+ launcher:folderStyle="@style/FolderStyleDefault"
+ launcher:numHotseatIcons="4"
+ launcher:numExtendedHotseatIcons="8"
+ launcher:numAllAppsColumns="8"
+ launcher:numExtendedAllAppsColumns="8"
+ launcher:workspaceSpecsId="@xml/spec_handheld_workspace_3_row"
+ launcher:allAppsSpecsId="@xml/spec_handheld_all_apps_3_row"
+ launcher:folderSpecsId="@xml/spec_handheld_folder_3_row"
+ launcher:hotseatSpecsId="@xml/spec_handheld_hotseat_3_row"
+ launcher:workspaceCellSpecsId="@xml/spec_handheld_workspace_cell_3_row"
+ launcher:allAppsCellSpecsId="@xml/spec_all_apps_cell_match_workspace"
+ launcher:isScalable="true"
+ launcher:inlineQsb="landscape"
+ launcher:devicePaddingId="@xml/paddings_3_row"
+ launcher:gridSizeSpecsId="@xml/spec_col_count_3_row"
+ launcher:isFixedLandscape="true"
+ launcher:defaultLayoutId="@xml/default_workspace_8x3"
+ launcher:deviceCategory="phone">
+
+ <display-option
+ launcher:name="Fixed Landscape"
+ launcher:minWidthDps="387"
+ launcher:minHeightDps="750"
+ launcher:minCellHeight="108"
+ launcher:minCellWidth="61"
+ launcher:borderSpace="16"
+ launcher:horizontalMargin="22"
+ launcher:iconImageSize="57"
+ launcher:iconSizeLandscape="59"
+ launcher:iconTextSize="12"
+ launcher:allAppsBorderSpace="16"
+ launcher:allAppsCellHeight="104"
+ launcher:canBeDefault="true" />
+
+ </grid-option>
+
</profiles>
\ No newline at end of file
diff --git a/res/xml/paddings_3_row.xml b/res/xml/paddings_3_row.xml
new file mode 100644
index 0000000..147c9d1
--- /dev/null
+++ b/res/xml/paddings_3_row.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<device-paddings xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+ <device-padding
+ launcher:maxEmptySpace="100dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="160dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ </device-padding>
+
+ <device-padding
+ launcher:maxEmptySpace="9999dp">
+ <workspaceTopPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <workspaceBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ <hotseatBottomPadding
+ launcher:a="0"
+ launcher:b="0"/>
+ </device-padding>
+</device-paddings>
\ No newline at end of file
diff --git a/res/xml/spec_all_apps_cell_match_workspace.xml b/res/xml/spec_all_apps_cell_match_workspace.xml
new file mode 100644
index 0000000..5843490
--- /dev/null
+++ b/res/xml/spec_all_apps_cell_match_workspace.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<cellSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <cellSpec
+ launcher:dimensionType="height"
+ launcher:maxAvailableSize="9999dp">
+ <iconDrawablePadding launcher:matchWorkspace="true" />
+ <iconSize launcher:matchWorkspace="true" />
+ <iconTextSize launcher:matchWorkspace="true" />
+ </cellSpec>
+ </specs>
+</cellSpecs>
\ No newline at end of file
diff --git a/res/xml/spec_col_count_3_row.xml b/res/xml/spec_col_count_3_row.xml
new file mode 100644
index 0000000..bbde542
--- /dev/null
+++ b/res/xml/spec_col_count_3_row.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+<GridSizeSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <GridSize
+ launcher:numGridColumns="7"
+ launcher:numGridRows="3"
+ launcher:dbFile="launcher_7_by_3.db"
+ launcher:defaultLayoutId="@xml/default_workspace_7x3"
+ launcher:minDeviceWidthPx="0"
+ launcher:minDeviceHeightPx="0"
+ />
+ <GridSize
+ launcher:numGridColumns="8"
+ launcher:numGridRows="3"
+ launcher:dbFile="launcher_8_by_3.db"
+ launcher:defaultLayoutId="@xml/default_workspace_8x3"
+ launcher:minDeviceWidthPx="0"
+ launcher:minDeviceHeightPx="2093"
+ />
+</GridSizeSpecs>
\ No newline at end of file
diff --git a/res/xml/spec_handheld_all_apps_3_row.xml b/res/xml/spec_handheld_all_apps_3_row.xml
new file mode 100644
index 0000000..00b3310
--- /dev/null
+++ b/res/xml/spec_handheld_all_apps_3_row.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <!-- landscape -->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <allAppsSpec
+ launcher:dimensionType="height"
+ launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:fixedSize="0dp" />
+ <endPadding launcher:fixedSize="0dp" />
+ <gutter launcher:matchWorkspace="true" />
+ <cellSize launcher:matchWorkspace="true" />
+ </allAppsSpec>
+
+ <allAppsSpec
+ launcher:dimensionType="width"
+ launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:ofRemainderSpace="0.5" />
+ <endPadding launcher:ofRemainderSpace="0.5" />
+ <gutter launcher:matchWorkspace="true" />
+ <cellSize launcher:matchWorkspace="true" />
+ </allAppsSpec>
+ </specs>
+</allAppsSpecs>
diff --git a/res/xml/spec_handheld_folder_3_row.xml b/res/xml/spec_handheld_folder_3_row.xml
new file mode 100644
index 0000000..2614d96
--- /dev/null
+++ b/res/xml/spec_handheld_folder_3_row.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Handheld folders 7x3 -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <!-- landscape -->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <folderSpec launcher:dimensionType="width" launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:fixedSize="16dp" />
+ <endPadding launcher:fixedSize="16dp" />
+ <gutter launcher:fixedSize="16dp" />
+ <cellSize launcher:fixedSize="66dp" />
+ </folderSpec>
+ <folderSpec launcher:dimensionType="height" launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:fixedSize="16dp" />
+ <!-- mapped to footer height size -->
+ <endPadding launcher:fixedSize="48dp" />
+ <gutter launcher:fixedSize="0dp" />
+ <cellSize launcher:fixedSize="80dp" />
+ </folderSpec>
+ </specs>
+</folderSpecs>
diff --git a/res/xml/spec_handheld_hotseat_3_row.xml b/res/xml/spec_handheld_hotseat_3_row.xml
new file mode 100644
index 0000000..bd47c90
--- /dev/null
+++ b/res/xml/spec_handheld_hotseat_3_row.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <!-- landscape -->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <hotseatSpec
+ launcher:maxAvailableSize="9999dp"
+ launcher:dimensionType="width">
+ <hotseatQsbSpace launcher:fixedSize="0dp" />
+ <edgePadding launcher:fixedSize="0dp" />
+ </hotseatSpec>
+ </specs>
+</hotseatSpecs>
\ No newline at end of file
diff --git a/res/xml/spec_handheld_workspace_3_row.xml b/res/xml/spec_handheld_workspace_3_row.xml
new file mode 100644
index 0000000..8e024d3
--- /dev/null
+++ b/res/xml/spec_handheld_workspace_3_row.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <!-- landscape -->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <workspaceSpec
+ launcher:dimensionType="height"
+ launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:ofAvailableSpace="0.01" />
+ <endPadding launcher:ofAvailableSpace="0.055" />
+ <gutter launcher:ofAvailableSpace="0.02" />
+ <cellSize launcher:ofRemainderSpace="1" />
+ </workspaceSpec>
+ <workspaceSpec
+ launcher:dimensionType="width"
+ launcher:maxAvailableSize="9999dp">
+ <startPadding launcher:ofAvailableSpace="0.0660867583" />
+ <endPadding launcher:ofAvailableSpace="0.0660867583" />
+ <gutter launcher:ofAvailableSpace="0.0125" />
+ <cellSize launcher:ofRemainderSpace="1" />
+ </workspaceSpec>
+ </specs>
+</workspaceSpecs>
diff --git a/res/xml/spec_handheld_workspace_cell_3_row.xml b/res/xml/spec_handheld_workspace_cell_3_row.xml
new file mode 100644
index 0000000..607dbb9
--- /dev/null
+++ b/res/xml/spec_handheld_workspace_cell_3_row.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<cellSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+ <!-- portrait TODO: remove portrait specs-->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_portrait">
+ <cellSpec
+ launcher:dimensionType="height"
+ launcher:maxAvailableSize="9999dp">
+ <iconDrawablePadding launcher:fixedSize="0dp" />
+ <iconSize launcher:fixedSize="@dimen/iconSize52dp" />
+ <iconTextSize launcher:fixedSize="12sp" />
+ </cellSpec>
+ </specs>
+ <!-- landscape -->
+ <specs launcher:maxAspectRatio="@dimen/aspect_ratio_landscape">
+ <cellSpec
+ launcher:dimensionType="height"
+ launcher:maxAvailableSize="9999dp">
+ <iconDrawablePadding launcher:fixedSize="4dp" />
+ <iconSize launcher:fixedSize="@dimen/iconSize52dp" />
+ <iconTextSize launcher:fixedSize="12sp" />
+ </cellSpec>
+ </specs>
+</cellSpecs>
\ No newline at end of file
diff --git a/shared/src/com/android/launcher3/testing/shared/TestProtocol.java b/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
index 0583d6d..cdeab95 100644
--- a/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/shared/src/com/android/launcher3/testing/shared/TestProtocol.java
@@ -125,6 +125,8 @@
"is-predictive-back-swipe-enabled";
public static final String REQUEST_ENABLE_TASKBAR_NAVBAR_UNIFICATION =
"enable-taskbar-navbar-unification";
+ public static final String REQUEST_TASKBAR_SHOWN_ON_HOME =
+ "taskbar-shown-on-home";
public static final String REQUEST_NUM_ALL_APPS_COLUMNS = "num-all-apps-columns";
public static final String REQUEST_IS_TWO_PANELS = "is-two-panel";
public static final String REQUEST_CELL_LAYOUT_BOARDER_HEIGHT = "cell-layout-boarder-height";
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 730ad78..250bbe5 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -983,11 +983,14 @@
@Override
public void setTextColor(ColorStateList colors) {
- mTextColor = (shouldDrawAppContrastTile() && !TextUtils.isEmpty(getText()))
- ? PillColorProvider.getInstance(
- getContext()).getAppTitleTextPaint().getColor()
- : colors.getDefaultColor();
- mTextColorStateList = colors;
+ if (shouldDrawAppContrastTile()) {
+ mTextColor = PillColorProvider.getInstance(
+ getContext()).getAppTitleTextPaint().getColor();
+ } else {
+ mTextColor = colors.getDefaultColor();
+ mTextColorStateList = colors;
+ }
+
if (Float.compare(mTextAlpha, 1) == 0) {
super.setTextColor(colors);
} else {
diff --git a/src/com/android/launcher3/PillColorPorovider.kt b/src/com/android/launcher3/PillColorPorovider.kt
index 347c5d6..e7f37bd 100644
--- a/src/com/android/launcher3/PillColorPorovider.kt
+++ b/src/com/android/launcher3/PillColorPorovider.kt
@@ -58,13 +58,8 @@
}
fun setup() {
- appTitlePillPaint.color =
- context.resources.getColor(
- R.color.material_color_surface_container_lowest,
- context.theme,
- )
- appTitleTextPaint.color =
- context.resources.getColor(R.color.material_color_on_surface, context.theme)
+ appTitlePillPaint.color = context.getColor(R.color.materialColorSurfaceContainer)
+ appTitleTextPaint.color = context.getColor(R.color.materialColorOnSurface)
isMatchaEnabledInternal = Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
isMatchaEnabled = isMatchaEnabledInternal != 0
}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 613b430..c4086b2 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -521,17 +521,13 @@
mDragObject.dragComplete = true;
if (mIsInPreDrag) {
- if (dropTarget != null) {
- dropTarget.onDragExit(mDragObject);
- }
- return;
+ mDragObject.cancelled = true;
}
-
// Drop onto the target.
boolean accepted = false;
if (dropTarget != null) {
dropTarget.onDragExit(mDragObject);
- if (dropTarget.acceptDrop(mDragObject)) {
+ if (!mIsInPreDrag && dropTarget.acceptDrop(mDragObject)) {
if (flingAnimation != null) {
flingAnimation.run();
} else {
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index 4aa3673..e3b8965 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.dragndrop;
+import static android.view.View.VISIBLE;
+
import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherState.EDIT_MODE;
@@ -25,6 +27,7 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
@@ -33,10 +36,13 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
+import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.TouchUtil;
import com.android.launcher3.widget.util.WidgetDragScaleUtils;
/**
@@ -47,6 +53,9 @@
private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
private final FlingToDeleteHelper mFlingToDeleteHelper;
+ /** Whether or not the drag operation is triggered by mouse right click. */
+ private boolean mIsInMouseRightClick = false;
+
public LauncherDragController(Launcher launcher) {
super(launcher);
mFlingToDeleteHelper = new FlingToDeleteHelper(launcher);
@@ -69,6 +78,27 @@
android.os.Debug.startMethodTracing("Launcher");
}
+ if (mIsInMouseRightClick && options.preDragCondition == null
+ && originalView instanceof View v) {
+ options.preDragCondition = new PreDragCondition() {
+
+ @Override
+ public boolean shouldStartDrag(double distanceDragged) {
+ return false;
+ }
+
+ @Override
+ public void onPreDragStart(DragObject dragObject) {
+ // Set it to visible so the text of FolderIcon would not flash (avoid it from
+ // being invisible and then visible)
+ v.setVisibility(VISIBLE);
+ }
+
+ @Override
+ public void onPreDragEnd(DragObject dragObject, boolean dragStarted) { }
+ };
+ }
+
mActivity.hideKeyboard();
AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_DISCOVERY_BOUNCE);
@@ -191,7 +221,7 @@
@Override
protected void exitDrag() {
- if (!mActivity.isInState(EDIT_MODE)) {
+ if (!mIsInPreDrag && !mActivity.isInState(EDIT_MODE)) {
mActivity.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
}
}
@@ -218,4 +248,13 @@
dropCoordinates);
return mActivity.getWorkspace();
}
+
+ /**
+ * Intercepts touch events from a drag source view.
+ */
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ mIsInMouseRightClick = TouchUtil.isMouseRightClickDownOrMove(ev);
+ return super.onControllerInterceptTouchEvent(ev);
+ }
}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 81d362f..37f5189 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -81,7 +81,6 @@
// This is used to optimize the onDraw method by not constructing a new RectF each draw.
private static final RectF sTempRect = new RectF();
- private static final Rect sLastActiveRect = new Rect();
private static final FloatProperty<PageIndicatorDots> CURRENT_POSITION =
new FloatProperty<PageIndicatorDots>("current_position") {
@@ -516,9 +515,6 @@
}
}
}
- if (Math.round(mCurrentPosition) == i) {
- sTempRect.roundOut(sLastActiveRect);
- }
canvas.drawRoundRect(sTempRect, mDotRadius, mDotRadius, mPaginationPaint);
// TODO(b/394355070) Verify RTL experience works correctly with visual updates
@@ -571,7 +567,6 @@
sTempRect.left = sTempRect.right - rectWidth;
}
- sTempRect.roundOut(sLastActiveRect);
return sTempRect;
}
@@ -595,7 +590,15 @@
@Override
public void getOutline(View view, Outline outline) {
if (mEntryAnimationRadiusFactors == null) {
- outline.setRoundRect(sLastActiveRect, mDotRadius);
+ // TODO(b/394355070): Verify Outline works correctly with visual updates
+ RectF activeRect = getActiveRect();
+ outline.setRoundRect(
+ (int) activeRect.left,
+ (int) activeRect.top,
+ (int) activeRect.right,
+ (int) activeRect.bottom,
+ mDotRadius
+ );
}
}
}
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 7e08c6e..b7efdec 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -210,30 +210,6 @@
}
}
- public static final Factory<ActivityContext> PIN_UNPIN_ITEM =
- (context, itemInfo, originalView) -> {
- // Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut
- // to pin.
- if (itemInfo.isPredictedItem()) {
- return null;
- }
- return new PinUnpinItem<>(context, itemInfo, originalView);
- };
-
- private static class PinUnpinItem<T extends ActivityContext> extends SystemShortcut<T> {
- PinUnpinItem(T target, ItemInfo itemInfo, @NonNull View originalView) {
- // TODO(b/375648361): Check the pin state of the item to determine if the pin or the
- // unpin option should be used.
- super(R.drawable.ic_pin, R.string.pin_to_taskbar, target,
- itemInfo, originalView);
- }
-
- @Override
- public void onClick(View view) {
- // TODO(b/375648361): Pin/Unpin the item here.
- }
- }
-
public static final Factory<ActivityContext> PRIVATE_PROFILE_INSTALL =
(context, itemInfo, originalView) -> {
if (originalView == null) {
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 943a913..e5105cd 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -214,6 +214,10 @@
ENABLE_TASKBAR_NAVBAR_UNIFICATION);
return response;
+ case TestProtocol.REQUEST_TASKBAR_SHOWN_ON_HOME:
+ response.putBoolean(TEST_INFO_RESPONSE_FIELD,
+ DisplayController.showLockedTaskbarOnHome(mContext));
+ return response;
case TestProtocol.REQUEST_NUM_ALL_APPS_COLUMNS:
response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
mDeviceProfile.numShownAllAppsColumns);
diff --git a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
index 0e06051..6483bd5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
@@ -6,7 +6,7 @@
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS
import android.appwidget.AppWidgetManager.EXTRA_HOST_ID
import android.content.Intent
-import android.platform.uiautomator_helpers.DeviceHelpers
+import android.platform.uiautomatorhelpers.DeviceHelpers
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.LauncherPrefs.Companion.APP_WIDGET_IDS
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 124c18f..41685d7 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -97,19 +97,22 @@
}
private void evaluateInLandscape() throws Throwable {
- if (Flags.oneGridSpecs()
- && WindowManagerProxy.INSTANCE.get(mTest.mTargetContext)
- .isTaskbarDrawnInProcess()) {
- mTest.executeOnLauncher(launcher -> LauncherPrefs.get(launcher)
- .put(FIXED_LANDSCAPE_MODE, true)
- );
- }
+ mTest.executeOnLauncher(launcher -> LauncherPrefs.get(launcher)
+ .put(FIXED_LANDSCAPE_MODE, shouldHaveFixedLandscape(launcher)));
mTest.mDevice.setOrientationLeft();
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher, true);
base.evaluate();
mTest.getDevice().pressHome();
}
+
+ private boolean shouldHaveFixedLandscape(Launcher launcher) {
+ return Flags.oneGridSpecs()
+ && !launcher.getDeviceProfile().isTablet
+ && !launcher.getDeviceProfile().isMultiDisplay
+ && WindowManagerProxy.INSTANCE.get(mTest.mTargetContext)
+ .isTaskbarDrawnInProcess();
+ }
};
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index de31c4d..e0d2f39 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -929,7 +929,11 @@
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
- waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ if (isTaskbarShownOnHome()) {
+ waitForSystemLauncherObject(TASKBAR_RES_ID);
+ } else {
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ }
return waitForLauncherObject(WORKSPACE_RES_ID);
}
@@ -961,7 +965,8 @@
waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
- if (is3PLauncher() && isTablet() && !isTransientTaskbar()) {
+ if ((is3PLauncher() && isTablet() && !isTransientTaskbar())
+ || isTaskbarShownOnHome()) {
waitForSystemLauncherObject(TASKBAR_RES_ID);
} else {
waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
@@ -2423,6 +2428,12 @@
.getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ /** Whether taskbar will be shown on home for current default display. */
+ public boolean isTaskbarShownOnHome() {
+ return getTestInfo(TestProtocol.REQUEST_TASKBAR_SHOWN_ON_HOME).getBoolean(
+ TEST_INFO_RESPONSE_FIELD);
+ }
+
public boolean isImeDocked() {
return getTestInfo(TestProtocol.REQUEST_TASKBAR_IME_DOCKED).getBoolean(
TestProtocol.TEST_INFO_RESPONSE_FIELD);
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index b4aaab7..d4e6d31 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -115,6 +115,23 @@
}
}
+ /**
+ * Opens the Home all apps page by clicking the taskbar all apps icon. To be used to open all
+ * apps when taskbar is visible on home.
+ */
+ public HomeAllApps openAllAppsOnHome() {
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to open home all apps from taskbar");
+ LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+
+ mLauncher.clickLauncherObject(mLauncher.waitForObjectInContainer(
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
+ getAllAppsButtonSelector()));
+
+ return mLauncher.getAllApps();
+ }
+ }
+
/** Opens the Taskbar all apps page with the meta keyboard shortcut. */
public TaskbarAllApps openAllAppsFromKeyboardShortcut() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {