Merge "[Test Week] add unit tests for InstallSessionTracker" into main
diff --git a/go/quickstep/res/values/styles.xml b/go/quickstep/res/values/styles.xml
index c659331..2524c76 100644
--- a/go/quickstep/res/values/styles.xml
+++ b/go/quickstep/res/values/styles.xml
@@ -16,7 +16,7 @@
-->
<resources>
<!-- App themes -->
- <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+ <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
<item name="overviewButtonTextColor">@color/go_overview_text_color</item>
<item name="overviewButtonIconColor">@color/go_overview_text_color</item>
<item name="overviewButtonBackgroundColor">@color/go_overview_button_color</item>
diff --git a/quickstep/res/color/all_set_bg_primary.xml b/quickstep/res/color/all_set_bg_primary.xml
index 4cf857d..013de7a 100644
--- a/quickstep/res/color/all_set_bg_primary.xml
+++ b/quickstep/res/color/all_set_bg_primary.xml
@@ -15,5 +15,5 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:color="?androidprv:attr/materialColorPrimaryContainer"/>
+ <item android:color="?attr/materialColorPrimaryContainer"/>
</selector>
diff --git a/quickstep/res/color/all_set_bg_tertiary.xml b/quickstep/res/color/all_set_bg_tertiary.xml
index e62b094..b58d61c 100644
--- a/quickstep/res/color/all_set_bg_tertiary.xml
+++ b/quickstep/res/color/all_set_bg_tertiary.xml
@@ -15,5 +15,5 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:color="?androidprv:attr/materialColorTertiary"/>
+ <item android:color="?attr/materialColorTertiary"/>
</selector>
diff --git a/quickstep/res/color/bubblebar_drop_target_bg_color.xml b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
index ca37c7f..bae8c4e 100644
--- a/quickstep/res/color/bubblebar_drop_target_bg_color.xml
+++ b/quickstep/res/color/bubblebar_drop_target_bg_color.xml
@@ -15,5 +15,5 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ <item android:alpha="0.35" android:color="?attr/materialColorPrimaryContainer" />
</selector>
\ No newline at end of file
diff --git a/quickstep/res/color/menu_item_hover_state_color.xml b/quickstep/res/color/menu_item_hover_state_color.xml
index 1dd7654..3c68789 100644
--- a/quickstep/res/color/menu_item_hover_state_color.xml
+++ b/quickstep/res/color/menu_item_hover_state_color.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:state_hovered="false" android:color="?androidprv:attr/materialColorSurfaceBright" />
- <item android:state_hovered="true" android:color="?androidprv:attr/materialColorSurfaceVariant" />
+ <item android:state_hovered="false" android:color="?attr/materialColorSurfaceBright" />
+ <item android:state_hovered="true" android:color="?attr/materialColorSurfaceVariant" />
</selector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
index 79e4318..f597cb5 100644
--- a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml
@@ -20,5 +20,5 @@
<solid android:color="@color/bubblebar_drop_target_bg_color" />
<stroke
android:width="1dp"
- android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ android:color="?attr/materialColorPrimaryContainer" />
</shape>
diff --git a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
index d722dd7..169e396 100644
--- a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
+++ b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
@@ -22,6 +22,6 @@
<solid android:color="@color/bubblebar_drop_target_bg_color" />
<stroke
android:width="1dp"
- android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ android:color="?attr/materialColorPrimaryContainer" />
</shape>
</inset>
diff --git a/quickstep/res/drawable/bg_floating_desktop_select.xml b/quickstep/res/drawable/bg_floating_desktop_select.xml
index d7df338..6481be4 100644
--- a/quickstep/res/drawable/bg_floating_desktop_select.xml
+++ b/quickstep/res/drawable/bg_floating_desktop_select.xml
@@ -19,5 +19,5 @@
android:shape="rectangle">
<corners android:radius="@dimen/rounded_button_radius" />
- <solid android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ <solid android:color="?attr/materialColorPrimaryContainer" />
</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml
index 143761f..0d12274 100644
--- a/quickstep/res/drawable/bg_overview_clear_all_button.xml
+++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml
@@ -21,7 +21,7 @@
<shape android:shape="rectangle"
android:tint="?colorButtonNormal">
<corners android:radius="@dimen/recents_clear_all_outline_radius" />
- <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+ <solid android:color="?attr/materialColorSurfaceBright"/>
</shape>
</item>
</ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
index 20c524d..9e9bb2b 100644
--- a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
+++ b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml
@@ -18,5 +18,5 @@
android:shape="rectangle">
<corners android:radius="@dimen/dialogCornerRadius" />
- <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
+ <solid android:color="?attr/materialColorSurfaceBright"/>
</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_wellbeing_toast.xml b/quickstep/res/drawable/bg_wellbeing_toast.xml
index 0b66849..418caae 100644
--- a/quickstep/res/drawable/bg_wellbeing_toast.xml
+++ b/quickstep/res/drawable/bg_wellbeing_toast.xml
@@ -16,6 +16,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="?androidprv:attr/materialColorSecondaryFixed" />
+ <solid android:color="?attr/materialColorSecondaryFixed" />
<corners android:radius="?android:attr/dialogCornerRadius" />
</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_chevron_down.xml b/quickstep/res/drawable/ic_chevron_down.xml
index f246cbc..b586e50 100644
--- a/quickstep/res/drawable/ic_chevron_down.xml
+++ b/quickstep/res/drawable/ic_chevron_down.xml
@@ -19,7 +19,7 @@
android:width="48dp"
android:height="48dp"
android:autoMirrored="true"
- android:tint="?androidprv:attr/materialColorOnSurface"
+ android:tint="?attr/materialColorOnSurface"
android:viewportHeight="48"
android:viewportWidth="48">
<group
diff --git a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
index 2a4f087..8180293 100644
--- a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
+++ b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml
@@ -17,6 +17,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
+ <solid android:color="?attr/materialColorSurfaceBright" />
<corners android:radius="@dimen/keyboard_quick_switch_task_view_radius" />
</shape>
diff --git a/quickstep/res/drawable/rotate_tutorial_warning.xml b/quickstep/res/drawable/rotate_tutorial_warning.xml
index d1117a4..90b7d64 100644
--- a/quickstep/res/drawable/rotate_tutorial_warning.xml
+++ b/quickstep/res/drawable/rotate_tutorial_warning.xml
@@ -21,6 +21,6 @@
android:viewportHeight="960"
android:viewportWidth="960">
<path
- android:fillColor="?androidprv:attr/materialColorOnSurface"
+ android:fillColor="?attr/materialColorOnSurface"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500L480,500Z" />
</vector>
diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index 38df756..613edac 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
index e4942ae..40d2322 100644
--- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
android:theme="@style/GestureTutorialActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?androidprv:attr/materialColorSurfaceContainer"
+ android:background="?attr/materialColorSurfaceContainer"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -163,7 +163,7 @@
android:layout_marginVertical="16dp"
android:text="@string/gesture_tutorial_action_button_label"
android:background="@drawable/gesture_tutorial_action_button_background"
- android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:backgroundTint="?attr/materialColorPrimary"
android:stateListAnimator="@null"
app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index 9144c7f..6a99a3b 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -24,7 +24,7 @@
android:forceHasOverlappingRendering="false"
android:gravity="center"
android:importantForAccessibility="noHideDescendants"
- android:textColor="?androidprv:attr/materialColorOnSecondaryFixed"
+ android:textColor="?attr/materialColorOnSecondaryFixed"
android:textSize="14sp"
android:autoSizeTextType="uniform"
android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml
index 668a2e1..7feb882 100644
--- a/quickstep/res/layout/gesture_tutorial_step_menu.xml
+++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml
@@ -20,7 +20,7 @@
android:theme="@style/GestureTutorialActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?androidprv:attr/materialColorSurfaceContainer"
+ android:background="?attr/materialColorSurfaceContainer"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -161,7 +161,7 @@
android:layout_marginVertical="16dp"
android:text="@string/gesture_tutorial_action_button_label"
android:background="@drawable/gesture_tutorial_action_button_background"
- android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:backgroundTint="?attr/materialColorPrimary"
android:stateListAnimator="@null"
app:layout_constraintTop_toBottomOf="@id/guideline"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index fb9bf99..36ece2a 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -26,7 +26,7 @@
android:importantForAccessibility="no"
android:autoMirrored="true"
android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
- android:background="?androidprv:attr/materialColorSurfaceBright">
+ android:background="?attr/materialColorSurfaceBright">
<!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
<!-- This anchor's bounds is in the expected location after rotations and translations are
diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
index c0ace9a..8f09176 100644
--- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml
@@ -23,7 +23,7 @@
android:importantForAccessibility="yes"
android:background="@drawable/keyboard_quick_switch_task_view_background"
android:clipToOutline="true"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
diff --git a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
index e48794e..c76a2e3 100644
--- a/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_textonly_taskview.xml
@@ -22,7 +22,7 @@
android:layout_height="@dimen/keyboard_quick_switch_taskview_height"
android:clipToOutline="true"
android:importantForAccessibility="yes"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline">
+ launcher:focusBorderColor="?attr/materialColorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
@@ -40,7 +40,7 @@
android:layout_width="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_height="@dimen/keyboard_quick_switch_recents_icon_size"
android:layout_marginBottom="8dp"
- android:tint="?androidprv:attr/materialColorOnSurface"
+ android:tint="?attr/materialColorOnSurface"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 2bba788..2420a46 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -43,7 +43,7 @@
android:layout_height="@dimen/keyboard_quick_switch_no_recent_items_icon_size"
android:layout_marginBottom="@dimen/keyboard_quick_switch_no_recent_items_icon_margin"
android:src="@drawable/view_carousel"
- android:tint="?androidprv:attr/materialColorOnSurface"
+ android:tint="?attr/materialColorOnSurface"
android:importantForAccessibility="no"
app:layout_constraintVertical_chainStyle="packed"
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 3380ea4..40f38f4 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -23,6 +23,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recents_clear_all"
- android:textColor="?androidprv:attr/materialColorOnSurface"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
+ android:textColor="?attr/materialColorOnSurface"
+ launcher:focusBorderColor="?attr/materialColorOutline"
android:textSize="14sp" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 46c1332..ac1a50a 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -25,8 +25,8 @@
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
- launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+ launcher:focusBorderColor="?attr/materialColorOutline"
+ launcher:hoverBorderColor="?attr/materialColorPrimary">
<include layout="@layout/task_thumbnail_deprecated" />
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 453057c..64aa7e1 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -25,8 +25,8 @@
android:clipChildren="true"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
- launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary"
+ launcher:focusBorderColor="?attr/materialColorOutline"
+ launcher:hoverBorderColor="?attr/materialColorPrimary"
android:clipToPadding="true"
android:padding="0.1dp">
<!-- Setting a padding of 0.1 dp since android:clipToPadding needs a non-zero value for
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 708aa3c..da2b29f 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -30,8 +30,8 @@
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
- launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
- launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
+ launcher:focusBorderColor="?attr/materialColorOutline"
+ launcher:hoverBorderColor="?attr/materialColorPrimary">
<include layout="@layout/task_thumbnail_deprecated"/>
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index 34640e6..b1fe89e 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -23,6 +23,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
+ android:scaleType="matrix"
android:visibility="gone"/>
<com.android.quickstep.task.thumbnail.LiveTileView
diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml
index ffe2401..5218de0 100644
--- a/quickstep/res/layout/task_view_menu_option.xml
+++ b/quickstep/res/layout/task_view_menu_option.xml
@@ -31,7 +31,7 @@
android:layout_height="@dimen/system_shortcut_icon_size"
android:layout_marginStart="@dimen/task_menu_option_start_margin"
android:layout_gravity="center_horizontal"
- android:backgroundTint="?androidprv:attr/materialColorOnSurface"/>
+ android:backgroundTint="?attr/materialColorOnSurface"/>
<TextView
style="@style/BaseIcon"
@@ -40,7 +40,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/task_menu_option_text_start_margin"
android:textSize="14sp"
- android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:textColor="?attr/materialColorOnSurface"
android:focusable="false"
android:gravity="start"
android:ellipsize="end" />
diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml
index 94100ba..2052446 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -26,6 +26,6 @@
<!-- Turn on work apps button -->
<color name="work_turn_on_stroke">?androidprv:attr/colorAccentSecondaryVariant</color>
- <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
- <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+ <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
+ <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values-night/styles.xml b/quickstep/res/values-night/styles.xml
index 401351f..2cb633a 100644
--- a/quickstep/res/values-night/styles.xml
+++ b/quickstep/res/values-night/styles.xml
@@ -82,16 +82,16 @@
<style name="GestureTutorialActivity" parent="@style/AppTheme">
<item name="background">@android:color/transparent</item>
<item name="tutorialSubtitle">@android:color/white</item>
- <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
- <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixedDim</item>
- <item name="surfaceHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
- <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixed</item>
- <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixedDim</item>
- <item name="surfaceBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
- <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixed</item>
- <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
- <item name="surfaceOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
- <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixed</item>
+ <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+ <item name="onSurfaceHome">?attr/materialColorPrimaryFixedDim</item>
+ <item name="surfaceHome">?attr/materialColorOnPrimaryFixedVariant</item>
+ <item name="secondaryHome">?attr/materialColorOnPrimaryFixed</item>
+ <item name="onSurfaceBack">?attr/materialColorTertiaryFixedDim</item>
+ <item name="surfaceBack">?attr/materialColorOnTertiaryFixedVariant</item>
+ <item name="secondaryBack">?attr/materialColorOnTertiaryFixed</item>
+ <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
+ <item name="surfaceOverview">?attr/materialColorOnSecondaryFixedVariant</item>
+ <item name="secondaryOverview">?attr/materialColorOnSecondaryFixed</item>
</style>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 0f997f9..0bb971e 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -95,6 +95,6 @@
<!-- Turn on work apps button -->
<color name="work_turn_on_stroke">?androidprv:attr/colorAccentPrimaryVariant</color>
- <color name="work_fab_bg_color">?androidprv:attr/materialColorPrimaryFixedDim</color>
- <color name="work_fab_icon_color">?androidprv:attr/materialColorOnPrimaryFixed</color>
+ <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
+ <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index d4f66e2..867ce17 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -449,11 +449,13 @@
<dimen name="bubblebar_icon_size_small">32dp</dimen>
<dimen name="bubblebar_icon_size">36dp</dimen>
+ <dimen name="bubblebar_icon_size_persistent_taskbar">28dp</dimen>
<dimen name="bubblebar_badge_size">24dp</dimen>
<dimen name="bubblebar_icon_overlap">12dp</dimen>
<dimen name="bubblebar_overflow_inset">16dp</dimen>
<dimen name="bubblebar_icon_spacing">6dp</dimen>
<dimen name="bubblebar_icon_spacing_large">8dp</dimen>
+ <dimen name="bubblebar_icon_spacing_persistent_taskbar">@dimen/bubblebar_icon_spacing</dimen>
<dimen name="bubblebar_expanded_icon_spacing">12dp</dimen>
<dimen name="bubblebar_icon_elevation">1dp</dimen>
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
new file mode 100644
index 0000000..3091d9e
--- /dev/null
+++ b/quickstep/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+-->
+<resources>
+ <!-- Used for A11y actions for bubble bar -->
+ <item type="id" name="action_move_left" />
+ <item type="id" name="action_move_right" />
+ <item type="id" name="action_dismiss_all" />
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 340d25b..98a2783 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -342,4 +342,10 @@
<string name="bubble_bar_bubble_description"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string>
<!-- Content description for bubble bar when it has multiple bubbles. [CHAR_LIMIT=NONE] -->
<string name="bubble_bar_description_multiple_bubbles"><xliff:g id="bubble_bar_bubble_description" example="some title from YouTube">%1$s</xliff:g> and <xliff:g id="bubble_count" example="4">%2$d</xliff:g> more</string>
+ <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_move_left">Move left</string>
+ <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_move_right">Move right</string>
+ <!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_dismiss_all">Dismiss all</string>
</resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 952505a..1f4720c 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -124,7 +124,7 @@
<style name="TextAppearance.GestureTutorial.ButtonLabel"
parent="TextAppearance.GestureTutorial.CallToAction">
<item name="android:gravity">center</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+ <item name="android:textColor">?attr/materialColorOnPrimary</item>
<item name="android:letterSpacing">0.02</item>
<item name="android:textSize">16sp</item>
<item name="android:textAllCaps">false</item>
@@ -268,53 +268,59 @@
<style name="KeyboardQuickSwitchText">
<item name="fontFamily">google-sans-text</item>
<item name="android:textSize">14sp</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="android:textColor">?attr/materialColorOnSurface</item>
<item name="lineHeight">20sp</item>
</style>
<style name="KeyboardQuickSwitchText.OnBackground" parent="KeyboardQuickSwitchText">
- <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="android:textColor">?attr/materialColorOnSurface</item>
</style>
<style name="GestureTutorialActivity" parent="@style/AppTheme">
<item name="background">@android:color/transparent</item>
<item name="tutorialSubtitle">@android:color/black</item>
- <item name="surfaceContainer">?androidprv:attr/materialColorSurfaceContainer</item>
- <item name="onSurfaceHome">?androidprv:attr/materialColorPrimaryFixed</item>
+ <item name="surfaceContainer">?attr/materialColorSurfaceContainer</item>
+ <item name="onSurfaceHome">?attr/materialColorPrimaryFixed</item>
<item name="surfaceHome">@android:color/system_accent1_300</item>
- <item name="secondaryHome">?androidprv:attr/materialColorOnPrimaryFixedVariant</item>
- <item name="onSurfaceBack">?androidprv:attr/materialColorTertiaryFixed</item>
+ <item name="secondaryHome">?attr/materialColorOnPrimaryFixedVariant</item>
+ <item name="onSurfaceBack">?attr/materialColorTertiaryFixed</item>
<item name="surfaceBack">@android:color/system_accent3_300</item>
- <item name="secondaryBack">?androidprv:attr/materialColorOnTertiaryFixedVariant</item>
- <item name="onSurfaceOverview">?androidprv:attr/materialColorPrimaryFixed</item>
+ <item name="secondaryBack">?attr/materialColorOnTertiaryFixedVariant</item>
+ <item name="onSurfaceOverview">?attr/materialColorPrimaryFixed</item>
<item name="surfaceOverview">@android:color/system_accent2_300</item>
- <item name="secondaryOverview">?androidprv:attr/materialColorOnSecondaryFixedVariant</item>
+ <item name="secondaryOverview">?attr/materialColorOnSecondaryFixedVariant</item>
</style>
<style name="rotate_prompt_title" parent="TextAppearance.GestureTutorial.Dialog.Title">
- <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="android:textColor">?attr/materialColorOnSurface</item>
</style>
<style name="rotate_prompt_subtitle" parent="TextAppearance.GestureTutorial.Dialog.Subtitle">
- <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+ <item name="android:textColor">?attr/materialColorOnSurfaceVariant</item>
</style>
<style name="ArrowTipTaskbarStyle">
- <item name="arrowTipBackground">?androidprv:attr/materialColorSurfaceContainer</item>
- <item name="arrowTipTextColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="arrowTipBackground">?attr/materialColorSurfaceContainer</item>
+ <item name="arrowTipTextColor">?attr/materialColorOnSurface</item>
</style>
<style name="IconAppChipMenuTextStyle">
<item name="android:fontFamily">google-sans-text-medium</item>
<item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="android:textColor">?attr/materialColorOnSurface</item>
<item name="android:letterSpacing">0.025</item>
<item name="android:lineHeight">20sp</item>
</style>
- <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
- <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+ <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+ <item name="widgetsTheme">@style/WidgetContainerTheme</item>
<item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
</style>
</resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5e5487b..44601b7 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -356,6 +356,14 @@
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
+ // Prepare taskbar for animation synchronization. This needs to happen here before any
+ // app transition is created.
+ LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+ if (enableScalingRevealHomeAnimation() && taskbarController != null) {
+ taskbarController.setIgnoreInAppFlagForSync(true);
+ onEndCallback.add(() -> taskbarController.setIgnoreInAppFlagForSync(false));
+ }
+
IBinder cookie = mAppLaunchRunner.supportsReturnTransition()
? ((ContainerAnimationRunner) mAppLaunchRunner).getCookie() : null;
addLaunchCookie(cookie, (ItemInfo) v.getTag(), options);
@@ -1924,6 +1932,21 @@
anim.addListener(mForceInvisibleListener);
}
+ // Syncs the app launch animation and taskbar stash animation (if exists).
+ if (enableScalingRevealHomeAnimation()) {
+ LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
+ if (taskbarController != null) {
+ taskbarController.setIgnoreInAppFlagForSync(false);
+
+ if (launcherClosing) {
+ Animator taskbar = taskbarController.createAnimToApp();
+ if (taskbar != null) {
+ anim.play(taskbar);
+ }
+ }
+ }
+ }
+
result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy,
skipFirstFrame);
}
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 44d8a5c..0b18633 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -28,6 +28,7 @@
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
import android.content.ClipDescription;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -46,11 +47,11 @@
import com.android.launcher3.model.WidgetPredictionsRequester;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
-import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
@@ -59,9 +60,8 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
/** An Activity that can host Launcher's widget picker. */
public class WidgetPickerActivity extends BaseActivity {
@@ -110,7 +110,8 @@
private WidgetsModel mModel;
private LauncherAppState mApp;
private WidgetPredictionsRequester mWidgetPredictionsRequester;
- private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+ private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {
+ });
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
@@ -128,9 +129,23 @@
/** A set of user ids that should be filtered out from the selected widgets. */
@NonNull
Set<Integer> mFilteredUserIds = new HashSet<>();
+
@Nullable
private WidgetsFullSheet mWidgetSheet;
+ private final Predicate<WidgetItem> mWidgetsFilter = widget -> {
+ final WidgetAcceptabilityVerdict verdict =
+ isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
+ verdict.maybeLogVerdict();
+ return verdict.isAcceptable;
+ };
+ private final Predicate<WidgetItem> mDefaultWidgetsFilter = widget -> {
+ final WidgetAcceptabilityVerdict verdict =
+ isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
+ verdict.maybeLogVerdict();
+ return verdict.isAcceptable;
+ };
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -271,47 +286,41 @@
};
}
- /** Updates the model with widgets, applies filters and launches the widgets sheet once
- * widgets are available */
+ /**
+ * Updates the model with widgets, applies filters and launches the widgets sheet once
+ * widgets are available
+ */
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
LauncherAppState app = LauncherAppState.getInstance(this);
+ Context context = app.getContext();
+
mModel.update(app, null);
- final List<WidgetsListBaseEntry> allWidgets =
- mModel.getFilteredWidgetsListForPicker(
- app.getContext(),
- /*widgetItemFilter=*/ widget -> {
- final WidgetAcceptabilityVerdict verdict =
- isWidgetAcceptable(widget);
- verdict.maybeLogVerdict();
- return verdict.isAcceptable;
- }
- );
- bindWidgets(allWidgets);
+ bindWidgets(mModel.getWidgetsByPackageItem());
// Open sheet once widgets are available, so that it doesn't interrupt the open
// animation.
openWidgetsSheet();
if (mUiSurface != null) {
- Map<ComponentKey, WidgetItem> allWidgetItems = allWidgets.stream()
- .filter(entry -> entry instanceof WidgetsListContentEntry)
- .flatMap(entry -> entry.mWidgets.stream())
- .distinct()
- .collect(Collectors.toMap(
- widget -> new ComponentKey(widget.componentName, widget.user),
- Function.identity()
- ));
mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
- mUiSurface, allWidgetItems);
+ mUiSurface, mModel.getWidgetsByComponentKey());
mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
}
});
}
- private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
- MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
+ private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets) {
+ WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
+ mApp.getContext());
+
+ final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mWidgetsFilter);
+ final List<WidgetsListBaseEntry> defaultWidgets =
+ shouldShowDefaultWidgets() ? builder.build(widgets,
+ mDefaultWidgetsFilter) : List.of();
+
+ MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(allWidgets, defaultWidgets));
}
- private void openWidgetsSheet() {
+ private void openWidgetsSheet() {
MAIN_EXECUTOR.execute(() -> {
mWidgetSheet = WidgetsFullSheet.show(this, true);
mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
@@ -378,9 +387,15 @@
mActiveOnBackAnimationCallback.onBackCancelled();
mActiveOnBackAnimationCallback = null;
}
- };
+ }
- private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
+ private boolean shouldShowDefaultWidgets() {
+ // If optional filters such as size filter are present, we display them as default widgets.
+ return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
+ }
+
+ private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget,
+ boolean applySizeFilter) {
final AppWidgetProviderInfo info = widget.widgetInfo;
if (info == null) {
return rejectWidget(widget, "shortcut");
@@ -401,61 +416,63 @@
info.widgetCategory);
}
- if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
- // Accept the widget if the desired dimensions are unspecified.
- return acceptWidget(widget);
- }
-
- final boolean isHorizontallyResizable =
- (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
- if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
- if (info.maxResizeWidth > 0
- && info.maxResizeWidth >= info.minWidth
- && info.maxResizeWidth < mDesiredWidgetWidth) {
- return rejectWidget(
- widget,
- "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
- info.maxResizeWidth,
- mDesiredWidgetWidth);
+ if (applySizeFilter) {
+ if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
+ // Accept the widget if the desired dimensions are unspecified.
+ return acceptWidget(widget);
}
- final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
- if (minWidth > mDesiredWidgetWidth) {
- return rejectWidget(
- widget,
- "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
- info.minWidth,
- info.minResizeWidth,
- mDesiredWidgetWidth);
- }
- }
+ final boolean isHorizontallyResizable =
+ (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
+ if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
+ if (info.maxResizeWidth > 0
+ && info.maxResizeWidth >= info.minWidth
+ && info.maxResizeWidth < mDesiredWidgetWidth) {
+ return rejectWidget(
+ widget,
+ "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+ info.maxResizeWidth,
+ mDesiredWidgetWidth);
+ }
- final boolean isVerticallyResizable =
- (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
- if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
- if (info.maxResizeHeight > 0
- && info.maxResizeHeight >= info.minHeight
- && info.maxResizeHeight < mDesiredWidgetHeight) {
- return rejectWidget(
- widget,
- "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
- info.maxResizeHeight,
- mDesiredWidgetHeight);
+ final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+ if (minWidth > mDesiredWidgetWidth) {
+ return rejectWidget(
+ widget,
+ "min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
+ info.minWidth,
+ info.minResizeWidth,
+ mDesiredWidgetWidth);
+ }
}
- final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
- if (minHeight > mDesiredWidgetHeight) {
- return rejectWidget(
- widget,
- "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
- info.minHeight,
- info.minResizeHeight,
- mDesiredWidgetHeight);
- }
- }
+ final boolean isVerticallyResizable =
+ (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
+ if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
+ if (info.maxResizeHeight > 0
+ && info.maxResizeHeight >= info.minHeight
+ && info.maxResizeHeight < mDesiredWidgetHeight) {
+ return rejectWidget(
+ widget,
+ "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+ info.maxResizeHeight,
+ mDesiredWidgetHeight);
+ }
- if (!isHorizontallyResizable || !isVerticallyResizable) {
- return rejectWidget(widget, "not resizeable");
+ final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+ if (minHeight > mDesiredWidgetHeight) {
+ return rejectWidget(
+ widget,
+ "min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
+ info.minHeight,
+ info.minResizeHeight,
+ mDesiredWidgetHeight);
+ }
+ }
+
+ if (!isHorizontallyResizable || !isVerticallyResizable) {
+ return rejectWidget(widget, "not resizeable");
+ }
}
return acceptWidget(widget);
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 84c2ed2..7a8b58e 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,12 +31,12 @@
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.FloatingHeaderRow;
import com.android.launcher3.allapps.FloatingHeaderView;
+import com.android.launcher3.util.Themes;
/**
* A view which shows a horizontal divider
@@ -84,10 +84,9 @@
getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
};
- mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
+ mStrokeColor = Themes.getAttrColor(context, R.attr.materialColorOutlineVariant);
- mAllAppsLabelTextColor = ContextCompat.getColor(context,
- R.color.material_color_on_surface_variant);
+ mAllAppsLabelTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurfaceVariant);
mAccessibilityManager = AccessibilityManager.getInstance(context);
setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 62cc0bb..6c7fe5b 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -19,10 +19,9 @@
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.os.Debug;
-import android.os.SystemProperties;
import android.util.Log;
import android.view.View;
@@ -145,7 +144,7 @@
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
- if (!enableDesktopWindowingWallpaperActivity() && wasVisible != isVisible) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher) && wasVisible != isVisible) {
// TODO: b/333533253 - Remove after flag rollout
if (mVisibleDesktopTasksCount > 0) {
setLauncherViewsVisibility(View.INVISIBLE);
@@ -189,7 +188,7 @@
notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow);
}
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
// TODO: b/333533253 - Clean up after flag rollout
@@ -289,7 +288,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void setLauncherViewsVisibility(int visibility) {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
@@ -314,7 +313,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherPaused() {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
@@ -331,7 +330,7 @@
* TODO: b/333533253 - Remove after flag rollout
*/
private void markLauncherResumed() {
- if (enableDesktopWindowingWallpaperActivity()) {
+ if (WALLPAPER_ACTIVITY.isEnabled(mLauncher)) {
return;
}
if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 779009a..93a023d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -19,8 +19,9 @@
import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
+import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.animation.Animator;
import android.animation.AnimatorSet;
@@ -213,7 +214,7 @@
DesktopVisibilityController desktopController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (!enableDesktopWindowingWallpaperActivity()
+ if (!WALLPAPER_ACTIVITY.isEnabled(mLauncher)
&& desktopController != null
&& desktopController.areDesktopTasksVisible()) {
// TODO: b/333533253 - Remove after flag rollout
@@ -256,6 +257,24 @@
return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
}
+ /**
+ * Create Taskbar animation to be played alongside the Launcher app launch animation.
+ */
+ public @Nullable Animator createAnimToApp() {
+ TaskbarStashController stashController = mControllers.taskbarStashController;
+ stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true);
+ return stashController.createApplyStateAnimator(stashController.getStashDuration());
+ }
+
+ /**
+ * Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing.
+ * This is needed because taskbar gets a signal to stash before we actually start the
+ * app launch animation.
+ */
+ public void setIgnoreInAppFlagForSync(boolean enabled) {
+ mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
+ }
+
public void updateTaskbarLauncherStateGoingHome() {
mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, true);
mTaskbarLauncherStateController.applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3048243..800c594 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -315,6 +315,7 @@
new TaskbarTranslationController(this),
new TaskbarSpringOnStashController(this),
new TaskbarRecentAppsController(
+ this,
RecentsModel.INSTANCE.get(this),
LauncherActivityInterface.INSTANCE::getDesktopVisibilityController),
TaskbarEduTooltipController.newInstance(this),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index 7f9d8a3..19e9872 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -53,8 +53,7 @@
private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
- private val backgroundColor =
- Themes.getAttrColor(context, com.android.internal.R.attr.materialColorSurfaceBright)
+ private val backgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceBright)
private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 5c08116..2cb950c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar
+import android.content.Context
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.model.data.ItemInfo
@@ -26,8 +27,8 @@
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
-import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE
import java.io.PrintWriter
/**
@@ -36,6 +37,7 @@
* - When in Desktop Mode: show the currently running (open) Tasks
*/
class TaskbarRecentAppsController(
+ context: Context,
private val recentsModel: RecentsModel,
// Pass a provider here instead of the actual DesktopVisibilityController instance since that
// instance might not be available when this constructor is called.
@@ -44,7 +46,7 @@
// TODO(b/335401172): unify DesktopMode checks in Launcher.
var canShowRunningApps =
- enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+ DESKTOP_WINDOWING_MODE.isEnabled(context) && enableDesktopWindowingTaskbarRunningApps()
@VisibleForTesting
set(isEnabledFromTest) {
field = isEnabledFromTest
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 430c003..0c5ad42 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -95,6 +95,7 @@
public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,...
public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
+ public static final int FLAG_IGNORE_IN_APP = 1 << 12; // used to sync with app launch animation
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -1263,6 +1264,11 @@
*/
@Nullable
public Animator createSetStateAnimator(long flags, long duration) {
+ // We do this when we want to synchronize the app launch and taskbar stash animations.
+ if (hasAnyFlag(FLAG_IGNORE_IN_APP) && hasAnyFlag(flags, FLAG_IN_APP)) {
+ flags = flags & ~FLAG_IN_APP;
+ }
+
boolean isStashed = mStashCondition.test(flags);
if (DEBUG) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 7426dc7..5be0171 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,7 +33,6 @@
import android.annotation.BinderThread;
import android.annotation.Nullable;
-import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
@@ -463,15 +462,6 @@
/** Tells WMShell to show the currently selected bubble. */
public void showSelectedBubble() {
if (getSelectedBubbleKey() != null) {
- if (mSelectedBubble instanceof BubbleBarBubble) {
- // Because we've visited this bubble, we should suppress the notification.
- // This is updated on WMShell side when we show the bubble, but that update isn't
- // passed to launcher, instead we apply it directly here.
- BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
- info.setFlags(
- info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
- mSelectedBubble.getView().updateDotVisibility(true /* animate */);
- }
mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen();
mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop);
} else {
@@ -645,8 +635,8 @@
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{
- com.android.internal.R.attr.materialColorOnPrimaryFixed,
- com.android.internal.R.attr.materialColorPrimaryFixed
+ R.attr.materialColorOnPrimaryFixed,
+ R.attr.materialColorPrimaryFixed
});
int overflowIconColor = ta.getColor(0, Color.WHITE);
int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index fd989b1..fab7975 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -30,6 +30,7 @@
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.LayoutDirection;
@@ -38,6 +39,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.SpringForce;
@@ -240,6 +242,10 @@
BubbleView firstBubble = (BubbleView) getChildAt(0);
mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
}
+ // If the bar was just expanded, remove the dot from the selected bubble.
+ if (mIsBarExpanded && mSelectedBubbleView != null) {
+ mSelectedBubbleView.markSeen();
+ }
updateWidth();
},
/* onUpdate= */ animator -> {
@@ -363,6 +369,47 @@
}
}
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ // Always show only expand action as the menu is only for collapsed bubble bar
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
+ getResources().getString(R.string.bubble_bar_action_dismiss_all)));
+ if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+ getResources().getString(R.string.bubble_bar_action_move_right)));
+ } else {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+ getResources().getString(R.string.bubble_bar_action_move_left)));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action,
+ @androidx.annotation.Nullable Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+ mController.expandBubbleBar();
+ return true;
+ }
+ if (action == R.id.action_dismiss_all) {
+ mController.dismissBubbleBar();
+ return true;
+ }
+ if (action == R.id.action_move_left) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+ return true;
+ }
+ if (action == R.id.action_move_right) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+ return true;
+ }
+ return false;
+ }
+
@SuppressLint("RtlHardcoded")
private void onBubbleBarLocationChanged() {
final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
@@ -665,7 +712,7 @@
}
/** Add a new bubble to the bubble bar. */
- public void addBubble(View bubble) {
+ public void addBubble(BubbleView bubble) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
Gravity.LEFT);
if (isExpanded()) {
@@ -673,6 +720,7 @@
bubble.setScaleX(0f);
bubble.setScaleY(0f);
addView(bubble, 0, lp);
+ bubble.showDotIfNeeded(/* animate= */ false);
mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -753,7 +801,6 @@
listener);
}
- // TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
@@ -825,6 +872,23 @@
updateBubbleAccessibilityStates();
updateContentDescription();
mDismissedByDragBubbleView = null;
+ updateNotificationDotsIfCollapsed();
+ }
+
+ private void updateNotificationDotsIfCollapsed() {
+ if (isExpanded()) {
+ return;
+ }
+ for (int i = 0; i < getChildCount(); i++) {
+ BubbleView bubbleView = (BubbleView) getChildAt(i);
+ // when we're collapsed, the first bubble should show the dot if it has it. the rest of
+ // the bubbles should hide their dots.
+ if (i == 0 && bubbleView.hasUnseenContent()) {
+ bubbleView.showDotIfNeeded(/* animate= */ true);
+ } else {
+ bubbleView.hideDot();
+ }
+ }
}
private void updateWidth() {
@@ -865,7 +929,6 @@
float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
// When translating X & Y the scale is ignored, so need to deduct it from the translations
final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift();
- final boolean animate = getVisibility() == VISIBLE;
final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
// elevation state is opposite to widthState - when expanded all icons are flat
float elevationState = (1 - widthState);
@@ -897,10 +960,10 @@
bv.setZ(fullElevationForChild * elevationState);
// only update the dot scale if we're expanding or collapsing
- // TODO b/351904597: update the dot for the first bubble after removal and reorder
- // since those might happen when the bar is collapsed and will need their dot back
if (mWidthAnimator.isRunning()) {
- bv.setDotScale(widthState);
+ // The dot for the selected bubble scales in the opposite direction of the expansion
+ // animation.
+ bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState);
}
if (mIsBarExpanded) {
@@ -1025,6 +1088,7 @@
}
updateBubblesLayoutProperties(mBubbleBarLocation);
updateContentDescription();
+ updateNotificationDotsIfCollapsed();
}
}
@@ -1049,6 +1113,14 @@
if (mBubbleAnimator == null) {
updateArrowForSelected(previouslySelectedBubble != null);
}
+ if (view != null) {
+ if (isExpanded()) {
+ view.markSeen();
+ } else {
+ // when collapsed, the selected bubble should show the dot if it has it
+ view.showDotIfNeeded(/* animate= */ true);
+ }
+ }
}
/**
@@ -1316,6 +1388,7 @@
public void dump(PrintWriter pw) {
pw.println("BubbleBarView state:");
pw.println(" visibility: " + getVisibility());
+ pw.println(" alpha: " + getAlpha());
pw.println(" translation Y: " + getTranslationY());
pw.println(" bubbles in bar (childCount = " + getChildCount() + ")");
for (BubbleView bubbleView: getBubbles()) {
@@ -1351,5 +1424,14 @@
/** Notifies the controller that the bubble bar was touched while it was animating. */
void onBubbleBarTouchedWhileAnimating();
+
+ /** Requests the controller to expand bubble bar */
+ void expandBubbleBar();
+
+ /** Requests the controller to dismiss the bubble bar */
+ void dismissBubbleBar();
+
+ /** Requests the controller to update bubble bar location to the given value */
+ void updateBubbleBarLocation(BubbleBarLocation location);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 24b9139..74a673b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -31,6 +31,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -62,6 +63,7 @@
private final TaskbarActivityContext mActivity;
private final BubbleBarView mBarView;
private int mIconSize;
+ private int mBubbleBarPadding;
// Initialized in init.
private BubbleStashController mBubbleStashController;
@@ -110,13 +112,12 @@
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
-
+ onBubbleBarConfigurationChanged(/* animate= */ false);
mActivity.addOnDeviceProfileChangeListener(
- dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true));
- updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false);
+ dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
mBubbleBarScale.updateValue(1f);
- mBubbleClickListener = v -> onBubbleClicked(v);
- mBubbleBarClickListener = v -> onBubbleBarClicked();
+ mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
+ mBubbleBarClickListener = v -> expandBubbleBar();
mBubbleDragController.setupBubbleBarView(mBarView);
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener(
@@ -136,11 +137,27 @@
public void onBubbleBarTouchedWhileAnimating() {
BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
}
+
+ @Override
+ public void expandBubbleBar() {
+ BubbleBarViewController.this.expandBubbleBar();
+ }
+
+ @Override
+ public void dismissBubbleBar() {
+ onDismissAllBubbles();
+ }
+
+ @Override
+ public void updateBubbleBarLocation(BubbleBarLocation location) {
+ mBubbleBarController.updateBubbleBarLocation(location);
+ }
});
}
- private void onBubbleClicked(View v) {
- BubbleBarItem bubble = ((BubbleView) v).getBubble();
+ private void onBubbleClicked(BubbleView bubbleView) {
+ bubbleView.markSeen();
+ BubbleBarItem bubble = bubbleView.getBubble();
if (bubble == null) {
Log.e(TAG, "bubble click listener, bubble was null");
}
@@ -160,7 +177,7 @@
mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
}
- private void onBubbleBarClicked() {
+ private void expandBubbleBar() {
if (mShouldShowEducation) {
mShouldShowEducation = false;
// Get the bubble bar bounds on screen
@@ -334,27 +351,60 @@
// Modifying view related properties.
//
- private void updateBubbleBarIconSize(int newIconSize, boolean animate) {
+ /** Notifies controller of configuration change, so bubble bar can be adjusted */
+ public void onBubbleBarConfigurationChanged(boolean animate) {
+ int newIconSize;
+ int newPadding;
Resources res = mActivity.getResources();
+ if (mBubbleStashController.isBubblesShowingOnHome()) {
+ newIconSize = getBubbleBarIconSizeFromDeviceProfile(res);
+ newPadding = getBubbleBarPaddingFromDeviceProfile(res);
+ } else {
+ // the bubble bar is shown inside the persistent task bar, use preset sizes
+ newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+ newPadding = res.getDimensionPixelSize(
+ R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+ }
+ updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
+ }
+
+
+ private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
DisplayMetrics dm = res.getDisplayMetrics();
float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_SMALL_DP, dm);
float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
APP_ICON_MEDIUM_DP, dm);
- float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- APP_ICON_LARGE_DP, dm);
float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f;
- float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
- mIconSize = newIconSize <= smallMediumThreshold
+ int taskbarIconSize = deviceProfile.taskbarIconSize;
+ return taskbarIconSize <= smallMediumThreshold
? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) :
res.getDimensionPixelSize(R.dimen.bubblebar_icon_size);
- float bubbleBarPadding = newIconSize >= mediumLargeThreshold
+
+ }
+
+ private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
+ DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+ DisplayMetrics dm = res.getDisplayMetrics();
+ float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ APP_ICON_MEDIUM_DP, dm);
+ float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ APP_ICON_LARGE_DP, dm);
+ float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f;
+ return deviceProfile.taskbarIconSize >= mediumLargeThreshold
? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) :
res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+ }
+
+ private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) {
+ if (mIconSize == iconSize && mBubbleBarPadding == padding) return;
+ mIconSize = iconSize;
+ mBubbleBarPadding = padding;
if (animate) {
- mBarView.animateBubbleBarIconSize(mIconSize, bubbleBarPadding);
+ mBarView.animateBubbleBarIconSize(iconSize, padding);
} else {
- mBarView.setIconSizeAndPadding(mIconSize, bubbleBarPadding);
+ mBarView.setIconSizeAndPadding(iconSize, padding);
}
}
@@ -574,17 +624,17 @@
}
/**
- * Called when bubble was dragged into the dismiss target. Notifies System
+ * Called when given bubble was dismissed. Notifies SystemUI
* @param bubble dismissed bubble item
*/
- public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+ public void onDismissBubble(@NonNull BubbleBarItem bubble) {
mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
}
/**
- * Called when bubble stack was dragged into the dismiss target
+ * Called when bubble stack was dismissed
*/
- public void onDismissAllBubblesWhileDragging() {
+ public void onDismissAllBubbles() {
mSystemUiProxy.removeAllBubbles();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index a6096e2..6a63da8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -143,10 +143,10 @@
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
- mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+ mBubbleBarViewController.onDismissBubble(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
- mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+ mBubbleBarViewController.onDismissAllBubbles();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 4c468bb..acb6b4e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar.bubbles;
import android.annotation.Nullable;
+import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -35,6 +36,7 @@
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
@@ -217,9 +219,9 @@
}
void updateDotVisibility(boolean animate) {
- final float targetScale = shouldDrawDot() ? 1f : 0f;
+ final float targetScale = hasUnseenContent() ? 1f : 0f;
if (animate) {
- animateDotScale();
+ animateDotScale(targetScale);
} else {
mDotScale = targetScale;
mAnimatingToDotScale = targetScale;
@@ -241,18 +243,27 @@
mAppIcon.setVisibility(show ? VISIBLE : GONE);
}
- /** Whether the dot indicating unseen content in a bubble should be shown. */
- private boolean shouldDrawDot() {
- boolean bubbleHasUnseenContent = mBubble != null
+ boolean hasUnseenContent() {
+ return mBubble != null
&& mBubble instanceof BubbleBarBubble
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
- // Always render the dot if it's animating, since it could be animating out. Otherwise, show
- // it if the bubble wants to show it, and we aren't suppressing it.
- return bubbleHasUnseenContent || mDotIsAnimating;
}
- /** How big the dot should be, fraction from 0 to 1. */
- void setDotScale(float fraction) {
+ /**
+ * Used to determine if we can skip drawing frames.
+ *
+ * <p>Generally we should draw the dot when it is requested to be shown and there is unseen
+ * content. But when the dot is removed, we still want to draw frames so that it can be scaled
+ * out.
+ */
+ private boolean shouldDrawDot() {
+ // if there's no dot there's nothing to draw, unless the dot was removed and we're in the
+ // middle of removing it
+ return hasUnseenContent() || mDotIsAnimating;
+ }
+
+ /** Updates the dot scale to the specified fraction from 0 to 1. */
+ private void setDotScale(float fraction) {
if (!shouldDrawDot()) {
return;
}
@@ -260,11 +271,41 @@
invalidate();
}
- /**
- * Animates the dot to the given scale.
- */
- private void animateDotScale() {
- float toScale = shouldDrawDot() ? 1f : 0f;
+ void showDotIfNeeded(float fraction) {
+ if (!hasUnseenContent()) {
+ return;
+ }
+ setDotScale(fraction);
+ }
+
+ void showDotIfNeeded(boolean animate) {
+ // only show the dot if we have unseen content
+ if (!hasUnseenContent()) {
+ return;
+ }
+ if (animate) {
+ animateDotScale(1f);
+ } else {
+ setDotScale(1f);
+ }
+ }
+
+ void hideDot() {
+ animateDotScale(0f);
+ }
+
+ /** Marks this bubble such that it no longer has unseen content, and hides the dot. */
+ void markSeen() {
+ if (mBubble instanceof BubbleBarBubble bubble) {
+ BubbleInfo info = bubble.getInfo();
+ info.setFlags(
+ info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+ hideDot();
+ }
+ }
+
+ /** Animates the dot to the given scale. */
+ private void animateDotScale(float toScale) {
boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
// Don't restart the animation if we're already animating to the given value or if the dot
@@ -277,8 +318,6 @@
final boolean showDot = toScale > 0f;
- // Do NOT wait until after animation ends to setShowDot
- // to avoid overriding more recent showDot states.
clearAnimation();
animate()
.setDuration(200)
@@ -293,7 +332,6 @@
}).start();
}
-
@Override
public String toString() {
String toString = mBubble != null ? mBubble.getKey() : "null";
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index be6f690..df8c05c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -64,10 +64,10 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -201,6 +201,8 @@
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
import com.android.systemui.unfold.updates.RotationChangeProvider;
+import kotlin.Unit;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -213,8 +215,6 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
-import kotlin.Unit;
-
public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
@@ -276,7 +276,7 @@
// TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
asContext(), deviceState);
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, getIApplicationThread(),
getDepthController());
@@ -296,7 +296,7 @@
mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
mDepthController = new DepthController(this);
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopVisibilityController = new DesktopVisibilityController(this);
mDesktopVisibilityController.registerSystemUiListener();
mSplitSelectStateController.initSplitFromDesktopController(this,
@@ -1004,7 +1004,7 @@
@Override
public void setResumed() {
- if (!enableDesktopWindowingWallpaperActivity()
+ if (!WALLPAPER_ACTIVITY.isEnabled(this)
&& mDesktopVisibilityController != null
&& mDesktopVisibilityController.areDesktopTasksVisible()
&& !mDesktopVisibilityController.isRecentsGestureInProgress()) {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f020c8f..b059186 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -62,6 +62,8 @@
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -152,6 +154,8 @@
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
+import kotlin.Unit;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -161,8 +165,6 @@
import java.util.OptionalInt;
import java.util.function.Consumer;
-import kotlin.Unit;
-
/**
* Handles the navigation gestures when Launcher is the default home activity.
*/
@@ -952,7 +954,7 @@
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
super.onRecentsAnimationStart(controller, targets);
- if (targets.hasDesktopTasks()) {
+ if (targets.hasDesktopTasks(mContext)) {
mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
} else {
int untrimmedAppCount = mRemoteTargetHandles.length;
@@ -1272,8 +1274,8 @@
TaskView currentPageTaskView = mRecentsView != null
? mRecentsView.getCurrentPageTaskView() : null;
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if ((nextPageTaskView instanceof DesktopTaskView
|| currentPageTaskView instanceof DesktopTaskView)
@@ -1445,8 +1447,8 @@
setClampScrollOffset(false);
};
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
&& !(mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView))) {
@@ -1631,14 +1633,17 @@
mRecentsAnimationController.screenshotTask(taskId));
});
- // let SystemUi reparent the overlay leash as soon as possible
+ // let SystemUi reparent the overlay leash as soon as possible;
+ // make sure to pass in an empty src-rect-hint if overlay is present, since we
+ // use our own calculated source-rect-hint for the animation.
SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
mSwipePipToHomeAnimator.getTaskId(),
mSwipePipToHomeAnimator.getComponentName(),
mSwipePipToHomeAnimator.getDestinationBounds(),
mSwipePipToHomeAnimator.getContentOverlay(),
mSwipePipToHomeAnimator.getAppBounds(),
- mSwipePipToHomeAnimator.getSourceRectHint());
+ mSwipePipToHomeAnimator.getContentOverlay() != null ? new Rect()
+ : mSwipePipToHomeAnimator.getSourceRectHint());
windowAnim = mSwipePipToHomeAnimators;
} else {
@@ -2290,8 +2295,8 @@
mRecentsAnimationController, mRecentsAnimationTargets);
});
- if (Flags.enableDesktopWindowingMode()
- && !(Flags.enableDesktopWindowingWallpaperActivity()
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && !(WALLPAPER_ACTIVITY.isEnabled(mContext)
&& Flags.enableDesktopWindowingQuickSwitch())) {
if (mRecentsView.getNextPageTaskView() instanceof DesktopTaskView
|| mRecentsView.getCurrentPageTaskView() instanceof DesktopTaskView) {
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
index 27bd03d..7d22c52 100644
--- a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -5,6 +5,7 @@
import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType
import android.app.backup.BackupRestoreEventLogger.BackupRestoreError
import android.content.Context
+import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableLauncherBrMetricsFixed
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
@@ -29,8 +30,8 @@
@BackupRestoreDataType private const val DATA_TYPE_APP_PAIR = "app_pair"
}
- private val restoreEventLogger: BackupRestoreEventLogger =
- BackupManager(context).delayedRestoreLogger
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ val restoreEventLogger: BackupRestoreEventLogger = BackupManager(context).delayedRestoreLogger
/**
* For logging when multiple items of a given data type failed to restore.
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 3d4167a..95c86fa 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,13 +20,14 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.TaskInfo;
import android.content.ComponentName;
+import android.content.Context;
import android.os.Process;
import android.os.RemoteException;
import android.util.SparseBooleanArray;
@@ -58,6 +59,7 @@
private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
+ private final Context mContext;
private final KeyguardManager mKeyguardManager;
private final LooperExecutor mMainThreadExecutor;
private final SystemUiProxy mSysUiProxy;
@@ -76,8 +78,10 @@
// Tasks are stored in order of least recently launched to most recently launched.
private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
- public RecentTasksList(LooperExecutor mainThreadExecutor, KeyguardManager keyguardManager,
- SystemUiProxy sysUiProxy, TopTaskTracker topTaskTracker) {
+ public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
+ KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
+ TopTaskTracker topTaskTracker) {
+ mContext = context;
mMainThreadExecutor = mainThreadExecutor;
mKeyguardManager = keyguardManager;
mChangeId = 1;
@@ -300,8 +304,12 @@
@VisibleForTesting
TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
int currentUserId = Process.myUserHandle().getIdentifier();
- ArrayList<GroupedRecentTaskInfo> rawTasks =
- mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+ ArrayList<GroupedRecentTaskInfo> rawTasks;
+ try {
+ rawTasks = mSysUiProxy.getRecentTasks(numTasks, currentUserId);
+ } catch (SystemUiProxy.GetRecentTasksException e) {
+ return INVALID_RESULT;
+ }
// The raw tasks are given in most-recent to least-recent order, we need to reverse it
Collections.reverse(rawTasks);
@@ -321,9 +329,9 @@
int numVisibleTasks = 0;
for (GroupedRecentTaskInfo rawTask : rawTasks) {
if (rawTask.getType() == TYPE_FREEFORM) {
- // TYPE_FREEFORM tasks is only created when enableDesktopWindowingMode() is true,
+ // TYPE_FREEFORM tasks is only created whenDESKTOP_WINDOWING_MODE.isEnabled is true,
// leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)) {
GroupTask desktopTask = createDesktopTask(rawTask);
if (desktopTask != null) {
allTasks.add(desktopTask);
@@ -416,8 +424,12 @@
}
writer.println(prefix + " ]");
int currentUserId = Process.myUserHandle().getIdentifier();
- ArrayList<GroupedRecentTaskInfo> rawTasks =
- mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+ ArrayList<GroupedRecentTaskInfo> rawTasks;
+ try {
+ rawTasks = mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+ } catch (SystemUiProxy.GetRecentTasksException e) {
+ rawTasks = new ArrayList<>();
+ }
writer.println(prefix + " rawTasks=[");
for (GroupedRecentTaskInfo task : rawTasks) {
TaskInfo taskInfo1 = task.getTaskInfo1();
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 18461a6..e84200d 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -27,7 +27,7 @@
import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.quickstep.TaskViewUtils.createRecentsWindowAnimator;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -147,7 +147,7 @@
mActionsView = findViewById(R.id.overview_actions_view);
getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
mDragLayer.recreateControllers();
- if (enableDesktopWindowingMode()) {
+ if (DESKTOP_WINDOWING_MODE.isEnabled(this)) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, getIApplicationThread(),
null /* depthController */
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265..f902284 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -33,6 +33,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -416,7 +417,8 @@
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED
| SYSUI_STATE_MAGNIFICATION_OVERLAP
| SYSUI_STATE_DEVICE_DREAMING
- | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+ | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+ | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 82bb453..d104911 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -18,9 +18,10 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.app.WindowConfiguration;
+import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.RemoteAnimationTarget;
@@ -54,8 +55,8 @@
*
* @return {@code true} if at least one target app is a desktop task
*/
- public boolean hasDesktopTasks() {
- if (!enableDesktopWindowingMode()) {
+ public boolean hasDesktopTasks(Context context) {
+ if (!DESKTOP_WINDOWING_MODE.isEnabled(context)) {
return false;
}
for (RemoteAnimationTarget target : apps) {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index b796951..db03dac 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -18,6 +18,7 @@
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
@@ -89,7 +90,9 @@
private RecentsModel(Context context, IconProvider iconProvider) {
this(context,
- new RecentTasksList(MAIN_EXECUTOR,
+ new RecentTasksList(
+ context,
+ MAIN_EXECUTOR,
context.getSystemService(KeyguardManager.class),
SystemUiProxy.INSTANCE.get(context),
TopTaskTracker.INSTANCE.get(context)),
@@ -108,7 +111,7 @@
mIconCache = iconCache;
mIconCache.registerTaskVisualsChangeListener(this);
mThumbnailCache = thumbnailCache;
- if (enableGridOnlyOverview()) {
+ if (isCachePreloadingEnabled()) {
mCallbacks = new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration configuration) {
@@ -342,7 +345,7 @@
* highResLoadingState is enabled
*/
public void preloadCacheIfNeeded() {
- if (!enableGridOnlyOverview()) {
+ if (!isCachePreloadingEnabled()) {
return;
}
@@ -368,7 +371,7 @@
* Updates cache size and preloads more tasks if cache size increases
*/
public void updateCacheSizeAndPreloadIfNeeded() {
- if (!enableGridOnlyOverview()) {
+ if (!isCachePreloadingEnabled()) {
return;
}
@@ -387,6 +390,10 @@
mTaskStackChangeListeners.unregisterTaskStackListener(this);
}
+ private boolean isCachePreloadingEnabled() {
+ return enableGridOnlyOverview() || enableRefactorTaskThumbnail();
+ }
+
/**
* Listener for receiving running tasks changes
*/
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 66aa897..f2db5af 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -23,8 +23,8 @@
import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -1394,10 +1394,26 @@
}
}
- public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
+ public static class GetRecentTasksException extends Exception {
+ public GetRecentTasksException(String message) {
+ super(message);
+ }
+
+ public GetRecentTasksException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * Retrieves a list of Recent tasks from ActivityManager.
+ * @throws GetRecentTasksException if IRecentTasks is not initialized, or when we get
+ * RemoteException from server side
+ */
+ public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId)
+ throws GetRecentTasksException {
if (mRecentTasks == null) {
- Log.w(TAG, "getRecentTasks() failed due to null mRecentTasks");
- return new ArrayList<>();
+ Log.e(TAG, "getRecentTasks() failed due to null mRecentTasks");
+ throw new GetRecentTasksException("null mRecentTasks");
}
try {
final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,
@@ -1407,8 +1423,8 @@
}
return new ArrayList<>(Arrays.asList(rawTasks));
} catch (RemoteException e) {
- Log.w(TAG, "Failed call getRecentTasks", e);
- return new ArrayList<>();
+ Log.e(TAG, "Failed call getRecentTasks", e);
+ throw new GetRecentTasksException("Failed call getRecentTasks", e);
}
}
@@ -1428,7 +1444,8 @@
private boolean shouldEnableRunningTasksForDesktopMode() {
// TODO(b/335401172): unify DesktopMode checks in Launcher
- return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
+ return DESKTOP_WINDOWING_MODE.isEnabled(mContext)
+ && enableDesktopWindowingTaskbarRunningApps();
}
private boolean handleMessageAsync(Message msg) {
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 80902e3..f4ff4b2 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -184,6 +184,10 @@
return mTaskContainer.getTaskView();
}
+ public View getSnapshotView() {
+ return mTaskContainer.getSnapshotView();
+ }
+
/**
* Called when the current task is interactive for the user
*/
@@ -312,7 +316,7 @@
// the difference between the bitmap bounds and the projected view bounds.
Matrix boundsToBitmapSpace = new Matrix();
Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
- ? mHelper.getEnabledState().getThumbnailMatrix()
+ ? mHelper.getThumbnailMatrix()
: mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
thumbnailMatrix.invert(boundsToBitmapSpace);
RectF boundsInBitmapSpace = new RectF();
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 77124bf..dad34ac 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -23,7 +23,6 @@
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
import android.app.ActivityOptions;
import android.graphics.Bitmap;
@@ -63,6 +62,7 @@
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
import com.android.systemui.shared.recents.view.RecentsTransition;
import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import java.util.Collections;
import java.util.List;
@@ -394,7 +394,7 @@
return Settings.Global.getInt(
container.asContext().getContentResolver(),
Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0
- && !enableDesktopWindowingMode();
+ && !DesktopModeStatus.canEnterDesktopMode(container.asContext());
}
};
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 5ac04da..717f6c8 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -39,7 +39,8 @@
import android.util.Log;
import android.util.Xml;
-import com.android.launcher3.AutoInstallsLayout;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.logging.InstanceId;
@@ -73,7 +74,6 @@
new MainThreadInitializedObject<>(SettingsChangeLogger::new);
private static final String TAG = "SettingsChangeLogger";
- private static final String ROOT_TAG = "androidx.preference.PreferenceScreen";
private static final String BOOLEAN_PREF = "SwitchPreference";
private final Context mContext;
@@ -85,8 +85,13 @@
private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
private SettingsChangeLogger(Context context) {
+ this(context, StatsLogManager.newInstance(context));
+ }
+
+ @VisibleForTesting
+ SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
mContext = context;
- mStatsLogManager = StatsLogManager.newInstance(mContext);
+ mStatsLogManager = statsLogManager;
mLoggablePrefs = loadPrefKeys(context);
DisplayController.INSTANCE.get(context).addChangeListener(this);
mNavMode = DisplayController.getNavigationMode(context);
@@ -105,7 +110,13 @@
ArrayMap<String, LoggablePref> result = new ArrayMap<>();
try {
- AutoInstallsLayout.beginDocument(parser, ROOT_TAG);
+ // Move cursor to first tag because it could be
+ // androidx.preference.PreferenceScreen or PreferenceScreen
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG
+ && eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ }
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG
@@ -189,13 +200,19 @@
prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
}
+ @VisibleForTesting
+ ArrayMap<String, LoggablePref> getLoggingPrefs() {
+ return mLoggablePrefs;
+ }
+
@Override
public void close() {
getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
}
- private static class LoggablePref {
+ @VisibleForTesting
+ static class LoggablePref {
public boolean defaultValue;
public int eventIdOn;
public int eventIdOff;
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
new file mode 100644
index 0000000..3a6d5c0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.TasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import java.util.logging.Level
+
+internal typealias RecentsScopeId = String
+
+class RecentsDependencies private constructor(private val appContext: Context) {
+ private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
+
+ init {
+ startDefaultScope(appContext)
+ }
+
+ /**
+ * This function initialised the default scope with RecentsView dependencies. These dependencies
+ * are used multiple times and should be a singleton to share across Recents classes.
+ */
+ private fun startDefaultScope(appContext: Context) {
+ createScope(DEFAULT_SCOPE_ID).apply {
+ set(RecentsViewData::class.java.simpleName, RecentsViewData())
+
+ // Create RecentsTaskRepository singleton
+ val recentTasksRepository: RecentTasksRepository =
+ with(RecentsModel.INSTANCE.get(appContext)) {
+ TasksRepository(this, thumbnailCache, iconCache)
+ }
+ set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
+ }
+ }
+
+ inline fun <reified T> inject(
+ scopeId: RecentsScopeId = "",
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+ ): T = inject(T::class.java, scopeId = scopeId, extras = extras, factory = factory)
+
+ @Suppress("UNCHECKED_CAST")
+ @JvmOverloads
+ fun <T> inject(
+ modelClass: Class<T>,
+ scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+ ): T {
+ val currentScopeId = scopeId.ifEmpty { DEFAULT_SCOPE_ID }
+ val scope = scopes[currentScopeId] ?: createScope(currentScopeId)
+
+ log("inject ${modelClass.simpleName} into ${scope.scopeId}", Log.INFO)
+ var instance: T?
+ synchronized(this) {
+ instance = getDependency(scope, modelClass)
+ log("found instance? $instance", Log.INFO)
+ if (instance == null) {
+ instance =
+ factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
+ scope[modelClass.simpleName] = instance!!
+ }
+ }
+ return instance!!
+ }
+
+ inline fun <reified T> provide(scopeId: RecentsScopeId = "", noinline factory: () -> T): T =
+ provide(T::class.java, scopeId = scopeId, factory = factory)
+
+ @JvmOverloads
+ fun <T> provide(
+ modelClass: Class<T>,
+ scopeId: RecentsScopeId = DEFAULT_SCOPE_ID,
+ factory: () -> T,
+ ) = inject(modelClass, scopeId, factory = { factory.invoke() })
+
+ private fun <T> getDependency(scope: RecentsDependenciesScope, modelClass: Class<T>): T? {
+ var instance: T? = scope[modelClass.simpleName] as T?
+ if (instance == null) {
+ instance =
+ scope.scopeIdsLinked.firstNotNullOfOrNull { scopeId ->
+ getScope(scopeId)[modelClass.simpleName]
+ } as T?
+ }
+ if (instance != null) log("Found dependency: $instance", Log.INFO)
+ return instance
+ }
+
+ fun getScope(scope: Any): RecentsDependenciesScope {
+ val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+ return getScope(scopeId)
+ }
+
+ fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
+ scopes[scopeId] ?: createScope(scopeId)
+
+ // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
+ // Each class should be responsible for providing a factory function to create a new instance.
+ @Suppress("UNCHECKED_CAST")
+ private fun <T> createDependency(
+ modelClass: Class<T>,
+ scopeId: RecentsScopeId,
+ extras: RecentsDependenciesExtras,
+ ): T {
+ log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
+ val instance: Any =
+ when (modelClass) {
+ RecentTasksRepository::class.java -> {
+ with(RecentsModel.INSTANCE.get(appContext)) {
+ TasksRepository(this, thumbnailCache, iconCache)
+ }
+ }
+ RecentsViewData::class.java -> RecentsViewData()
+ TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
+ TaskViewData::class.java -> {
+ val taskViewType = extras["TaskViewType"] as TaskViewType
+ TaskViewData(taskViewType)
+ }
+ TaskContainerData::class.java -> TaskContainerData()
+ TaskThumbnailViewModel::class.java ->
+ TaskThumbnailViewModel(
+ recentsViewData = inject(),
+ taskViewData = inject(scopeId, extras),
+ taskContainerData = inject(),
+ getThumbnailPositionUseCase = inject(),
+ tasksRepository = inject()
+ )
+ TaskOverlayViewModel::class.java -> {
+ val task = extras["Task"] as Task
+ TaskOverlayViewModel(
+ task = task,
+ recentsViewData = inject(),
+ recentTasksRepository = inject(),
+ getThumbnailPositionUseCase = inject()
+ )
+ }
+ GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+ SysUiStatusNavFlagsUseCase::class.java ->
+ SysUiStatusNavFlagsUseCase(taskRepository = inject())
+ GetThumbnailPositionUseCase::class.java ->
+ GetThumbnailPositionUseCase(
+ deviceProfileRepository = inject(),
+ rotationStateRepository = inject(),
+ tasksRepository = inject()
+ )
+ else -> {
+ log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
+ error("Factory for ${modelClass.simpleName} not defined!")
+ }
+ }
+ return instance as T
+ }
+
+ private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
+ return RecentsDependenciesScope(scopeId).also { scopes[scopeId] = it }
+ }
+
+ private fun log(message: String, @Log.Level level: Int = Log.DEBUG) {
+ if (DEBUG) {
+ when (level) {
+ Log.WARN -> Log.w(TAG, message)
+ Log.VERBOSE -> Log.v(TAG, message)
+ Log.INFO -> Log.i(TAG, message)
+ Log.ERROR -> Log.e(TAG, message)
+ else -> Log.d(TAG, message)
+ }
+ }
+ }
+
+ companion object {
+ private const val DEFAULT_SCOPE_ID = "RecentsDependencies::GlobalScope"
+ private const val TAG = "RecentsDependencies"
+ private const val DEBUG = false
+
+ @Volatile private lateinit var instance: RecentsDependencies
+
+ fun initialize(view: View): RecentsDependencies = initialize(view.context)
+
+ fun initialize(context: Context): RecentsDependencies {
+ synchronized(this) {
+ if (!Companion::instance.isInitialized) {
+ instance = RecentsDependencies(context.applicationContext)
+ }
+ }
+ return instance
+ }
+
+ fun getInstance(): RecentsDependencies {
+ if (!Companion::instance.isInitialized) {
+ throw UninitializedPropertyAccessException(
+ "Recents dependencies are not initialized. " +
+ "Call `RecentsDependencies.initialize` before using this container."
+ )
+ }
+ return instance
+ }
+
+ fun destroy() {
+ instance.scopes.clear()
+ instance.startDefaultScope(instance.appContext)
+ }
+ }
+}
+
+inline fun <reified T> RecentsDependencies.Companion.inject(
+ scope: Any = "",
+ vararg extras: Pair<String, Any>,
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): Lazy<T> = lazy { get(scope, RecentsDependenciesExtras(extras), factory) }
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+ scope: Any = "",
+ extras: RecentsDependenciesExtras = RecentsDependenciesExtras(),
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T {
+ val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+ return getInstance().inject(scopeId, extras, factory)
+}
+
+inline fun <reified T> RecentsDependencies.Companion.get(
+ scope: Any = "",
+ vararg extras: Pair<String, Any>,
+ noinline factory: ((extras: RecentsDependenciesExtras) -> T)? = null,
+): T = get(scope, RecentsDependenciesExtras(extras), factory)
+
+fun RecentsDependencies.Companion.getScope(scopeId: Any) = getInstance().getScope(scopeId)
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
new file mode 100644
index 0000000..753cb6e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesExtras.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+data class RecentsDependenciesExtras(private val data: MutableMap<String, Any> = mutableMapOf()) {
+ constructor(value: Array<out Pair<String, Any>>) : this(value.toMap().toMutableMap())
+
+ operator fun get(key: String) = data[key]
+
+ operator fun set(key: String, value: Any) {
+ data[key] = value
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
new file mode 100644
index 0000000..56bb1ed
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependenciesScope.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.di
+
+import android.util.Log
+
+class RecentsDependenciesScope(
+ val scopeId: RecentsScopeId,
+ private val dependencies: MutableMap<String, Any> = mutableMapOf(),
+ private val scopeIds: MutableList<RecentsScopeId> = mutableListOf()
+) {
+ val scopeIdsLinked: List<RecentsScopeId>
+ get() = scopeIds.toList()
+
+ operator fun get(identifier: String): Any? {
+ log("get $identifier")
+ return dependencies[identifier]
+ }
+
+ operator fun set(key: String, value: Any) {
+ synchronized(this) {
+ log("set $key")
+ dependencies[key] = value
+ }
+ }
+
+ fun remove(key: String): Any? {
+ synchronized(this) {
+ log("remove $key")
+ return dependencies.remove(key)
+ }
+ }
+
+ fun linkTo(scope: RecentsDependenciesScope) {
+ log("linking to ${scope.scopeId}")
+ scopeIds += scope.scopeId
+ }
+
+ fun close() {
+ log("reset")
+ synchronized(this) { dependencies.clear() }
+ }
+
+ private fun log(message: String) {
+ if (DEBUG) Log.d(TAG, "[scopeId=$scopeId] $message")
+ }
+
+ override fun toString(): String =
+ "scopeId: $scopeId" +
+ "\n dependencies: ${dependencies.map { "${it.key}=${it.value}" }.joinToString(", ")}" +
+ "\n linked to: ${scopeIds.joinToString(", ")}"
+
+ private companion object {
+ private const val TAG = "RecentsDependenciesScope"
+ private const val DEBUG = false
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
new file mode 100644
index 0000000..f060d7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.graphics.Matrix
+import android.graphics.Rect
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import kotlinx.coroutines.flow.firstOrNull
+
+/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
+class GetThumbnailPositionUseCase(
+ private val deviceProfileRepository: RecentsDeviceProfileRepository,
+ private val rotationStateRepository: RecentsRotationStateRepository,
+ private val tasksRepository: RecentTasksRepository,
+ private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper()
+) {
+ suspend fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+ val thumbnailData =
+ tasksRepository.getThumbnailById(taskId).firstOrNull() ?: return MissingThumbnail
+ val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+ previewPositionHelper.updateThumbnailMatrix(
+ Rect(0, 0, thumbnail.width, thumbnail.height),
+ thumbnailData,
+ width,
+ height,
+ deviceProfileRepository.getRecentsDeviceProfile().isLargeScreen,
+ rotationStateRepository.getRecentsRotationState().activityRotation,
+ isRtl
+ )
+ return MatrixScaling(
+ previewPositionHelper.matrix,
+ previewPositionHelper.isOrientationChanged
+ )
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
similarity index 95%
rename from quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
index e8dd04c3..3aa808e 100644
--- a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
import android.graphics.Bitmap
import com.android.quickstep.recents.data.RecentTasksRepository
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
new file mode 100644
index 0000000..1d19c7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.view.WindowInsetsController
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** UseCase to calculate flags for status bar and navigation bar */
+class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) {
+ fun getSysUiStatusNavFlags(taskId: Int): Int {
+ val thumbnailData =
+ runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0
+
+ val thumbnailAppearance = thumbnailData.appearance
+ var flags = 0
+ flags =
+ flags or
+ if (
+ thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0
+ )
+ FLAG_LIGHT_STATUS
+ else FLAG_DARK_STATUS
+ flags =
+ flags or
+ if (
+ thumbnailAppearance and
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0
+ )
+ FLAG_LIGHT_NAV
+ else FLAG_DARK_NAV
+ return flags
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
new file mode 100644
index 0000000..1a1bef7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.graphics.Matrix
+
+/** State on how a task Thumbnail can fit on given canvas */
+sealed class ThumbnailPositionState {
+ data object MissingThumbnail : ThumbnailPositionState()
+
+ data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
new file mode 100644
index 0000000..8b03a84
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.viewmodel
+
+import com.android.quickstep.recents.data.RecentTasksRepository
+
+class RecentsViewModel(
+ private val recentsTasksRepository: RecentTasksRepository,
+ private val recentsViewData: RecentsViewData
+) {
+ fun refreshAllTaskData() {
+ recentsTasksRepository.getAllTaskData(true)
+ }
+
+ fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
+ recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+ }
+
+ fun updateScale(scale: Float) {
+ recentsViewData.scale.value = scale
+ }
+
+ fun updateFullscreenProgress(fullscreenProgress: Float) {
+ recentsViewData.fullscreenProgress.value = fullscreenProgress
+ }
+
+ fun updateTasksFullyVisible(taskIds: Set<Int>) {
+ recentsViewData.settledFullyVisibleTaskIds.value = taskIds
+ }
+
+ fun setOverlayEnabled(isOverlayEnabled: Boolean) {
+ recentsViewData.overlayEnabled.value = isOverlayEnabled
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
new file mode 100644
index 0000000..8b8bc3e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.viewmodel
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+
+class TaskContainerViewModel(
+ private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
+ private val getThumbnailUseCase: GetThumbnailUseCase
+) {
+ fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
+
+ fun getSysUiStatusNavFlags(taskId: Int) =
+ sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
index feee11f..5fb5b90 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -17,15 +17,10 @@
package com.android.quickstep.task.thumbnail
import android.graphics.Bitmap
-import android.graphics.Matrix
/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
sealed class TaskOverlayUiState {
data object Disabled : TaskOverlayUiState()
- data class Enabled(
- val isRealSnapshot: Boolean,
- val thumbnail: Bitmap?,
- val thumbnailMatrix: Matrix
- ) : TaskOverlayUiState()
+ data class Enabled(val isRealSnapshot: Boolean, val thumbnail: Bitmap?) : TaskOverlayUiState()
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 40f9b28..3b3a811 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -17,18 +17,17 @@
package com.android.quickstep.task.thumbnail
import android.graphics.Bitmap
-import android.graphics.Rect
import androidx.annotation.ColorInt
sealed class TaskThumbnailUiState {
data object Uninitialized : TaskThumbnailUiState()
+
data object LiveTile : TaskThumbnailUiState()
+
data class BackgroundOnly(@ColorInt val backgroundColor: Int) : TaskThumbnailUiState()
- data class Snapshot(
- val bitmap: Bitmap,
- val drawnRect: Rect,
- @ColorInt val backgroundColor: Int
- ) : TaskThumbnailUiState()
+
+ data class Snapshot(val bitmap: Bitmap, @ColorInt val backgroundColor: Int) :
+ TaskThumbnailUiState()
}
data class TaskThumbnail(val taskId: Int, val isRunning: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index d22fc94..fcc2af3 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -31,14 +31,14 @@
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.ViewPool
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.inject
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.util.TaskCornerRadius
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView
import com.android.systemui.shared.system.QuickStepContract
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -49,26 +49,16 @@
import kotlinx.coroutines.flow.onEach
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
- // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
- // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
- // This is using a lazy for now because the dependencies cannot be obtained without DI.
- val viewModel by lazy {
- val recentsView =
- RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
- .getOverviewPanel<RecentsView<*, *>>()
- TaskThumbnailViewModel(
- recentsView.mRecentsViewData!!,
- (parent as TaskView).taskViewData,
- (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
- recentsView.mTasksRepository!!,
- )
- }
+
+ private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
+
private lateinit var viewAttachedScope: CoroutineScope
private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
- private val thumbnail: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+ private val thumbnailView: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+ private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
private val _measuredBounds = Rect()
@@ -97,6 +87,7 @@
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
viewModel.uiState
.onEach { viewModelUiState ->
+ uiState = viewModelUiState
resetViews()
when (viewModelUiState) {
is Uninitialized -> {}
@@ -135,7 +126,14 @@
}
override fun onRecycle() {
- // Do nothing
+ uiState = Uninitialized
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ if (uiState is Snapshot) {
+ setImageMatrix()
+ }
}
override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -148,7 +146,7 @@
private fun resetViews() {
liveTileView.isVisible = false
- thumbnail.isVisible = false
+ thumbnailView.isVisible = false
scrimView.alpha = 0f
setBackgroundColor(Color.BLACK)
}
@@ -163,8 +161,13 @@
private fun drawSnapshot(snapshot: Snapshot) {
drawBackground(snapshot.backgroundColor)
- thumbnail.setImageBitmap(snapshot.bitmap)
- thumbnail.isVisible = true
+ thumbnailView.setImageBitmap(snapshot.bitmap)
+ thumbnailView.isVisible = true
+ setImageMatrix()
+ }
+
+ private fun setImageMatrix() {
+ thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
}
private fun getCurrentCornerRadius() =
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index a9f7041..9253dbf 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -18,12 +18,12 @@
import android.util.Log
import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskOverlayUiState
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -41,48 +41,62 @@
private lateinit var overlayInitializedScope: CoroutineScope
private var uiState: TaskOverlayUiState = Disabled
- // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
- // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
- // This is using a lazy for now because the dependencies cannot be obtained without DI.
- private val taskOverlayViewModel by lazy {
- val recentsView =
- RecentsViewContainer.containerFromContext<RecentsViewContainer>(
- overlay.taskView.context
- )
- .getOverviewPanel<RecentsView<*, *>>()
- TaskOverlayViewModel(task, recentsView.mRecentsViewData!!, recentsView.mTasksRepository!!)
+ private val viewModel: TaskOverlayViewModel by lazy {
+ TaskOverlayViewModel(
+ task = task,
+ recentsViewData = RecentsDependencies.get(),
+ getThumbnailPositionUseCase = RecentsDependencies.get(),
+ recentTasksRepository = RecentsDependencies.get()
+ )
}
// TODO(b/331753115): TaskOverlay should listen for state changes and react.
val enabledState: Enabled
get() = uiState as Enabled
+ fun getThumbnailMatrix() = getThumbnailPositionState().matrix
+
+ private fun getThumbnailPositionState() =
+ viewModel.getThumbnailPositionState(
+ overlay.snapshotView.width,
+ overlay.snapshotView.height,
+ overlay.snapshotView.isLayoutRtl
+ )
+
fun init() {
overlayInitializedScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper"))
- taskOverlayViewModel.overlayState
+ viewModel.overlayState
.onEach {
uiState = it
if (it is Enabled) {
- Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}")
- overlay.initOverlay(
- task,
- it.thumbnail,
- it.thumbnailMatrix,
- /* rotated= */ false
- )
+ initOverlay(it)
} else {
- Log.d(TAG, "reset - taskId: ${task.key.id}")
- overlay.reset()
+ reset()
}
}
.launchIn(overlayInitializedScope)
+ overlay.snapshotView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ (uiState as? Enabled)?.let { initOverlay(it) }
+ }
+ }
+
+ private fun initOverlay(enabledState: Enabled) {
+ Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+ with(getThumbnailPositionState()) {
+ overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated)
+ }
+ }
+
+ private fun reset() {
+ Log.d(TAG, "reset - taskId: ${task.key.id}")
+ overlay.reset()
}
fun destroy() {
overlayInitializedScope.cancel()
uiState = Disabled
- overlay.reset()
+ reset()
}
companion object {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 47f32fb..4e13d1c 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -18,6 +18,9 @@
import android.graphics.Matrix
import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -25,31 +28,52 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
/** View model for TaskOverlay */
class TaskOverlayViewModel(
- task: Task,
+ private val task: Task,
recentsViewData: RecentsViewData,
- tasksRepository: RecentTasksRepository,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+ recentTasksRepository: RecentTasksRepository,
) {
val overlayState =
combine(
recentsViewData.overlayEnabled,
recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
- tasksRepository.getThumbnailById(task.key.id)
+ recentTasksRepository.getThumbnailById(task.key.id)
) { isOverlayEnabled, isFullyVisible, thumbnailData ->
if (isOverlayEnabled && isFullyVisible) {
Enabled(
isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
thumbnailData?.thumbnail,
- // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
- // with
- // TaskThumbnailView.
- Matrix.IDENTITY_MATRIX
)
} else {
Disabled
}
}
.distinctUntilChanged()
+
+ fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
+ return runBlocking {
+ val matrix: Matrix
+ val isRotated: Boolean
+ when (
+ val thumbnailPositionState =
+ getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
+ ) {
+ is MatrixScaling -> {
+ matrix = thumbnailPositionState.matrix
+ isRotated = thumbnailPositionState.isRotated
+ }
+ is MissingThumbnail -> {
+ matrix = Matrix.IDENTITY_MATRIX
+ isRotated = false
+ }
+ }
+ ThumbnailPositionState(matrix, isRotated)
+ }
+ }
+
+ data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
similarity index 75%
rename from quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
rename to quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index d8729a6..6465645 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -14,19 +14,21 @@
* limitations under the License.
*/
-package com.android.quickstep.task.thumbnail
+package com.android.quickstep.task.viewmodel
import android.annotation.ColorInt
-import android.graphics.Rect
+import android.graphics.Matrix
import androidx.core.graphics.ColorUtils
import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState
import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnail
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -37,6 +39,7 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModel(
@@ -44,9 +47,10 @@
taskViewData: TaskViewData,
taskContainerData: TaskContainerData,
private val tasksRepository: RecentTasksRepository,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
- private var boundTaskIsRunning = false
+ private lateinit var taskThumbnail: TaskThumbnail
/**
* Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
@@ -66,16 +70,12 @@
taskFlow.map { taskVal ->
when {
taskVal == null -> Uninitialized
- boundTaskIsRunning -> LiveTile
+ taskThumbnail.isRunning -> LiveTile
isBackgroundOnly(taskVal) ->
BackgroundOnly(taskVal.colorBackground.removeAlpha())
isSnapshotState(taskVal) -> {
val bitmap = taskVal.thumbnail?.thumbnail!!
- Snapshot(
- bitmap,
- Rect(0, 0, bitmap.width, bitmap.height),
- taskVal.colorBackground.removeAlpha()
- )
+ Snapshot(bitmap, taskVal.colorBackground.removeAlpha())
}
else -> Uninitialized
}
@@ -84,10 +84,22 @@
.distinctUntilChanged()
fun bind(taskThumbnail: TaskThumbnail) {
- boundTaskIsRunning = taskThumbnail.isRunning
+ this.taskThumbnail = taskThumbnail
task.value = tasksRepository.getTaskDataById(taskThumbnail.taskId)
}
+ fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
+ return runBlocking {
+ when (
+ val thumbnailPositionState =
+ getThumbnailPositionUseCase.run(taskThumbnail.taskId, width, height, isRtl)
+ ) {
+ is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
+ is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
+ }
+ }
+ }
+
private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
private fun isSnapshotState(task: Task): Boolean {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
new file mode 100644
index 0000000..ec75d59
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import androidx.lifecycle.ViewModel
+
+class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
+ fun updateScale(scale: Float) {
+ taskViewData.scale.value = scale
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
package com.android.quickstep.util;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.view.animation.Interpolator;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
/**
* Utility class containing methods to help manage animations, interpolators, and timings.
*/
public class AnimUtils {
+ private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
/**
* Fetches device-specific timings for the Overview > Split animation
* (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
}
/**
+ * Synchronizes the timing for the split dismiss animation to the current transition to
+ * NORMAL (launcher home/workspace)
+ */
+ public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+ @NonNull RecentsViewContainer container,
+ @NonNull StatsLogManager.LauncherEvent exitReason,
+ @NonNull SplitAnimationController animationController) {
+ StateAnimationConfig config = new StateAnimationConfig();
+ BaseState startState = stateManager.getState();
+ long duration = startState.getTransitionDuration(container.asContext(),
+ false /*isToState*/);
+ if (duration == 0) {
+ // Case where we're in contextual on workspace (NORMAL), which by default has 0
+ // transition duration
+ duration = DURATION_DEFAULT_SPLIT_DISMISS;
+ }
+ config.duration = duration;
+ AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+ startState, NORMAL, config);
+ AnimatorSet dismissAnim = animationController
+ .createPlaceholderDismissAnim(container, exitReason, duration);
+ stateAnim.play(dismissAnim);
+ stateManager.setCurrentAnimation(stateAnim, NORMAL);
+ stateAnim.start();
+ }
+
+ /**
* Returns a IRemoteCallback which completes the provided list as a result
*/
public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ec..0000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
- private static final int NUM_ALPHA_CHANNELS = 2;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_MODAL_ALPHA = 1;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- @Nullable
- private Drawable mDrawable;
- private int mDrawableWidth, mDrawableHeight;
-
- public IconView(Context context) {
- this(context, null);
- }
-
- public IconView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
- }
-
- /**
- * Sets a {@link Drawable} to be displayed.
- */
- @Override
- public void setDrawable(@Nullable Drawable d) {
- if (mDrawable != null) {
- mDrawable.setCallback(null);
- }
- mDrawable = d;
- if (mDrawable != null) {
- mDrawable.setCallback(this);
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- invalidate();
- }
-
- /**
- * Sets the size of the icon drawable.
- */
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- mDrawableWidth = iconWidth;
- mDrawableHeight = iconHeight;
- if (mDrawable != null) {
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- }
-
- private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
- Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
- Rect drawableRect = new Rect();
- Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
- mDrawable.setBounds(drawableRect);
- }
-
- @Override
- @Nullable
- public Drawable getDrawable() {
- return mDrawable;
- }
-
- @Override
- public int getDrawableWidth() {
- return mDrawableWidth;
- }
-
- @Override
- public int getDrawableHeight() {
- return mDrawableHeight;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mDrawable != null) {
- setDrawableSizeInternal(w, h);
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mDrawable;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final Drawable drawable = mDrawable;
- if (drawable != null && drawable.isStateful()
- && drawable.setState(getDrawableState())) {
- invalidateDrawable(drawable);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDrawable != null) {
- mDrawable.draw(canvas);
- }
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- /**
- * Set the tint color of the icon, useful for scrimming or dimming.
- *
- * @param color to blend in.
- * @param amount [0,1] 0 no tint, 1 full tint
- */
- @Override
- public void setIconColorTint(int color, float amount) {
- if (mDrawable != null) {
- mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
- }
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- DeviceProfile deviceProfile =
- ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
- FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
- int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
- int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
- int taskMargin = deviceProfile.overviewTaskMarginPx;
-
- orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
- thumbnailTopMargin, isRtl);
- iconParams.width = iconParams.height = taskIconHeight;
- setLayoutParams(iconParams);
-
- setRotation(orientationHandler.getDegreesRotated());
- int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
- : deviceProfile.overviewTaskIconDrawableSizePx;
- setDrawableSize(iconDrawableSize, iconDrawableSize);
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 0000000..583207f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+ private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+ private var drawable: Drawable? = null
+ private var drawableWidth = 0
+ private var drawableHeight = 0
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ init {
+ multiValueAlpha.setUpdateVisibility(true)
+ }
+
+ /** Sets a [Drawable] to be displayed. */
+ override fun setDrawable(d: Drawable?) {
+ drawable?.callback = null
+
+ drawable = d
+ drawable?.let {
+ it.callback = this
+ setDrawableSizeInternal(width, height)
+ }
+ invalidate()
+ }
+
+ /** Sets the size of the icon drawable. */
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ drawableWidth = iconWidth
+ drawableHeight = iconHeight
+ drawable?.let { setDrawableSizeInternal(width, height) }
+ }
+
+ private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+ val selfRect = Rect(0, 0, selfWidth, selfHeight)
+ val drawableRect = Rect()
+ Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+ drawable?.bounds = drawableRect
+ }
+
+ override fun getDrawable(): Drawable? = drawable
+
+ override fun getDrawableWidth(): Int = drawableWidth
+
+ override fun getDrawableHeight(): Int = drawableHeight
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ drawable?.let { setDrawableSizeInternal(w, h) }
+ }
+
+ override fun verifyDrawable(who: Drawable): Boolean =
+ super.verifyDrawable(who) || who === drawable
+
+ override fun drawableStateChanged() {
+ super.drawableStateChanged()
+ drawable?.let {
+ if (it.isStateful && it.setState(drawableState)) {
+ invalidateDrawable(it)
+ }
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ drawable?.draw(canvas)
+ }
+
+ override fun hasOverlappingRendering(): Boolean = false
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+ }
+
+ /**
+ * Set the tint color of the icon, useful for scrimming or dimming.
+ *
+ * @param color to blend in.
+ * @param amount [0,1] 0 no tint, 1 full tint
+ */
+ override fun setIconColorTint(color: Int, amount: Float) {
+ drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ val deviceProfile: DeviceProfile =
+ (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+ orientationHandler.setTaskIconParams(
+ iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+ taskIconMargin = deviceProfile.overviewTaskMarginPx,
+ taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+ thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+ isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+ )
+ updateLayoutParams<FrameLayout.LayoutParams> {
+ height = deviceProfile.overviewTaskIconSizePx
+ width = height
+ }
+ setRotation(orientationHandler.degreesRotated)
+ val iconDrawableSize =
+ if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+ else deviceProfile.overviewTaskIconDrawableSizePx
+ setDrawableSize(iconDrawableSize, iconDrawableSize)
+ }
+
+ override fun asView(): View = this
+
+ companion object {
+ private const val NUM_ALPHA_CHANNELS = 2
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_MODAL_ALPHA = 1
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..d20d0a5 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -26,7 +26,7 @@
import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
-import static com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY;
import android.annotation.TargetApi;
import android.content.Context;
@@ -54,6 +54,7 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.systemui.shared.recents.model.Task;
@@ -91,10 +92,12 @@
protected void handleStartHome(boolean animated) {
StateManager stateManager = getStateManager();
animated &= stateManager.shouldAnimateStateChange();
- stateManager.goToState(NORMAL, animated);
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.getSplitAnimationController()
- .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+ if (mSplitSelectStateController.isSplitSelectActive()) {
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+ mSplitSelectStateController.getSplitAnimationController());
+ } else {
+ stateManager.goToState(NORMAL, animated);
}
AbstractFloatingView.closeAllOpenViews(mContainer, animated);
}
@@ -265,7 +268,7 @@
super.onGestureAnimationStart(runningTasks, rotationTouchHelper);
DesktopVisibilityController desktopVisibilityController =
mContainer.getDesktopVisibilityController();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
// TODO: b/333533253 - Remove after flag rollout
desktopVisibilityController.setRecentsGestureStart();
}
@@ -288,7 +291,7 @@
}
}
super.onGestureAnimationEnd();
- if (!enableDesktopWindowingWallpaperActivity() && desktopVisibilityController != null) {
+ if (!WALLPAPER_ACTIVITY.isEnabled(mContext) && desktopVisibilityController != null) {
// TODO: b/333533253 - Remove after flag rollout
desktopVisibilityController.setRecentsGestureEnd(endTarget);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d888eb9..d63ac56 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -191,10 +191,12 @@
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.recents.data.RecentTasksRepository;
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
import com.android.quickstep.recents.data.RecentsRotationStateRepository;
-import com.android.quickstep.recents.data.TasksRepository;
+import com.android.quickstep.recents.di.RecentsDependencies;
import com.android.quickstep.recents.viewmodel.RecentsViewData;
+import com.android.quickstep.recents.viewmodel.RecentsViewModel;
import com.android.quickstep.util.ActiveGestureErrorDetector;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AnimUtils;
@@ -241,8 +243,9 @@
/**
* A list of recent tasks.
+ *
* @param <CONTAINER_TYPE> : the container that should host recents view
- * @param <STATE_TYPE> : the type of base state that will be used
+ * @param <STATE_TYPE> : the type of base state that will be used
*/
public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer,
@@ -391,7 +394,7 @@
view.setScaleX(scale);
view.setScaleY(scale);
if (enableRefactorTaskThumbnail()) {
- view.mRecentsViewData.getScale().setValue(scale);
+ view.mRecentsViewModel.updateScale(scale);
}
view.mLastComputedTaskStartPushOutDistance = null;
view.mLastComputedTaskEndPushOutDistance = null;
@@ -463,15 +466,6 @@
private static final float FOREGROUND_SCRIM_TINT = 0.32f;
- @Nullable
- public final RecentsViewData mRecentsViewData = new RecentsViewData();
- @Nullable
- public final TasksRepository mTasksRepository;
- @Nullable
- public final RecentsRotationStateRepository mOrientedStateRepository;
- @Nullable
- public final RecentsDeviceProfileRepository mDeviceProfileRepository;
-
protected final RecentsOrientedState mOrientationState;
protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
@Nullable
@@ -726,10 +720,12 @@
private final SplitSelectionListener mSplitSelectionListener = new SplitSelectionListener() {
@Override
- public void onSplitSelectionConfirmed() { }
+ public void onSplitSelectionConfirmed() {
+ }
@Override
- public void onSplitSelectionActive() { }
+ public void onSplitSelectionActive() {
+ }
@Override
public void onSplitSelectionExit(boolean launchedSplit) {
@@ -808,10 +804,13 @@
*/
private boolean mAnyTaskHasBeenDismissed;
+ private final RecentsViewModel mRecentsViewModel;
+
public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
BaseContainerInterface sizeStrategy) {
super(context, attrs, defStyleAttr);
setEnableFreeScroll(true);
+
mSizeStrategy = sizeStrategy;
mContainer = RecentsViewContainer.containerFromContext(context);
mOrientationState = new RecentsOrientedState(
@@ -819,22 +818,30 @@
final int rotation = mContainer.getDisplay().getRotation();
mOrientationState.setRecentsRotation(rotation);
+ // Start Recents Dependency graph
+ if (enableRefactorTaskThumbnail()) {
+ RecentsDependencies recentsDependencies = RecentsDependencies.Companion.initialize(
+ this);
+ mRecentsViewModel = new RecentsViewModel(
+ recentsDependencies.inject(RecentTasksRepository.class),
+ recentsDependencies.inject(RecentsViewData.class)
+ );
+
+ recentsDependencies.provide(RecentsRotationStateRepository.class,
+ () -> new RecentsRotationStateRepository(mOrientationState));
+
+ recentsDependencies.provide(RecentsDeviceProfileRepository.class,
+ () -> new RecentsDeviceProfileRepository(mContainer));
+ } else {
+ mRecentsViewModel = null;
+ }
+
mScrollHapticMinGapMillis = getResources()
.getInteger(R.integer.recentsScrollHapticMinGapMillis);
mFastFlingVelocity = getResources()
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mModel = RecentsModel.INSTANCE.get(context);
mIdp = InvariantDeviceProfile.INSTANCE.get(context);
- if (enableRefactorTaskThumbnail()) {
- mTasksRepository = new TasksRepository(
- mModel, mModel.getThumbnailCache(), mModel.getIconCache());
- mOrientedStateRepository = new RecentsRotationStateRepository(mOrientationState);
- mDeviceProfileRepository = new RecentsDeviceProfileRepository(mContainer);
- } else {
- mTasksRepository = null;
- mOrientedStateRepository = null;
- mDeviceProfileRepository = null;
- }
mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
.inflate(R.layout.overview_clear_all_button, this, false);
@@ -1077,6 +1084,7 @@
/**
* Update the thumbnail(s) of the relevant TaskView.
+ *
* @param refreshNow Refresh immediately if it's true.
*/
@Nullable
@@ -1137,6 +1145,7 @@
/**
* See overridden implementations
+ *
* @return {@code true} if child TaskViews can be launched when user taps on them
*/
protected boolean canLaunchFullscreenTask() {
@@ -2061,7 +2070,7 @@
public void setFullscreenProgress(float fullscreenProgress) {
mFullscreenProgress = fullscreenProgress;
if (enableRefactorTaskThumbnail()) {
- mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
+ mRecentsViewModel.updateFullscreenProgress(mFullscreenProgress);
}
int taskCount = getTaskViewCount();
for (int i = 0; i < taskCount; i++) {
@@ -2459,7 +2468,7 @@
}
}
if (enableRefactorTaskThumbnail()) {
- mTasksRepository.setVisibleTasks(visibleTaskIds);
+ mRecentsViewModel.updateVisibleTasks(visibleTaskIds);
}
}
@@ -2526,6 +2535,13 @@
mFocusedTaskViewId = -1;
mAnyTaskHasBeenDismissed = false;
+
+ if (enableRefactorTaskThumbnail()) {
+ // TODO(b/353917593): RecentsView is never destroyed, so its dependencies need to
+ // be cleaned up during the reset, but re-created when RecentsView is "resumed".
+ // RecentsDependencies.Companion.destroy();
+ }
+
Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
+ ", mRecentsAnimationController: " + mRecentsAnimationController);
if (mEnableDrawingLiveTile) {
@@ -2636,6 +2652,7 @@
/**
* Get the index of the task view whose id matches {@param taskId}.
+ *
* @return -1 if there is no task view for the task id, else the index of the task view.
*/
public int getTaskIndexForId(int taskId) {
@@ -2651,7 +2668,7 @@
mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState
.getFilter(mFilterState.getPackageNameToFilter()));
if (enableRefactorTaskThumbnail()) {
- mTasksRepository.getAllTaskData(/* forceRefresh = */ true);
+ mRecentsViewModel.refreshAllTaskData();
}
}
}
@@ -2820,7 +2837,7 @@
int[] runningTaskIds = Arrays.stream(runningTasks).mapToInt(task -> task.key.id).toArray();
TaskView matchingTaskView = null;
if (hasDesktopTask(runningTasks) && runningTaskIds.length == 1) {
- // TODO(b/249371338): Unsure if it's expected, desktop runningTasks only have a single
+ // TODO(b/342635213): Unsure if it's expected, desktop runningTasks only have a single
// taskId, therefore we match any DesktopTaskView that contains the runningTaskId.
TaskView taskview = getTaskViewByTaskId(runningTaskIds[0]);
if (taskview instanceof DesktopTaskView) {
@@ -2861,7 +2878,7 @@
// When we create a placeholder task view mSplitBoundsConfig will be null, but with
// the actual app running we won't need to show the thumbnail until all the tasks
// load later anyways
- ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
+ ((GroupedTaskView) taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
} else {
taskView = getTaskViewFromPool(TaskViewType.SINGLE);
@@ -3189,7 +3206,7 @@
(mIsRtl
? mLastComputedTaskSize.right
: deviceProfile.widthPx - mLastComputedTaskSize.left)
- - longRowWidth - deviceProfile.overviewGridSideMargin;
+ - longRowWidth - deviceProfile.overviewGridSideMargin;
clearAllShortTotalWidthTranslation = mIsRtl
? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation;
if (snappedTaskRowWidth == longRowWidth) {
@@ -3219,9 +3236,9 @@
(mIsRtl
? mLastComputedTaskSize.left
: deviceProfile.widthPx - mLastComputedTaskSize.right)
- - deviceProfile.overviewGridSideMargin - mPageSpacing
- + (mTaskWidth - snappedTaskView.getLayoutParams().width)
- - mClearAllShortTotalWidthTranslation;
+ - deviceProfile.overviewGridSideMargin - mPageSpacing
+ + (mTaskWidth - snappedTaskView.getLayoutParams().width)
+ - mClearAllShortTotalWidthTranslation;
if (distanceFromClearAll < minimumDistance) {
int distanceDifference = minimumDistance - distanceFromClearAll;
snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
@@ -3301,12 +3318,12 @@
mLayoutTransition.addTransitionListener(new TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
+ View view, int i) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
- View view, int i) {
+ View view, int i) {
// When the unpinned task is added, snap to first page and disable transitions
if (view instanceof TaskView) {
snapToPage(0);
@@ -3458,11 +3475,13 @@
/**
* Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
- * @param dismissedTaskView the {@link TaskView} to be dismissed
- * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
- * @param shouldRemoveTask whether the associated {@link Task} should be removed from
- * ActivityManager after dismissal
- * @param duration duration of the animation
+ *
+ * @param dismissedTaskView the {@link TaskView} to be dismissed
+ * @param animateTaskView whether the {@link TaskView} to be dismissed should be
+ * animated
+ * @param shouldRemoveTask whether the associated {@link Task} should be removed from
+ * ActivityManager after dismissal
+ * @param duration duration of the animation
* @param dismissingForSplitSelection task dismiss animation is used for entering split
* selection state from app icon
*/
@@ -3727,7 +3746,7 @@
float animationEndProgress = isSplitSelectionActive()
? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
- + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
+ + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
: 1f;
// Slide tiles in horizontally to fill dismissed area
@@ -3771,16 +3790,16 @@
// dismissed index or next focused index. Offset successive task dismissal
// durations for a staggered effect.
distanceFromDismissedTask++;
- int staggerColumn = isStagingFocusedTask
+ int staggerColumn = isStagingFocusedTask
? (int) Math.ceil(distanceFromDismissedTask / 2f)
: distanceFromDismissedTask;
// Set timings based on if user is initiating splitscreen on the focused task,
// or splitting/dismissing some other task.
float animationStartProgress = isStagingFocusedTask
? Utilities.boundToRange(
- splitTimings.getGridSlideStartOffset()
- + (splitTimings.getGridSlideStaggerOffset()
- * staggerColumn),
+ splitTimings.getGridSlideStartOffset()
+ + (splitTimings.getGridSlideStaggerOffset()
+ * staggerColumn),
0f,
dismissTranslationInterpolationEnd)
: Utilities.boundToRange(
@@ -3789,9 +3808,9 @@
* staggerColumn, 0f, dismissTranslationInterpolationEnd);
float animationEndProgress = isStagingFocusedTask
? Utilities.boundToRange(
- splitTimings.getGridSlideStartOffset()
- + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
- + splitTimings.getGridSlideDurationOffset(),
+ splitTimings.getGridSlideStartOffset()
+ + (splitTimings.getGridSlideStaggerOffset() * staggerColumn)
+ + splitTimings.getGridSlideDurationOffset(),
0f,
dismissTranslationInterpolationEnd)
: dismissTranslationInterpolationEnd;
@@ -4007,8 +4026,9 @@
RecentsView.this);
int taskSize = (int) (
getPagedOrientationHandler().getMeasuredSize(
- taskView) * taskView
- .getSizeAdjustment(/*fullscreenEnabled=*/false));
+ taskView) * taskView
+ .getSizeAdjustment(/*fullscreenEnabled=*/
+ false));
int taskEnd = taskStart + taskSize;
shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
@@ -4651,6 +4671,7 @@
/**
* Computes the distance to offset the given child such that it is completely offscreen when
* translating away from the given midpoint.
+ *
* @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
*/
private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
@@ -4872,7 +4893,7 @@
mContainer.getDeviceProfile(),
mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
primaryTaskSelected);
- builder.addOnFrameCallback(() ->{
+ builder.addOnFrameCallback(() -> {
// TODO(b/334826842): Handle splash icon for new TTV.
if (!enableRefactorTaskThumbnail()) {
taskContainer.getThumbnailViewDeprecated().refreshSplashView();
@@ -4896,17 +4917,21 @@
* @param containerTaskView If our second selected app is currently running in Recents, this is
* the "container" TaskView from Recents. If we are starting a fresh
* instance of the app from an Intent, this will be null.
- * @param task The Task corresponding to our second selected app. If we are starting a fresh
- * instance of the app from an Intent, this will be null.
- * @param drawable The Drawable corresponding to our second selected app's icon.
- * @param secondView The View representing the current space on the screen where the second app
- * is (either the ThumbnailView or the tapped icon).
- * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If
- * the second app is already running in Recents, this will be null.
- * @param user If we are launching a fresh instance of the app, this is the UserHandle for it.
- * If the second app is already running in Recents, this will be null.
+ * @param task The Task corresponding to our second selected app. If we are
+ * starting a fresh
+ * instance of the app from an Intent, this will be null.
+ * @param drawable The Drawable corresponding to our second selected app's icon.
+ * @param secondView The View representing the current space on the screen where the
+ * second app
+ * is (either the ThumbnailView or the tapped icon).
+ * @param intent If we are launching a fresh instance of the app, this is the Intent
+ * for it. If
+ * the second app is already running in Recents, this will be null.
+ * @param user If we are launching a fresh instance of the app, this is the
+ * UserHandle for it.
+ * If the second app is already running in Recents, this will be null.
* @return true if waiting for confirmation of second app or if split animations are running,
- * false otherwise
+ * false otherwise
*/
public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable,
View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user,
@@ -4994,7 +5019,7 @@
"Second tile selected");
// Fade out all other views underneath placeholders
- ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
+ ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA, 1, 0);
pendingAnimation.add(tvFade, DECELERATE_2, SpringProperty.DEFAULT);
pendingAnimation.buildAnim().start();
return true;
@@ -5142,7 +5167,7 @@
if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) {
int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding;
mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(),
- mEmptyMessagePaint, availableWidth)
+ mEmptyMessagePaint, availableWidth)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
int totalHeight = mEmptyTextLayout.getHeight()
@@ -5269,7 +5294,7 @@
updateScrollSynchronously();
int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
- final boolean[] passedOverviewThreshold = new boolean[] {false};
+ final boolean[] passedOverviewThreshold = new boolean[]{false};
ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
progressAnim.addUpdateListener(animator -> {
// Once we pass a certain threshold, update the sysui flags to match the target
@@ -5390,7 +5415,7 @@
fullyVisibleTaskIds.addAll(taskIds);
}
}
- mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+ mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
}
}
@@ -5473,7 +5498,7 @@
}
RemoteTargetGluer gluer;
- if (recentsAnimationTargets.hasDesktopTasks()) {
+ if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
true /* forDesktop */);
mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
@@ -5789,6 +5814,7 @@
* Sets whether or not we should clamp the scroll offset.
* This is used to avoid x-axis movement when swiping up transient taskbar.
* Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
+ *
* @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
* met.
*/
@@ -5821,11 +5847,11 @@
// Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
// the page can move freely given there's no visual indication why it shouldn't.
int overScrollShift = mAdjacentPageHorizontalOffset > 0
- ? (int) Utilities.mapRange(
- mAdjacentPageHorizontalOffset,
- getOverScrollShift(),
- getUndampedOverScrollShift())
- : getOverScrollShift();
+ ? (int) Utilities.mapRange(
+ mAdjacentPageHorizontalOffset,
+ getOverScrollShift(),
+ getUndampedOverScrollShift())
+ : getOverScrollShift();
return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
+ overScrollShift + getOffsetFromScrollPosition(pageIndex);
}
@@ -5942,7 +5968,10 @@
if (mOverlayEnabled != overlayEnabled) {
mOverlayEnabled = overlayEnabled;
updateEnabledOverlays();
- mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
+
+ if (enableRefactorTaskThumbnail()) {
+ mRecentsViewModel.setOverlayEnabled(overlayEnabled);
+ }
}
}
@@ -6130,7 +6159,7 @@
public boolean showAsGrid() {
return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
&& mSizeStrategy.stateFromGestureEndTarget(mCurrentGestureEndTarget)
- .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
+ .displayOverviewTasksAsGrid(mContainer.getDeviceProfile()));
}
private boolean showAsFullscreen() {
@@ -6171,7 +6200,7 @@
/**
* @return Corner radius in pixel value for PiP window, which is updated via
- * {@link #mIPipAnimationListener}
+ * {@link #mIPipAnimationListener}
*/
public int getPipCornerRadius() {
return mPipCornerRadius;
@@ -6179,7 +6208,7 @@
/**
* @return Shadow radius in pixel value for PiP window, which is updated via
- * {@link #mIPipAnimationListener}
+ * {@link #mIPipAnimationListener}
*/
public int getPipShadowRadius() {
return mPipShadowRadius;
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
package com.android.quickstep.views;
-import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -42,9 +40,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
/**
@@ -57,7 +54,6 @@
public class SplitInstructionsView extends LinearLayout {
private static final int BOUNCE_DURATION = 250;
private static final float BOUNCE_HEIGHT = 20;
- private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
private final RecentsViewContainer mContainer;
public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
private void exitSplitSelection() {
RecentsView recentsView = mContainer.getOverviewPanel();
SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
StateManager stateManager = recentsView.getStateManager();
- BaseState startState = stateManager.getState();
- long duration = startState.getTransitionDuration(mContainer.asContext(), false);
- if (duration == 0) {
- // Case where we're in contextual on workspace (NORMAL), which by default has 0
- // transition duration
- duration = DURATION_DEFAULT_SPLIT_DISMISS;
- }
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = duration;
- AnimatorSet stateAnim = stateManager.createAtomicAnimation(
- startState, NORMAL, config);
- AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
- .createPlaceholderDismissAnim(mContainer,
- LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
- stateAnim.play(dismissAnim);
- stateManager.setCurrentAnimation(stateAnim, NORMAL);
- stateAnim.start();
+
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+ splitSelectController.getSplitAnimationController());
}
void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 74d120f..e7a8720 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,10 +29,15 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskUtils
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.di.getScope
+import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.util.GetThumbnailUseCase
import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.systemui.shared.recents.model.Task
/** Holder for all Task dependent information. */
@@ -55,21 +60,30 @@
taskOverlayFactory: TaskOverlayFactory
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- val taskContainerData = TaskContainerData()
+ lateinit var taskContainerData: TaskContainerData
- private val getThumbnailUseCase by lazy {
- // TODO(b/335649589): Ideally create and obtain this from DI.
- val recentsView =
- RecentsViewContainer.containerFromContext<RecentsViewContainer>(
- overlay.taskView.context
- )
- .getOverviewPanel<RecentsView<*, *>>()
- GetThumbnailUseCase(recentsView.mTasksRepository!!)
+ private val taskThumbnailViewModel: TaskThumbnailViewModel by
+ RecentsDependencies.inject(snapshotView)
+
+ // TODO(b/335649589): Ideally create and obtain this from DI.
+ private val taskContainerViewModel: TaskContainerViewModel by lazy {
+ TaskContainerViewModel(
+ sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+ getThumbnailUseCase = RecentsDependencies.get()
+ )
}
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
+ taskContainerData = RecentsDependencies.get(this)
+ RecentsDependencies.getScope(snapshotView).apply {
+ val taskViewScope = RecentsDependencies.getScope(taskView)
+ linkTo(taskViewScope)
+
+ val taskContainerScope = RecentsDependencies.getScope(this)
+ linkTo(taskContainerScope)
+ }
} else {
require(snapshotView is TaskThumbnailViewDeprecated)
}
@@ -78,7 +92,7 @@
val splitAnimationThumbnail: Bitmap?
get() =
if (enableRefactorTaskThumbnail()) {
- getThumbnailUseCase.run(task.key.id)
+ taskContainerViewModel.getThumbnail(task.key.id)
} else {
thumbnailViewDeprecated.thumbnail
}
@@ -104,7 +118,9 @@
// TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
val sysUiStatusNavFlags: Int
get() =
- if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+ if (enableRefactorTaskThumbnail())
+ taskContainerViewModel.getSysUiStatusNavFlags(task.key.id)
+ else thumbnailViewDeprecated.sysUiStatusNavFlags
/** Builds proto for logging */
val itemInfo: WorkspaceItemInfo
@@ -146,12 +162,10 @@
overlay.destroy()
}
- // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
- // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
fun bindThumbnailView() {
// TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
// this should be decided inside TaskThumbnailViewModel.
- thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+ taskThumbnailViewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
}
fun setOverlayEnabled(enabled: Boolean) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 004003c..f2f036a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -80,8 +80,10 @@
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.task.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.BorderAnimator
@@ -115,7 +117,8 @@
@IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
annotation class TaskDataChanges
- val taskViewData = TaskViewData(type)
+ private lateinit var taskViewModel: TaskViewModel
+
val taskIds: IntArray
/** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -441,6 +444,11 @@
init {
setOnClickListener { _ -> onClick() }
+
+ if (enableRefactorTaskThumbnail()) {
+ taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
+ }
+
val keyboardFocusHighlightEnabled =
(ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline())
val cursorHoverStatesEnabled = enableCursorHoverStates()
@@ -638,6 +646,7 @@
orientedState: RecentsOrientedState,
taskOverlayFactory: TaskOverlayFactory
) {
+
cancelPendingLoadTasks()
taskContainers =
listOf(
@@ -1404,7 +1413,7 @@
scaleX = scale
scaleY = scale
if (enableRefactorTaskThumbnail()) {
- taskViewData.scale.value = scale
+ taskViewModel.updateScale(scale)
}
updateSnapshotRadius()
}
@@ -1483,8 +1492,6 @@
/** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */
fun notifyIsRunningTaskUpdated() {
- // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
- // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
taskContainers.forEach { it.bindThumbnailView() }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
new file mode 100644
index 0000000..8bad3b9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Path
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.BubbleInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleViewTest {
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var bubbleView: BubbleView
+ private lateinit var overflowView: BubbleView
+ private lateinit var bubble: BubbleBarBubble
+
+ @Test
+ fun hasUnseenContent_bubble() {
+ setupBubbleViews()
+ assertThat(bubbleView.hasUnseenContent()).isTrue()
+
+ bubbleView.markSeen()
+ assertThat(bubbleView.hasUnseenContent()).isFalse()
+ }
+
+ @Test
+ fun hasUnseenContent_overflow() {
+ setupBubbleViews()
+ assertThat(overflowView.hasUnseenContent()).isFalse()
+ }
+
+ private fun setupBubbleViews() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ val inflater = LayoutInflater.from(context)
+
+ val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+ overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
+ overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+
+ val bubbleInfo =
+ BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false)
+ bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
+ bubble =
+ BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+ bubbleView.setBubble(bubble)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
new file mode 100644
index 0000000..24f9696
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherRestoreEventLoggerImplTest.kt
@@ -0,0 +1,139 @@
+package com.android.quickstep
+
+/*
+ * Copyright (C) 2024 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.
+ */
+
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_ENABLE_LAUNCHER_BR_METRICS_FIXED)
+class LauncherRestoreEventLoggerImplTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val mLauncherModelHelper = LauncherModelHelper()
+ private val mSandboxContext: SandboxModelContext = mLauncherModelHelper.sandboxContext
+ private lateinit var loggerUnderTest: LauncherRestoreEventLoggerImpl
+
+ @Before
+ fun setup() {
+ loggerUnderTest = LauncherRestoreEventLoggerImpl(mSandboxContext)
+ }
+
+ @After
+ fun teardown() {
+ loggerUnderTest.restoreEventLogger.clearData()
+ mLauncherModelHelper.destroy()
+ }
+
+ @Test
+ fun `logLauncherItemsRestoreFailed logs multiple items as failing restore`() {
+ // Given
+ val expectedDataType = "application"
+ val expectedError = "test_failure"
+ // When
+ loggerUnderTest.logLauncherItemsRestoreFailed(
+ dataType = expectedDataType,
+ count = 5,
+ error = expectedError
+ )
+ // Then
+ val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+ assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+ assertThat(actualResult.successCount).isEqualTo(0)
+ assertThat(actualResult.failCount).isEqualTo(5)
+ assertThat(actualResult.errors.keys).containsExactly(expectedError)
+ }
+
+ @Test
+ fun `logLauncherItemsRestored logs multiple items as restored`() {
+ // Given
+ val expectedDataType = "application"
+ // When
+ loggerUnderTest.logLauncherItemsRestored(dataType = expectedDataType, count = 5)
+ // Then
+ val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+ assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+ assertThat(actualResult.successCount).isEqualTo(5)
+ assertThat(actualResult.failCount).isEqualTo(0)
+ assertThat(actualResult.errors.keys).isEmpty()
+ }
+
+ @Test
+ fun `logSingleFavoritesItemRestored logs a single Favorites Item as restored`() {
+ // Given
+ val expectedDataType = "widget"
+ // When
+ loggerUnderTest.logSingleFavoritesItemRestored(favoritesId = Favorites.ITEM_TYPE_APPWIDGET)
+ // Then
+ val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+ assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+ assertThat(actualResult.successCount).isEqualTo(1)
+ assertThat(actualResult.failCount).isEqualTo(0)
+ assertThat(actualResult.errors.keys).isEmpty()
+ }
+
+ @Test
+ fun `logSingleFavoritesItemRestoreFailed logs a single Favorites Item as failing restore`() {
+ // Given
+ val expectedDataType = "widget"
+ val expectedError = "test_failure"
+ // When
+ loggerUnderTest.logSingleFavoritesItemRestoreFailed(
+ favoritesId = Favorites.ITEM_TYPE_APPWIDGET,
+ error = expectedError
+ )
+ // Then
+ val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+ assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+ assertThat(actualResult.successCount).isEqualTo(0)
+ assertThat(actualResult.failCount).isEqualTo(1)
+ assertThat(actualResult.errors.keys).containsExactly(expectedError)
+ }
+
+ @Test
+ fun `logFavoritesItemsRestoreFailed logs multiple Favorites Items as failing restore`() {
+ // Given
+ val expectedDataType = "deep_shortcut"
+ val expectedError = "test_failure"
+ // When
+ loggerUnderTest.logFavoritesItemsRestoreFailed(
+ favoritesId = Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+ count = 5,
+ error = expectedError
+ )
+ // Then
+ val actualResult = loggerUnderTest.restoreEventLogger.loggingResults.first()
+ assertThat(actualResult.dataType).isEqualTo(expectedDataType)
+ assertThat(actualResult.successCount).isEqualTo(0)
+ assertThat(actualResult.failCount).isEqualTo(5)
+ assertThat(actualResult.errors.keys).containsExactly(expectedError)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
new file mode 100644
index 0000000..070eeaf
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.logging
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
+import com.android.launcher3.logging.InstanceId
+import com.android.launcher3.logging.StatsLogManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class SettingsChangeLoggerTest {
+ private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+ private val mInstanceId = InstanceId.fakeInstanceId(1)
+
+ private lateinit var mSystemUnderTest: SettingsChangeLogger
+
+ @Mock private lateinit var mStatsLogManager: StatsLogManager
+
+ @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
+
+ @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(mStatsLogManager.logger()).doReturn(mMockLogger)
+ whenever(mStatsLogManager.logger().withInstanceId(any())).doReturn(mMockLogger)
+
+ mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
+ }
+
+ @After
+ fun tearDown() {
+ mSystemUnderTest.close()
+ }
+
+ @Test
+ fun loggingPrefs_correctDefaultValue() {
+ assertThat(mSystemUnderTest.loggingPrefs["pref_allowRotation"]!!.defaultValue).isFalse()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_add_icon_to_home"]!!.defaultValue).isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_overview_action_suggestions"]!!.defaultValue)
+ .isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_smartspace_home_screen"]!!.defaultValue)
+ .isTrue()
+ assertThat(mSystemUnderTest.loggingPrefs["pref_enable_minus_one"]!!.defaultValue).isTrue()
+ }
+
+ @Test
+ fun logSnapshot_defaultValue() {
+ mSystemUnderTest.logSnapshot(mInstanceId)
+
+ verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+ val capturedEvents = mEventCaptor.allValues
+ assertThat(capturedEvents.isNotEmpty()).isTrue()
+ verifyDefaultEvent(capturedEvents)
+ // pref_allowRotation false
+ assertThat(capturedEvents.any { it.id == 616 }).isTrue()
+ }
+
+ @Test
+ fun logSnapshot_updateValue() {
+ LauncherPrefs.get(mContext)
+ .put(
+ item =
+ backedUpItem(
+ sharedPrefKey = "pref_allowRotation",
+ defaultValue = false,
+ ),
+ value = true
+ )
+
+ mSystemUnderTest.logSnapshot(mInstanceId)
+
+ verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
+ val capturedEvents = mEventCaptor.allValues
+ assertThat(capturedEvents.isNotEmpty()).isTrue()
+ verifyDefaultEvent(capturedEvents)
+ // pref_allowRotation true
+ assertThat(capturedEvents.any { it.id == 615 }).isTrue()
+ }
+
+ private fun verifyDefaultEvent(capturedEvents: MutableList<StatsLogManager.EventEnum>) {
+ // LAUNCHER_NOTIFICATION_DOT_ENABLED
+ assertThat(capturedEvents.any { it.id == 611 }).isTrue()
+ // LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON
+ assertThat(capturedEvents.any { it.id == 625 }).isTrue()
+ // LAUNCHER_THEMED_ICON_DISABLED
+ assertThat(capturedEvents.any { it.id == 837 }).isTrue()
+ // pref_add_icon_to_home true
+ assertThat(capturedEvents.any { it.id == 613 }).isTrue()
+ // pref_overview_action_suggestions true
+ assertThat(capturedEvents.any { it.id == 619 }).isTrue()
+ // pref_smartspace_home_screen true
+ assertThat(capturedEvents.any { it.id == 621 }).isTrue()
+ // pref_enable_minus_one true
+ assertThat(capturedEvents.any { it.id == 617 }).isTrue()
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
new file mode 100644
index 0000000..e657d59
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.view.Surface.ROTATION_90
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Test for [GetThumbnailPositionUseCase] */
+@RunWith(AndroidJUnit4::class)
+class GetThumbnailPositionUseCaseTest {
+ private val task =
+ Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ private val thumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
+
+ private val deviceProfileRepository = mock<RecentsDeviceProfileRepository>()
+ private val rotationStateRepository = mock<RecentsRotationStateRepository>()
+ private val tasksRepository = FakeTasksRepository()
+ private val previewPositionHelper = mock<PreviewPositionHelper>()
+
+ private val systemUnderTest =
+ GetThumbnailPositionUseCase(
+ deviceProfileRepository,
+ rotationStateRepository,
+ tasksRepository,
+ previewPositionHelper
+ )
+
+ @Test
+ fun invisibleTask_returnsIdentityMatrix() = runTest {
+ tasksRepository.seedTasks(listOf(task))
+
+ assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+ .isInstanceOf(MissingThumbnail::class.java)
+ }
+
+ @Test
+ fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+
+ assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
+ .isInstanceOf(MissingThumbnail::class.java)
+ }
+
+ @Test
+ fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+
+ val isLargeScreen = true
+ val activityRotation = ROTATION_90
+ val isRtl = true
+ val isRotated = true
+
+ whenever(deviceProfileRepository.getRecentsDeviceProfile())
+ .thenReturn(RecentsDeviceProfileRepository.RecentsDeviceProfile(isLargeScreen))
+ whenever(rotationStateRepository.getRecentsRotationState())
+ .thenReturn(RecentsRotationStateRepository.RecentsRotationState(activityRotation))
+
+ whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
+ whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
+
+ assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .isEqualTo(MatrixScaling(MATRIX, isRotated))
+
+ verify(previewPositionHelper)
+ .updateThumbnailMatrix(
+ Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
+ thumbnailData,
+ CANVAS_WIDTH,
+ CANVAS_HEIGHT,
+ isLargeScreen,
+ activityRotation,
+ isRtl
+ )
+ }
+
+ companion object {
+ const val TASK_ID = 2
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ const val CANVAS_WIDTH = 300
+ const val CANVAS_HEIGHT = 600
+ val MATRIX =
+ Matrix().apply {
+ setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
similarity index 98%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 414f8ca..12a94cf 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.quickstep.task.util
+package com.android.quickstep.recents.usecase
import android.content.ComponentName
import android.content.Intent
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
new file mode 100644
index 0000000..ba4e206
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [SysUiStatusNavFlagsUseCase] */
+class SysUiStatusNavFlagsUseCaseTest {
+ private lateinit var tasksRepository: FakeTasksRepository
+ private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase
+
+ @Before
+ fun setup() {
+ tasksRepository = FakeTasksRepository()
+ sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository)
+ initTaskRepository()
+ }
+
+ @Test
+ fun onLightAppearanceReturnExpectedFlags() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID))
+ .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME)
+ }
+
+ @Test
+ fun onDarkAppearanceReturnExpectedFlags() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID))
+ .isEqualTo(FLAGS_APPEARANCE_DARK_THEME)
+ }
+
+ @Test
+ fun whenThumbnailIsNullReturnDefault() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID))
+ .isEqualTo(FLAGS_DEFAULT)
+ }
+
+ private fun initTaskRepository() {
+ val firstTask =
+ Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ val firstThumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ },
+ appearance = APPEARANCE_LIGHT_THEME
+ )
+
+ val secondTask =
+ Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply {
+ colorBackground = Color.BLACK
+ }
+ val secondThumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ },
+ appearance = APPEARANCE_DARK_THEME
+ )
+
+ tasksRepository.seedTasks(listOf(firstTask, secondTask))
+ tasksRepository.seedThumbnailData(
+ mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
+ )
+ tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+ }
+
+ companion object {
+ const val FIRST_TASK_ID = 0
+ const val SECOND_TASK_ID = 100
+ const val UNKNOWN_TASK_ID = 404
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ const val APPEARANCE_LIGHT_THEME = 24
+ const val FLAGS_APPEARANCE_LIGHT_THEME = 5
+ const val APPEARANCE_DARK_THEME = 0
+ const val FLAGS_APPEARANCE_DARK_THEME = 10
+ const val FLAGS_DEFAULT = 0
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index b78f871..754c9d1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -20,15 +20,19 @@
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
-import android.graphics.Rect
+import android.graphics.Matrix
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
@@ -48,8 +52,15 @@
private val taskViewData by lazy { TaskViewData(taskViewType) }
private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
+ private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val systemUnderTest by lazy {
- TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
+ TaskThumbnailViewModel(
+ recentsViewData,
+ taskViewData,
+ taskContainerData,
+ tasksRepository,
+ mGetThumbnailPositionUseCase
+ )
}
private val tasks = (0..5).map(::createTaskWithId)
@@ -149,7 +160,6 @@
Snapshot(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
- drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
)
)
}
@@ -170,11 +180,38 @@
Snapshot(
backgroundColor = Color.rgb(2, 2, 2),
bitmap = expectedThumbnailData.thumbnail!!,
- drawnRect = Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
)
)
}
+ @Test
+ fun getSnapshotMatrix_MissingThumbnail() = runTest {
+ val taskId = 2
+ val recentTask = TaskThumbnail(taskId = taskId, isRunning = false)
+ val isRtl = true
+
+ whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MissingThumbnail)
+
+ systemUnderTest.bind(recentTask)
+ assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .isEqualTo(Matrix.IDENTITY_MATRIX)
+ }
+
+ @Test
+ fun getSnapshotMatrix_MatrixScaling() = runTest {
+ val taskId = 2
+ val recentTask = TaskThumbnail(taskId = taskId, isRunning = false)
+ val isRtl = true
+
+ whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MatrixScaling(MATRIX, isRotated = false))
+
+ systemUnderTest.bind(recentTask)
+ assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .isEqualTo(MATRIX)
+ }
+
private fun createTaskWithId(taskId: Int) =
Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
colorBackground = Color.argb(taskId, taskId, taskId, taskId)
@@ -191,5 +228,11 @@
companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
+ const val CANVAS_WIDTH = 300
+ const val CANVAS_HEIGHT = 600
+ val MATRIX =
+ Matrix().apply {
+ setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+ }
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index 40482c4..d0887df 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -23,9 +23,13 @@
import android.graphics.Matrix
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
@@ -53,7 +57,9 @@
)
private val recentsViewData = RecentsViewData()
private val tasksRepository = FakeTasksRepository()
- private val systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+ private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+ private val systemUnderTest =
+ TaskOverlayViewModel(task, recentsViewData, mGetThumbnailPositionUseCase, tasksRepository)
@Test
fun initialStateIsDisabled() = runTest {
@@ -87,7 +93,6 @@
Enabled(
isRealSnapshot = false,
thumbnail = null,
- thumbnailMatrix = Matrix.IDENTITY_MATRIX
)
)
}
@@ -107,7 +112,6 @@
Enabled(
isRealSnapshot = true,
thumbnail = thumbnailData.thumbnail,
- thumbnailMatrix = Matrix.IDENTITY_MATRIX
)
)
}
@@ -127,7 +131,6 @@
Enabled(
isRealSnapshot = false,
thumbnail = thumbnailData.thumbnail,
- thumbnailMatrix = Matrix.IDENTITY_MATRIX
)
)
}
@@ -147,14 +150,42 @@
Enabled(
isRealSnapshot = false,
thumbnail = thumbnailData.thumbnail,
- thumbnailMatrix = Matrix.IDENTITY_MATRIX
)
)
}
+ @Test
+ fun getThumbnailMatrix_MissingThumbnail() = runTest {
+ val isRtl = true
+
+ whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MissingThumbnail)
+
+ assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
+ }
+
+ @Test
+ fun getThumbnailMatrix_MatrixScaling() = runTest {
+ val isRtl = true
+ val isRotated = true
+
+ whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .thenReturn(MatrixScaling(MATRIX, isRotated))
+
+ assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+ .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
+ }
+
companion object {
const val TASK_ID = 0
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
+ const val CANVAS_WIDTH = 300
+ const val CANVAS_HEIGHT = 600
+ val MATRIX =
+ Matrix().apply {
+ setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+ }
}
}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 27e761a..07c4ffc 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -18,6 +18,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
import android.os.Process
import android.os.UserHandle
@@ -56,6 +57,7 @@
@Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
+ @Mock private lateinit var mockContext: Context
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
private var taskListChangeId: Int = 1
@@ -71,7 +73,9 @@
whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
- TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
+ TaskbarRecentAppsController(mockContext, mockRecentsModel) {
+ mockDesktopVisibilityController
+ }
recentAppsController.init(taskbarControllers)
recentAppsController.canShowRunningApps = true
recentAppsController.canShowRecentApps = true
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 5d00255..c213dbb 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -16,6 +16,8 @@
package com.android.quickstep;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.TestCase.assertNull;
import static org.junit.Assert.assertEquals;
@@ -27,6 +29,7 @@
import android.app.ActivityManager;
import android.app.KeyguardManager;
+import android.content.Context;
import androidx.test.filters.SmallTest;
@@ -52,7 +55,9 @@
public class RecentTasksListTest {
@Mock
- private SystemUiProxy mockSystemUiProxy;
+ private Context mContext;
+ @Mock
+ private SystemUiProxy mSystemUiProxy;
@Mock
private TopTaskTracker mTopTaskTracker;
@@ -64,22 +69,22 @@
MockitoAnnotations.initMocks(this);
LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
- mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManager,
- mockSystemUiProxy, mTopTaskTracker);
+ mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
+ mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
}
@Test
- public void onRecentTasksChanged_doesNotFetchTasks() {
+ public void onRecentTasksChanged_doesNotFetchTasks() throws Exception {
mRecentTasksList.onRecentTasksChanged();
- verify(mockSystemUiProxy, times(0))
+ verify(mSystemUiProxy, times(0))
.getRecentTasks(anyInt(), anyInt());
}
@Test
- public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
+ public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception {
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -91,7 +96,19 @@
}
@Test
- public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
+ public void loadTasksInBackground_GetRecentTasksException() throws Exception {
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ .thenThrow(new SystemUiProxy.GetRecentTasksException("task load failed"));
+
+ RecentTasksList.TaskLoadResult taskList = mRecentTasksList.loadTasksInBackground(
+ Integer.MAX_VALUE, -1, false);
+
+ assertThat(taskList.mRequestId).isEqualTo(-1);
+ assertThat(taskList).isEmpty();
+ }
+
+ @Test
+ public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception {
String taskDescription = "Wheeee!";
ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo();
task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
@@ -99,7 +116,7 @@
task2.taskDescription = new ActivityManager.TaskDescription();
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2,
null);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, -1,
@@ -111,14 +128,14 @@
}
@Test
- public void loadTasksInBackground_freeformTask_createsDesktopTask() {
+ public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception {
ActivityManager.RecentTaskInfo[] tasks = {
createRecentTaskInfo(1 /* taskId */),
createRecentTaskInfo(4 /* taskId */),
createRecentTaskInfo(5 /* taskId */)};
GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks(
tasks, Collections.emptySet() /* minimizedTaskIds */);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
@@ -134,7 +151,8 @@
}
@Test
- public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask() {
+ public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask()
+ throws Exception {
ActivityManager.RecentTaskInfo[] tasks = {
createRecentTaskInfo(1 /* taskId */),
createRecentTaskInfo(4 /* taskId */),
@@ -143,7 +161,7 @@
Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
GroupedRecentTaskInfo recentTaskInfos =
GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
- when(mockSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
+ when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
List<GroupTask> taskList = mRecentTasksList.loadTasksInBackground(
diff --git a/res/color-night-v31/material_color_surface.xml b/res/color-night-v31/material_color_surface.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_bright.xml b/res/color-night-v31/material_color_surface_bright.xml
deleted file mode 100644
index f34ed6c..0000000
--- a/res/color-night-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="24" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container.xml b/res/color-night-v31/material_color_surface_container.xml
deleted file mode 100644
index 002b88e..0000000
--- a/res/color-night-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="12" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_high.xml b/res/color-night-v31/material_color_surface_container_high.xml
deleted file mode 100644
index edd36fc..0000000
--- a/res/color-night-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="17" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_highest.xml b/res/color-night-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e54f953..0000000
--- a/res/color-night-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="22" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_low.xml b/res/color-night-v31/material_color_surface_container_low.xml
deleted file mode 100644
index 40f0d4c..0000000
--- a/res/color-night-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="10" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_container_lowest.xml b/res/color-night-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 24f559b..0000000
--- a/res/color-night-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="4" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_dim.xml b/res/color-night-v31/material_color_surface_dim.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_inverse.xml b/res/color-night-v31/material_color_surface_inverse.xml
deleted file mode 100644
index ac63072..0000000
--- a/res/color-night-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/material_color_surface_variant.xml b/res/color-night-v31/material_color_surface_variant.xml
deleted file mode 100644
index a645f24..0000000
--- a/res/color-night-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-night-v31/popup_shade_first.xml b/res/color-night-v31/popup_shade_first.xml
index 83822a6..28995e3 100644
--- a/res/color-night-v31/popup_shade_first.xml
+++ b/res/color-night-v31/popup_shade_first.xml
@@ -12,7 +12,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/materialColorSurfaceContainer"/>
</selector>
diff --git a/res/color-v31/material_color_surface.xml b/res/color-v31/material_color_surface.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_bright.xml b/res/color-v31/material_color_surface_bright.xml
deleted file mode 100644
index b049851..0000000
--- a/res/color-v31/material_color_surface_bright.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="98" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container.xml b/res/color-v31/material_color_surface_container.xml
deleted file mode 100644
index b031c08..0000000
--- a/res/color-v31/material_color_surface_container.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="94" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_high.xml b/res/color-v31/material_color_surface_container_high.xml
deleted file mode 100644
index a996d51..0000000
--- a/res/color-v31/material_color_surface_container_high.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="92" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_highest.xml b/res/color-v31/material_color_surface_container_highest.xml
deleted file mode 100644
index e7a535a..0000000
--- a/res/color-v31/material_color_surface_container_highest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="90" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_low.xml b/res/color-v31/material_color_surface_container_low.xml
deleted file mode 100644
index b8fe01e..0000000
--- a/res/color-v31/material_color_surface_container_low.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="96" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_container_lowest.xml b/res/color-v31/material_color_surface_container_lowest.xml
deleted file mode 100644
index 25e8666..0000000
--- a/res/color-v31/material_color_surface_container_lowest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="100" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_dim.xml b/res/color-v31/material_color_surface_dim.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_dim.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_inverse.xml b/res/color-v31/material_color_surface_inverse.xml
deleted file mode 100644
index e189862..0000000
--- a/res/color-v31/material_color_surface_inverse.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="6" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/material_color_surface_variant.xml b/res/color-v31/material_color_surface_variant.xml
deleted file mode 100644
index e2d226f..0000000
--- a/res/color-v31/material_color_surface_variant.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:color="@android:color/system_neutral1_500" android:lStar="87" />
-</selector>
\ No newline at end of file
diff --git a/res/color-v31/popup_shade_first.xml b/res/color-v31/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color-v31/popup_shade_first.xml
+++ b/res/color-v31/popup_shade_first.xml
@@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/materialColorSurfaceContainer"/>
</selector>
diff --git a/res/color/overview_button.xml b/res/color/overview_button.xml
index 1dd8da6..0b317bd 100644
--- a/res/color/overview_button.xml
+++ b/res/color/overview_button.xml
@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:alpha="1"
- android:color="?androidprv:attr/materialColorOnSurface"
+ android:color="?attr/materialColorOnSurface"
android:state_enabled="true" />
<item
android:alpha="?android:disabledAlpha"
- android:color="?androidprv:attr/materialColorOnSurface"
+ android:color="?attr/materialColorOnSurface"
android:state_enabled="false" />
</selector>
\ No newline at end of file
diff --git a/res/color/popup_shade_first.xml b/res/color/popup_shade_first.xml
index 1278bb4..be73698 100644
--- a/res/color/popup_shade_first.xml
+++ b/res/color/popup_shade_first.xml
@@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <item android:color="?androidprv:attr/materialColorSurfaceContainer"/>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/materialColorSurfaceContainer"/>
</selector>
diff --git a/res/drawable/add_item_dialog_background.xml b/res/drawable/add_item_dialog_background.xml
index e279fa0..be4765a 100644
--- a/res/drawable/add_item_dialog_background.xml
+++ b/res/drawable/add_item_dialog_background.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
- <solid android:color="@color/material_color_surface_container_highest" />
+ <solid android:color="?attr/materialColorSurfaceContainerHighest" />
<corners
android:topLeftRadius="?android:attr/dialogCornerRadius"
android:topRightRadius="?android:attr/dialogCornerRadius" />
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 1e7cff2..62927af 100644
--- a/res/drawable/all_apps_tabs_background.xml
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -30,7 +30,7 @@
android:state_selected="false">
<shape android:shape="rectangle">
<corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
- <solid android:color="@color/material_color_surface_bright" />
+ <solid android:color="?attr/materialColorSurfaceBright" />
</shape>
</item>
@@ -39,7 +39,7 @@
android:state_selected="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
- <solid android:color="@color/material_color_primary" />
+ <solid android:color="?attr/materialColorPrimary" />
</shape>
</item>
</selector>
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
index 379e0e5..a19465d 100644
--- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
+++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
@@ -15,8 +15,7 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle" >
- <solid android:color="?androidprv:attr/materialColorOutlineVariant"/>
+ <solid android:color="?attr/materialColorOutlineVariant"/>
<corners android:radius="@dimen/bottom_sheet_handle_corner_radius" />
</shape>
diff --git a/res/drawable/button_top_rounded_bordered_ripple.xml b/res/drawable/button_top_rounded_bordered_ripple.xml
index f5b6886..13959f6 100644
--- a/res/drawable/button_top_rounded_bordered_ripple.xml
+++ b/res/drawable/button_top_rounded_bordered_ripple.xml
@@ -25,7 +25,7 @@
android:topRightRadius="12dp"
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp" />
- <solid android:color="@color/material_color_surface_container_highest"/>
+ <solid android:color="?attr/materialColorSurfaceContainerHighest"/>
<stroke
android:width="2dp"
android:color="@color/button_bg"/>
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
index f336eea..e4053e3 100644
--- a/res/drawable/ic_close_work_edu.xml
+++ b/res/drawable/ic_close_work_edu.xml
@@ -20,6 +20,6 @@
android:viewportWidth="960"
android:viewportHeight="960">
<path
- android:fillColor="@color/material_color_on_surface"
+ android:fillColor="?attr/materialColorOnSurface"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
</vector>
diff --git a/res/drawable/ic_more_vert_dots.xml b/res/drawable/ic_more_vert_dots.xml
new file mode 100644
index 0000000..c4659f8
--- /dev/null
+++ b/res/drawable/ic_more_vert_dots.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2024 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"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_private_space_with_background.xml b/res/drawable/ic_private_space_with_background.xml
index cb37c9a..cc73f6d 100644
--- a/res/drawable/ic_private_space_with_background.xml
+++ b/res/drawable/ic_private_space_with_background.xml
@@ -13,14 +13,13 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:viewportWidth="48"
android:viewportHeight="48"
android:width="48dp"
android:height="48dp">
<path
android:pathData="M48 24A24 24 0 0 1 0 24A24 24 0 0 1 48 24Z"
- android:fillColor="?androidprv:attr/materialColorSurfaceContainerLowest" />
+ android:fillColor="?attr/materialColorSurfaceContainerLowest" />
<path
android:pathData="M24.0021 10.6641L13.3354 14.6641V22.7841C13.3354 29.5174 17.8821 35.7974 24.0021 37.3307C30.1221 35.7974 34.6688 29.5174 34.6688 22.7841V14.6641L24.0021 10.6641ZM32.0021 22.7841C32.0021 28.1174 28.6021 33.0507 24.0021 34.5574C19.4021 33.0507 16.0021 28.1307 16.0021 22.7841V16.5174L24.0021 13.5174L32.0021 16.5174V22.7841Z"
android:fillType="evenOdd"
diff --git a/res/drawable/icon_menu_arrow_background.xml b/res/drawable/icon_menu_arrow_background.xml
index 2eb1dfc..6345c2b 100644
--- a/res/drawable/icon_menu_arrow_background.xml
+++ b/res/drawable/icon_menu_arrow_background.xml
@@ -15,14 +15,13 @@
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:autoMirrored="true">
<gradient
android:type="linear"
android:angle="0"
android:startColor="#00000000"
android:centerX="0.25"
- android:centerColor="?androidprv:attr/materialColorSurfaceBright"
- android:endColor="?androidprv:attr/materialColorSurfaceBright" />
+ android:centerColor="?attr/materialColorSurfaceBright"
+ android:endColor="?attr/materialColorSurfaceBright" />
<corners android:radius="@dimen/dialogCornerRadius" />
</shape>
\ No newline at end of file
diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
index cfec2b1..12c4a82 100644
--- a/res/drawable/private_space_install_app_icon.xml
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -23,9 +23,9 @@
android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
<path
android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z"
- android:fillColor="@color/material_color_surface_container_lowest" />
+ android:fillColor="?attr/materialColorSurfaceContainerLowest" />
<path
android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
- android:fillColor="@color/material_color_on_surface" />
+ android:fillColor="?attr/materialColorOnSurface" />
</group>
</vector>
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index e283d3f..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -7,7 +7,7 @@
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
- ~ Unless required by applicable law or agreed to in writing, software
+ ~ Unless required by applicable law or agreed to in writing, soft]ware
~ 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
@@ -16,15 +16,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="@color/material_color_surface_container_low" />
+ <solid android:color="?attr/materialColorSurfaceContainerLow" />
<corners android:radius="@dimen/rounded_button_radius" />
<stroke
android:width="1dp"
- android:color="@color/material_color_surface_container_low" />
- <padding
- android:left="@dimen/rounded_button_padding"
- android:right="@dimen/rounded_button_padding" />
+ android:color="?attr/materialColorSurfaceContainerLow" />
</shape>
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
index 9bf2b8d..01ec947 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
- <solid android:color="@color/material_color_surface_container_highest" />
+ <solid android:color="?attr/materialColorSurfaceContainerHighest" />
<corners android:radius="@dimen/work_edu_card_radius" />
</shape>
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
index 52180cf..e68f0f3 100644
--- a/res/layout/private_space_header.xml
+++ b/res/layout/private_space_header.xml
@@ -61,7 +61,7 @@
android:layout_marginBottom="@dimen/ps_lock_icon_margin_bottom"
android:importantForAccessibility="no"
android:src="@drawable/ic_lock"
- app:tint="@color/material_color_primary_fixed_dim"
+ app:tint="?attr/materialColorPrimaryFixedDim"
android:scaleType="center"/>
<TextView
android:id="@+id/lock_text"
@@ -69,7 +69,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/ps_lock_icon_text_margin_start_expanded"
android:layout_marginEnd="@dimen/ps_lock_icon_text_margin_end_expanded"
- android:textColor="@color/material_color_on_primary_fixed"
+ android:textColor="?attr/materialColorOnPrimaryFixed"
android:textSize="14sp"
android:text="@string/ps_container_lock_title"
android:maxLines="1"
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 887efb8..1f41680 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -55,18 +55,39 @@
android:clipToOutline="true"
android:orientation="vertical">
- <FrameLayout
+ <LinearLayout
android:id="@+id/search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:orientation="horizontal"
android:background="?attr/widgetPickerPrimarySurfaceColor"
- android:clipToPadding="false"
- android:elevation="0.1dp"
- android:paddingBottom="8dp"
+ android:gravity="center_vertical"
launcher:layout_sticky="true">
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:elevation="0.1dp"
+ android:paddingBottom="8dp">
- <include layout="@layout/widgets_search_bar" />
- </FrameLayout>
+ <include layout="@layout/widgets_search_bar" />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/widget_picker_widget_options_menu"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginBottom="8dp"
+ android:layout_gravity="bottom"
+ android:background="@drawable/full_rounded_transparent_ripple"
+ android:contentDescription="@string/widget_picker_widget_options_button_description"
+ android:padding="12dp"
+ android:src="@drawable/ic_more_vert_dots"
+ android:visibility="gone"
+ android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+ </LinearLayout>
<FrameLayout
android:layout_width="match_parent"
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index f3d3b16..c6b3b74 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -39,19 +39,40 @@
android:clipToOutline="true"
android:orientation="vertical">
- <FrameLayout
+ <LinearLayout
android:id="@+id/search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
android:background="?attr/widgetPickerPrimarySurfaceColor"
- android:clipToPadding="false"
- android:elevation="0.1dp"
- android:paddingBottom="16dp"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
launcher:layout_sticky="true">
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:elevation="0.1dp"
+ android:paddingBottom="16dp">
- <include layout="@layout/widgets_search_bar" />
- </FrameLayout>
+ <include layout="@layout/widgets_search_bar" />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/widget_picker_widget_options_menu"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginBottom="16dp"
+ android:layout_gravity="bottom"
+ android:background="@drawable/full_rounded_transparent_ripple"
+ android:contentDescription="@string/widget_picker_widget_options_button_description"
+ android:padding="12dp"
+ android:src="@drawable/ic_more_vert_dots"
+ android:visibility="gone"
+ android:tint="?attr/widgetPickerWidgetOptionsMenuColor" />
+ </LinearLayout>
<FrameLayout
android:layout_width="match_parent"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
<FrameLayout
android:layout_width="@dimen/rounded_button_width"
android:layout_height="@dimen/rounded_button_width"
- android:background="@drawable/rounded_action_button"
- android:padding="@dimen/rounded_button_padding">
+ android:background="@drawable/rounded_action_button">
<ImageButton
android:id="@+id/action_btn"
android:layout_width="@dimen/x_icon_size"
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index 2688b83..07c450e 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -57,39 +57,4 @@
@android:color/system_accent1_200</color>
<color name="work_fab_icon_color">
@android:color/system_accent1_900</color>
-
- <color name="material_color_on_secondary_fixed_variant">@android:color/system_accent2_700</color>
- <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
- <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
- <color name="material_color_on_secondary_container">@android:color/system_accent2_100</color>
- <color name="material_color_on_tertiary_container">@android:color/system_accent3_100</color>
- <color name="material_color_on_primary_container">@android:color/system_accent1_100</color>
- <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
- <color name="material_color_on_error_container">#FFDAD5</color>
- <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
- <color name="material_color_on_surface_inverse">@android:color/system_neutral1_900</color>
- <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
- <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
- <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
- <color name="material_color_secondary_container">@android:color/system_accent2_700</color>
- <color name="material_color_error_container">#930001</color>
- <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
- <color name="material_color_primary_inverse">@android:color/system_accent1_600</color>
- <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
- <color name="material_color_tertiary_container">@android:color/system_accent3_700</color>
- <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
- <color name="material_color_primary_container">@android:color/system_accent1_700</color>
- <color name="material_color_on_background">@android:color/system_neutral1_800</color>
- <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
- <color name="material_color_on_secondary">@android:color/system_accent2_800</color>
- <color name="material_color_on_tertiary">@android:color/system_accent3_800</color>
- <color name="material_color_on_error">#690001</color>
- <color name="material_color_on_surface_variant">@android:color/system_neutral2_200</color>
- <color name="material_color_outline">@android:color/system_neutral2_400</color>
- <color name="material_color_outline_variant">@android:color/system_neutral2_700</color>
- <color name="material_color_on_primary">@android:color/system_accent1_800</color>
- <color name="material_color_on_surface">@android:color/system_neutral1_100</color>
- <color name="material_color_primary">@android:color/system_accent1_200</color>
- <color name="material_color_secondary">@android:color/system_accent2_200</color>
- <color name="material_color_tertiary">@android:color/system_accent3_200</color>
</resources>
\ No newline at end of file
diff --git a/res/values-night-v34/colors.xml b/res/values-night-v34/colors.xml
index af28119..abce763 100644
--- a/res/values-night-v34/colors.xml
+++ b/res/values-night-v34/colors.xml
@@ -27,4 +27,7 @@
@android:color/system_on_surface_dark</color>
<color name="widget_cell_subtitle_color_dark">
@android:color/system_on_surface_variant_dark</color>
+ <color name="widget_picker_menu_options_color_dark">
+ @android:color/system_on_surface_variant_dark
+ </color>
</resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
deleted file mode 100644
index 95b3a63..0000000
--- a/res/values-night/colors.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 2023, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<resources>
- <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
- <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
- <color name="material_color_surface_container_lowest">#0D0E11</color>
- <color name="material_color_on_primary_fixed_variant">#2B4678</color>
- <color name="material_color_on_secondary_container">#DBE2F9</color>
- <color name="material_color_on_tertiary_container">#FBD7FC</color>
- <color name="material_color_surface_container_low">#1B1B1F</color>
- <color name="material_color_on_primary_container">#D8E2FF</color>
- <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
- <color name="material_color_on_error_container">#FFDAD5</color>
- <color name="material_color_on_secondary_fixed">#141B2C</color>
- <color name="material_color_on_surface_inverse">#1B1B1F</color>
- <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
- <color name="material_color_on_tertiary_fixed">#29132D</color>
- <color name="material_color_primary_fixed_dim">#ADC6FF</color>
- <color name="material_color_secondary_container">#3F4759</color>
- <color name="material_color_error_container">#930001</color>
- <color name="material_color_on_primary_fixed">#001A41</color>
- <color name="material_color_primary_inverse">#445E91</color>
- <color name="material_color_secondary_fixed">#DBE2F9</color>
- <color name="material_color_surface_inverse">#FAF9FD</color>
- <color name="material_color_surface_variant">#44474F</color>
- <color name="material_color_tertiary_container">#583E5B</color>
- <color name="material_color_tertiary_fixed">#FBD7FC</color>
- <color name="material_color_primary_container">#2B4678</color>
- <color name="material_color_on_background">#E3E2E6</color>
- <color name="material_color_primary_fixed">#D8E2FF</color>
- <color name="material_color_on_secondary">#293041</color>
- <color name="material_color_on_tertiary">#402843</color>
- <color name="material_color_surface_dim">#121316</color>
- <color name="material_color_surface_bright">#38393C</color>
- <color name="material_color_on_error">#690001</color>
- <color name="material_color_surface">#121316</color>
- <color name="material_color_surface_container_high">#292A2D</color>
- <color name="material_color_surface_container_highest">#343538</color>
- <color name="material_color_on_surface_variant">#C4C6D0</color>
- <color name="material_color_outline">#72747D</color>
- <color name="material_color_outline_variant">#444746</color>
- <color name="material_color_on_primary">#102F60</color>
- <color name="material_color_on_surface">#E3E2E6</color>
- <color name="material_color_surface_container">#1F1F23</color>
- <color name="material_color_primary">#ADC6FF</color>
- <color name="material_color_secondary">#BFC6DC</color>
- <color name="material_color_tertiary">#DEBCDF</color>
-</resources>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index c95722f..06f0eee 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -26,9 +26,64 @@
<item name="android:backgroundDimEnabled">true</item>
</style>
- <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+ <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+ <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+ <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_dark</item>
+ <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+ <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_dark</item>
+ <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_dark</item>
+ <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_dark</item>
+ <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_dark</item>
+ <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
+ <item name="materialColorOnErrorContainer">@color/system_on_error_container_dark</item>
+ <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
+ <item name="materialColorOnSurfaceInverse">@color/system_on_surface_light</item>
+ <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
+ <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
+ <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
+ <item name="materialColorSecondaryContainer">@color/system_secondary_container_dark</item>
+ <item name="materialColorErrorContainer">@color/system_error_container_dark</item>
+ <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
+ <item name="materialColorPrimaryInverse">@color/system_primary_light</item>
+ <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+ <item name="materialColorSurfaceInverse">@color/system_surface_light</item>
+ <item name="materialColorSurfaceVariant">@color/system_surface_variant_dark</item>
+ <item name="materialColorTertiaryContainer">@color/system_tertiary_container_dark</item>
+ <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
+ <item name="materialColorPrimaryContainer">@color/system_primary_container_dark</item>
+ <item name="materialColorOnBackground">@color/system_on_background_dark</item>
+ <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+ <item name="materialColorOnSecondary">@color/system_on_secondary_dark</item>
+ <item name="materialColorOnTertiary">@color/system_on_tertiary_dark</item>
+ <item name="materialColorSurfaceDim">@color/system_surface_dim_dark</item>
+ <item name="materialColorSurfaceBright">@color/system_surface_bright_dark</item>
+ <item name="materialColorOnError">@color/system_on_error_dark</item>
+ <item name="materialColorSurface">@color/system_surface_dark</item>
+ <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_dark</item>
+ <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_dark</item>
+ <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_dark</item>
+ <item name="materialColorOutline">@color/system_outline_dark</item>
+ <item name="materialColorOutlineVariant">@color/system_outline_variant_dark</item>
+ <item name="materialColorOnPrimary">@color/system_on_primary_dark</item>
+ <item name="materialColorOnSurface">@color/system_on_surface_dark</item>
+ <item name="materialColorSurfaceContainer">@color/system_surface_container_dark</item>
+ <item name="materialColorPrimary">@color/system_primary_dark</item>
+ <item name="materialColorSecondary">@color/system_secondary_dark</item>
+ <item name="materialColorTertiary">@color/system_tertiary_dark</item>
+ <item name="materialColorError">@color/system_error_dark</item>
+ </style>
+
+ <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
<item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation</item>
+
+ <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
<item name="pageIndicatorDotColor">@color/page_indicator_dot_color_dark</item>
</style>
</resources>
diff --git a/res/values-v30/styles.xml b/res/values-v30/styles.xml
index ec5c113..a5c57c8 100644
--- a/res/values-v30/styles.xml
+++ b/res/values-v30/styles.xml
@@ -29,5 +29,6 @@
<item name="android:windowLayoutInDisplayCutoutMode">always</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
+ <item name="materialColorOnPrimaryFixed">#FFFFFFFF</item>
</style>
</resources>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index 7270366..5c81d49 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -110,39 +110,4 @@
@android:color/system_accent1_900</color>
<color name="overview_foreground_scrim_color">@android:color/system_neutral1_1000</color>
-
- <color name="material_color_on_secondary_fixed_variant">@android:color/system_accent2_700</color>
- <color name="material_color_on_tertiary_fixed_variant">@android:color/system_accent3_700</color>
- <color name="material_color_on_primary_fixed_variant">@android:color/system_accent1_700</color>
- <color name="material_color_on_secondary_container">@android:color/system_accent2_900</color>
- <color name="material_color_on_tertiary_container">@android:color/system_accent3_900</color>
- <color name="material_color_on_primary_container">@android:color/system_accent1_900</color>
- <color name="material_color_secondary_fixed_dim">@android:color/system_accent2_200</color>
- <color name="material_color_on_error_container">#410000</color>
- <color name="material_color_on_secondary_fixed">@android:color/system_accent2_900</color>
- <color name="material_color_on_surface_inverse">@android:color/system_neutral1_100</color>
- <color name="material_color_tertiary_fixed_dim">@android:color/system_accent3_200</color>
- <color name="material_color_on_tertiary_fixed">@android:color/system_accent3_900</color>
- <color name="material_color_primary_fixed_dim">@android:color/system_accent1_200</color>
- <color name="material_color_secondary_container">@android:color/system_accent2_100</color>
- <color name="material_color_error_container">#FFDAD5</color>
- <color name="material_color_on_primary_fixed">@android:color/system_accent1_900</color>
- <color name="material_color_primary_inverse">@android:color/system_accent1_200</color>
- <color name="material_color_secondary_fixed">@android:color/system_accent2_100</color>
- <color name="material_color_tertiary_container">@android:color/system_accent3_100</color>
- <color name="material_color_tertiary_fixed">@android:color/system_accent3_100</color>
- <color name="material_color_primary_container">@android:color/system_accent1_100</color>
- <color name="material_color_on_background">@android:color/system_neutral1_50</color>
- <color name="material_color_primary_fixed">@android:color/system_accent1_100</color>
- <color name="material_color_on_secondary">@android:color/system_accent2_0</color>
- <color name="material_color_on_tertiary">@android:color/system_accent3_0</color>
- <color name="material_color_on_error">#FFFFFF</color>
- <color name="material_color_on_surface_variant">@android:color/system_neutral2_700</color>
- <color name="material_color_outline">@android:color/system_neutral2_500</color>
- <color name="material_color_outline_variant">@android:color/system_neutral2_200</color>
- <color name="material_color_on_primary">@android:color/system_accent1_0</color>
- <color name="material_color_on_surface">@android:color/system_neutral1_900</color>
- <color name="material_color_primary">@android:color/system_accent1_600</color>
- <color name="material_color_secondary">@android:color/system_accent2_600</color>
- <color name="material_color_tertiary">@android:color/system_accent3_600</color>
</resources>
diff --git a/res/values-v34/colors.xml b/res/values-v34/colors.xml
index 26d3712..4f3a769 100644
--- a/res/values-v34/colors.xml
+++ b/res/values-v34/colors.xml
@@ -27,4 +27,108 @@
@android:color/system_on_surface_light</color>
<color name="widget_cell_subtitle_color_light">
@android:color/system_on_surface_variant_light</color>
+ <color name="widget_picker_menu_options_color_light">
+ @android:color/system_on_surface_variant_light
+ </color>
+
+ <color name="system_primary_container_light">@android:color/system_primary_container_light</color>
+ <color name="system_on_primary_container_light">@android:color/system_on_primary_container_light</color>
+ <color name="system_primary_light">@android:color/system_primary_light</color>
+ <color name="system_on_primary_light">@android:color/system_on_primary_light</color>
+ <color name="system_secondary_container_light">@android:color/system_secondary_container_light</color>
+ <color name="system_on_secondary_container_light">@android:color/system_on_secondary_container_light</color>
+ <color name="system_secondary_light">@android:color/system_secondary_light</color>
+ <color name="system_on_secondary_light">@android:color/system_on_secondary_light</color>
+ <color name="system_tertiary_container_light">@android:color/system_tertiary_container_light</color>
+ <color name="system_on_tertiary_container_light">@android:color/system_on_tertiary_container_light</color>
+ <color name="system_tertiary_light">@android:color/system_tertiary_light</color>
+ <color name="system_on_tertiary_light">@android:color/system_on_tertiary_light</color>
+ <color name="system_background_light">@android:color/system_background_light</color>
+ <color name="system_on_background_light">@android:color/system_on_background_light</color>
+ <color name="system_surface_light">@android:color/system_surface_light</color>
+ <color name="system_on_surface_light">@android:color/system_on_surface_light</color>
+ <color name="system_surface_container_low_light">@android:color/system_surface_container_low_light</color>
+ <color name="system_surface_container_lowest_light">@android:color/system_surface_container_lowest_light</color>
+ <color name="system_surface_container_light">@android:color/system_surface_container_light</color>
+ <color name="system_surface_container_high_light">@android:color/system_surface_container_high_light</color>
+ <color name="system_surface_container_highest_light">@android:color/system_surface_container_highest_light</color>
+ <color name="system_surface_bright_light">@android:color/system_surface_bright_light</color>
+ <color name="system_surface_dim_light">@android:color/system_surface_dim_light</color>
+ <color name="system_surface_variant_light">@android:color/system_surface_variant_light</color>
+ <color name="system_on_surface_variant_light">@android:color/system_on_surface_variant_light</color>
+ <color name="system_outline_light">@android:color/system_outline_light</color>
+ <color name="system_outline_variant_light">@android:color/system_outline_variant_light</color>
+ <color name="system_error_light">@android:color/system_error_light</color>
+ <color name="system_on_error_light">@android:color/system_on_error_light</color>
+ <color name="system_error_container_light">@android:color/system_error_container_light</color>
+ <color name="system_on_error_container_light">@android:color/system_on_error_container_light</color>
+ <color name="system_control_activated_light">@android:color/system_control_activated_light</color>
+ <color name="system_control_normal_light">@android:color/system_control_normal_light</color>
+ <color name="system_control_highlight_light">@android:color/system_control_highlight_light</color>
+ <color name="system_text_primary_inverse_light">@android:color/system_text_primary_inverse_light</color>
+ <color name="system_text_secondary_and_tertiary_inverse_light">@android:color/system_text_secondary_and_tertiary_inverse_light</color>
+ <color name="system_text_primary_inverse_disable_only_light">@android:color/system_text_primary_inverse_disable_only_light</color>
+ <color name="system_text_secondary_and_tertiary_inverse_disabled_light">@android:color/system_text_secondary_and_tertiary_inverse_disabled_light</color>
+ <color name="system_text_hint_inverse_light">@android:color/system_text_hint_inverse_light</color>
+ <color name="system_palette_key_color_primary_light">@android:color/system_palette_key_color_primary_light</color>
+ <color name="system_palette_key_color_secondary_light">@android:color/system_palette_key_color_secondary_light</color>
+ <color name="system_palette_key_color_tertiary_light">@android:color/system_palette_key_color_tertiary_light</color>
+ <color name="system_palette_key_color_neutral_light">@android:color/system_palette_key_color_neutral_light</color>
+ <color name="system_palette_key_color_neutral_variant_light">@android:color/system_palette_key_color_neutral_variant_light</color>
+ <color name="system_primary_container_dark">@android:color/system_primary_container_dark</color>
+ <color name="system_on_primary_container_dark">@android:color/system_on_primary_container_dark</color>
+ <color name="system_primary_dark">@android:color/system_primary_dark</color>
+ <color name="system_on_primary_dark">@android:color/system_on_primary_dark</color>
+ <color name="system_secondary_container_dark">@android:color/system_secondary_container_dark</color>
+ <color name="system_on_secondary_container_dark">@android:color/system_on_secondary_container_dark</color>
+ <color name="system_secondary_dark">@android:color/system_secondary_dark</color>
+ <color name="system_on_secondary_dark">@android:color/system_on_secondary_dark</color>
+ <color name="system_tertiary_container_dark">@android:color/system_tertiary_container_dark</color>
+ <color name="system_on_tertiary_container_dark">@android:color/system_on_tertiary_container_dark</color>
+ <color name="system_tertiary_dark">@android:color/system_tertiary_dark</color>
+ <color name="system_on_tertiary_dark">@android:color/system_on_tertiary_dark</color>
+ <color name="system_background_dark">@android:color/system_background_dark</color>
+ <color name="system_on_background_dark">@android:color/system_on_background_dark</color>
+ <color name="system_surface_dark">@android:color/system_surface_dark</color>
+ <color name="system_on_surface_dark">@android:color/system_on_surface_dark</color>
+ <color name="system_surface_container_low_dark">@android:color/system_surface_container_low_dark</color>
+ <color name="system_surface_container_lowest_dark">@android:color/system_surface_container_lowest_dark</color>
+ <color name="system_surface_container_dark">@android:color/system_surface_container_dark</color>
+ <color name="system_surface_container_high_dark">@android:color/system_surface_container_high_dark</color>
+ <color name="system_surface_container_highest_dark">@android:color/system_surface_container_highest_dark</color>
+ <color name="system_surface_bright_dark">@android:color/system_surface_bright_dark</color>
+ <color name="system_surface_dim_dark">@android:color/system_surface_dim_dark</color>
+ <color name="system_surface_variant_dark">@android:color/system_surface_variant_dark</color>
+ <color name="system_on_surface_variant_dark">@android:color/system_on_surface_variant_dark</color>
+ <color name="system_outline_dark">@android:color/system_outline_dark</color>
+ <color name="system_outline_variant_dark">@android:color/system_outline_variant_dark</color>
+ <color name="system_error_dark">@android:color/system_error_dark</color>
+ <color name="system_on_error_dark">@android:color/system_on_error_dark</color>
+ <color name="system_error_container_dark">@android:color/system_error_container_dark</color>
+ <color name="system_on_error_container_dark">@android:color/system_on_error_container_dark</color>
+ <color name="system_control_activated_dark">@android:color/system_control_activated_dark</color>
+ <color name="system_control_normal_dark">@android:color/system_control_normal_dark</color>
+ <color name="system_control_highlight_dark">@android:color/system_control_highlight_dark</color>
+ <color name="system_text_primary_inverse_dark">@android:color/system_text_primary_inverse_dark</color>
+ <color name="system_text_secondary_and_tertiary_inverse_dark">@android:color/system_text_secondary_and_tertiary_inverse_dark</color>
+ <color name="system_text_primary_inverse_disable_only_dark">@android:color/system_text_primary_inverse_disable_only_dark</color>
+ <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">@android:color/system_text_secondary_and_tertiary_inverse_disabled_dark</color>
+ <color name="system_text_hint_inverse_dark">@android:color/system_text_hint_inverse_dark</color>
+ <color name="system_palette_key_color_primary_dark">@android:color/system_palette_key_color_primary_dark</color>
+ <color name="system_palette_key_color_secondary_dark">@android:color/system_palette_key_color_secondary_dark</color>
+ <color name="system_palette_key_color_tertiary_dark">@android:color/system_palette_key_color_tertiary_dark</color>
+ <color name="system_palette_key_color_neutral_dark">@android:color/system_palette_key_color_neutral_dark</color>
+ <color name="system_palette_key_color_neutral_variant_dark">@android:color/system_palette_key_color_neutral_variant_dark</color>
+ <color name="system_primary_fixed">@android:color/system_primary_fixed</color>
+ <color name="system_primary_fixed_dim">@android:color/system_primary_fixed_dim</color>
+ <color name="system_on_primary_fixed">@android:color/system_on_primary_fixed</color>
+ <color name="system_on_primary_fixed_variant">@android:color/system_on_primary_fixed_variant</color>
+ <color name="system_secondary_fixed">@android:color/system_secondary_fixed</color>
+ <color name="system_secondary_fixed_dim">@android:color/system_secondary_fixed_dim</color>
+ <color name="system_on_secondary_fixed">@android:color/system_on_secondary_fixed</color>
+ <color name="system_on_secondary_fixed_variant">@android:color/system_on_secondary_fixed_variant</color>
+ <color name="system_tertiary_fixed">@android:color/system_tertiary_fixed</color>
+ <color name="system_tertiary_fixed_dim">@android:color/system_tertiary_fixed_dim</color>
+ <color name="system_on_tertiary_fixed">@android:color/system_on_tertiary_fixed</color>
+ <color name="system_on_tertiary_fixed_variant">@android:color/system_on_tertiary_fixed_variant</color>
</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e4e047e..6151b5f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -45,6 +45,55 @@
<attr name="focusOutlineColor" format="color" />
<attr name="focusInnerOutlineColor" format="color" />
+ <!-- Recreating Dynamic Color attributes found in the system. This should be the way to go on
+ all launcher projects since they all inherit from launcher3. Avoid creating other color
+ attributes if these can be user directly. -->
+ <attr name="materialColorOnSecondaryFixedVariant" format="color" />
+ <attr name="materialColorOnTertiaryFixedVariant" format="color" />
+ <attr name="materialColorSurfaceContainerLowest" format="color" />
+ <attr name="materialColorOnPrimaryFixedVariant" format="color" />
+ <attr name="materialColorOnSecondaryContainer" format="color" />
+ <attr name="materialColorOnTertiaryContainer" format="color" />
+ <attr name="materialColorSurfaceContainerLow" format="color" />
+ <attr name="materialColorOnPrimaryContainer" format="color" />
+ <attr name="materialColorSecondaryFixedDim" format="color" />
+ <attr name="materialColorOnErrorContainer" format="color" />
+ <attr name="materialColorOnSecondaryFixed" format="color" />
+ <attr name="materialColorOnSurfaceInverse" format="color" />
+ <attr name="materialColorTertiaryFixedDim" format="color" />
+ <attr name="materialColorOnTertiaryFixed" format="color" />
+ <attr name="materialColorPrimaryFixedDim" format="color" />
+ <attr name="materialColorSecondaryContainer" format="color" />
+ <attr name="materialColorErrorContainer" format="color" />
+ <attr name="materialColorOnPrimaryFixed" format="color" />
+ <attr name="materialColorPrimaryInverse" format="color" />
+ <attr name="materialColorSecondaryFixed" format="color" />
+ <attr name="materialColorSurfaceInverse" format="color" />
+ <attr name="materialColorSurfaceVariant" format="color" />
+ <attr name="materialColorTertiaryContainer" format="color" />
+ <attr name="materialColorTertiaryFixed" format="color" />
+ <attr name="materialColorPrimaryContainer" format="color" />
+ <attr name="materialColorOnBackground" format="color" />
+ <attr name="materialColorPrimaryFixed" format="color" />
+ <attr name="materialColorOnSecondary" format="color" />
+ <attr name="materialColorOnTertiary" format="color" />
+ <attr name="materialColorSurfaceDim" format="color" />
+ <attr name="materialColorSurfaceBright" format="color" />
+ <attr name="materialColorOnError" format="color" />
+ <attr name="materialColorSurface" format="color" />
+ <attr name="materialColorSurfaceContainerHigh" format="color" />
+ <attr name="materialColorSurfaceContainerHighest" format="color" />
+ <attr name="materialColorOnSurfaceVariant" format="color" />
+ <attr name="materialColorOutline" format="color" />
+ <attr name="materialColorOutlineVariant" format="color" />
+ <attr name="materialColorOnPrimary" format="color" />
+ <attr name="materialColorOnSurface" format="color" />
+ <attr name="materialColorSurfaceContainer" format="color" />
+ <attr name="materialColorPrimary" format="color" />
+ <attr name="materialColorSecondary" format="color" />
+ <attr name="materialColorTertiary" format="color" />
+ <attr name="materialColorError" format="color" />
+
<attr name="pageIndicatorDotColor" format="color" />
<attr name="folderPreviewColor" format="color" />
<attr name="folderBackgroundColor" format="color" />
@@ -62,6 +111,7 @@
<attr name="preloadIconBackgroundColor" format="color" />
<attr name="widgetPickerTitleColor" format="color"/>
<attr name="widgetPickerDescriptionColor" format="color"/>
+ <attr name="widgetPickerWidgetOptionsMenuColor" format="color"/>
<attr name="widgetPickerPrimarySurfaceColor" format="color"/>
<attr name="widgetPickerSecondarySurfaceColor" format="color"/>
<attr name="widgetPickerHeaderAppTitleColor" format="color"/>
@@ -581,51 +631,6 @@
<attr name="collapsable" format="boolean" />
</declare-styleable>
- <attr name="materialColorOnSecondaryFixedVariant" format="color" />
- <attr name="materialColorOnTertiaryFixedVariant" format="color" />
- <attr name="materialColorSurfaceContainerLowest" format="color" />
- <attr name="materialColorOnPrimaryFixedVariant" format="color" />
- <attr name="materialColorOnSecondaryContainer" format="color" />
- <attr name="materialColorOnTertiaryContainer" format="color" />
- <attr name="materialColorSurfaceContainerLow" format="color" />
- <attr name="materialColorOnPrimaryContainer" format="color" />
- <attr name="materialColorSecondaryFixedDim" format="color" />
- <attr name="materialColorOnErrorContainer" format="color" />
- <attr name="materialColorOnSecondaryFixed" format="color" />
- <attr name="materialColorOnSurfaceInverse" format="color" />
- <attr name="materialColorTertiaryFixedDim" format="color" />
- <attr name="materialColorOnTertiaryFixed" format="color" />
- <attr name="materialColorPrimaryFixedDim" format="color" />
- <attr name="materialColorSecondaryContainer" format="color" />
- <attr name="materialColorErrorContainer" format="color" />
- <attr name="materialColorOnPrimaryFixed" format="color" />
- <attr name="materialColorPrimaryInverse" format="color" />
- <attr name="materialColorSecondaryFixed" format="color" />
- <attr name="materialColorTertiaryContainer" format="color" />
- <attr name="materialColorTertiaryFixed" format="color" />
- <attr name="materialColorPrimaryContainer" format="color" />
- <attr name="materialColorOnBackground" format="color" />
- <attr name="materialColorPrimaryFixed" format="color" />
- <attr name="materialColorOnSecondary" format="color" />
- <attr name="materialColorOnTertiary" format="color" />
- <attr name="materialColorOnError" format="color" />
- <attr name="materialColorOnSurfaceVariant" format="color" />
- <attr name="materialColorOutline" format="color" />
- <attr name="materialColorOutlineVariant" format="color" />
- <attr name="materialColorOnPrimary" format="color" />
- <attr name="materialColorOnSurface" format="color" />
- <attr name="materialColorPrimary" format="color" />
- <attr name="materialColorSecondary" format="color" />
- <attr name="materialColorTertiary" format="color" />
- <attr name="materialColorSurfaceInverse" format="color" />
- <attr name="materialColorSurfaceVariant" format="color" />
- <attr name="materialColorSurfaceDim" format="color" />
- <attr name="materialColorSurfaceBright" format="color" />
- <attr name="materialColorSurface" format="color" />
- <attr name="materialColorSurfaceContainerHigh" format="color" />
- <attr name="materialColorSurfaceContainerHighest" format="color" />
- <attr name="materialColorSurfaceContainer" format="color" />
-
<declare-styleable name="WidgetSections">
<!-- Component name of an app widget provider. -->
<attr name="provider" format="string" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 8fa1992..c82358b 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,8 +17,7 @@
** limitations under the License.
*/
-->
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The color tints to apply to the text and drag view when hovering
over the delete target or the info target -->
<color name="delete_target_hover_tint">#FFC1C1C1</color>
@@ -105,6 +104,7 @@
<color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
<color name="widget_picker_title_color_light">#1F1F1F</color>
<color name="widget_picker_description_color_light">#4C4D50</color>
+ <color name="widget_picker_menu_options_color_light">@color/system_on_surface_variant_light</color>
<color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
<color name="widget_picker_header_app_subtitle_color_light">#444746</color>
<color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -118,13 +118,14 @@
<color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
<color name="widget_picker_add_button_background_color_light">#0B57D0</color>
<color name="widget_picker_add_button_text_color_light">#0B57D0</color>
- <color name="widget_cell_title_color_light">@color/material_color_on_surface</color>
- <color name="widget_cell_subtitle_color_light">@color/material_color_on_surface_variant</color>
+ <color name="widget_cell_title_color_light">@color/system_on_surface_light</color>
+ <color name="widget_cell_subtitle_color_light">@color/system_on_surface_variant_light</color>
<color name="widget_picker_primary_surface_color_dark">#1F2020</color>
<color name="widget_picker_secondary_surface_color_dark">#393939</color>
<color name="widget_picker_title_color_dark">#E3E3E3</color>
<color name="widget_picker_description_color_dark">#CCCDCF</color>
+ <color name="widget_picker_menu_options_color_dark">@color/system_on_surface_variant_dark</color>
<color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
<color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
<color name="widget_picker_header_background_color_dark">#004A77</color>
@@ -138,51 +139,107 @@
<color name="widget_picker_collapse_handle_color_dark">#444746</color>
<color name="widget_picker_add_button_background_color_dark">#062E6F</color>
<color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
- <color name="widget_cell_title_color_dark">@color/material_color_on_surface</color>
- <color name="widget_cell_subtitle_color_dark">@color/material_color_on_surface_variant</color>
+ <color name="widget_cell_title_color_dark">@color/system_on_surface_dark</color>
+ <color name="widget_cell_subtitle_color_dark">@color/system_on_surface_variant_dark</color>
- <color name="material_color_on_secondary_fixed_variant">#3F4759</color>
- <color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
- <color name="material_color_surface_container_lowest">#FFFFFF</color>
- <color name="material_color_on_primary_fixed_variant">#2B4678</color>
- <color name="material_color_on_secondary_container">#141B2C</color>
- <color name="material_color_on_tertiary_container">#29132D</color>
- <color name="material_color_surface_container_low">#F5F3F7</color>
- <color name="material_color_on_primary_container">#001A41</color>
- <color name="material_color_secondary_fixed_dim">#BFC6DC</color>
- <color name="material_color_on_error_container">#410000</color>
- <color name="material_color_on_secondary_fixed">#141B2C</color>
- <color name="material_color_on_surface_inverse">#E3E2E6</color>
- <color name="material_color_tertiary_fixed_dim">#DEBCDF</color>
- <color name="material_color_on_tertiary_fixed">#29132D</color>
- <color name="material_color_primary_fixed_dim">#ADC6FF</color>
- <color name="material_color_secondary_container">#DBE2F9</color>
- <color name="material_color_error_container">#FFDAD5</color>
- <color name="material_color_on_primary_fixed">#001A41</color>
- <color name="material_color_primary_inverse">#ADC6FF</color>
- <color name="material_color_secondary_fixed">#DBE2F9</color>
- <color name="material_color_surface_inverse">#121316</color>
- <color name="material_color_surface_variant">#E1E2EC</color>
- <color name="material_color_tertiary_container">#FBD7FC</color>
- <color name="material_color_tertiary_fixed">#FBD7FC</color>
- <color name="material_color_primary_container">#D8E2FF</color>
- <color name="material_color_on_background">#1B1B1F</color>
- <color name="material_color_primary_fixed">#D8E2FF</color>
- <color name="material_color_on_secondary">#FFFFFF</color>
- <color name="material_color_on_tertiary">#FFFFFF</color>
- <color name="material_color_surface_dim">#DBD9DD</color>
- <color name="material_color_surface_bright">#FAF9FD</color>
- <color name="material_color_on_error">#FFFFFF</color>
- <color name="material_color_surface">#FAF9FD</color>
- <color name="material_color_surface_container_high">#E9E7EC</color>
- <color name="material_color_surface_container_highest">#E3E2E6</color>
- <color name="material_color_on_surface_variant">#44474F</color>
- <color name="material_color_outline">#72747D</color>
- <color name="material_color_outline_variant">#C4C7C5</color>
- <color name="material_color_on_primary">#FFFFFF</color>
- <color name="material_color_on_surface">#1B1B1F</color>
- <color name="material_color_surface_container">#EFEDF1</color>
- <color name="material_color_primary">#445E91</color>
- <color name="material_color_secondary">#575E71</color>
- <color name="material_color_tertiary">#715573</color>
+ <color name="system_primary_container_light">#D9E2FF</color>
+ <color name="system_on_primary_container_light">#001945</color>
+ <color name="system_primary_light">#475D92</color>
+ <color name="system_on_primary_light">#FFFFFF</color>
+ <color name="system_secondary_container_light">#DCE2F9</color>
+ <color name="system_on_secondary_container_light">#151B2C</color>
+ <color name="system_secondary_light">#575E71</color>
+ <color name="system_on_secondary_light">#FFFFFF</color>
+ <color name="system_tertiary_container_light">#FDD7FA</color>
+ <color name="system_on_tertiary_container_light">#2A122C</color>
+ <color name="system_tertiary_light">#725572</color>
+ <color name="system_on_tertiary_light">#FFFFFF</color>
+ <color name="system_background_light">#FAF8FF</color>
+ <color name="system_on_background_light">#1A1B20</color>
+ <color name="system_surface_light">#FAF8FF</color>
+ <color name="system_on_surface_light">#1A1B20</color>
+ <color name="system_surface_container_low_light">#F4F3FA</color>
+ <color name="system_surface_container_lowest_light">#FFFFFF</color>
+ <color name="system_surface_container_light">#EEEDF4</color>
+ <color name="system_surface_container_high_light">#E8E7EF</color>
+ <color name="system_surface_container_highest_light">#E2E2E9</color>
+ <color name="system_surface_bright_light">#FAF8FF</color>
+ <color name="system_surface_dim_light">#DAD9E0</color>
+ <color name="system_surface_variant_light">#E1E2EC</color>
+ <color name="system_on_surface_variant_light">#44464F</color>
+ <color name="system_outline_light">#757780</color>
+ <color name="system_outline_variant_light">#C5C6D0</color>
+ <color name="system_error_light">#BA1A1A</color>
+ <color name="system_on_error_light">#FFFFFF</color>
+ <color name="system_error_container_light">#FFDAD6</color>
+ <color name="system_on_error_container_light">#410002</color>
+ <color name="system_control_activated_light">#D9E2FF</color>
+ <color name="system_control_normal_light">#44464F</color>
+ <color name="system_control_highlight_light">#000000</color>
+ <color name="system_text_primary_inverse_light">#E2E2E9</color>
+ <color name="system_text_secondary_and_tertiary_inverse_light">#C5C6D0</color>
+ <color name="system_text_primary_inverse_disable_only_light">#E2E2E9</color>
+ <color name="system_text_secondary_and_tertiary_inverse_disabled_light">#E2E2E9</color>
+ <color name="system_text_hint_inverse_light">#E2E2E9</color>
+ <color name="system_palette_key_color_primary_light">#6076AC</color>
+ <color name="system_palette_key_color_secondary_light">#70778B</color>
+ <color name="system_palette_key_color_tertiary_light">#8C6D8C</color>
+ <color name="system_palette_key_color_neutral_light">#76777D</color>
+ <color name="system_palette_key_color_neutral_variant_light">#757780</color>
+ <color name="system_primary_container_dark">#2F4578</color>
+ <color name="system_on_primary_container_dark">#D9E2FF</color>
+ <color name="system_primary_dark">#B0C6FF</color>
+ <color name="system_on_primary_dark">#152E60</color>
+ <color name="system_secondary_container_dark">#404659</color>
+ <color name="system_on_secondary_container_dark">#DCE2F9</color>
+ <color name="system_secondary_dark">#C0C6DC</color>
+ <color name="system_on_secondary_dark">#2A3042</color>
+ <color name="system_tertiary_container_dark">#593D59</color>
+ <color name="system_on_tertiary_container_dark">#FDD7FA</color>
+ <color name="system_tertiary_dark">#E0BBDD</color>
+ <color name="system_on_tertiary_dark">#412742</color>
+ <color name="system_background_dark">#121318</color>
+ <color name="system_on_background_dark">#E2E2E9</color>
+ <color name="system_surface_dark">#121318</color>
+ <color name="system_on_surface_dark">#E2E2E9</color>
+ <color name="system_surface_container_low_dark">#1A1B20</color>
+ <color name="system_surface_container_lowest_dark">#0C0E13</color>
+ <color name="system_surface_container_dark">#1E1F25</color>
+ <color name="system_surface_container_high_dark">#282A2F</color>
+ <color name="system_surface_container_highest_dark">#33343A</color>
+ <color name="system_surface_bright_dark">#38393F</color>
+ <color name="system_surface_dim_dark">#121318</color>
+ <color name="system_surface_variant_dark">#44464F</color>
+ <color name="system_on_surface_variant_dark">#C5C6D0</color>
+ <color name="system_outline_dark">#8F9099</color>
+ <color name="system_outline_variant_dark">#44464F</color>
+ <color name="system_error_dark">#FFB4AB</color>
+ <color name="system_on_error_dark">#690005</color>
+ <color name="system_error_container_dark">#93000A</color>
+ <color name="system_on_error_container_dark">#FFDAD6</color>
+ <color name="system_control_activated_dark">#2F4578</color>
+ <color name="system_control_normal_dark">#C5C6D0</color>
+ <color name="system_control_highlight_dark">#FFFFFF</color>
+ <color name="system_text_primary_inverse_dark">#1A1B20</color>
+ <color name="system_text_secondary_and_tertiary_inverse_dark">#44464F</color>
+ <color name="system_text_primary_inverse_disable_only_dark">#1A1B20</color>
+ <color name="system_text_secondary_and_tertiary_inverse_disabled_dark">#1A1B20</color>
+ <color name="system_text_hint_inverse_dark">#1A1B20</color>
+ <color name="system_palette_key_color_primary_dark">#6076AC</color>
+ <color name="system_palette_key_color_secondary_dark">#70778B</color>
+ <color name="system_palette_key_color_tertiary_dark">#8C6D8C</color>
+ <color name="system_palette_key_color_neutral_dark">#76777D</color>
+ <color name="system_palette_key_color_neutral_variant_dark">#757780</color>
+ <color name="system_primary_fixed">#D9E2FF</color>
+ <color name="system_primary_fixed_dim">#B0C6FF</color>
+ <color name="system_on_primary_fixed">#001945</color>
+ <color name="system_on_primary_fixed_variant">#2F4578</color>
+ <color name="system_secondary_fixed">#DCE2F9</color>
+ <color name="system_secondary_fixed_dim">#C0C6DC</color>
+ <color name="system_on_secondary_fixed">#151B2C</color>
+ <color name="system_on_secondary_fixed_variant">#404659</color>
+ <color name="system_tertiary_fixed">#FDD7FA</color>
+ <color name="system_tertiary_fixed_dim">#E0BBDD</color>
+ <color name="system_on_tertiary_fixed">#2A122C</color>
+ <color name="system_on_tertiary_fixed_variant">#593D59</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 05724e2..af91b5a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -172,8 +172,6 @@
<dimen name="padded_rounded_button_height">48dp</dimen>
<dimen name="rounded_button_height">48dp</dimen>
<dimen name="rounded_button_radius">200dp</dimen>
- <dimen name="rounded_button_padding">8dp</dimen>
-
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d33adc4..3b458c2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -61,6 +61,12 @@
<string name="long_press_widget_to_add">Touch & hold to move a widget.</string>
<!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
<string name="long_accessible_way_to_add">Double-tap & hold to move a widget or use custom actions.</string>
+ <!-- Accessibility label for the icon button shown in the widget picker that opens a overflow
+ menu with widgets viewing options. [CHAR_LIMIT=25] -->
+ <string name="widget_picker_widget_options_button_description">More options</string>
+ <!-- Label for the checkbox shown in the widget picker toggles whether to show all widgets or
+ the default set. [CHAR_LIMIT=25] -->
+ <string name="widget_picker_show_all_widgets_menu_item_title">Show all widgets</string>
<!-- The format string for the dimensions of a widget in the drawer -->
<!-- There is a special version of this format string for Farsi -->
<string name="widget_dims_format">%1$d \u00d7 %2$d</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f7273a0..ee7ed26 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,7 +17,7 @@
*/
-->
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Launcher theme -->
<style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.Light">
<item name="disabledIconAlpha">.54</item>
@@ -29,17 +29,65 @@
<item name="android:windowShowWallpaper">true</item>
</style>
- <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
+ <style name="DynamicColorsBaseLauncherTheme" parent="@style/BaseLauncherTheme">
+ <item name="materialColorOnSecondaryFixedVariant">@color/system_on_secondary_fixed_variant</item>
+ <item name="materialColorOnTertiaryFixedVariant">@color/system_on_tertiary_fixed_variant</item>
+ <item name="materialColorSurfaceContainerLowest">@color/system_surface_container_lowest_light</item>
+ <item name="materialColorOnPrimaryFixedVariant">@color/system_on_primary_fixed_variant</item>
+ <item name="materialColorOnSecondaryContainer">@color/system_on_secondary_container_light</item>
+ <item name="materialColorOnTertiaryContainer">@color/system_on_tertiary_container_light</item>
+ <item name="materialColorSurfaceContainerLow">@color/system_surface_container_low_light</item>
+ <item name="materialColorOnPrimaryContainer">@color/system_on_primary_container_light</item>
+ <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
+ <item name="materialColorOnErrorContainer">@color/system_on_error_container_light</item>
+ <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
+ <item name="materialColorOnSurfaceInverse">@color/system_on_surface_dark</item>
+ <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
+ <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
+ <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
+ <item name="materialColorSecondaryContainer">@color/system_secondary_container_light</item>
+ <item name="materialColorErrorContainer">@color/system_error_container_light</item>
+ <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
+ <item name="materialColorPrimaryInverse">@color/system_primary_dark</item>
+ <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
+ <item name="materialColorSurfaceInverse">@color/system_surface_dark</item>
+ <item name="materialColorSurfaceVariant">@color/system_surface_variant_light</item>
+ <item name="materialColorTertiaryContainer">@color/system_tertiary_container_light</item>
+ <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
+ <item name="materialColorPrimaryContainer">@color/system_primary_container_light</item>
+ <item name="materialColorOnBackground">@color/system_on_background_light</item>
+ <item name="materialColorPrimaryFixed">@color/system_primary_fixed</item>
+ <item name="materialColorOnSecondary">@color/system_on_secondary_light</item>
+ <item name="materialColorOnTertiary">@color/system_on_tertiary_light</item>
+ <item name="materialColorSurfaceDim">@color/system_surface_dim_light</item>
+ <item name="materialColorSurfaceBright">@color/system_surface_bright_light</item>
+ <item name="materialColorOnError">@color/system_on_error_light</item>
+ <item name="materialColorSurface">@color/system_surface_light</item>
+ <item name="materialColorSurfaceContainerHigh">@color/system_surface_container_high_light</item>
+ <item name="materialColorSurfaceContainerHighest">@color/system_surface_container_highest_light</item>
+ <item name="materialColorOnSurfaceVariant">@color/system_on_surface_variant_light</item>
+ <item name="materialColorOutline">@color/system_outline_light</item>
+ <item name="materialColorOutlineVariant">@color/system_outline_variant_light</item>
+ <item name="materialColorOnPrimary">@color/system_on_primary_light</item>
+ <item name="materialColorOnSurface">@color/system_on_surface_light</item>
+ <item name="materialColorSurfaceContainer">@color/system_surface_container_light</item>
+ <item name="materialColorPrimary">@color/system_primary_light</item>
+ <item name="materialColorSecondary">@color/system_secondary_light</item>
+ <item name="materialColorTertiary">@color/system_tertiary_light</item>
+ <item name="materialColorError">@color/system_error_light</item>
+ </style>
+
+
+ <style name="LauncherTheme" parent="@style/DynamicColorsBaseLauncherTheme">
<item name="android:textColorSecondary">#DE000000</item>
<item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
- <item name="allappsHeaderProtectionColor">
- @color/material_color_surface_container_highest</item>
+ <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerHighest</item>
<item name="allAppsNavBarScrimColor">#66FFFFFF</item>
<item name="popupColorPrimary">@color/popup_color_primary_light</item>
<item name="popupColorSecondary">@color/popup_color_secondary_light</item>
<item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
<item name="popupColorBackground">#EFEDED</item>
- <item name="popupTextColor">@color/popup_text_color_light</item>
+ <item name="popupTextColor">@color/system_on_surface_light</item>
<item name="popupShadeFirst">@color/popup_shade_first_light</item>
<item name="popupShadeSecond">@color/popup_shade_second_light</item>
<item name="popupShadeThird">@color/popup_shade_third_light</item>
@@ -53,15 +101,15 @@
<item name="workspaceKeyShadowColor">#89000000</item>
<item name="widgetsTheme">@style/WidgetContainerTheme</item>
<item name="pageIndicatorDotColor">@color/page_indicator_dot_color_light</item>
- <item name="focusOutlineColor">@color/material_color_secondary_fixed</item>
- <item name="focusInnerOutlineColor">@color/material_color_on_secondary_fixed_variant</item>
+ <item name="focusOutlineColor">?attr/materialColorSecondaryFixed</item>
+ <item name="focusInnerOutlineColor">?attr/materialColorOnSecondaryFixedVariant</item>
<item name="folderPreviewColor">@color/folder_preview_light</item>
<item name="folderBackgroundColor">@color/folder_background_light</item>
<item name="folderIconBorderColor">?android:attr/colorPrimary</item>
<item name="isFolderDarkText">true</item>
<item name="folderTextColor">@color/folder_text_color_light</item>
<item name="folderHintTextColor">@color/folder_hint_text_color_light</item>
- <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+ <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
<item name="loadingIconColor">#CCFFFFFF</item>
<item name="iconOnlyShortcutColor">?android:attr/textColorSecondary</item>
<item name="eduHalfSheetBGColor">?android:attr/colorAccent</item>
@@ -78,51 +126,6 @@
<item name="android:statusBarColor">#00000000</item>
<item name="android:navigationBarColor">#00000000</item>
<item name="android:switchStyle">@style/SwitchStyle</item>
-
- <item name="materialColorOnSecondaryFixedVariant">@color/material_color_on_secondary_fixed_variant</item>
- <item name="materialColorOnTertiaryFixedVariant">@color/material_color_on_tertiary_fixed_variant</item>
- <item name="materialColorSurfaceContainerLowest">@color/material_color_surface_container_lowest</item>
- <item name="materialColorOnPrimaryFixedVariant">@color/material_color_on_primary_fixed_variant</item>
- <item name="materialColorOnSecondaryContainer">@color/material_color_on_secondary_container</item>
- <item name="materialColorOnTertiaryContainer">@color/material_color_on_tertiary_container</item>
- <item name="materialColorSurfaceContainerLow">@color/material_color_surface_container_low</item>
- <item name="materialColorOnPrimaryContainer">@color/material_color_on_primary_container</item>
- <item name="materialColorSecondaryFixedDim">@color/material_color_secondary_fixed_dim</item>
- <item name="materialColorOnErrorContainer">@color/material_color_on_error_container</item>
- <item name="materialColorOnSecondaryFixed">@color/material_color_on_secondary_fixed</item>
- <item name="materialColorOnSurfaceInverse">@color/material_color_on_surface_inverse</item>
- <item name="materialColorTertiaryFixedDim">@color/material_color_tertiary_fixed_dim</item>
- <item name="materialColorOnTertiaryFixed">@color/material_color_on_tertiary_fixed</item>
- <item name="materialColorPrimaryFixedDim">@color/material_color_primary_fixed_dim</item>
- <item name="materialColorSecondaryContainer">@color/material_color_secondary_container</item>
- <item name="materialColorErrorContainer">@color/material_color_error_container</item>
- <item name="materialColorOnPrimaryFixed">@color/material_color_on_primary_fixed</item>
- <item name="materialColorPrimaryInverse">@color/material_color_primary_inverse</item>
- <item name="materialColorSecondaryFixed">@color/material_color_secondary_fixed</item>
- <item name="materialColorSurfaceInverse">@color/material_color_surface_inverse</item>
- <item name="materialColorSurfaceVariant">@color/material_color_surface_variant</item>
- <item name="materialColorTertiaryContainer">@color/material_color_tertiary_container</item>
- <item name="materialColorTertiaryFixed">@color/material_color_tertiary_fixed</item>
- <item name="materialColorPrimaryContainer">@color/material_color_primary_container</item>
- <item name="materialColorOnBackground">@color/material_color_on_background</item>
- <item name="materialColorPrimaryFixed">@color/material_color_primary_fixed</item>
- <item name="materialColorOnSecondary">@color/material_color_on_secondary</item>
- <item name="materialColorOnTertiary">@color/material_color_on_tertiary</item>
- <item name="materialColorSurfaceDim">@color/material_color_surface_dim</item>
- <item name="materialColorSurfaceBright">@color/material_color_surface_bright</item>
- <item name="materialColorOnError">@color/material_color_on_error</item>
- <item name="materialColorSurface">@color/material_color_surface</item>
- <item name="materialColorSurfaceContainerHigh">@color/material_color_surface_container_high</item>
- <item name="materialColorSurfaceContainerHighest">@color/material_color_surface_container_highest</item>
- <item name="materialColorOnSurfaceVariant">@color/material_color_on_surface_variant</item>
- <item name="materialColorOutline">@color/material_color_outline</item>
- <item name="materialColorOutlineVariant">@color/material_color_outline_variant</item>
- <item name="materialColorOnPrimary">@color/material_color_on_primary</item>
- <item name="materialColorOnSurface">@color/material_color_on_surface</item>
- <item name="materialColorSurfaceContainer">@color/material_color_surface_container</item>
- <item name="materialColorPrimary">@color/material_color_primary</item>
- <item name="materialColorSecondary">@color/material_color_secondary</item>
- <item name="materialColorTertiary">@color/material_color_tertiary</item>
</style>
<style name="SwitchStyle"
@@ -153,13 +156,13 @@
<item name="android:colorControlHighlight">#19FFFFFF</item>
<item name="android:colorPrimary">#FF212121</item>
<item name="allAppsScrimColor">?attr/materialColorSurfaceDim</item>
- <item name="allappsHeaderProtectionColor">@color/material_color_surface_container_low</item>
+ <item name="allappsHeaderProtectionColor">?attr/materialColorSurfaceContainerLow</item>
<item name="allAppsNavBarScrimColor">#80000000</item>
<item name="popupColorPrimary">@color/popup_color_primary_dark</item>
<item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
<item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
<item name="popupColorBackground">#1F2020</item>
- <item name="popupTextColor">@color/popup_text_color_dark</item>
+ <item name="popupTextColor">@color/system_on_surface_dark</item>
<item name="popupNotificationDotColor">@color/popup_notification_dot_dark</item>
<item name="popupShadeFirst">@color/popup_shade_first_dark</item>
<item name="popupShadeSecond">@color/popup_shade_second_dark</item>
@@ -173,7 +176,7 @@
<item name="isFolderDarkText">false</item>
<item name="folderTextColor">@color/folder_text_color_dark</item>
<item name="folderHintTextColor">@color/folder_hint_text_color_dark</item>
- <item name="appPairSurfaceInFolder">@color/material_color_surface_container_lowest</item>
+ <item name="appPairSurfaceInFolder">?attr/materialColorSurfaceContainerLowest</item>
<item name="isMainColorDark">true</item>
<item name="loadingIconColor">#99FFFFFF</item>
<item name="iconOnlyShortcutColor">#B3FFFFFF</item>
@@ -244,6 +247,7 @@
@color/widget_picker_secondary_surface_color_light</item>
<item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
<item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
+ <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_light</item>
<item name="widgetPickerHeaderAppTitleColor">
@color/widget_picker_header_app_title_color_light</item>
<item name="widgetPickerHeaderAppSubtitleColor">
@@ -284,6 +288,7 @@
<item name="widgetPickerTitleColor">
@color/widget_picker_title_color_dark</item>
<item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
+ <item name="widgetPickerWidgetOptionsMenuColor">@color/widget_picker_menu_options_color_dark</item>
<item name="widgetPickerHeaderAppTitleColor">
@color/widget_picker_header_app_title_color_dark</item>
<item name="widgetPickerHeaderAppSubtitleColor">
@@ -463,7 +468,7 @@
<style name="PrivateSpaceHeaderTextStyle">
<item name="android:textSize">16sp</item>
- <item name="android:textColor">@color/material_color_on_surface</item>
+ <item name="android:textColor">?attr/materialColorOnSurface</item>
<item name="android:fontFamily">google-sans-text-medium</item>
<item name="android:ellipsize">end</item>
</style>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index cb897dc..5949732 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -70,6 +70,7 @@
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
import static com.android.launcher3.logging.StatsLogManager.EventEnum;
@@ -188,7 +189,6 @@
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.dragndrop.LauncherDragController;
import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderGridOrganizer;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
@@ -816,7 +816,7 @@
View collectionIcon = getWorkspace().getHomescreenIconByItemId(info.container);
if (collectionIcon instanceof FolderIcon folderIcon
&& collectionIcon.getTag() instanceof FolderInfo) {
- if (new FolderGridOrganizer(getDeviceProfile())
+ if (createFolderGridOrganizer(getDeviceProfile())
.setFolderInfo((FolderInfo) folderIcon.getTag())
.isItemInPreview(info.rank)) {
folderIcon.invalidate();
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index a448228..53fed20 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -746,7 +746,8 @@
* | +--+ |
* | |
* +----------------+
- * This would be case delta % 4 == 2:
+ * This would be case delta % 4 == 2: // This is case was reverted to previous behaviour which
+ * doesn't match the illustration due to b/353965234
* +-------------+
* | |
* | |
@@ -768,7 +769,6 @@
int delta) {
int rdelta = ((delta % 4) + 4) % 4;
int origLeft = inOutBounds.left;
- int origTop = inOutBounds.top;
switch (rdelta) {
case 0:
return;
@@ -780,8 +780,6 @@
return;
case 2:
inOutBounds.left = parentWidth - inOutBounds.right;
- inOutBounds.top = parentHeight - inOutBounds.bottom;
- inOutBounds.bottom = parentHeight - origTop;
inOutBounds.right = parentWidth - origLeft;
return;
case 3:
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
deleted file mode 100644
index 79b8187..0000000
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2016 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.accessibility;
-
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.OnHierarchyChangeListener;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragOptions;
-
-import java.util.function.Function;
-
-/**
- * Utility listener to enable/disable accessibility drag flags for a ViewGroup
- * containing CellLayouts
- */
-public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
-
- private final ViewGroup mViewGroup;
- private final Function<CellLayout, DragAndDropAccessibilityDelegate> mDelegateFactory;
-
- /**
- * @param parent the viewgroup containing all the children
- * @param delegateFactory function to create no delegates
- */
- public AccessibleDragListenerAdapter(ViewGroup parent,
- Function<CellLayout, DragAndDropAccessibilityDelegate> delegateFactory) {
- mViewGroup = parent;
- mDelegateFactory = delegateFactory;
- }
-
- @Override
- public void onDragStart(DragObject dragObject, DragOptions options) {
- mViewGroup.setOnHierarchyChangeListener(this);
- enableAccessibleDrag(true, dragObject);
- }
-
- @Override
- public void onDragEnd() {
- mViewGroup.setOnHierarchyChangeListener(null);
- enableAccessibleDrag(false, null);
- Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
- }
-
-
- @Override
- public void onChildViewAdded(View parent, View child) {
- if (parent == mViewGroup) {
- setEnableForLayout((CellLayout) child, true);
- }
- }
-
- @Override
- public void onChildViewRemoved(View parent, View child) {
- if (parent == mViewGroup) {
- setEnableForLayout((CellLayout) child, false);
- }
- }
-
- protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
- for (int i = 0; i < mViewGroup.getChildCount(); i++) {
- setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
- }
- }
-
- protected final void setEnableForLayout(CellLayout layout, boolean enable) {
- layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
- }
-}
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
new file mode 100644
index 0000000..21c2caf
--- /dev/null
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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.accessibility
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.views.ActivityContext
+import java.util.function.Function
+
+/**
+ * Utility listener to enable/disable accessibility drag flags for a ViewGroup containing
+ * CellLayouts
+ */
+open class AccessibleDragListenerAdapter
+/**
+ * @param parent the viewgroup containing all the children
+ * @param delegateFactory function to create no delegates
+ */
+(
+ private val mViewGroup: ViewGroup,
+ private val mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+) : DragController.DragListener, ViewGroup.OnHierarchyChangeListener {
+ override fun onDragStart(dragObject: DragObject, options: DragOptions) {
+ mViewGroup.setOnHierarchyChangeListener(this)
+ enableAccessibleDrag(true, dragObject)
+ }
+
+ override fun onDragEnd() {
+ mViewGroup.setOnHierarchyChangeListener(null)
+ enableAccessibleDrag(false, null)
+ val activityContext = ActivityContext.lookupContext(mViewGroup.context) as ActivityContext
+ activityContext.getDragController<DragController<*>>()?.removeDragListener(this)
+ }
+
+ override fun onChildViewAdded(parent: View, child: View) {
+ if (parent === mViewGroup) {
+ setEnableForLayout(child as CellLayout, true)
+ }
+ }
+
+ override fun onChildViewRemoved(parent: View, child: View) {
+ if (parent === mViewGroup) {
+ setEnableForLayout(child as CellLayout, false)
+ }
+ }
+
+ protected open fun enableAccessibleDrag(enable: Boolean, dragObject: DragObject?) {
+ for (i in 0 until mViewGroup.childCount) {
+ setEnableForLayout(mViewGroup.getChildAt(i) as CellLayout, enable)
+ }
+ }
+
+ protected fun setEnableForLayout(layout: CellLayout, enable: Boolean) {
+ layout.setDragAndDropAccessibilityDelegate(
+ if (enable) mDelegateFactory.apply(layout) else null
+ )
+ }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index aefb7e9..c1264d6 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -364,6 +364,7 @@
/** Add Private Space Header view elements based upon {@link UserProfileState} */
public void bindPrivateSpaceHeaderViewElements(RelativeLayout parent) {
mPSHeader = parent;
+ Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Binding private space.");
updateView();
if (mOnPSHeaderAdded != null) {
MAIN_EXECUTOR.execute(mOnPSHeaderAdded);
@@ -376,6 +377,8 @@
if (mPSHeader == null) {
return;
}
+ Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+ + getCurrentState());
mPSHeader.setAlpha(1);
ViewGroup lockPill = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
assert lockPill != null;
@@ -761,6 +764,9 @@
alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ if (mFloatingMaskView == null) {
+ return;
+ }
mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
}
});
@@ -844,8 +850,22 @@
if (!Flags.privateSpaceAddFloatingMaskView()) {
return;
}
+ // Use getLocationOnScreen() as simply checking for mPSHeader.getBottom() is only relative
+ // to its parent.
+ int[] psHeaderLocation = new int[2];
+ mPSHeader.getLocationOnScreen(psHeaderLocation);
+ int psHeaderBottomY = psHeaderLocation[1] + mPsHeaderHeight;
+ // Calculate the topY of the floatingMaskView as if it was added.
+ int floatingMaskViewBottomBoxTopY =
+ (int) (mAllApps.getBottom() - getMainRecyclerView().getPaddingBottom());
+ // Don't attach if the header will be clipped by the floating mask view.
+ if (psHeaderBottomY > floatingMaskViewBottomBoxTopY) {
+ mFloatingMaskView = null;
+ return;
+ }
mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
R.layout.private_space_mask_view, mAllApps, false);
+ assert mFloatingMaskView != null;
mAllApps.addView(mFloatingMaskView);
// Translate off the screen first if its collapsing so this header view isn't visible to
// user when animation starts.
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
index ac9b146..eaeb8bb 100644
--- a/src/com/android/launcher3/allapps/SectionDecorationHandler.java
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -25,7 +25,6 @@
import android.view.View;
import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.launcher3.util.Themes;
@@ -61,10 +60,10 @@
mContext = context;
mFillAlpha = fillAlpha;
- mFocusColor = ContextCompat.getColor(context,
- R.color.material_color_surface_bright); // UX recommended
- mFillColor = ContextCompat.getColor(context,
- R.color.material_color_surface_container_high); // UX recommended
+ mFocusColor = Themes.getAttrColor(context,
+ R.attr.materialColorSurfaceBright); // UX recommended
+ mFillColor = Themes.getAttrColor(context,
+ R.attr.materialColorSurfaceContainerHigh); // UX recommended
mIsTopLeftRound = isTopLeftRound;
mIsTopRightRound = isTopRightRound;
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index db693f0..8b1f42b 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -157,7 +157,8 @@
isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
isEventOverAccessibleDropTargetBar(ev);
if (!isOverFolderOrSearchBar) {
- sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+ sendTapOutsideFolderAccessibilityEvent(
+ currentFolder.getIsEditingName());
mHoverPointClosesFolder = true;
return true;
}
@@ -167,7 +168,8 @@
isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
isEventOverAccessibleDropTargetBar(ev);
if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
- sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+ sendTapOutsideFolderAccessibilityEvent(
+ currentFolder.getIsEditingName());
mHoverPointClosesFolder = true;
return true;
} else if (!isOverFolderOrSearchBar) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index d3c1a02..3edf1f2 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -26,6 +26,7 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
import static com.android.launcher3.testing.shared.TestProtocol.FOLDER_OPENED_MESSAGE;
@@ -135,7 +136,8 @@
* We avoid measuring {@link #mContent} with a 0 width or height, as this
* results in CellLayout being measured as UNSPECIFIED, which it does not support.
*/
- private static final int MIN_CONTENT_DIMEN = 5;
+ @VisibleForTesting
+ static final int MIN_CONTENT_DIMEN = 5;
public static final int STATE_CLOSED = 0;
public static final int STATE_ANIMATING = 1;
@@ -143,7 +145,8 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN})
- public @interface FolderState {}
+ public @interface FolderState {
+ }
/**
* Time for which the scroll hint is shown before automatically changing page.
@@ -164,7 +167,7 @@
private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
private static final int REORDER_DELAY = 250;
- private static final int ON_EXIT_CLOSE_DELAY = 400;
+ static final int ON_EXIT_CLOSE_DELAY = 400;
private static final Rect sTempRect = new Rect();
private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
@@ -184,10 +187,10 @@
|| itemType == ITEM_TYPE_APP_PAIR;
}
- private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
- private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
- private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
- final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
@@ -197,7 +200,7 @@
// Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
// Anything specific to Launcher should use mLauncherDelegate, otherwise should
// use mActivityContext.
- protected final LauncherDelegate mLauncherDelegate;
+ protected LauncherDelegate mLauncherDelegate;
protected final ActivityContext mActivityContext;
protected DragController mDragController;
@@ -210,7 +213,7 @@
@Thunk
FolderPagedView mContent;
- public FolderNameEditText mFolderName;
+ private FolderNameEditText mFolderName;
private PageIndicatorDots mPageIndicator;
protected View mFooter;
@@ -234,10 +237,10 @@
private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mRearrangeOnClose = false;
- boolean mItemsInvalidated = false;
+ private boolean mItemsInvalidated = false;
private View mCurrentDragView;
private boolean mIsExternalDrag;
- private boolean mDragInProgress = false;
+ private boolean mIsDragInProgress = false;
private boolean mDeleteFolderOnDropCompleted = false;
private boolean mSuppressFolderDeletion = false;
private boolean mItemAddedBackToSelfViaIcon = false;
@@ -250,7 +253,7 @@
private int mScrollAreaOffset;
@Thunk
- int mScrollHintDir = SCROLL_NONE;
+ private int mScrollHintDir = SCROLL_NONE;
@Thunk
int mCurrentScrollDir = SCROLL_NONE;
@@ -315,9 +318,9 @@
| InputType.TYPE_TEXT_FLAG_CAP_WORDS);
mFolderName.forceDisableSuggestions(true);
mFolderName.setPadding(mFolderName.getPaddingLeft(),
- (mFooterHeight - mFolderName.getLineHeight()) / 2,
+ (getFooterHeight() - mFolderName.getLineHeight()) / 2,
mFolderName.getPaddingRight(),
- (mFooterHeight - mFolderName.getLineHeight()) / 2);
+ (getFooterHeight() - mFolderName.getLineHeight()) / 2);
mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
@@ -325,42 +328,54 @@
public boolean onLongClick(View v) {
// Return if global dragging is not enabled
- if (!mLauncherDelegate.isDraggingEnabled()) return true;
+ if (!getIsLauncherDraggingEnabled()) return true;
return startDrag(v, new DragOptions());
}
+ @VisibleForTesting
+ boolean getIsLauncherDraggingEnabled() {
+ return mLauncherDelegate.isDraggingEnabled();
+ }
+
public boolean startDrag(View v, DragOptions options) {
Object tag = v.getTag();
if (tag instanceof ItemInfo item) {
mEmptyCellRank = item.rank;
mCurrentDragView = v;
- mDragController.addDragListener(this);
- if (options.isAccessibleDrag) {
- mDragController.addDragListener(new AccessibleDragListenerAdapter(
- mContent, FolderAccessibilityHelper::new) {
- @Override
- protected void enableAccessibleDrag(boolean enable,
- @Nullable DragObject dragObject) {
- super.enableAccessibleDrag(enable, dragObject);
- mFooter.setImportantForAccessibility(enable
- ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
- });
- }
-
- mLauncherDelegate.beginDragShared(v, this, options);
+ addDragListener(options);
+ callBeginDragShared(v, options);
}
return true;
}
+ void callBeginDragShared(View v, DragOptions options) {
+ mLauncherDelegate.beginDragShared(v, this, options);
+ }
+
+ void addDragListener(DragOptions options) {
+ getDragController().addDragListener(this);
+ if (!options.isAccessibleDrag) {
+ return;
+ }
+ getDragController().addDragListener(new AccessibleDragListenerAdapter(
+ mContent, FolderAccessibilityHelper::new) {
+ @Override
+ protected void enableAccessibleDrag(boolean enable,
+ @Nullable DragObject dragObject) {
+ super.enableAccessibleDrag(enable, dragObject);
+ mFooter.setImportantForAccessibility(enable
+ ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ });
+ }
+
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
if (dragObject.dragSource != this) {
return;
}
-
mContent.removeItem(mCurrentDragView);
mItemsInvalidated = true;
@@ -369,29 +384,23 @@
try (SuppressInfoChanges s = new SuppressInfoChanges()) {
mInfo.remove(dragObject.dragInfo, true);
}
- mDragInProgress = true;
+ mIsDragInProgress = true;
mItemAddedBackToSelfViaIcon = false;
}
@Override
public void onDragEnd() {
- if (mIsExternalDrag && mDragInProgress) {
+ if (mIsExternalDrag && mIsDragInProgress) {
completeDragExit();
}
- mDragInProgress = false;
- mDragController.removeDragListener(this);
- }
-
- public boolean isEditingName() {
- return mIsEditingName;
+ mIsDragInProgress = false;
+ getDragController().removeDragListener(this);
}
public void startEditingFolderName() {
- post(() -> {
- showLabelSuggestions();
- mFolderName.setHint("");
- mIsEditingName = true;
- });
+ showLabelSuggestions();
+ mFolderName.setHint("");
+ mIsEditingName = true;
}
@Override
@@ -459,7 +468,11 @@
return mFolderIcon;
}
- public void setDragController(DragController dragController) {
+ DragController getDragController() {
+ return mDragController;
+ }
+
+ void setDragController(DragController dragController) {
mDragController = dragController;
}
@@ -540,7 +553,7 @@
* Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
* rest of the suggestions to InputMethodManager.
*/
- private void showLabelSuggestions() {
+ void showLabelSuggestions() {
if (mInfo.suggestedFolderNames == null) {
return;
}
@@ -634,11 +647,11 @@
*/
public void beginExternalDrag() {
mIsExternalDrag = true;
- mDragInProgress = true;
+ mIsDragInProgress = true;
// Since this folder opened by another controller, it might not get onDrop or
// onDropComplete. Perform cleanup once drag-n-drop ends.
- mDragController.addDragListener(this);
+ getDragController().addDragListener(this);
ArrayList<ItemInfo> items = new ArrayList<>(mInfo.getContents());
mEmptyCellRank = items.size();
@@ -662,16 +675,12 @@
* is played.
*/
private void animateOpen(List<ItemInfo> items, int pageNo) {
- if (items == null || items.size() <= 1) {
- Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+ if (!shouldAnimateOpen(items)) {
return;
}
Folder openFolder = getOpen(mActivityContext);
- if (openFolder != null && openFolder != this) {
- // Close any open folder before opening a folder.
- openFolder.close(true);
- }
+ closeOpenFolder(openFolder);
mContent.bindItems(items);
centerAboutIcon();
@@ -685,7 +694,7 @@
// There was a one-off crash where the folder had a parent already.
if (getParent() == null) {
dragLayer.addView(this);
- mDragController.addDropTarget(this);
+ getDragController().addDropTarget(this);
} else {
if (FeatureFlags.IS_STUDIO_BUILD) {
Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
@@ -734,7 +743,7 @@
// Do not update the flag if we are in drag mode. The flag will be updated, when we
// actually drop the icon.
- final boolean updateAnimationFlag = !mDragInProgress;
+ final boolean updateAnimationFlag = !mIsDragInProgress;
anim.addListener(new AnimatorListenerAdapter() {
@SuppressLint("InlinedApi")
@@ -768,12 +777,36 @@
anim.start();
// Make sure the folder picks up the last drag move even if the finger doesn't move.
- if (mDragController.isDragging()) {
- mDragController.forceTouchMove();
+ if (getDragController().isDragging()) {
+ getDragController().forceTouchMove();
}
mContent.verifyVisibleHighResIcons(mContent.getNextPage());
}
+ /**
+ * Determines whether we should animate the folder opening.
+ */
+ boolean shouldAnimateOpen(List<ItemInfo> items) {
+ if (items == null || items.size() <= 1) {
+ Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * If there's a folder already open, we want to close it before opening another one.
+ */
+ @VisibleForTesting
+ boolean closeOpenFolder(Folder openFolder) {
+ if (openFolder != null && openFolder != this) {
+ // Close any open folder before opening a folder.
+ openFolder.close(true);
+ return true;
+ }
+ return false;
+ }
+
@Override
protected boolean isOfType(int type) {
return (type & TYPE_FOLDER) != 0;
@@ -787,7 +820,7 @@
mCurrentAnimator.cancel();
}
- if (isEditingName()) {
+ if (mIsEditingName) {
mFolderName.dispatchBackKey();
}
@@ -871,7 +904,7 @@
if (parent != null) {
parent.removeView(this);
}
- mDragController.removeDropTarget(this);
+ getDragController().removeDropTarget(this);
clearFocus();
if (mFolderIcon != null) {
mFolderIcon.setVisibility(View.VISIBLE);
@@ -892,12 +925,12 @@
mRearrangeOnClose = false;
}
if (getItemCount() <= 1) {
- if (!mDragInProgress && !mSuppressFolderDeletion) {
+ if (!mIsDragInProgress && !mSuppressFolderDeletion) {
replaceFolderWithFinalItem();
- } else if (mDragInProgress) {
+ } else if (mIsDragInProgress) {
mDeleteFolderOnDropCompleted = true;
}
- } else if (!mDragInProgress) {
+ } else if (!mIsDragInProgress) {
mContent.unbindItems();
}
mSuppressFolderDeletion = false;
@@ -1017,7 +1050,8 @@
}
}
- private void clearDragInfo() {
+ @VisibleForTesting
+ void clearDragInfo() {
mCurrentDragView = null;
mIsExternalDrag = false;
}
@@ -1058,7 +1092,8 @@
if (getItemCount() <= 1) {
mDeleteFolderOnDropCompleted = true;
}
- if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
+ if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
+ && target != this) {
replaceFolderWithFinalItem();
}
} else {
@@ -1089,7 +1124,7 @@
}
mDeleteFolderOnDropCompleted = false;
- mDragInProgress = false;
+ mIsDragInProgress = false;
mItemAddedBackToSelfViaIcon = false;
mCurrentDragView = null;
@@ -1106,7 +1141,7 @@
}
private void updateItemLocationsInDatabaseBatch(boolean isBind) {
- FolderGridOrganizer verifier = new FolderGridOrganizer(
+ FolderGridOrganizer verifier = createFolderGridOrganizer(
mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
ArrayList<ItemInfo> items = new ArrayList<>();
@@ -1132,7 +1167,7 @@
}
public void notifyDrop() {
- if (mDragInProgress) {
+ if (mIsDragInProgress) {
mItemAddedBackToSelfViaIcon = true;
}
}
@@ -1175,28 +1210,41 @@
}
protected int getContentAreaHeight() {
- DeviceProfile grid = mActivityContext.getDeviceProfile();
- int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
- - mFooterHeight;
- int height = Math.min(maxContentAreaHeight,
+ int height = Math.min(getMaxContentAreaHeight(),
mContent.getDesiredHeight());
return Math.max(height, MIN_CONTENT_DIMEN);
}
- private int getContentAreaWidth() {
+ @VisibleForTesting
+ int getMaxContentAreaHeight() {
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
+ return grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+ - getFooterHeight();
+ }
+
+ @VisibleForTesting
+ int getContentAreaWidth() {
return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
}
- private int getFolderWidth() {
+ @VisibleForTesting
+ int getFolderWidth() {
return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
}
- private int getFolderHeight() {
+ @VisibleForTesting
+ int getFolderHeight() {
return getFolderHeight(getContentAreaHeight());
}
- private int getFolderHeight(int contentAreaHeight) {
- return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
+ @VisibleForTesting
+ int getFolderHeight(int contentAreaHeight) {
+ return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight();
+ }
+
+ @VisibleForTesting
+ int getFooterHeight() {
+ return mFooterHeight;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -1366,7 +1414,7 @@
}
// Clear the drag info, as it is no longer being dragged.
- mDragInProgress = false;
+ mIsDragInProgress = false;
if (mContent.getPageCount() > 1) {
// The animation has already been shown while opening the folder.
@@ -1404,7 +1452,7 @@
@Override
public void onAdd(ItemInfo item, int rank) {
- FolderGridOrganizer verifier = new FolderGridOrganizer(
+ FolderGridOrganizer verifier = createFolderGridOrganizer(
mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
verifier.updateRankAndPos(item, rank);
mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
@@ -1435,7 +1483,8 @@
}
}
- private View getViewForInfo(final ItemInfo item) {
+ @VisibleForTesting
+ View getViewForInfo(final ItemInfo item) {
return mContent.iterateOverItems((info, view) -> info == item);
}
@@ -1493,7 +1542,7 @@
if (hasFocus) {
mFromLabelState = mInfo.getFromLabelState();
mFromTitle = mInfo.title;
- startEditingFolderName();
+ post(this::startEditingFolderName);
} else {
StatsLogger statsLogger = mStatsLogManager.logger()
.withItemInfo(mInfo)
@@ -1626,7 +1675,7 @@
/** Navigation bar back key or hardware input back key has been issued. */
@Override
public void onBackInvoked() {
- if (isEditingName()) {
+ if (mIsEditingName) {
mFolderName.dispatchBackKey();
} else {
super.onBackInvoked();
@@ -1638,7 +1687,7 @@
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
BaseDragLayer dl = (BaseDragLayer) getParent();
- if (isEditingName()) {
+ if (mIsEditingName) {
if (!dl.isEventOverView(mFolderName, ev)) {
mFolderName.dispatchBackKey();
return true;
@@ -1685,6 +1734,95 @@
return mContent;
}
+ @VisibleForTesting
+ void setItemAddedBackToSelfViaIcon(boolean value) {
+ mItemAddedBackToSelfViaIcon = value;
+ }
+
+ @VisibleForTesting
+ boolean getItemAddedBackToSelfViaIcon() {
+ return mItemAddedBackToSelfViaIcon;
+ }
+
+ @VisibleForTesting
+ void setIsDragInProgress(boolean value) {
+ mIsDragInProgress = value;
+ }
+
+ @VisibleForTesting
+ boolean getIsDragInProgress() {
+ return mIsDragInProgress;
+ }
+
+ @VisibleForTesting
+ View getCurrentDragView() {
+ return mCurrentDragView;
+ }
+
+ @VisibleForTesting
+ void setCurrentDragView(View view) {
+ mCurrentDragView = view;
+ }
+
+ @VisibleForTesting
+ boolean getItemsInvalidated() {
+ return mItemsInvalidated;
+ }
+
+ @VisibleForTesting
+ void setItemsInvalidated(boolean value) {
+ mItemsInvalidated = value;
+ }
+
+ @VisibleForTesting
+ boolean getIsExternalDrag() {
+ return mIsExternalDrag;
+ }
+
+ @VisibleForTesting
+ void setIsExternalDrag(boolean value) {
+ mIsExternalDrag = value;
+ }
+
+ public boolean getIsEditingName() {
+ return mIsEditingName;
+ }
+
+ @VisibleForTesting
+ void setIsEditingName(boolean value) {
+ mIsEditingName = value;
+ }
+
+ @VisibleForTesting
+ void setFolderName(FolderNameEditText value) {
+ mFolderName = value;
+ }
+
+ @VisibleForTesting
+ FolderNameEditText getFolderName() {
+ return mFolderName;
+ }
+
+ @VisibleForTesting
+ boolean getIsOpen() {
+ return mIsOpen;
+ }
+
+ @VisibleForTesting
+ void setIsOpen(boolean value) {
+ mIsOpen = value;
+ }
+
+ @VisibleForTesting
+ boolean getRearrangeOnClose() {
+ return mRearrangeOnClose;
+ }
+
+ @VisibleForTesting
+ void setRearrangeOnClose(boolean value) {
+ mRearrangeOnClose = value;
+ }
+
/** Returns the height of the current folder's bottom edge from the bottom of the screen. */
private int getHeightFromBottom() {
BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
@@ -1695,10 +1833,15 @@
}
@VisibleForTesting
- public boolean getDeleteFolderOnDropCompleted() {
+ boolean getDeleteFolderOnDropCompleted() {
return mDeleteFolderOnDropCompleted;
}
+ @VisibleForTesting
+ void setDeleteFolderOnDropCompleted(boolean value) {
+ mDeleteFolderOnDropCompleted = value;
+ }
+
/**
* Save this listener for the special case of when we update the state and concurrently
* add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
@@ -1708,7 +1851,13 @@
mPriorityOnFolderStateChangedListener = listener;
}
- private void setState(@FolderState int newState) {
+ @VisibleForTesting
+ int getState() {
+ return mState;
+ }
+
+ @VisibleForTesting
+ void setState(@FolderState int newState) {
mState = newState;
if (mPriorityOnFolderStateChangedListener != null) {
mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
@@ -1720,6 +1869,60 @@
}
}
+ @VisibleForTesting
+ Alarm getOnExitAlarm() {
+ return mOnExitAlarm;
+ }
+
+ @VisibleForTesting
+ void setOnExitAlarm(Alarm value) {
+ mOnExitAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getReorderAlarm() {
+ return mReorderAlarm;
+ }
+
+ @VisibleForTesting
+ void setReorderAlarm(Alarm value) {
+ mReorderAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getOnScrollHintAlarm() {
+ return mOnScrollHintAlarm;
+ }
+
+ @VisibleForTesting
+ void setOnScrollHintAlarm(Alarm value) {
+ mOnScrollHintAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getScrollPauseAlarm() {
+ return mScrollPauseAlarm;
+ }
+
+ @VisibleForTesting
+ void setScrollPauseAlarm(Alarm value) {
+ mScrollPauseAlarm = value;
+ }
+
+ @VisibleForTesting
+ int getScrollHintDir() {
+ return mScrollHintDir;
+ }
+
+ @VisibleForTesting
+ void setScrollHintDir(int value) {
+ mScrollHintDir = value;
+ }
+
+ @VisibleForTesting
+ int getScrollAreaOffset() {
+ return mScrollAreaOffset;
+ }
/**
* Adds the provided listener to the running list of Folder listeners
* {@link #mOnFolderStateChangedListeners}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 37a8d9b..588a6db 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -99,7 +100,7 @@
mContext = folder.getContext();
mDeviceProfile = folder.mActivityContext.getDeviceProfile();
- mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile);
+ mPreviewVerifier = createFolderGridOrganizer(mDeviceProfile);
mIsOpening = isOpening;
@@ -255,8 +256,8 @@
mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
// Fade in the folder name, as the text can overlap the icons when grid size is small.
- mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
- play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+ mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f);
+ play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1),
mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
@@ -317,7 +318,7 @@
mFolder.mFooter.setScaleX(1f);
mFolder.mFooter.setScaleY(1f);
mFolder.mFooter.setTranslationX(0f);
- mFolder.mFolderName.setAlpha(1f);
+ mFolder.getFolderName().setAlpha(1f);
mFolder.setClipChildren(mFolderClipChildren);
mFolder.setClipToPadding(mFolderClipToPadding);
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 593673d..a7ab7b9 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -47,13 +47,20 @@
/**
* Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
*/
- public FolderGridOrganizer(DeviceProfile profile) {
- mMaxCountX = profile.numFolderColumns;
- mMaxCountY = profile.numFolderRows;
+ public FolderGridOrganizer(int maxCountX, int maxCountY) {
+ mMaxCountX = maxCountX;
+ mMaxCountY = maxCountY;
mMaxItemsPerPage = mMaxCountX * mMaxCountY;
}
/**
+ * Creates a FolderGridOrganizer for the given DeviceProfile
+ */
+ public static FolderGridOrganizer createFolderGridOrganizer(DeviceProfile profile) {
+ return new FolderGridOrganizer(profile.numFolderColumns, profile.numFolderRows);
+ }
+
+ /**
* Updates the organizer with the provided folder info
*/
public FolderGridOrganizer setFolderInfo(FolderInfo info) {
@@ -194,6 +201,7 @@
int row = rank / mCountX;
return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS;
}
+ // If we have less than 4 items do this
return rank < MAX_NUM_ITEMS_IN_PREVIEW;
}
}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 00636a3..a0b695a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.Flags.enableCursorHoverStates;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY;
@@ -223,7 +224,7 @@
icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
- icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile());
+ icon.mPreviewVerifier = createFolderGridOrganizer(activity.getDeviceProfile());
icon.mPreviewVerifier.setFolderInfo(folderInfo);
icon.updatePreviewItems(false);
@@ -458,7 +459,7 @@
mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
onTitleChanged(mInfo.title);
- mFolder.mFolderName.setText(mInfo.title);
+ mFolder.getFolderName().setText(mInfo.title);
// Logging for folder creation flow
StatsLogManager.newInstance(getContext()).logger()
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 8eaa0dc..9dc2d24 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
+import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -100,10 +101,21 @@
private boolean mViewsBound = false;
public FolderPagedView(Context context, AttributeSet attrs) {
+ this(
+ context,
+ attrs,
+ createFolderGridOrganizer(ActivityContext.lookupContext(context).getDeviceProfile())
+ );
+ }
+
+ public FolderPagedView(
+ Context context,
+ AttributeSet attrs,
+ FolderGridOrganizer folderGridOrganizer
+ ) {
super(context, attrs);
ActivityContext activityContext = ActivityContext.lookupContext(context);
- DeviceProfile profile = activityContext.getDeviceProfile();
- mOrganizer = new FolderGridOrganizer(profile);
+ mOrganizer = folderGridOrganizer;
mIsRtl = Utilities.isRtl(getResources());
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -361,8 +373,8 @@
// Update footer
mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
// Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
- mFolder.mFolderName.setGravity(getPageCount() > 1 ?
- (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
+ mFolder.getFolderName().setGravity(getPageCount() > 1
+ ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
}
public int getDesiredWidth() {
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index e6ade61..5faa2b8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -52,6 +52,7 @@
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
@@ -195,8 +196,8 @@
if (!WIDGETS_ENABLED) {
return;
}
- final List<WidgetsListBaseEntry> widgets =
- mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
+ List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
+ .build(mBgDataModel.widgetsModel.getWidgetsByPackageItem());
executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 312c6f4..269cb9f 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -568,7 +568,7 @@
private void processFolderItems() {
// Sort the folder items, update ranks, and make sure all preview items are high res.
List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
- .stream().map(FolderGridOrganizer::new).toList();
+ .stream().map(FolderGridOrganizer::createFolderGridOrganizer).toList();
for (CollectionInfo collection : mBgDataModel.collections) {
if (!(collection instanceof FolderInfo folder)) {
continue;
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 266ed0c..cf2cadc 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -24,6 +24,7 @@
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder
import java.util.Objects
import java.util.concurrent.Executor
import java.util.function.Predicate
@@ -78,7 +79,9 @@
}
fun bindUpdatedWidgets(dataModel: BgDataModel) {
- val widgets = dataModel.widgetsModel.getWidgetsListForPicker(app.context)
+ val widgets =
+ WidgetsListBaseEntriesBuilder(app.context)
+ .build(dataModel.widgetsModel.widgetsByPackageItem)
scheduleCallbackTask { it.bindAllWidgets(widgets) }
}
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index 58ebf0f..35064cf 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -26,7 +26,6 @@
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabelAndIcon;
import com.android.launcher3.icons.IconCache;
@@ -39,9 +38,6 @@
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import com.android.launcher3.widget.WidgetSections;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.wm.shell.Flags;
import java.util.ArrayList;
@@ -75,6 +71,9 @@
* Returns all widgets keyed by their component key.
*/
public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKey() {
+ if (!WIDGETS_ENABLED) {
+ return Collections.emptyMap();
+ }
return mWidgetsByPackageItem.values().stream()
.flatMap(Collection::stream).distinct()
.collect(Collectors.toMap(
@@ -87,51 +86,10 @@
* Returns widgets grouped by the package item that they should belong to.
*/
public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
- return mWidgetsByPackageItem;
- }
-
- /**
- * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
- * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
- * list of {@link WidgetsListBaseEntry}s is not sorted.
- *
- * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
- */
- public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
- Context context,
- Predicate<WidgetItem> widgetItemFilter) {
if (!WIDGETS_ENABLED) {
- return new ArrayList<>();
+ return Collections.emptyMap();
}
- ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
- AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
-
- for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry :
- mWidgetsByPackageItem.entrySet()) {
- PackageItemInfo pkgItem = entry.getKey();
- List<WidgetItem> widgetItems = entry.getValue()
- .stream()
- .filter(widgetItemFilter).toList();
- if (!widgetItems.isEmpty()) {
- String sectionName = (pkgItem.title == null) ? "" :
- indexer.computeSectionName(pkgItem.title);
- result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
- result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
- }
- }
- return result;
- }
-
- /**
- * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
- * are sorted (based on label and user), but the overall list of
- * {@link WidgetsListBaseEntry}s is not sorted.
- *
- * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
- */
- public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
- // return all items
- return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
+ return mWidgetsByPackageItem;
}
/**
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
import android.view.animation.OvershootInterpolator;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
@@ -131,7 +132,8 @@
private float mCurrentPosition;
private float mFinalPosition;
private boolean mIsScrollPaused;
- private boolean mIsTwoPanels;
+ @VisibleForTesting
+ boolean mIsTwoPanels;
private ObjectAnimator mAnimator;
private @Nullable ObjectAnimator mAlphaAnimator;
@@ -477,6 +479,21 @@
return sTempRect;
}
+ @VisibleForTesting
+ int getActivePage() {
+ return mActivePage;
+ }
+
+ @VisibleForTesting
+ int getNumPages() {
+ return mNumPages;
+ }
+
+ @VisibleForTesting
+ float getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
private class MyOutlineProver extends ViewOutlineProvider {
@Override
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 7339111..e861961 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -183,6 +183,11 @@
mUserToSerialMap.put(userHandle, info);
}
+ @VisibleForTesting
+ public void putToPreInstallCache(UserHandle userHandle, List<String> preInstalledApps) {
+ mUserToPreInstallAppMap.put(userHandle, preInstalledApps);
+ }
+
/**
* @see UserManager#getUserProfiles()
*/
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index fb463f7..7e139c3 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -64,6 +64,11 @@
/** All installed widgets. */
private List<WidgetsListBaseEntry> mAllWidgets = List.of();
+ /**
+ * Selectively chosen installed widgets which may be preferred for default display over the list
+ * of all widgets.
+ */
+ private List<WidgetsListBaseEntry> mDefaultWidgets = List.of();
/** Widgets that can be recommended to the users. */
private List<ItemInfo> mRecommendedWidgets = List.of();
@@ -194,6 +199,18 @@
public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
mAllWidgets = allWidgets;
+ mDefaultWidgets = List.of();
+ mChangeListener.onWidgetsBound();
+ }
+
+ /**
+ * Sets the list of widgets to be displayed by default and a complete list that can be displayed
+ * when user chooses to show all widgets.
+ */
+ public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets,
+ List<WidgetsListBaseEntry> defaultWidgets) {
+ mAllWidgets = allWidgets;
+ mDefaultWidgets = defaultWidgets;
mChangeListener.onWidgetsBound();
}
@@ -205,6 +222,14 @@
return mAllWidgets;
}
+ /**
+ * Returns a "selectively" chosen list of widgets that may be preferred to be shown by default
+ * instead of a complete list.
+ */
+ public List<WidgetsListBaseEntry> getDefaultWidgets() {
+ return mDefaultWidgets;
+ }
+
/** Returns a list of recommended widgets. */
public List<WidgetItem> getRecommendedWidgets() {
HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
@@ -259,8 +284,10 @@
}
/** Gets the WidgetsListContentEntry for the currently selected header. */
- public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) {
- return (WidgetsListContentEntry) mAllWidgets.stream()
+ public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey,
+ boolean useDefault) {
+ List<WidgetsListBaseEntry> widgets = useDefault ? mDefaultWidgets : mAllWidgets;
+ return (WidgetsListContentEntry) widgets.stream()
.filter(row -> row instanceof WidgetsListContentEntry
&& PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey))
.findAny()
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 6d6b3b6..f895b30 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -17,6 +17,9 @@
package com.android.launcher3.recyclerview
import android.content.Context
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -38,21 +41,18 @@
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
var hasWorkProfile = false
- private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+ @VisibleForTesting(otherwise = PROTECTED)
+ var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
- fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+ fun preInflateAllAppsViewHolders(context: T) {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
- val preInflateCount = getPreinflateCount(context)
- if (preInflateCount <= 0) {
- return
- }
// Create a separate context dedicated for all apps preinflation thread. The goal is to
// create a separate AssetManager obj internally to avoid lock contention with
@@ -77,34 +77,51 @@
null
) {
override fun setAppsPerRow(appsPerRow: Int) = Unit
+
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
+ preInflateAllAppsViewHolders(adapter, BaseAllAppsAdapter.VIEW_TYPE_ICON, activeRv) {
+ getPreinflateCount(context)
+ }
+ }
+
+ @VisibleForTesting(otherwise = PROTECTED)
+ fun preInflateAllAppsViewHolders(
+ adapter: RecyclerView.Adapter<*>,
+ viewType: Int,
+ parent: ViewGroup,
+ preInflationCountProvider: () -> Int
+ ) {
+ val preinflationCount = preInflationCountProvider.invoke()
+ if (preinflationCount <= 0) {
+ return
+ }
mCancellableTask?.cancel()
var task: CancellableTask<List<ViewHolder>>? = null
task =
CancellableTask(
{
val list: ArrayList<ViewHolder> = ArrayList()
- for (i in 0 until preInflateCount) {
+ for (i in 0 until preinflationCount) {
if (task?.canceled == true) {
break
}
- list.add(
- adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
- )
+ list.add(adapter.createViewHolder(parent, viewType))
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
- for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+ // Run preInflationCountProvider again as the needed VH might have changed
+ val newPreinflationCount = preInflationCountProvider.invoke()
+ for (i in 0 until minOf(viewHolders.size, newPreinflationCount)) {
putRecycledView(viewHolders[i])
}
}
)
mCancellableTask = task
- VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+ VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
}
/**
@@ -125,7 +142,7 @@
* app icons in size of one all apps pages, so that opening all apps don't need to inflate app
* icons.
*/
- fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+ fun getPreinflateCount(context: T): Int {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 3d4b409..f183f18 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -76,4 +76,5 @@
* When turned on, we enable zero state web data loader related logging.
*/
public static final String ZERO_WEB_DATA_LOADER = "ZeroStateWebDataLoaderLog";
+ public static final String SEARCH_TARGET_UTIL_LOG = "SearchTargetUtilLog";
}
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 12eff61..8ee799a 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -24,7 +24,10 @@
import android.content.Context;
import android.content.Intent;
+import androidx.annotation.VisibleForTesting;
+
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
/**
* Utility class for tracking if the screen is currently on or off
@@ -34,8 +37,7 @@
public static final MainThreadInitializedObject<ScreenOnTracker> INSTANCE =
new MainThreadInitializedObject<>(ScreenOnTracker::new);
- private final SimpleBroadcastReceiver mReceiver =
- new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+ private final SimpleBroadcastReceiver mReceiver;
private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
private final Context mContext;
@@ -44,8 +46,20 @@
private ScreenOnTracker(Context context) {
// Assume that the screen is on to begin with
mContext = context;
+ mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+ init();
+ }
+
+ @VisibleForTesting
+ ScreenOnTracker(Context context, SimpleBroadcastReceiver receiver) {
+ mContext = context;
+ mReceiver = receiver;
+ init();
+ }
+
+ private void init() {
mIsScreenOn = true;
- mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+ mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
}
@Override
@@ -53,7 +67,8 @@
mReceiver.unregisterReceiverSafely(mContext);
}
- private void onReceive(Intent intent) {
+ @VisibleForTesting
+ void onReceive(Intent intent) {
String action = intent.getAction();
if (ACTION_SCREEN_ON.equals(action)) {
mIsScreenOn = true;
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 07b7941..aa4f8af 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -54,14 +54,6 @@
? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY;
}
- /**
- * Returns true if the item is a deep shortcut.
- */
- public static boolean isDeepShortcut(ItemInfo info) {
- return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
- && info instanceof WorkspaceItemInfo;
- }
-
private static boolean isActive(ItemInfo info) {
boolean isLoading = info instanceof WorkspaceItemInfo
&& ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 51749a7..6bae1ba 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -31,6 +31,7 @@
import android.provider.Settings;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Utilities;
@@ -49,14 +50,14 @@
public static final VibrationEffect EFFECT_CLICK =
createPredefined(VibrationEffect.EFFECT_CLICK);
- private static final Uri HAPTIC_FEEDBACK_URI =
- Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
+ @VisibleForTesting
+ static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
- private static final float LOW_TICK_SCALE = 0.9f;
- private static final float DRAG_TEXTURE_SCALE = 0.03f;
- private static final float DRAG_COMMIT_SCALE = 0.5f;
- private static final float DRAG_BUMP_SCALE = 0.4f;
- private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+ @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f;
+ @VisibleForTesting static final float DRAG_TEXTURE_SCALE = 0.03f;
+ @VisibleForTesting static final float DRAG_COMMIT_SCALE = 0.5f;
+ @VisibleForTesting static final float DRAG_BUMP_SCALE = 0.4f;
+ @VisibleForTesting static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
@Nullable
private final VibrationEffect mDragEffect;
@@ -73,22 +74,29 @@
*/
public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
- private final Context mContext;
private final Vibrator mVibrator;
private final boolean mHasVibrator;
- private final SettingsCache.OnChangeListener mHapticChangeListener =
+
+ private final SettingsCache mSettingsCache;
+
+ @VisibleForTesting
+ final SettingsCache.OnChangeListener mHapticChangeListener =
isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
private boolean mIsHapticFeedbackEnabled;
private VibratorWrapper(Context context) {
- mContext = context;
- mVibrator = context.getSystemService(Vibrator.class);
+ this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context));
+ }
+
+ @VisibleForTesting
+ VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) {
+ mVibrator = vibrator;
mHasVibrator = mVibrator.hasVibrator();
+ mSettingsCache = settingsCache;
if (mHasVibrator) {
- SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
- cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
- mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0);
+ mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+ mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
} else {
mIsHapticFeedbackEnabled = false;
}
@@ -98,12 +106,7 @@
// Drag texture, Commit, and Bump should only be used for premium phones.
// Before using these haptics make sure check if the device can use it
- VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
- for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
- dragEffect.addPrimitive(
- PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
- }
- mDragEffect = dragEffect.compose();
+ mDragEffect = getDragEffect();
mCommitEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
mBumpEffect = VibrationEffect.startComposition().addPrimitive(
@@ -124,8 +127,7 @@
@Override
public void close() {
if (mHasVibrator) {
- SettingsCache.INSTANCE.get(mContext)
- .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+ mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
}
}
@@ -215,4 +217,13 @@
vibrate(primitiveLowTickEffect);
}
}
+
+ static VibrationEffect getDragEffect() {
+ VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+ for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+ dragEffect.addPrimitive(
+ PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+ }
+ return dragEffect.compose();
+ }
}
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index e413d7f..2fa8bf4 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -24,6 +24,7 @@
import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.util.ViewPool.Reusable;
@@ -43,9 +44,16 @@
public ViewPool(Context context, @Nullable ViewGroup parent,
int layoutId, int maxSize, int initialSize) {
+ this(LayoutInflater.from(context).cloneInContext(context),
+ parent, layoutId, maxSize, initialSize);
+ }
+
+ @VisibleForTesting
+ ViewPool(LayoutInflater inflater, @Nullable ViewGroup parent,
+ int layoutId, int maxSize, int initialSize) {
mLayoutId = layoutId;
mParent = parent;
- mInflater = LayoutInflater.from(context);
+ mInflater = inflater;
mPool = new Object[maxSize];
if (initialSize > 0) {
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209e..856f4b3 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -110,7 +110,7 @@
}
View background = RoundedCornerEnforcement.findBackground(this);
if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
resetRoundedCorners();
return;
}
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd..2e5e251 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -67,7 +67,7 @@
/**
* Check whether the app widget has opted out of the enforcement.
*/
- public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+ public static boolean hasAppWidgetOptedOut(@NonNull View background) {
return background.getId() == android.R.id.background && background.getClipToOutline();
}
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 398b1df..5ad9222 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -22,6 +22,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -52,6 +54,9 @@
}
}
+ @VisibleForTesting
+ CustomAppWidgetProviderInfo() {}
+
@Override
public void initSpans(Context context, InvariantDeviceProfile idp) {
mIsMinSizeFulfilled = Math.min(spanX, minSpanX) <= idp.numColumns
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 50012b3..faa5d12 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -30,6 +30,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.R;
import com.android.launcher3.util.MainThreadInitializedObject;
@@ -45,6 +46,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -62,9 +64,16 @@
private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins;
private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
private Consumer<PackageUserKey> mWidgetRefreshCallback;
+ private final @NonNull AppWidgetManager mAppWidgetManager;
private CustomWidgetManager(Context context) {
+ this(context, AppWidgetManager.getInstance(context));
+ }
+
+ @VisibleForTesting
+ CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) {
mContext = context;
+ mAppWidgetManager = widgetManager;
mPlugins = new HashMap<>();
mCustomWidgets = new ArrayList<>();
PluginManagerWrapper.INSTANCE.get(context)
@@ -94,7 +103,7 @@
@Override
public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
- List<AppWidgetProviderInfo> providers = AppWidgetManager.getInstance(context)
+ List<AppWidgetProviderInfo> providers = mAppWidgetManager
.getInstalledProvidersForProfile(Process.myUserHandle());
if (providers.isEmpty()) return;
Parcel parcel = Parcel.obtain();
@@ -113,6 +122,12 @@
mCustomWidgets.removeIf(w -> w.getComponent().equals(cn));
}
+ @VisibleForTesting
+ @NonNull
+ Map<ComponentName, CustomWidgetPlugin> getPlugins() {
+ return mPlugins;
+ }
+
/**
* Inject a callback function to refresh the widgets.
*/
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
new file mode 100644
index 0000000..1abe4da
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.widget.model
+
+import android.content.Context
+import com.android.launcher3.compat.AlphabeticIndexCompat
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import java.util.function.Predicate
+
+/**
+ * A helper class that builds the list of [WidgetsListBaseEntry]s used by the UI to display widgets.
+ */
+class WidgetsListBaseEntriesBuilder(val context: Context) {
+
+ /** Builds the widgets list entries in a format understandable by the widget picking UI. */
+ @JvmOverloads
+ fun build(
+ widgetsByPackageItem: Map<PackageItemInfo, List<WidgetItem>>,
+ widgetFilter: Predicate<WidgetItem> = Predicate<WidgetItem> { true },
+ ): List<WidgetsListBaseEntry> {
+ val indexer = AlphabeticIndexCompat(context)
+
+ return buildList {
+ for ((pkgItem, widgetItems) in widgetsByPackageItem.entries) {
+ val filteredWidgetItems = widgetItems.filter { widgetFilter.test(it) }
+ if (filteredWidgetItems.isNotEmpty()) {
+ // Enables fast scroll popup to show right characters in all locales.
+ val sectionName = pkgItem.title?.let { indexer.computeSectionName(it) } ?: ""
+
+ add(WidgetsListHeaderEntry.create(pkgItem, sectionName, filteredWidgetItems))
+ add(WidgetsListContentEntry(pkgItem, sectionName, filteredWidgetItems))
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 2e36583..21b7be4 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -69,6 +69,7 @@
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.search.SearchModeListener;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
@@ -135,7 +136,7 @@
private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
@Nullable
PersonalWorkPagedView mViewPager;
- private boolean mIsInSearchMode;
+ protected boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
@Px
protected int mMaxSpanPerRow;
@@ -245,8 +246,12 @@
mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
- mSearchBar.initialize(
- mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
+ mSearchBar.initialize(new WidgetsSearchDataProvider() {
+ @Override
+ public List<WidgetsListBaseEntry> getWidgets() {
+ return getWidgetsToDisplay();
+ }
+ }, /* searchModeListener= */ this);
}
private void setDeviceManagementResources() {
@@ -462,22 +467,28 @@
setTranslationShift(mTranslationShift);
}
+ /**
+ * Returns all displayable widgets.
+ */
+ protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
+ return mActivityContext.getPopupDataProvider().getAllWidgets();
+ }
+
@Override
public void onWidgetsBound() {
if (mIsInSearchMode) {
return;
}
- List<WidgetsListBaseEntry> allWidgets =
- mActivityContext.getPopupDataProvider().getAllWidgets();
+ List<WidgetsListBaseEntry> widgets = getWidgetsToDisplay();
AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
- primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+ primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
if (mHasWorkProfile) {
mViewPager.setVisibility(VISIBLE);
mTabBar.setVisibility(VISIBLE);
AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
- workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+ workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
onActivePageChanged(mViewPager.getCurrentPage());
} else {
onActivePageChanged(0);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c84680d..c4c755a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -27,7 +27,9 @@
import android.graphics.Rect;
import android.os.Process;
import android.util.AttributeSet;
+import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -35,6 +37,7 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
+import android.widget.PopupMenu;
import android.widget.ScrollView;
import android.widget.TextView;
@@ -83,6 +86,17 @@
private PackageUserKey mSelectedHeader;
private TextView mHeaderDescription;
+ /**
+ * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the
+ * picker.
+ */
+ protected View mWidgetOptionsMenu;
+ /**
+ * State of the options in the menu (if displayed to the user).
+ */
+ @Nullable
+ protected WidgetOptionsMenuState mWidgetOptionsMenuState = null;
+
public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@@ -130,6 +144,9 @@
mHeaderTitle = mContent.findViewById(R.id.title);
mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
+ mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu);
+ setupWidgetOptionsMenu();
+
mRightPane = mContent.findViewById(R.id.right_pane);
mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -155,6 +172,40 @@
}
}
+ protected void setupWidgetOptionsMenu() {
+ mWidgetOptionsMenu.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mWidgetOptionsMenuState != null) {
+ PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v,
+ Gravity.END);
+ MenuItem menuItem = popupMenu.getMenu().add(
+ R.string.widget_picker_show_all_widgets_menu_item_title);
+ menuItem.setCheckable(true);
+ menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+ menuItem.setOnMenuItemClickListener(
+ item -> onShowAllWidgetsMenuItemClick(item));
+ popupMenu.show();
+ }
+ }
+ });
+ }
+
+ private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) {
+ mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets;
+ menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
+
+ // Refresh widgets
+ onWidgetsBound();
+ if (mIsInSearchMode) {
+ mSearchBar.reset();
+ } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) {
+ mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
+ mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
+ }
+ return true;
+ }
+
@Override
protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
if (enableCategorizedWidgetSuggestions()) {
@@ -234,6 +285,29 @@
}
@Override
+ protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
+ List<WidgetsListBaseEntry> allWidgets =
+ mActivityContext.getPopupDataProvider().getAllWidgets();
+ List<WidgetsListBaseEntry> defaultWidgets =
+ mActivityContext.getPopupDataProvider().getDefaultWidgets();
+
+ if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) {
+ // no menu if there are no default widgets to show
+ mWidgetOptionsMenuState = null;
+ mWidgetOptionsMenu.setVisibility(GONE);
+ } else {
+ if (mWidgetOptionsMenuState == null) {
+ mWidgetOptionsMenuState = new WidgetOptionsMenuState();
+ }
+
+ mWidgetOptionsMenu.setVisibility(VISIBLE);
+ return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets;
+ }
+
+ return allWidgets;
+ }
+
+ @Override
public void onWidgetsBound() {
super.onWidgetsBound();
if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) {
@@ -435,8 +509,11 @@
final boolean isUserClick = mSelectedHeader != null
&& !getAccessibilityInitialFocusView().isAccessibilityFocused();
mSelectedHeader = selectedHeader;
- WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
- .getSelectedAppWidgets(selectedHeader);
+ WidgetsListContentEntry contentEntry =
+ mActivityContext.getPopupDataProvider().getSelectedAppWidgets(
+ selectedHeader, /*useDefault=*/
+ (mWidgetOptionsMenuState != null
+ && !mWidgetOptionsMenuState.showAllWidgets));
if (contentEntry == null || mRightPane == null) {
return;
@@ -570,4 +647,15 @@
*/
void onHeaderChanged(@NonNull PackageUserKey selectedHeader);
}
+
+ /**
+ * Holds the selection state of the options menu (if presented to the user).
+ */
+ protected static class WidgetOptionsMenuState {
+ /**
+ * UI state indicating whether to show default or all widgets.
+ * <p>If true, shows all widgets; else shows the default widgets.</p>
+ */
+ public boolean showAllWidgets = false;
+ }
}
diff --git a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
index 65937b6..92caf3e 100644
--- a/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/LauncherWidgetsSearchBar.java
@@ -26,7 +26,6 @@
import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.R;
-import com.android.launcher3.popup.PopupDataProvider;
/**
* View for a search bar with an edit text with a cancel button.
@@ -51,7 +50,8 @@
}
@Override
- public void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener) {
+ public void initialize(WidgetsSearchDataProvider dataProvider,
+ SearchModeListener searchModeListener) {
mController = new WidgetsSearchBarController(
new SimpleWidgetsSearchAlgorithm(dataProvider),
mEditText, mCancelButton, searchModeListener);
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
index 613066a..0e88787 100644
--- a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
@@ -21,13 +21,13 @@
import android.os.Handler;
import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.search.SearchAlgorithm;
import com.android.launcher3.search.SearchCallback;
import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
import java.util.ArrayList;
import java.util.List;
@@ -39,9 +39,9 @@
public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {
private final Handler mResultHandler;
- private final PopupDataProvider mDataProvider;
+ private final WidgetsSearchDataProvider mDataProvider;
- public SimpleWidgetsSearchAlgorithm(PopupDataProvider dataProvider) {
+ public SimpleWidgetsSearchAlgorithm(WidgetsSearchDataProvider dataProvider) {
mResultHandler = new Handler();
mDataProvider = dataProvider;
}
@@ -63,9 +63,9 @@
* Returns entries for all matched widgets
*/
public static ArrayList<WidgetsListBaseEntry> getFilteredWidgets(
- PopupDataProvider dataProvider, String input) {
+ WidgetsSearchDataProvider dataProvider, String input) {
ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
- dataProvider.getAllWidgets().stream()
+ dataProvider.getWidgets().stream()
.filter(entry -> entry instanceof WidgetsListHeaderEntry)
.forEach(headerEntry -> {
List<WidgetItem> matchedWidgetItems = filterWidgetItems(
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
index 44a5e80..ab504e7 100644
--- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -16,7 +16,9 @@
package com.android.launcher3.widget.picker.search;
-import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
/**
* Interface for a widgets picker search bar.
@@ -25,7 +27,7 @@
/**
* Attaches a controller to the search bar which interacts with {@code searchModeListener}.
*/
- void initialize(PopupDataProvider dataProvider, SearchModeListener searchModeListener);
+ void initialize(WidgetsSearchDataProvider dataProvider, SearchModeListener searchModeListener);
/**
* Clears search bar.
@@ -44,4 +46,15 @@
* Sets the vertical location, in pixels, of this search bar relative to its top position.
*/
void setTranslationY(float translationY);
+
+
+ /**
+ * Provides corpus from which search results must be returned.
+ */
+ interface WidgetsSearchDataProvider {
+ /**
+ * Returns the widgets from which the search should return the results.
+ */
+ List<WidgetsListBaseEntry> getWidgets();
+ }
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
index 5a26087..d0aa7a8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt
@@ -376,9 +376,10 @@
Utilities.rotateBounds(rect, 100, 100, 1)
assertEquals(Rect(70, 40, 80, 80), rect)
- rect = Rect(20, 70, 60, 80)
- Utilities.rotateBounds(rect, 100, 100, 2)
- assertEquals(Rect(40, 20, 80, 30), rect)
+ // case removed for b/28435189
+ // rect = Rect(20, 70, 60, 80)
+ // Utilities.rotateBounds(rect, 100, 100, 2)
+ // assertEquals(Rect(40, 20, 80, 30), rect)
rect = Rect(20, 70, 60, 80)
Utilities.rotateBounds(rect, 100, 100, 3)
diff --git a/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
new file mode 100644
index 0000000..e03ee46
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapterTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.accessibility
+
+import android.content.Context
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.util.ActivityContextWrapper
+import java.util.function.Function
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibleDragListenerAdapterTest {
+
+ private lateinit var mockViewGroup: ViewGroup
+ private lateinit var mockChildOne: CellLayout
+ private lateinit var mockChildTwo: CellLayout
+ private lateinit var mDelegateFactory: Function<CellLayout, DragAndDropAccessibilityDelegate>
+ private lateinit var adapter: AccessibleDragListenerAdapter
+ private lateinit var mContext: Context
+
+ @Before
+ fun setup() {
+ mContext = ActivityContextWrapper(getApplicationContext())
+ mockViewGroup = mock(ViewGroup::class.java)
+ mockChildOne = mock(CellLayout::class.java)
+ mockChildTwo = mock(CellLayout::class.java)
+ `when`(mockViewGroup.context).thenReturn(mContext)
+ `when`(mockViewGroup.childCount).thenReturn(2)
+ `when`(mockViewGroup.getChildAt(0)).thenReturn(mockChildOne)
+ `when`(mockViewGroup.getChildAt(1)).thenReturn(mockChildTwo)
+ // Mock Delegate factory
+ mDelegateFactory =
+ mock(Function::class.java) as Function<CellLayout, DragAndDropAccessibilityDelegate>
+ `when`(mDelegateFactory.apply(any()))
+ .thenReturn(mock(DragAndDropAccessibilityDelegate::class.java))
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ }
+
+ @Test
+ fun `onDragStart enables accessible drag for all view children`() {
+ // Create mock view children
+ val mockDragObject = mock(DragObject::class.java)
+ val mockDragOptions = mock(DragOptions::class.java)
+
+ // Action
+ adapter.onDragStart(mockDragObject, mockDragOptions)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+ verify(mockChildTwo).setDragAndDropAccessibilityDelegate(any())
+ }
+
+ @Test
+ fun `onDragEnd removes the accessibility delegate`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onDragEnd()
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+ verify(mockChildTwo).setDragAndDropAccessibilityDelegate(null)
+ }
+
+ @Test
+ fun `onChildViewAdded sets enabled as true for childview`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onChildViewAdded(mockViewGroup, mockChildOne)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(any())
+ }
+
+ @Test
+ fun `onChildViewRemoved sets enabled as false for childview`() {
+ // Action
+ adapter = AccessibleDragListenerAdapter(mockViewGroup, mDelegateFactory)
+ adapter.onChildViewRemoved(mockViewGroup, mockChildOne)
+
+ // Assertion
+ verify(mockChildOne).setDragAndDropAccessibilityDelegate(null)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000..3dd8dbc
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2024 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.pm
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+ private val packageManager = sandboxContext.packageManager
+ private val expectedAppPackage = "expectedAppPackage"
+ private val expectedInstallerPackage = "expectedInstallerPackage"
+ private val mockPackageInstaller: PackageInstaller = mock()
+
+ private lateinit var installSessionHelper: InstallSessionHelper
+ private lateinit var launcherApps: LauncherApps
+
+ @Before
+ fun setup() {
+ whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+ whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+ launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+ installSessionHelper = InstallSessionHelper(sandboxContext)
+ }
+
+ @Test
+ fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.getActiveSessions()
+ // Then
+ assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+ // Given
+ val expectedVerifiedSession =
+ PackageInstaller.SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions)
+ .thenReturn(listOf(expectedVerifiedSession))
+ // When
+ val actualSession =
+ installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+ }
+
+ @Test
+ fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+ // Given
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+ // When
+ val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedSession)
+ }
+
+ @Test
+ fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+ // Given
+ val expectedApplicationInfo =
+ ApplicationInfo().apply {
+ flags = flags or FLAG_INSTALLED
+ enabled = true
+ }
+ doReturn(expectedApplicationInfo)
+ .whenever(launcherApps)
+ .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+ // When
+ val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.allVerifiedSessions
+ // Then
+ assertThat(actualSessions).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `removePromiseIconId removes promiseIconId for given Session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = true
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isFalse()
+ }
+
+ @Test
+ fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var wasPromiseIconAdded = false
+ var actualPromiseIconIds = ""
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+ wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+ actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+ }
+ // Then
+ assertThat(wasPromiseIconAdded).isTrue()
+ assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+ }
+
+ @Test
+ fun `verifySessionInfo is true if can verify given SessionInfo`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000..8204313
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 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.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
+
+ private lateinit var underTest: AllAppsRecyclerViewPool<T>
+ private lateinit var adapter: RecyclerView.Adapter<*>
+
+ @Mock private lateinit var parent: ViewGroup
+ @Mock private lateinit var itemView: View
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = spy(AllAppsRecyclerViewPool())
+ adapter =
+ object : RecyclerView.Adapter<ViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ object : ViewHolder(itemView) {}
+
+ override fun getItemCount() = 0
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+ }
+ underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+ }
+
+ @Test
+ fun preinflate_success() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+ }
+
+ @Test
+ fun preinflate_not_triggered() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 0 }
+
+ awaitTasksCompleted()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_before_runOnMainThread() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+ underTest.clear()
+
+ awaitTasksCompleted()
+ verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ @Test
+ fun preinflate_cancel_after_run() {
+ underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 }
+ assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+ awaitTasksCompleted()
+
+ underTest.clear()
+
+ verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+ assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+ assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
+ Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+ }
+
+ companion object {
+ private const val VIEW_TYPE: Int = 4
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
index 191d284..4217d22 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ActivityContextWrapper.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.util;
+import android.R;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.ContextThemeWrapper;
@@ -39,11 +40,16 @@
private final MyDragLayer mMyDragLayer;
public ActivityContextWrapper(Context base) {
- super(base, android.R.style.Theme_DeviceDefault);
+ this(base, R.style.Theme_DeviceDefault);
+ }
+
+ public ActivityContextWrapper(Context base, int theme) {
+ super(base, theme);
mProfile = InvariantDeviceProfile.INSTANCE.get(base).getDeviceProfile(base).copy(base);
mMyDragLayer = new MyDragLayer(this);
}
+
@Override
public BaseDragLayer getDragLayer() {
return mMyDragLayer;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
new file mode 100644
index 0000000..daba024
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ItemInfoMatcherTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ItemInfoMatcher.ofShortcutKeys
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ItemInfoMatcherTest {
+
+ @Test
+ fun `ofUser returns Predicate for ItemInfo containing given UserHandle`() {
+ val expectedItemInfo = ItemInfo().apply { user = UserHandle(11) }
+ val unexpectedItemInfo = ItemInfo().apply { user = UserHandle(0) }
+ val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+ val predicate = ItemInfoMatcher.ofUser(UserHandle(11))
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `ofComponents returns Predicate for ItemInfo containing target Component and UserHandle`() {
+ // Given
+ val expectedUserHandle = UserHandle(0)
+ val expectedComponentName = ComponentName("expectedPackage", "expectedClass")
+ val expectedItemInfo = spy(ItemInfo())
+ expectedItemInfo.user = expectedUserHandle
+ whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+ val unexpectedComponentName = ComponentName("unexpectedPackage", "unexpectedClass")
+ val unexpectedItemInfo1 = spy(ItemInfo())
+ unexpectedItemInfo1.user = expectedUserHandle
+ whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+ val unexpectedItemInfo2 = spy(ItemInfo())
+ unexpectedItemInfo2.user = UserHandle(10)
+ whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+ val itemInfoStream =
+ listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+ // When
+ val predicate =
+ ItemInfoMatcher.ofComponents(hashSetOf(expectedComponentName), expectedUserHandle)
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `ofPackages returns Predicate for ItemInfo containing UserHandle and target package`() {
+ // Given
+ val expectedUserHandle = UserHandle(0)
+ val expectedPackage = "expectedPackage"
+ val expectedComponentName = ComponentName(expectedPackage, "expectedClass")
+ val expectedItemInfo = spy(ItemInfo())
+ expectedItemInfo.user = expectedUserHandle
+ whenever(expectedItemInfo.targetComponent).thenReturn(expectedComponentName)
+
+ val unexpectedPackage = "unexpectedPackage"
+ val unexpectedComponentName = ComponentName(unexpectedPackage, "unexpectedClass")
+ val unexpectedItemInfo1 = spy(ItemInfo())
+ unexpectedItemInfo1.user = expectedUserHandle
+ whenever(unexpectedItemInfo1.targetComponent).thenReturn(unexpectedComponentName)
+
+ val unexpectedItemInfo2 = spy(ItemInfo())
+ unexpectedItemInfo2.user = UserHandle(10)
+ whenever(unexpectedItemInfo2.targetComponent).thenReturn(expectedComponentName)
+
+ val itemInfoStream =
+ listOf(expectedItemInfo, unexpectedItemInfo1, unexpectedItemInfo2).stream()
+
+ // When
+ val predicate = ItemInfoMatcher.ofPackages(setOf(expectedPackage), expectedUserHandle)
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `ofShortcutKeys returns Predicate for Deep Shortcut Info containing given ShortcutKey`() {
+ // Given
+ val expectedItemInfo = spy(ItemInfo())
+ expectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+ val expectedIntent =
+ Intent().apply {
+ putExtra("shortcut_id", "expectedShortcut")
+ `package` = "expectedPackage"
+ }
+ whenever(expectedItemInfo.intent).thenReturn(expectedIntent)
+
+ val unexpectedIntent =
+ Intent().apply {
+ putExtra("shortcut_id", "unexpectedShortcut")
+ `package` = "unexpectedPackage"
+ }
+ val unexpectedItemInfo = spy(ItemInfo())
+ unexpectedItemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+ whenever(unexpectedItemInfo.intent).thenReturn(unexpectedIntent)
+
+ val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+ val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedItemInfo)
+
+ // When
+ val predicate = ItemInfoMatcher.ofShortcutKeys(setOf(expectedShortcutKey))
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `forFolderMatch returns Predicate to match against children within Folder ItemInfo`() {
+ // Given
+ val expectedItemInfo = spy(FolderInfo())
+ expectedItemInfo.itemType = ITEM_TYPE_FOLDER
+ val expectedIntent =
+ Intent().apply {
+ putExtra("shortcut_id", "expectedShortcut")
+ `package` = "expectedPackage"
+ }
+ val expectedChildInfo = spy(ItemInfo())
+ expectedChildInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+ whenever(expectedChildInfo.intent).thenReturn(expectedIntent)
+ whenever(expectedItemInfo.getContents()).thenReturn(arrayListOf(expectedChildInfo))
+
+ val unexpectedItemInfo = spy(FolderInfo())
+ unexpectedItemInfo.itemType = ITEM_TYPE_FOLDER
+
+ val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+ val expectedShortcutKey = ShortcutKey.fromItemInfo(expectedChildInfo)
+
+ // When
+ val predicate = ItemInfoMatcher.forFolderMatch(ofShortcutKeys(setOf(expectedShortcutKey)))
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `ofItemIds returns Predicate to match ItemInfo that contains given ids`() {
+ // Given
+ val expectedItemInfo = spy(ItemInfo())
+ expectedItemInfo.id = 1
+
+ val unexpectedItemInfo = spy(ItemInfo())
+ unexpectedItemInfo.id = 2
+
+ val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+ // When
+ val expectedIds = IntSet().apply { add(1) }
+ val predicate = ItemInfoMatcher.ofItemIds(expectedIds)
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+
+ @Test
+ fun `ofItems returns Predicate matching against provided ItemInfo`() {
+ // Given
+ val expectedItemInfo = spy(ItemInfo())
+ expectedItemInfo.id = 1
+
+ val unexpectedItemInfo = spy(ItemInfo())
+ unexpectedItemInfo.id = 2
+
+ val itemInfoStream = listOf(expectedItemInfo, unexpectedItemInfo).stream()
+
+ // When
+ val expectedItems = setOf(expectedItemInfo)
+ val predicate = ItemInfoMatcher.ofItems(expectedItems)
+ val actualResults = itemInfoStream.filter(predicate).toList()
+
+ // Then
+ assertThat(actualResults).containsExactly(expectedItemInfo)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
index 093afc9..94bd7c9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/RunnableListTest.kt
@@ -16,17 +16,20 @@
package com.android.launcher3.util
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
@SmallTest
+@RunWith(AndroidJUnit4::class)
class RunnableListTest {
@Mock private lateinit var runnable1: Runnable
@@ -73,7 +76,7 @@
underTest.executeAllAndDestroy()
- verifyZeroInteractions(runnable1)
+ verifyNoMoreInteractions(runnable1)
}
@Test
@@ -107,7 +110,7 @@
underTest.remove(runnable1)
underTest.executeAllAndClear()
- verifyZeroInteractions(runnable1)
+ verifyNoMoreInteractions(runnable1)
verify(runnable2).run()
}
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
new file mode 100644
index 0000000..430aad2
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SCREEN_OFF
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_USER_PRESENT
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ScreenOnTrackerTest {
+
+ @Mock private lateinit var receiver: SimpleBroadcastReceiver
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var listener: ScreenOnTracker.ScreenOnListener
+
+ private lateinit var underTest: ScreenOnTracker
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ underTest = ScreenOnTracker(context, receiver)
+ }
+
+ @Test
+ fun test_default_state() {
+ verify(receiver).register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun close_unregister_receiver() {
+ underTest.close()
+
+ verify(receiver).unregisterReceiverSafely(context)
+ }
+
+ @Test
+ fun add_listener_then_receive_screen_on_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_SCREEN_ON))
+
+ verify(listener).onScreenOnChanged(true)
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun add_listener_then_receive_screen_off_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_SCREEN_OFF))
+
+ verify(listener).onScreenOnChanged(false)
+ assertThat(underTest.isScreenOn).isFalse()
+ }
+
+ @Test
+ fun add_listener_then_receive_user_present_intent_notify_listener() {
+ underTest.addListener(listener)
+
+ underTest.onReceive(Intent(ACTION_USER_PRESENT))
+
+ verify(listener).onUserPresent()
+ assertThat(underTest.isScreenOn).isTrue()
+ }
+
+ @Test
+ fun remove_listener_then_receive_intent_noOp() {
+ underTest.addListener(listener)
+
+ underTest.removeListener(listener)
+
+ underTest.onReceive(Intent(ACTION_USER_PRESENT))
+ verifyNoMoreInteractions(listener)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
new file mode 100644
index 0000000..c43e563
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutUtilTest {
+
+ @Test
+ fun `supportsShortcuts returns true if the item is active and it is an app`() {
+ // A blank workspace item info should be active and should be ITEM_TYPE_APPLICATION
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.supportsShortcuts(itemInfo)
+ // Verify
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `supportsDeepShortcuts returns true if the app is active and an app and widgets are enabled`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.supportsDeepShortcuts(itemInfo)
+ // Verify
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `getShortcutIdIfPinnedShortcut returns null if the item is an app`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.getShortcutIdIfPinnedShortcut(itemInfo)
+ // Verify
+ assertNull(result)
+ }
+
+ @Test
+ fun `getPersonKeysIfPinnedShortcut returns empty string array if item type is an app`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.getPersonKeysIfPinnedShortcut(itemInfo)
+ // Verify
+ assertArrayEquals(Utilities.EMPTY_STRING_ARRAY, result)
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
similarity index 96%
rename from tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
index 1de99c5..d3e27b6 100644
--- a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -23,6 +23,7 @@
import android.os.Looper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
@@ -114,6 +115,7 @@
underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+ getInstrumentation().waitForIdleSync()
verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
verify(completionRunnable).run()
@@ -136,6 +138,7 @@
underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
underTest.unregisterReceiverSafely(context)
+ getInstrumentation().waitForIdleSync()
verify(context).unregisterReceiver(same(underTest))
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
new file mode 100644
index 0000000..612fcd4
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.view.View
+import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.launcher3.util.SystemUiController.UI_STATE_BASE_WINDOW
+import com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SystemUiControllerTest {
+
+ @Mock private lateinit var window: Window
+ @Mock private lateinit var decorView: View
+
+ private lateinit var underTest: SystemUiController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(window.decorView).thenReturn(decorView)
+ underTest = SystemUiController(window)
+ }
+
+ @Test
+ fun test_default_state() {
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_base_window_light() {
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+ val visibility =
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ verify(decorView).systemUiVisibility = eq(visibility)
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+ val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_scrim_view_light() {
+ underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ true)
+
+ verify(decorView).systemUiVisibility =
+ eq(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_base_window_dark() {
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ false)
+
+ verify(decorView, never()).systemUiVisibility = anyInt()
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[$flag, 0, 0, 0, 0]")
+ }
+
+ @Test
+ fun update_state_scrim_view_dark() {
+ underTest.updateUiState(UI_STATE_SCRIM_VIEW, /* isLight= */ false)
+
+ verify(decorView, never()).systemUiVisibility = anyInt()
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(0)
+ val flag = FLAG_DARK_NAV or FLAG_DARK_STATUS
+ assertThat(underTest.toString()).isEqualTo("mStates=[0, $flag, 0, 0, 0]")
+ }
+
+ @Test
+ fun get_base_sysui_visibility() {
+ `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+ }
+
+ @Test
+ fun update_state_base_window_light_with_existing_visibility() {
+ `when`(decorView.systemUiVisibility).thenReturn(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+ underTest.updateUiState(UI_STATE_BASE_WINDOW, /* isLight= */ true)
+
+ val visibility =
+ View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
+ SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ assertThat(underTest.baseSysuiVisibility).isEqualTo(visibility)
+ verify(decorView).systemUiVisibility = eq(visibility)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
new file mode 100644
index 0000000..330c394
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.media.AudioAttributes
+import android.os.SystemClock
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI
+import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC
+import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VibratorWrapperTest {
+
+ @Mock private lateinit var settingsCache: SettingsCache
+ @Mock private lateinit var vibrator: Vibrator
+ @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor<VibrationEffect>
+
+ private lateinit var underTest: VibratorWrapper
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true)
+ `when`(vibrator.hasVibrator()).thenReturn(true)
+ `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true)
+ `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true)
+ `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10))
+
+ underTest = VibratorWrapper(vibrator, settingsCache)
+ }
+
+ @Test
+ fun init_register_onChangeListener() {
+ verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+ }
+
+ @Test
+ fun close_unregister_onChangeListener() {
+ underTest.close()
+
+ verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+ }
+
+ @Test
+ fun vibrate() {
+ underTest.vibrate(OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+ }
+
+ @Test
+ fun vibrate_primitive_id() {
+ underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_with_invalid_primitive_id_use_fallback_effect() {
+ underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+ }
+
+ @Test
+ fun vibrate_for_taskbar_unstash() {
+ underTest.vibrateForTaskbarUnstash()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_bump() {
+ underTest.vibrateForDragBump()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.DRAG_BUMP_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_commit() {
+ underTest.vibrateForDragCommit()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_TICK, VibratorWrapper.DRAG_COMMIT_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_texture() {
+ SystemClock.setCurrentTimeMillis(40000)
+
+ underTest.vibrateForDragTexture()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ assertThat(vibrationEffectCaptor.value).isEqualTo(VibratorWrapper.getDragEffect())
+ }
+
+ @Test
+ fun vibrate_for_drag_texture_within_time_window_noOp() {
+ SystemClock.setCurrentTimeMillis(40000)
+ underTest.vibrateForDragTexture()
+ awaitTasksCompleted()
+ reset(vibrator)
+
+ underTest.vibrateForDragTexture()
+
+ verifyNoMoreInteractions(vibrator)
+ }
+
+ @Test
+ fun haptic_feedback_disabled_no_vibrate() {
+ `when`(vibrator.hasVibrator()).thenReturn(false)
+ underTest = VibratorWrapper(vibrator, settingsCache)
+
+ underTest.vibrate(OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator, never())
+ .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java))
+ }
+
+ @Test
+ fun cancel_vibrate() {
+ underTest.cancelVibrate()
+
+ awaitTasksCompleted()
+ verify(vibrator).cancel()
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
index bad21c9..d82818d 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewCacheTest.kt
@@ -18,6 +18,7 @@
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
import com.android.launcher3.util.ViewCache.CacheEntry
@@ -26,6 +27,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@SmallTest
@RunWith(AndroidJUnit4::class)
class ViewCacheTest {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
new file mode 100644
index 0000000..e535656
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ViewPoolTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.same
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+@UiThreadTest
+class ViewPoolTest {
+
+ @Mock private lateinit var viewParent: ViewGroup
+ @Mock private lateinit var view: ReusableView
+ @Mock private lateinit var inflater: LayoutInflater
+
+ private lateinit var underTest: ViewPool<ReusableView>
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(inflater.cloneInContext(any())).thenReturn(inflater)
+ underTest = ViewPool(inflater, viewParent, LAYOUT_ID, 10, 0)
+ }
+
+ @Test
+ fun get_view_from_empty_pool_inflate_new_view() {
+ underTest.view
+
+ verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+ }
+
+ @Test
+ fun recycle_view() {
+ underTest.recycle(view)
+
+ val returnedView = underTest.view
+
+ verify(view).onRecycle()
+ assertThat(returnedView).isSameInstanceAs(view)
+ verifyNoMoreInteractions(inflater)
+ }
+
+ @Test
+ fun get_view_twice_from_view_pool_with_one_view() {
+ underTest.recycle(view)
+ underTest.view
+ verifyNoMoreInteractions(inflater)
+
+ underTest.view
+
+ verify(inflater).inflate(eq(LAYOUT_ID), same(viewParent), eq(false))
+ }
+
+ companion object {
+ private const val LAYOUT_ID = 1000
+ }
+
+ private inner class ReusableView(context: Context) : View(context), ViewPool.Reusable {
+ override fun onRecycle() {
+ TODO("Not yet implemented")
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000..db77702
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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.widget
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+ @Test
+ fun `Widget view has one background`() {
+ val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+ doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+ assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+ }
+
+ @Test
+ fun `Widget opted out of rounded corner enforcement`() {
+ val mockView = mock(View::class.java)
+
+ doReturn(android.R.id.background).whenever(mockView).id
+ doReturn(true).whenever(mockView).clipToOutline
+
+ assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+ }
+
+ @Test
+ fun `Compute rect based on widget view with background`() {
+ val mockBackgroundView = mock(View::class.java)
+ val mockWidgetView = mock(ViewGroup::class.java)
+ val testRect = Rect(0, 0, 0, 0)
+
+ doReturn(WIDTH).whenever(mockBackgroundView).width
+ doReturn(HEIGHT).whenever(mockBackgroundView).height
+ doReturn(LEFT).whenever(mockBackgroundView).left
+ doReturn(TOP).whenever(mockBackgroundView).top
+ doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+ RoundedCornerEnforcement.computeRoundedRectangle(
+ mockWidgetView,
+ mockBackgroundView,
+ testRect
+ )
+
+ assertEquals(Rect(50, 75, 250, 275), testRect)
+ }
+
+ @Test
+ fun `Compute system radius`() {
+ val mockContext = mock(Context::class.java)
+ val mockRes = mock(Resources::class.java)
+
+ doReturn(mockRes).whenever(mockContext).resources
+ doReturn(RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+ doReturn(LAUNCHER_RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+ assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+ }
+
+ companion object {
+ const val WIDTH = 200
+ const val HEIGHT = 200
+ const val LEFT = 50
+ const val TOP = 75
+
+ const val RADIUS = 8f
+ const val LAUNCHER_RADIUS = 16f
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
new file mode 100644
index 0000000..0a3035a
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.widget.custom
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CustomAppWidgetProviderInfoTest {
+
+ private lateinit var underTest: CustomAppWidgetProviderInfo
+
+ @Before
+ fun setup() {
+ underTest = CustomAppWidgetProviderInfo()
+ underTest.provider = PROVIDER_NAME
+ }
+
+ @Test
+ fun info_to_string() {
+ assertEquals("WidgetProviderInfo($PROVIDER_NAME)", underTest.toString())
+ }
+
+ @Test
+ fun get_label() {
+ underTest.label = " TEST_LABEL"
+ assertEquals(LABEL_NAME, underTest.getLabel(mock(PackageManager::class.java)))
+ }
+
+ companion object {
+ private val PROVIDER_NAME =
+ ComponentName(getInstrumentation().targetContext.packageName, "TEST_PACKAGE")
+ private const val LABEL_NAME = "TEST_LABEL"
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
new file mode 100644
index 0000000..4b5710d
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 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.widget.custom
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.os.Process
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.PluginManagerWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.systemui.plugins.CustomWidgetPlugin
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomWidgetManagerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val context = SandboxModelContext()
+ private lateinit var underTest: CustomWidgetManager
+
+ @Mock private lateinit var pluginManager: PluginManagerWrapper
+ @Mock private lateinit var mockAppWidgetManager: AppWidgetManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.putObject(PluginManagerWrapper.INSTANCE, pluginManager)
+ underTest = CustomWidgetManager(context, mockAppWidgetManager)
+ }
+
+ @After
+ fun tearDown() {
+ underTest.close()
+ }
+
+ @Test
+ fun plugin_manager_added_after_initialization() {
+ verify(pluginManager)
+ .addPluginListener(same(underTest), same(CustomWidgetPlugin::class.java), eq(true))
+ }
+
+ @Test
+ fun close_widget_manager_should_remove_plugin_listener() {
+ underTest.close()
+ verify(pluginManager).removePluginListener(same(underTest))
+ }
+
+ @Test
+ fun on_plugin_connected_no_provider_info() {
+ doReturn(emptyList<LauncherAppWidgetProviderInfo>())
+ .whenever(mockAppWidgetManager)
+ .getInstalledProvidersForProfile(any())
+ val mockPlugin = mock(CustomWidgetPlugin::class.java)
+ underTest.onPluginConnected(mockPlugin, context)
+ assertEquals(0, underTest.plugins.size)
+ }
+
+ @Test
+ fun on_plugin_connected_exist_provider_info() {
+ doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+ .whenever(mockAppWidgetManager)
+ .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+ val mockPlugin = mock(CustomWidgetPlugin::class.java)
+ underTest.onPluginConnected(mockPlugin, context)
+ assertEquals(1, underTest.plugins.size)
+ }
+
+ @Test
+ fun on_plugin_disconnected() {
+ doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+ .whenever(mockAppWidgetManager)
+ .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+ val mockPlugin = mock(CustomWidgetPlugin::class.java)
+ underTest.onPluginConnected(mockPlugin, context)
+ underTest.onPluginDisconnected(mockPlugin)
+ assertEquals(0, underTest.plugins.size)
+ }
+
+ @Test
+ fun on_view_created() {
+ val mockPlugin = mock(CustomWidgetPlugin::class.java)
+ val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+ val mockProviderInfo = mock(CustomAppWidgetProviderInfo::class.java)
+ doReturn(mockProviderInfo).whenever(mockWidgetView).appWidgetInfo
+ mockProviderInfo.provider = TEST_COMPONENT_NAME
+ underTest.plugins.put(TEST_COMPONENT_NAME, mockPlugin)
+ underTest.onViewCreated(mockWidgetView)
+ verify(mockPlugin).onViewCreated(eq(mockWidgetView))
+ }
+
+ @Test
+ fun generate_stream() {
+ assertTrue(underTest.stream().toList().isEmpty())
+ doReturn(listOf(WidgetUtils.createAppWidgetProviderInfo(TEST_COMPONENT_NAME)))
+ .whenever(mockAppWidgetManager)
+ .getInstalledProvidersForProfile(eq(Process.myUserHandle()))
+ val mockPlugin = mock(CustomWidgetPlugin::class.java)
+ underTest.onPluginConnected(mockPlugin, context)
+ assertEquals(1, underTest.stream().toList().size)
+ }
+
+ companion object {
+ private const val TEST_CLASS = "TEST_CLASS"
+ private val TEST_COMPONENT_NAME =
+ ComponentName(getInstrumentation().targetContext.packageName, TEST_CLASS)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
new file mode 100644
index 0000000..5df7caa
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 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.widget.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetsListBaseEntriesBuilderTest {
+ @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+ @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var iconCache: IconCache
+
+ private lateinit var userHandle: UserHandle
+ private lateinit var context: Context
+ private lateinit var testInvariantProfile: InvariantDeviceProfile
+ private lateinit var allWidgets: Map<PackageItemInfo, List<WidgetItem>>
+ private lateinit var underTest: WidgetsListBaseEntriesBuilder
+
+ @Before
+ fun setUp() {
+ userHandle = UserHandle.CURRENT
+ context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ testInvariantProfile = LauncherAppState.getIDP(context)
+
+ doAnswer { invocation: InvocationOnMock ->
+ val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+ componentWithLabel.getComponent().shortClassName
+ }
+ .`when`(iconCache)
+ .getTitleNoCache(any<ComponentWithLabel>())
+ underTest = WidgetsListBaseEntriesBuilder(context)
+
+ allWidgets =
+ mapOf(
+ // app 1
+ packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE) to
+ listOf(
+ createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME),
+ createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+ ),
+ // app 2
+ packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE) to
+ listOf(createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)),
+ // app 3
+ packageItemInfoWithTitle(APP_3_PACKAGE_NAME, APP_3_PACKAGE_TITLE) to
+ listOf(createWidgetItem(APP_3_PACKAGE_NAME, APP_3_PROVIDER_1_CLASS_NAME))
+ )
+ }
+
+ @Test
+ fun widgetsListBaseEntriesBuilder_addsHeaderAndContentEntries_withCorrectSectionName() {
+ val expectedWidgetsCountBySection =
+ listOf(
+ APP_1_EXPECTED_SECTION_NAME to 2,
+ APP_2_EXPECTED_SECTION_NAME to 1,
+ APP_3_EXPECTED_SECTION_NAME to 1
+ )
+
+ val entries = underTest.build(allWidgets)
+
+ assertThat(entries).hasSize(6)
+ val headerEntrySectionAndWidgetSizes =
+ entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ val contentEntrySectionAndWidgetSizes =
+ entries.filterIsInstance<WidgetsListContentEntry>().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ assertThat(headerEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ assertThat(contentEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ }
+
+ @Test
+ fun widgetsListBaseEntriesBuilder_withFilter_addsFilteredHeaderAndContentEntries() {
+ val allowList = listOf(APP_1_PROVIDER_1_CLASS_NAME, APP_3_PROVIDER_1_CLASS_NAME)
+ val expectedWidgetsCountBySection =
+ listOf(
+ APP_1_EXPECTED_SECTION_NAME to 1, // one widget filtered out
+ APP_3_EXPECTED_SECTION_NAME to 1
+ )
+
+ val entries =
+ underTest.build(allWidgets) { w -> allowList.contains(w.componentName.shortClassName) }
+
+ assertThat(entries).hasSize(4) // app 2 filtered out
+ val headerEntrySectionAndWidgetSizes =
+ entries.filterIsInstance<WidgetsListHeaderEntry>().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ val contentEntrySectionAndWidgetSizes =
+ entries.filterIsInstance<WidgetsListContentEntry>().map {
+ it.mTitleSectionName to it.mWidgets.size
+ }
+ assertThat(headerEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ assertThat(contentEntrySectionAndWidgetSizes)
+ .containsExactlyElementsIn(expectedWidgetsCountBySection)
+ }
+
+ private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+ val packageItemInfo = PackageItemInfo(packageName, userHandle)
+ packageItemInfo.title = title
+ return packageItemInfo
+ }
+
+ private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+ val providerInfo =
+ WidgetUtils.createAppWidgetProviderInfo(
+ ComponentName.createRelative(packageName, widgetProviderName)
+ )
+ val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+ return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+ }
+
+ companion object {
+ const val APP_1_PACKAGE_NAME = "com.example.app1"
+ const val APP_1_PACKAGE_TITLE = "App1"
+ const val APP_1_EXPECTED_SECTION_NAME = "A" // for fast popup
+ const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+ const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+ const val APP_2_PACKAGE_NAME = "com.example.app2"
+ const val APP_2_PACKAGE_TITLE = "SomeApp2"
+ const val APP_2_EXPECTED_SECTION_NAME = "S" // for fast popup
+ const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+ const val APP_3_PACKAGE_NAME = "com.example.app3"
+ const val APP_3_PACKAGE_TITLE = "OtherApp3"
+ const val APP_3_EXPECTED_SECTION_NAME = "O" // for fast popup
+ const val APP_3_PROVIDER_1_CLASS_NAME = "app3Provider1"
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
index 0370a6b..24d66a3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -45,12 +45,12 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.search.SearchCallback;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
import org.junit.Before;
import org.junit.Test;
@@ -79,7 +79,7 @@
private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
@Mock
- private PopupDataProvider mDataProvider;
+ private WidgetsSearchDataProvider mDataProvider;
@Mock
private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
@@ -106,7 +106,7 @@
mSimpleWidgetsSearchAlgorithm = MAIN_EXECUTOR.submit(
() -> new SimpleWidgetsSearchAlgorithm(mDataProvider)).get();
- doReturn(Collections.EMPTY_LIST).when(mDataProvider).getAllWidgets();
+ doReturn(Collections.EMPTY_LIST).when(mDataProvider).getWidgets();
}
@Test
@@ -114,7 +114,7 @@
doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
.when(mDataProvider)
- .getAllWidgets();
+ .getWidgets();
assertEquals(List.of(
WidgetsListHeaderEntry.createForSearch(
@@ -135,7 +135,7 @@
doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
mCameraContentEntry))
.when(mDataProvider)
- .getAllWidgets();
+ .getWidgets();
assertEquals(List.of(
WidgetsListHeaderEntry.createForSearch(
@@ -162,7 +162,7 @@
doReturn(List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
mCameraContentEntry, mClockHeaderEntry, mClockContentEntry))
.when(mDataProvider)
- .getAllWidgets();
+ .getWidgets();
mSimpleWidgetsSearchAlgorithm.doSearch("Ca", mSearchCallback);
getInstrumentation().waitForIdleSync();
verify(mSearchCallback).onSearchResult(
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
index eac7f63..de48432 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java
@@ -116,7 +116,8 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = new ActivityContextWrapper(getApplicationContext());
+ mContext = new ActivityContextWrapper(getApplicationContext(),
+ R.style.DynamicColorsBaseLauncherTheme);
when(mAllApps.getContext()).thenReturn(mContext);
when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO);
when(mUserCache.getUserProfiles())
diff --git a/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 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.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+ companion object {
+ val statusList =
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ }
+
+ @Test
+ fun status() {
+ assertStatus(statusList)
+ assertStatus(
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ }
+
+ fun assertStatus(statusList: List<Int>) {
+ var infos = FolderNameInfos()
+ statusList.forEach { infos.setStatus(it) }
+ assert(infos.status() == statusList.sum()) {
+ "There is an overlap on the status constants!"
+ }
+ }
+
+ @Test
+ fun hasPrimary() {
+ assertHasPrimary(
+ createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+ hasPrimary = true
+ )
+ assertHasPrimary(
+ createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+ hasPrimary = false
+ )
+ assertHasPrimary(
+ createNameInfos(
+ listOf(Label(0, "label", 1f)),
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ ),
+ hasPrimary = false
+ )
+ }
+
+ private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+ assert(nameInfos.hasPrimary() == hasPrimary)
+
+ private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+ val infos = FolderNameInfos()
+ labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+ statusList?.forEach { infos.setStatus(it) }
+ return infos
+ }
+
+ @Test
+ fun hasSuggestions() {
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(0, "label", 1f)), null),
+ hasSuggestions = true
+ )
+ assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+ // There is a max of 4 suggestions
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(5, "label", 1f)), null),
+ hasSuggestions = false
+ )
+ assertHasSuggestions(
+ createNameInfos(
+ listOf(
+ Label(0, "label", 1f),
+ Label(1, "label", 1f),
+ Label(2, "label", 1f),
+ Label(3, "label", 1f)
+ ),
+ null
+ ),
+ hasSuggestions = true
+ )
+ }
+
+ private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+ assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+ @Test
+ fun hasContains() {
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label3", -1f),
+ contains = true
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label5", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(null, null),
+ label = Label(-1, "label1", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "lAbel3", 1f),
+ Label(3, "lEbel4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "LaBEl3", -1f),
+ contains = true
+ )
+ }
+
+ private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+ assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
new file mode 100644
index 0000000..e8681dc
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderPagedViewTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 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.folder
+
+import android.graphics.Point
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class TestCase(val maxCountX: Int, val maxCountY: Int, val totalItems: Int)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderPagedViewTest {
+
+ companion object {
+ private fun makeFolderGridOrganizer(testCase: TestCase): FolderGridOrganizer {
+ val folderGridOrganizer = FolderGridOrganizer(testCase.maxCountX, testCase.maxCountY)
+ folderGridOrganizer.setContentSize(testCase.totalItems)
+ return folderGridOrganizer
+ }
+ }
+
+ @Test
+ fun setContentSize() {
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedCountX = 4,
+ expectedCountY = 3
+ )
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 8),
+ expectedCountX = 3,
+ expectedCountY = 3
+ )
+ assertCountXandY(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 3),
+ expectedCountX = 2,
+ expectedCountY = 2
+ )
+ }
+
+ private fun assertCountXandY(testCase: TestCase, expectedCountX: Int, expectedCountY: Int) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ assert(folderGridOrganizer.countX == expectedCountX) {
+ "Error on expected countX $expectedCountX got ${folderGridOrganizer.countX} using test case $testCase"
+ }
+ assert(folderGridOrganizer.countY == expectedCountY) {
+ "Error on expected countY $expectedCountY got ${folderGridOrganizer.countY} using test case $testCase"
+ }
+ }
+
+ @Test
+ fun getPosForRank() {
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(0, 0),
+ rank = 0
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(1, 0),
+ rank = 1
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(3, 0),
+ rank = 3
+ )
+ assertFolderRank(
+ TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22),
+ expectedPos = Point(2, 1),
+ rank = 6
+ )
+ val testCase = TestCase(maxCountX = 4, maxCountY = 3, totalItems = 22)
+ // Rank 16 and 38 should yield the same point since 38 % 12 == 2
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ assertFolderRank(testCase, expectedPos = folderGridOrganizer.getPosForRank(2), rank = 38)
+ }
+
+ private fun assertFolderRank(testCase: TestCase, expectedPos: Point, rank: Int) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ val pos = folderGridOrganizer.getPosForRank(rank)
+ assert(pos == expectedPos) {
+ "Expected pos = $expectedPos doesn't match pos = $pos for the given rank $rank and the give test case $testCase"
+ }
+ }
+
+ @Test
+ fun isItemInPreview() {
+ val folderGridOrganizer = FolderGridOrganizer(5, 8)
+ folderGridOrganizer.setContentSize(ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1)
+ // Very few items
+ for (i in 0..3) {
+ assertItemsInPreview(
+ TestCase(
+ maxCountX = 5,
+ maxCountY = 8,
+ totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+ ),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = i
+ )
+ }
+ for (i in 4..40) {
+ assertItemsInPreview(
+ TestCase(
+ maxCountX = 5,
+ maxCountY = 8,
+ totalItems = ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW - 1
+ ),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = i
+ )
+ }
+ // Full of items
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = 2
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = false,
+ page = 0,
+ rank = 2
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = 5
+ )
+ assertItemsInPreview(
+ TestCase(maxCountX = 5, maxCountY = 8, totalItems = 40),
+ expectedIsInPreview = true,
+ page = 0,
+ rank = 6
+ )
+ }
+
+ private fun assertItemsInPreview(
+ testCase: TestCase,
+ expectedIsInPreview: Boolean,
+ page: Int,
+ rank: Int
+ ) {
+ val folderGridOrganizer = makeFolderGridOrganizer(testCase)
+ val isInPreview = folderGridOrganizer.isItemInPreview(page, rank)
+ assert(isInPreview == expectedIsInPreview) {
+ "Item preview state should be $expectedIsInPreview but got $isInPreview, for page $page and rank $rank, for test case $testCase"
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
index e1daa74..4eb335e 100644
--- a/tests/src/com/android/launcher3/folder/FolderTest.kt
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -18,23 +18,56 @@
import android.content.Context
import android.graphics.Point
+import android.view.KeyEvent
import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import androidx.core.view.isVisible
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.launcher3.Alarm
+import com.android.launcher3.DragSource
import com.android.launcher3.DropTarget.DragObject
import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.OnAlarmListener
+import com.android.launcher3.R
import com.android.launcher3.celllayout.board.FolderPoint
import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.dragndrop.DragView
+import com.android.launcher3.folder.Folder.MIN_CONTENT_DIMEN
+import com.android.launcher3.folder.Folder.ON_EXIT_CLOSE_DELAY
+import com.android.launcher3.folder.Folder.SCROLL_LEFT
+import com.android.launcher3.folder.Folder.SCROLL_NONE
+import com.android.launcher3.folder.Folder.STATE_ANIMATING
+import com.android.launcher3.folder.Folder.STATE_CLOSED
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import java.util.ArrayList
import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
+import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
/** Tests for [Folder] */
@SmallTest
@@ -44,7 +77,7 @@
private val context: Context =
ActivityContextWrapper(ApplicationProvider.getApplicationContext())
private val workspaceBuilder = TestWorkspaceBuilder(context)
- private val folder: Folder = Mockito.spy(Folder(context, null))
+ private val folder: Folder = spy(Folder(context, null))
@After
fun tearDown() {
@@ -60,8 +93,10 @@
folder.mContent = Mockito.mock(FolderPagedView::class.java)
val dragLayout = Mockito.mock(View::class.java)
val dragObject = Mockito.mock(DragObject::class.java)
- assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.deleteFolderOnDropCompleted = false
+
folder.onDropCompleted(dragLayout, dragObject, true)
+
verify(folder, times(1)).replaceFolderWithFinalItem()
assertEquals(folder.deleteFolderOnDropCompleted, false)
}
@@ -74,12 +109,819 @@
folder.mContent = Mockito.mock(FolderPagedView::class.java)
val dragLayout = Mockito.mock(View::class.java)
val dragObject = Mockito.mock(DragObject::class.java)
- assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.deleteFolderOnDropCompleted = false
+
folder.onDropCompleted(dragLayout, dragObject, true)
+
verify(folder, times(0)).replaceFolderWithFinalItem()
assertEquals(folder.deleteFolderOnDropCompleted, false)
}
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_APPLICATION`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPLICATION
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_DEEP_SHORTCUT`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_APP_PAIR`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APP_PAIR
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we do not accept invalid item type ITEM_TYPE_APPWIDGET`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPWIDGET
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertFalse(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we do not accept invalid item type ITEM_TYPE_FOLDER`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_FOLDER
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertFalse(willAcceptResult)
+ }
+
+ @Test
+ fun `We should not animate open if items is null or less than or equal to 1`() {
+ folder.mInfo = Mockito.mock(FolderInfo::class.java)
+ val shouldAnimateOpenResult = folder.shouldAnimateOpen(null)
+
+ assertFalse(shouldAnimateOpenResult)
+ assertFalse(
+ folder.shouldAnimateOpen(arrayListOf<ItemInfo>(Mockito.mock(ItemInfo::class.java)))
+ )
+ }
+
+ @Test
+ fun `We should animate open if items greater than 1`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val shouldAnimateOpenResult = folder.shouldAnimateOpen(folder.mInfo.getContents())
+
+ assertTrue(shouldAnimateOpenResult)
+ }
+
+ @Test
+ fun `Should be true if there is an open folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(Mockito.mock(Folder::class.java))
+
+ assertTrue(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `Should be false if the open folder is this folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(folder)
+
+ assertFalse(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `Should be false if there is not an open folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(null)
+
+ assertFalse(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `If drag is in progress we should set mItemAddedBackToSelfViaIcon to true`() {
+ folder.itemAddedBackToSelfViaIcon = false
+ folder.isDragInProgress = true
+
+ folder.notifyDrop()
+
+ assertTrue(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `If drag is not in progress we should not set mItemAddedBackToSelfViaIcon to true`() {
+ folder.itemAddedBackToSelfViaIcon = false
+ folder.isDragInProgress = false
+
+ folder.notifyDrop()
+
+ assertFalse(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `If launcher dragging is not enabled onLongClick should return true`() {
+ `when`(folder.isLauncherDraggingEnabled).thenReturn(false)
+
+ val onLongClickResult = folder.onLongClick(Mockito.mock(View::class.java))
+
+ assertTrue(onLongClickResult)
+ }
+
+ @Test
+ fun `If launcher dragging is enabled we should return startDrag result`() {
+ `when`(folder.isLauncherDraggingEnabled).thenReturn(true)
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = Mockito.mock(DragOptions::class.java)
+
+ val onLongClickResult = folder.onLongClick(viewMock)
+
+ assertEquals(onLongClickResult, folder.startDrag(viewMock, dragOptions))
+ verify(folder, times(1)).startDrag(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify start drag works as intended when view is instanceof ItemInfo`() {
+ val itemInfo = ItemInfo()
+ itemInfo.rank = 5
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = DragOptions()
+ `when`(viewMock.tag).thenReturn(itemInfo)
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.startDrag(viewMock, dragOptions)
+
+ assertEquals(folder.mEmptyCellRank, 5)
+ assertEquals(folder.currentDragView, viewMock)
+ verify(folder, times(1)).addDragListener(dragOptions)
+ verify(folder, times(1)).callBeginDragShared(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify start drag works as intended when view is not instanceof ItemInfo`() {
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = DragOptions()
+
+ folder.startDrag(viewMock, dragOptions)
+
+ verify(folder, times(0)).addDragListener(dragOptions)
+ verify(folder, times(0)).callBeginDragShared(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify that onDragStart has an effect if dragSource is this folder`() {
+ folder.itemsInvalidated = false
+ folder.isDragInProgress = false
+ folder.itemAddedBackToSelfViaIcon = true
+ folder.currentDragView = Mockito.mock(View::class.java)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ val dragObject = DragObject(context)
+ dragObject.dragInfo = Mockito.mock(ItemInfo::class.java)
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ dragObject.dragSource = folder
+
+ folder.onDragStart(dragObject, DragOptions())
+
+ verify(folder.mContent, times(1)).removeItem(folder.currentDragView)
+ verify(folder.mInfo, times(1)).remove(dragObject.dragInfo, true)
+ assertTrue(folder.itemsInvalidated)
+ assertTrue(folder.isDragInProgress)
+ assertFalse(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `Verify that onDragStart has no effects if dragSource is not this folder`() {
+ folder.itemsInvalidated = false
+ folder.isDragInProgress = false
+ folder.itemAddedBackToSelfViaIcon = true
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragObject = DragObject(context)
+ dragObject.dragSource = Mockito.mock(DragSource::class.java)
+
+ folder.onDragStart(dragObject, DragOptions())
+
+ verify(folder.mContent, times(0)).removeItem(folder.currentDragView)
+ assertFalse(folder.itemsInvalidated)
+ assertFalse(folder.isDragInProgress)
+ assertTrue(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `Verify onDragEnd that we call completeDragExit and set drag in progress false`() {
+ doNothing().`when`(folder).completeDragExit()
+ folder.isExternalDrag = true
+ folder.isDragInProgress = true
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.onDragEnd()
+
+ verify(folder, times(1)).completeDragExit()
+ verify(folder.dragController, times(1)).removeDragListener(folder)
+ assertFalse(folder.isDragInProgress)
+ }
+
+ @Test
+ fun `Verify onDragEnd that we do not call completeDragExit and set drag in progress false`() {
+ folder.isExternalDrag = false
+ folder.isDragInProgress = true
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.onDragEnd()
+
+ verify(folder, times(0)).completeDragExit()
+ verify(folder.dragController, times(1)).removeDragListener(folder)
+ assertFalse(folder.isDragInProgress)
+ }
+
+ @Test
+ fun `startEditingFolderName should set hint to empty and showLabelSuggestions`() {
+ doNothing().`when`(folder).showLabelSuggestions()
+ folder.isEditingName = false
+ folder.folderName = FolderNameEditText(context)
+ folder.folderName.hint = "hello"
+
+ folder.startEditingFolderName()
+
+ verify(folder, times(1)).showLabelSuggestions()
+ assertEquals("", folder.folderName.hint)
+ assertTrue(folder.isEditingName)
+ }
+
+ @Test
+ fun `Ensure we set the title and hint correctly onBackKey when we have a new title`() {
+ val expectedHint = null
+ val expectedTitle = "hello"
+ folder.isEditingName = true
+ folder.folderName = spy(FolderNameEditText(context))
+ folder.folderName.setText(expectedTitle)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ folder.mInfo.title = "world"
+ folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+ folder.onBackKey()
+
+ assertEquals(expectedTitle, folder.mInfo.title)
+ verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+ assertEquals(expectedHint, folder.folderName.hint)
+ assertFalse(folder.isEditingName)
+ verify(folder.folderName, times(1)).clearFocus()
+ }
+
+ @Test
+ fun `Ensure we set the title and hint correctly onBackKey when we do not have a new title`() {
+ val expectedHint = context.getString(R.string.folder_hint_text)
+ val expectedTitle = ""
+ folder.isEditingName = true
+ folder.folderName = spy(FolderNameEditText(context))
+ folder.folderName.setText(expectedTitle)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ folder.mInfo.title = "world"
+ folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+ folder.onBackKey()
+
+ assertEquals(expectedTitle, folder.mInfo.title)
+ verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+ assertEquals(expectedHint, folder.folderName.hint)
+ assertFalse(folder.isEditingName)
+ verify(folder.folderName, times(1)).clearFocus()
+ }
+
+ @Test
+ fun `ensure onEditorAction calls dispatchBackKey when actionId is IME_ACTION_DONE`() {
+ folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+ val result =
+ folder.onEditorAction(
+ Mockito.mock(TextView::class.java),
+ EditorInfo.IME_ACTION_DONE,
+ Mockito.mock(KeyEvent::class.java)
+ )
+
+ assertTrue(result)
+ verify(folder.folderName, times(1)).dispatchBackKey()
+ }
+
+ @Test
+ fun `ensure onEditorAction does not call dispatchBackKey when actionId is not IME_ACTION_DONE`() {
+ folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+ val result =
+ folder.onEditorAction(
+ Mockito.mock(TextView::class.java),
+ EditorInfo.IME_ACTION_NONE,
+ Mockito.mock(KeyEvent::class.java)
+ )
+
+ assertFalse(result)
+ verify(folder.folderName, times(0)).dispatchBackKey()
+ }
+
+ @Test
+ fun `in completeDragExit we close the folder when mIsOpen`() {
+ doNothing().`when`(folder).close(true)
+ folder.setIsOpen(true)
+ folder.rearrangeOnClose = false
+
+ folder.completeDragExit()
+
+ verify(folder, times(1)).close(true)
+ assertTrue(folder.rearrangeOnClose)
+ }
+
+ @Test
+ fun `in completeDragExit we want to rearrange on close when it is animating`() {
+ folder.setIsOpen(false)
+ folder.rearrangeOnClose = false
+ folder.state = STATE_ANIMATING
+
+ folder.completeDragExit()
+
+ verify(folder, times(0)).close(true)
+ assertTrue(folder.rearrangeOnClose)
+ }
+
+ @Test
+ fun `in completeDragExit we want to call rearrangeChildren and clearDragInfo when not open and not animating`() {
+ doNothing().`when`(folder).rearrangeChildren()
+ doNothing().`when`(folder).clearDragInfo()
+ folder.setIsOpen(false)
+ folder.rearrangeOnClose = false
+ folder.state = STATE_CLOSED
+
+ folder.completeDragExit()
+
+ verify(folder, times(0)).close(true)
+ assertFalse(folder.rearrangeOnClose)
+ verify(folder, times(1)).rearrangeChildren()
+ verify(folder, times(1)).clearDragInfo()
+ }
+
+ @Test
+ fun `clearDragInfo should set current drag view to null and isExternalDrag to false`() {
+ folder.currentDragView = Mockito.mock(DragView::class.java)
+ folder.isExternalDrag = true
+
+ folder.clearDragInfo()
+
+ assertNull(folder.currentDragView)
+ assertFalse(folder.isExternalDrag)
+ }
+
+ @Test
+ fun `onDragExit should set alarm if drag is not complete`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ dragObject.dragComplete = false
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.onExitAlarm, times(1)).setOnAlarmListener(folder.mOnExitAlarmListener)
+ verify(folder.onExitAlarm, times(1)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+ }
+
+ @Test
+ fun `onDragExit should not set alarm if drag is complete`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ dragObject.dragComplete = true
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.onExitAlarm, times(0)).setOnAlarmListener(folder.mOnExitAlarmListener)
+ verify(folder.onExitAlarm, times(0)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+ }
+
+ @Test
+ fun `onDragExit should not clear scroll hint if already SCROLL_NONE`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_NONE
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.mContent, times(0)).clearScrollHint()
+ }
+
+ @Test
+ fun `onDragExit should clear scroll hint if not SCROLL_NONE and then set scroll hint to scroll none`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_LEFT
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.mContent, times(1)).clearScrollHint()
+ assertEquals(folder.scrollHintDir, SCROLL_NONE)
+ }
+
+ @Test
+ fun `onDragExit we should cancel reorder pause and hint alarms`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_NONE
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.onScrollHintAlarm = Mockito.mock(Alarm::class.java)
+ folder.scrollPauseAlarm = Mockito.mock(Alarm::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.reorderAlarm, times(1)).cancelAlarm()
+ verify(folder.onScrollHintAlarm, times(1)).cancelAlarm()
+ verify(folder.scrollPauseAlarm, times(1)).cancelAlarm()
+ assertEquals(folder.scrollHintDir, SCROLL_NONE)
+ }
+
+ @Test
+ fun `when calling prepareAccessibilityDrop we should cancel pending reorder alarm and call onAlarm`() {
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+ `when`(folder.reorderAlarm.alarmPending()).thenReturn(true)
+
+ folder.prepareAccessibilityDrop()
+
+ verify(folder.reorderAlarm, times(1)).cancelAlarm()
+ verify(folder.mReorderAlarmListener, times(1)).onAlarm(folder.reorderAlarm)
+ }
+
+ @Test
+ fun `when calling prepareAccessibilityDrop we should not do anything if there is no pending alarm`() {
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+ `when`(folder.reorderAlarm.alarmPending()).thenReturn(false)
+
+ folder.prepareAccessibilityDrop()
+
+ verify(folder.reorderAlarm, times(0)).cancelAlarm()
+ verify(folder.mReorderAlarmListener, times(0)).onAlarm(folder.reorderAlarm)
+ }
+
+ @Test
+ fun `isDropEnabled should be true as long as state is not STATE_ANIMATING`() {
+ folder.state = STATE_CLOSED
+
+ val isDropEnabled = folder.isDropEnabled
+
+ assertTrue(isDropEnabled)
+ }
+
+ @Test
+ fun `isDropEnabled should be false if state is STATE_ANIMATING`() {
+ folder.state = STATE_ANIMATING
+
+ val isDropEnabled = folder.isDropEnabled
+
+ assertFalse(isDropEnabled)
+ }
+
+ @Test
+ fun `getItemCount should return the number of items in the folder`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val itemCount = folder.itemCount
+
+ assertEquals(itemCount, 2)
+ }
+
+ @Test
+ fun `hideItem should set the visibility of the corresponding ItemInfo to invisible`() {
+ val itemInfo = ItemInfo()
+ val view = View(context)
+ view.isVisible = true
+ doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+ folder.hideItem(itemInfo)
+
+ assertFalse(view.isVisible)
+ }
+
+ @Test
+ fun `showItem should set the visibility of the corresponding ItemInfo to visible`() {
+ val itemInfo = ItemInfo()
+ val view = View(context)
+ view.isVisible = false
+ doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+ folder.showItem(itemInfo)
+
+ assertTrue(view.isVisible)
+ }
+
+ @Test
+ fun `onDragEnter should cancel exit alarm and set the scroll area offset to dragRegionWidth divided by two minus xOffset`() {
+ folder.mPrevTargetRank = 1
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val dragView = Mockito.mock(DragView::class.java)
+ dragObject.dragView = dragView
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ `when`(dragObject.dragView.getDragRegionWidth()).thenReturn(100)
+ dragObject.xOffset = 20
+
+ folder.onDragEnter(dragObject)
+
+ verify(folder.onExitAlarm, times(1)).cancelAlarm()
+ assertEquals(-1, folder.mPrevTargetRank)
+ assertEquals(30, folder.scrollAreaOffset)
+ }
+
+ @Test
+ fun `acceptDrop should return true with the correct item type as a parameter`() {
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APP_PAIR
+ dragObject.dragInfo = itemInfo
+
+ val result = folder.acceptDrop(dragObject)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `acceptDrop should return false with the incorrect item type as a parameter`() {
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPWIDGET
+ dragObject.dragInfo = itemInfo
+
+ val result = folder.acceptDrop(dragObject)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `rearrangeChildren should return early if content view are not bound`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.itemsInvalidated = false
+ doReturn(false).whenever(folder.mContent).areViewsBound()
+
+ folder.rearrangeChildren()
+
+ verify(folder.mContent, times(0)).arrangeChildren(folder.iconsInReadingOrder)
+ assertFalse(folder.itemsInvalidated)
+ }
+
+ @Test
+ fun `rearrangeChildren should call arrange children and invalidate items`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.itemsInvalidated = false
+ doReturn(true).whenever(folder.mContent).areViewsBound()
+ val iconsInReadingOrderList = ArrayList<View>()
+ `when`(folder.iconsInReadingOrder).thenReturn(iconsInReadingOrderList)
+ doNothing().`when`(folder.mContent).arrangeChildren(iconsInReadingOrderList)
+
+ folder.rearrangeChildren()
+
+ verify(folder.mContent, times(1)).arrangeChildren(folder.iconsInReadingOrder)
+ assertTrue(folder.itemsInvalidated)
+ }
+
+ @Test
+ fun `getItemCount should return the size of info getContents size`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val itemCount = folder.itemCount
+
+ assertEquals(2, itemCount)
+ }
+
+ @Test
+ fun `replaceFolderWithFinalItem should set mDestroyed to true if we replace folder with final item`() {
+ val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+ folder.mLauncherDelegate = launcherDelegate
+ `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(true)
+
+ folder.replaceFolderWithFinalItem()
+
+ assertTrue(folder.isDestroyed)
+ }
+
+ @Test
+ fun `replaceFolderWithFinalItem should set mDestroyed to false if we do not replace folder with final item`() {
+ val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+ folder.mLauncherDelegate = launcherDelegate
+ `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(false)
+
+ folder.replaceFolderWithFinalItem()
+
+ assertFalse(folder.isDestroyed)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return maxContentAreaHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(100)
+ `when`(folder.maxContentAreaHeight).thenReturn(50)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(50, height)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return desiredHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(50)
+ `when`(folder.maxContentAreaHeight).thenReturn(100)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(50, height)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return MIN_CONTENT_DIMEN`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(1)
+ `when`(folder.maxContentAreaHeight).thenReturn(2)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(MIN_CONTENT_DIMEN, height)
+ }
+
+ @Test
+ fun `getContentAreaWidth should return desired width`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(50)
+
+ val width = folder.contentAreaWidth
+
+ assertEquals(50, width)
+ }
+
+ @Test
+ fun `getContentAreaWidth should return MIN_CONTENT_DIMEN`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(1)
+
+ val width = folder.contentAreaWidth
+
+ assertEquals(MIN_CONTENT_DIMEN, width)
+ }
+
+ @Test
+ fun `getFolderWidth should return padding left plus padding right plus desired width`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(1)
+ `when`(folder.paddingLeft).thenReturn(10)
+ `when`(folder.paddingRight).thenReturn(10)
+
+ val width = folder.folderWidth
+
+ assertEquals(21, width)
+ }
+
+ @Test
+ fun `getFolderHeight with no params should return getFolderHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.contentAreaHeight).thenReturn(100)
+ `when`(folder.getFolderHeight(folder.contentAreaHeight)).thenReturn(120)
+
+ val height = folder.folderHeight
+
+ assertEquals(120, height)
+ }
+
+ @Test
+ fun `getFolderWidth with contentAreaHeight should return padding top plus padding bottom plus contentAreaHeight plus footer height`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.footerHeight).thenReturn(100)
+ `when`(folder.paddingTop).thenReturn(10)
+ `when`(folder.paddingBottom).thenReturn(10)
+
+ val height = folder.getFolderHeight(100)
+
+ assertEquals(220, height)
+ }
+
+ @Test
+ fun `onRemove should call removeItem with the correct views`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ verify(folder.mContent, times(1)).removeItem(view1)
+ verify(folder.mContent, times(1)).removeItem(view2)
+ }
+
+ @Test
+ fun `onRemove should set mRearrangeOnClose to true and not call rearrangeChildren if animating`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.state = STATE_ANIMATING
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ assertTrue(folder.rearrangeOnClose)
+ verify(folder, times(0)).rearrangeChildren()
+ }
+
+ @Test
+ fun `onRemove should set not change mRearrangeOnClose and not call rearrangeChildren if not animating`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.state = STATE_CLOSED
+ folder.rearrangeOnClose = false
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ assertFalse(folder.rearrangeOnClose)
+ verify(folder, times(1)).rearrangeChildren()
+ }
+
+ @Test
+ fun `onRemove should call close if mIsOpen is true and item count is less than or equal to one`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(1).whenever(folder).itemCount
+ folder.setIsOpen(true)
+ doNothing().`when`(folder).close(true)
+
+ folder.onRemove(items)
+
+ verify(folder, times(1)).close(true)
+ }
+
+ @Test
+ fun `onRemove should call replaceFolderWithFinalItem if mIsOpen is false and item count is less than or equal to one`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(1).whenever(folder).itemCount
+ folder.setIsOpen(false)
+
+ folder.onRemove(items)
+
+ verify(folder, times(1)).replaceFolderWithFinalItem()
+ }
+
companion object {
const val TWO_ICON_FOLDER_TYPE = 'A'
}
diff --git a/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+ private val context: Context =
+ ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+ pageIndicatorDots.mIsTwoPanels = true
+
+ pageIndicatorDots.setActiveMarker(5)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setMarkersCount(3)
+
+ assertEquals(3, pageIndicatorDots.numPages)
+ }
+
+ @Test
+ fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ pageIndicatorDots.setMarkersCount(2)
+
+ assertEquals(1, pageIndicatorDots.activePage)
+ assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+ }
+}
diff --git a/tests/src/com/android/launcher3/pm/UserCacheTest.kt b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
new file mode 100644
index 0000000..b21219e
--- /dev/null
+++ b/tests/src/com/android/launcher3/pm/UserCacheTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.pm
+
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UserCacheTest {
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = launcherModelHelper.sandboxContext
+ private lateinit var userCache: UserCache
+
+ @Before
+ fun setup() {
+ userCache = UserCache.getInstance(sandboxContext)
+ }
+
+ @After
+ fun teardown() {
+ launcherModelHelper.destroy()
+ }
+
+ @Test
+ fun `getBadgeDrawable only returns a UserBadgeDrawable given a user in the cache`() {
+ // Given
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(myUserHandle(), expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualDrawable = UserCache.getBadgeDrawable(sandboxContext, myUserHandle())
+ val unexpectedDrawable = UserCache.getBadgeDrawable(sandboxContext, UserHandle(66))
+ // Then
+ assertThat(actualDrawable).isNotNull()
+ assertThat(unexpectedDrawable).isNull()
+ }
+
+ @Test
+ fun `getPreInstallApps returns list of pre installed apps given a user`() {
+ // Given
+ val expectedApps = listOf("Google")
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToPreInstallCache(myUserHandle(), expectedApps)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualApps = userCache.getPreInstallApps(myUserHandle())
+ // Then
+ assertThat(actualApps).isEqualTo(expectedApps)
+ }
+
+ @Test
+ fun `getUserProfiles returns copy of UserCache profiles`() {
+ // Given
+ val expectedProfiles = listOf(myUserHandle())
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(myUserHandle(), expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualProfiles = userCache.userProfiles
+ // Then
+ assertThat(actualProfiles).isEqualTo(expectedProfiles)
+ }
+
+ @Test
+ fun `getUserForSerialNumber returns user key matching given entry serial number`() {
+ // Given
+ val expectedSerial = 42L
+ val expectedProfile = UserHandle(42)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_MAIN, expectedSerial)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualProfile = userCache.getUserForSerialNumber(expectedSerial)
+ // Then
+ assertThat(actualProfile).isEqualTo(expectedProfile)
+ }
+
+ @Test
+ fun `getUserInfo returns cached UserIconInfo given user key`() {
+ // Given
+ val expectedProfile = UserHandle(1)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualIconInfo = userCache.getUserInfo(expectedProfile)
+ // Then
+ assertThat(actualIconInfo).isEqualTo(expectedIconInfo)
+ }
+
+ @Test
+ fun `getSerialNumberForUser returns cached UserIconInfo serial number given user key`() {
+ // Given
+ val expectedSerial = 42L
+ val expectedProfile = UserHandle(1)
+ val expectedIconInfo = UserIconInfo(myUserHandle(), UserIconInfo.TYPE_WORK, expectedSerial)
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ userCache.putToCache(expectedProfile, expectedIconInfo)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // When
+ val actualSerial = userCache.getSerialNumberForUser(expectedProfile)
+ // Then
+ assertThat(actualSerial).isEqualTo(expectedSerial)
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 25eae44..ad37f7b 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -98,6 +98,7 @@
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -2398,6 +2399,16 @@
disableSensorRotation();
final Integer initialPid = getPid();
final LogEventChecker eventChecker = new LogEventChecker(this);
+ eventChecker.setLogExclusionRule(event -> {
+ Matcher matcher = Pattern.compile("KeyEvent.*flags=0x([0-9a-fA-F]+)").matcher(event);
+ if (matcher.find()) {
+ int keyEventFlags = Integer.parseInt(matcher.group(1), 16);
+ // ignore KeyEvents with FLAG_CANCELED
+ return (keyEventFlags & KeyEvent.FLAG_CANCELED) != 0;
+ }
+ return false;
+ });
+
if (eventChecker.start()) mEventChecker = eventChecker;
return () -> {
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 055a357..3a49160 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -35,6 +35,8 @@
// Map from an event sequence name to an ordered list of expected events in that sequence.
private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
+ private LogExclusionRule mLogExclusionRule = null;
+
LogEventChecker(LauncherInstrumentation launcher) {
mLauncher = launcher;
}
@@ -48,6 +50,10 @@
mExpectedEvents.add(sequence, pattern);
}
+ void setLogExclusionRule(LogExclusionRule logExclusionRule) {
+ mLogExclusionRule = logExclusionRule;
+ }
+
// Waits for the expected number of events and returns them.
private ListMap<String> finishSync(long waitForExpectedCountMs) {
final long startTime = SystemClock.uptimeMillis();
@@ -74,7 +80,9 @@
final ListMap<String> eventSequences = new ListMap<>();
for (String rawEvent : rawEvents) {
final String[] split = rawEvent.split("/");
- eventSequences.add(split[0], split[1]);
+ if (mLogExclusionRule == null || !mLogExclusionRule.shouldExclude(split[1])) {
+ eventSequences.add(split[0], split[1]);
+ }
}
return eventSequences;
}
@@ -175,4 +183,8 @@
return list;
}
}
+
+ interface LogExclusionRule {
+ boolean shouldExclude(String event);
+ }
}