Snap for 9586404 from daf057472c to udc-release

Change-Id: I39bb360f87525b3e7fd74d6b0d33a7d8d73d2e16
This commit is contained in:
Android Build Coastguard Worker
2023-02-10 02:26:30 +00:00
42 changed files with 1417 additions and 163 deletions
+1
View File
@@ -40,6 +40,7 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- for rotating surface by arbitrary degree -->
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
+21
View File
@@ -0,0 +1,21 @@
<?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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mock_app_icon" />
<corners android:radius="@dimen/gesture_tutorial_hotseat_icon_corner_radius" />
</shape>
@@ -0,0 +1,21 @@
<?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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/gesture_home_tutorial_background" />
<corners android:radius="@dimen/gesture_tutorial_hotseat_icon_corner_radius" />
</shape>
@@ -0,0 +1,21 @@
<?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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mock_search_bar" />
<corners android:radius="@dimen/gesture_tutorial_hotseat_search_corner_radius" />
</shape>
+21
View File
@@ -0,0 +1,21 @@
<?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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/gesture_tutorial_fake_previous_task_view_color" />
<corners android:radius="@dimen/gesture_tutorial_small_task_view_corner_radius" />
</shape>
@@ -24,54 +24,50 @@
android:paddingStart="56dp"
android:paddingEnd="56dp">
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_2"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_3"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_2"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_4"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_3"
app:layout_constraintBottom_toBottomOf="parent"
@@ -22,84 +22,77 @@
android:paddingStart="@dimen/gesture_tutorial_hotseat_padding_start_end"
android:paddingEnd="@dimen/gesture_tutorial_hotseat_padding_start_end">
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_search_bar"
android:layout_width="200dp"
android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
android:background="@drawable/hotseat_search_bar"
android:clipToOutline="true"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
app:cardBackgroundColor="@color/mock_search_bar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_1"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_search_bar"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_5"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_5"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -57,33 +57,34 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/top_task_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="invisible"
android:layout_marginBottom="@dimen/gesture_tutorial_multi_row_task_view_spacing"
android:background="@drawable/top_task_view"
android:clipToOutline="true"
android:visibility="invisible"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
app:layout_constraintVertical_chainStyle="spread"
app:layout_constraintTop_toTopOf="@id/full_task_view"
app:layout_constraintBottom_toTopOf="@id/bottom_task_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintTop_toTopOf="@id/full_task_view" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/bottom_task_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@drawable/top_task_view"
android:clipToOutline="true"
android:visibility="invisible"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
app:layout_constraintTop_toBottomOf="@id/top_task_view"
app:layout_constraintBottom_toBottomOf="@id/full_task_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintTop_toBottomOf="@id/top_task_view" />
</com.android.quickstep.interaction.AnimatedTaskView>
@@ -8,67 +8,62 @@
android:paddingStart="26dp"
android:paddingEnd="26dp">
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:layout_width="0dp"
android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_search_bar"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
app:cardBackgroundColor="@color/mock_search_bar"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
@@ -22,84 +22,78 @@
android:paddingStart="@dimen/gesture_tutorial_hotseat_padding_start_end"
android:paddingEnd="@dimen/gesture_tutorial_hotseat_padding_start_end">
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_search_bar"
android:layout_width="0dp"
android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
android:background="@drawable/hotseat_search_bar"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
app:cardBackgroundColor="@color/mock_search_bar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_5"/>
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_5"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon"
android:clipToOutline="true"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardBackgroundColor="@color/mock_app_icon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/hotseat_search_bar"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_4"
@@ -57,29 +57,29 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/top_task_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/gesture_tutorial_multi_row_task_view_spacing"
android:background="@drawable/top_task_view"
android:clipToOutline="true"
android:visibility="invisible"
app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
app:cardElevation="0dp"
app:layout_constraintBottom_toTopOf="@id/bottom_task_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/full_task_view"
app:layout_constraintVertical_chainStyle="spread" />
app:layout_constraintTop_toTopOf="@id/full_task_view" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/bottom_task_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@drawable/top_task_view"
android:clipToOutline="true"
android:visibility="invisible"
app:cardCornerRadius="@dimen/gesture_tutorial_small_task_view_corner_radius"
app:cardElevation="0dp"
app:layout_constraintBottom_toBottomOf="@id/full_task_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -99,13 +99,13 @@
android:background="@drawable/gesture_tutorial_ripple" />
<include
layout="@layout/gesture_tutorial_tablet_mock_taskbar"
android:id="@+id/gesture_tutorial_fake_taskbar_view"
android:layout_width="match_parent"
android:layout_height="@dimen/gesture_tutorial_mock_taskbar_height"
layout="@layout/gesture_tutorial_tablet_mock_taskbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" />
android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/gesture_tutorial_taskbar_margin_bottom" />
<ImageView
android:id="@+id/gesture_tutorial_edge_gesture_video"
@@ -21,70 +21,64 @@
android:layout_height="wrap_content"
android:paddingBottom="70dp"
android:paddingStart="26dp"
android:paddingEnd="26dp"
android:background="@color/gesture_home_tutorial_swipe_up_rect">
android:paddingEnd="26dp">
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:cardBackgroundColor="@color/gesture_home_tutorial_background"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardElevation="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_2"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:cardBackgroundColor="@color/gesture_home_tutorial_background"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardElevation="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_3"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_1"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:cardBackgroundColor="@color/gesture_home_tutorial_background"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardElevation="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/hotseat_icon_4"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_2"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:cardBackgroundColor="@color/gesture_home_tutorial_background"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_icon_corner_radius"
app:cardElevation="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/hotseat_icon_3"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
<View
android:layout_width="0dp"
android:layout_height="@dimen/gesture_tutorial_hotseat_search_height"
android:layout_marginTop="@dimen/gesture_tutorial_hotseat_icon_search_margin"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:cardBackgroundColor="@color/gesture_home_tutorial_background"
app:cardCornerRadius="@dimen/gesture_tutorial_hotseat_search_corner_radius"
app:cardElevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1" />
@@ -39,6 +39,7 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.taskbar.LauncherTaskbarUIController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -62,6 +63,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
private final VibratorWrapper mVibratorWrapper;
private final RecentsView mRecentsView;
private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
@@ -82,6 +84,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
mRecentsView = l.getOverviewPanel();
mMotionPauseDetector = new MotionPauseDetector(l);
mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
}
@Override
@@ -188,6 +191,11 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
// need to manually set the duration to a reasonable value.
animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
}
if (FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() &&
((mFromState == NORMAL && mToState == ALL_APPS)
|| (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
mVibratorWrapper.vibrateForDragBump();
}
}
private void onMotionPauseDetected() {
@@ -30,7 +30,6 @@ import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
@@ -46,8 +45,8 @@ import java.util.ArrayList;
public class AnimatedTaskView extends ConstraintLayout {
private View mFullTaskView;
private CardView mTopTaskView;
private CardView mBottomTaskView;
private View mTopTaskView;
private View mBottomTaskView;
private ViewOutlineProvider mTaskViewOutlineProvider = null;
private final Rect mTaskViewAnimatedRect = new Rect();
@@ -185,8 +184,6 @@ public class AnimatedTaskView extends ConstraintLayout {
void setFakeTaskViewFillColor(@ColorInt int colorResId) {
mFullTaskView.setBackgroundColor(colorResId);
mTopTaskView.setCardBackgroundColor(colorResId);
mBottomTaskView.setCardBackgroundColor(colorResId);
}
@Override
@@ -61,10 +61,10 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll
@Override
protected int getMockAppTaskLayoutResId() {
return mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_webpage
: ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.layout.swipe_up_gesture_tutorial_shape
return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.layout.swipe_up_gesture_tutorial_shape
: mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_webpage
: R.layout.gesture_tutorial_mock_webpage;
}
@@ -154,7 +154,6 @@ abstract class TutorialController implements BackGestureAttemptCallback,
mFeedbackTitleView.setText(getIntroductionTitle());
mFeedbackSubtitleView.setText(getIntroductionSubtitle());
mSkipButton.setVisibility(GONE);
}
mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
@@ -355,6 +355,12 @@ public class GroupedTaskView extends TaskView {
mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha);
}
@Override
protected void refreshTaskThumbnailSplash() {
super.refreshTaskThumbnailSplash();
mSnapshotView2.refreshSplashView();
}
/**
* Sets visibility for thumbnails and associated elements (DWB banners).
* IconView is unaffected.
@@ -92,6 +92,7 @@ public class LauncherRecentsView extends RecentsView<QuickstepLauncher, Launcher
@Override
public void onTaskIconChanged(int taskId) {
super.onTaskIconChanged(taskId);
// If Launcher needs to return to split select state, do it now, after the icon has updated.
if (mActivity.hasPendingSplitSelectInfo()) {
PendingSplitSelectInfo recoveryData = mActivity.getPendingSplitSelectInfo();
@@ -956,6 +956,14 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
}
}
@Override
public void onTaskIconChanged(int taskId) {
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView != null) {
taskView.refreshTaskThumbnailSplash();
}
}
/**
* Update the thumbnail of the task.
* @param refreshNow Refresh immediately if it's true.
@@ -342,10 +342,11 @@ public class TaskThumbnailView extends View {
// Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios.
if (shouldShowSplashView()) {
// Always draw background for hiding inconsistencies, even if splash view is not yet
// loaded (which can happen as task icons are loaded asynchronously in the background)
canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius,
cornerRadius, mSplashBackgroundPaint);
if (mSplashView != null) {
canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius,
cornerRadius, mSplashBackgroundPaint);
mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1);
mSplashView.draw(canvas);
}
@@ -375,6 +376,13 @@ public class TaskThumbnailView extends View {
|| isThumbnailRotationDifferentFromTask();
}
protected void refreshSplashView() {
if (mTask != null) {
updateSplashView(mTask.icon);
invalidate();
}
}
private void updateSplashView(Drawable icon) {
if (icon == null || icon.getConstantState() == null) {
mSplashViewDrawable = null;
@@ -1215,6 +1215,10 @@ public class TaskView extends FrameLayout implements Reusable {
mSnapshotView.setSplashAlpha(mTaskThumbnailSplashAlpha);
}
protected void refreshTaskThumbnailSplash() {
mSnapshotView.refreshSplashView();
}
private void setSplitSelectTranslationX(float x) {
mSplitSelectTranslationX = x;
applyTranslationX();
@@ -19,7 +19,7 @@ import android.graphics.Rect
import android.graphics.RectF
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.DeviceProfileBaseTest
import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
import com.android.quickstep.views.TaskView.FullscreenDrawParams
@@ -36,7 +36,7 @@ import org.mockito.Mockito.mock
/** Test for FullscreenDrawParams class. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class FullscreenDrawParamsTest : DeviceProfileBaseTest() {
class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() {
private val TASK_SCALE = 0.7f
private var mThumbnailData: ThumbnailData = mock(ThumbnailData::class.java)
@@ -18,7 +18,7 @@ package com.android.quickstep
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.DeviceProfileBaseTest
import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.WindowBounds
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -26,7 +26,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
/**
* This is a case when after setting the hotseat, the space needs to be recalculated but it
@@ -193,7 +193,7 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
mLauncher.goHome().switchToOverview().getCurrentTask()
.tapMenu()
.tapSplitMenuItem()
.getTestActivityTask(2)
.getCurrentTask()
.open();
}
+15
View File
@@ -224,6 +224,21 @@
<attr name="alignOnIcon" format="boolean" />
</declare-styleable>
<!-- Responsive grids attributes -->
<declare-styleable name="WorkspaceSpec">
<attr name="specType" format="integer">
<enum name="height" value="0" />
<enum name="width" value="1" />
</attr>
<attr name="maxAvailableSize" format="dimension" />
</declare-styleable>
<declare-styleable name="SpecSize">
<attr name="fixedSize" format="dimension" />
<attr name="ofAvailableSpace" format="float" />
<attr name="ofRemainderSpace" format="float" />
</declare-styleable>
<declare-styleable name="ProfileDisplayOption">
<attr name="name" />
<attr name="minWidthDps" format="float" />
+3 -2
View File
@@ -501,7 +501,7 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
}
private boolean isTwoPanelEnabled() {
return mLauncher.mDeviceProfile.isTwoPanels;
return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels;
}
@Override
@@ -663,8 +663,9 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
// Inflate the cell layout, but do not add it automatically so that we can get the newly
// created CellLayout.
DeviceProfile dp = mLauncher.getDeviceProfile();
CellLayout newScreen;
if (FOLDABLE_SINGLE_PAGE.get() && isTwoPanelEnabled()) {
if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) {
newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
R.layout.workspace_screen_foldable, this, false /* attachToRoot */);
} else {
@@ -32,6 +32,7 @@ import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.util.FloatProperty;
import android.view.HapticFeedbackConstants;
import android.view.View;
@@ -47,17 +48,21 @@ import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.views.ScrimView;
/**
@@ -78,6 +83,8 @@ public class AllAppsTransitionController
private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f;
private static final float SWIPE_DRAG_COMMIT_THRESHOLD =
1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -181,6 +188,7 @@ public class AllAppsTransitionController
private boolean mIsTablet;
private boolean mHasScaleEffect;
private final VibratorWrapper mVibratorWrapper;
public AllAppsTransitionController(Launcher l) {
mLauncher = l;
@@ -193,6 +201,7 @@ public class AllAppsTransitionController
setShiftRange(dp.allAppsShiftRange);
mLauncher.addOnDeviceProfileChangeListener(this);
mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
}
public float getShiftRange() {
@@ -311,6 +320,11 @@ public class AllAppsTransitionController
/**
* Creates an animation which updates the vertical transition progress and updates all the
* dependent UI using various animation events
*
* This method also dictates where along the progress the haptics should be played. As the user
* scrolls up from workspace or down from AllApps, a drag haptic is being played until the
* commit point where it plays a commit haptic. Where we play the haptics differs when going
* from workspace -> allApps and vice versa.
*/
@Override
public void setStateWithAnimation(LauncherState toState,
@@ -339,6 +353,20 @@ public class AllAppsTransitionController
});
}
if(FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && config.userControlled
&& Utilities.ATLEAST_S) {
if (toState == ALL_APPS) {
builder.addOnFrameListener(
new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
SWIPE_DRAG_COMMIT_THRESHOLD, 1));
} else {
builder.addOnFrameListener(
new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
0, SWIPE_DRAG_COMMIT_THRESHOLD));
}
builder.addEndListener(mVibratorWrapper::cancelVibrate);
}
float targetProgress = toState.getVerticalProgress(mLauncher);
if (Float.compare(mProgress, targetProgress) == 0) {
setAlphas(toState, config, builder);
@@ -356,7 +384,7 @@ public class AllAppsTransitionController
setAlphas(toState, config, builder);
if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && !(Utilities.ATLEAST_S)) {
mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
}
@@ -494,4 +522,45 @@ public class AllAppsTransitionController
}
}
}
/**
* This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
* threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
* as it gives an accurate progress that dictates where the vibrator should vibrate.
* Note: once the user begins a gesture and does the commit haptic, there should not be anymore
* haptics played for that gesture.
*/
private static class VibrationAnimatorUpdateListener implements
ValueAnimator.AnimatorUpdateListener {
private final VibratorWrapper mVibratorWrapper;
private final AllAppsTransitionController mController;
private final float mStartThreshold;
private final float mEndThreshold;
private boolean mHasCommitted;
VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
VibratorWrapper vibratorWrapper, float startThreshold,
float endThreshold) {
mController = controller;
mVibratorWrapper = vibratorWrapper;
mStartThreshold = startThreshold;
mEndThreshold = endThreshold;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (mHasCommitted) {
return;
}
float currentProgress =
AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
mVibratorWrapper.vibrateForDragTexture();
} else if (!(currentProgress == 0 || currentProgress == 1)) {
// This check guards against committing at the location of the start of the gesture
mVibratorWrapper.vibrateForDragCommit();
mHasCommitted = true;
}
}
}
}
@@ -374,6 +374,8 @@ public final class FeatureFlags {
"ENABLE_LAUNCH_FROM_STAGED_APP", true,
"Enable the ability to tap a staged app during split select to launch it in full screen"
);
public static final BooleanFlag ENABLE_HAPTICS_ALL_APPS = getDebugFlag(
"ENABLE_HAPTICS_ALL_APPS", false, "Enables haptics opening/closing All apps");
public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(
"ENABLE_FORCED_MONO_ICON", false,
@@ -129,10 +129,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL =
Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, 1f),
ALL_APPS_STATE_TRANSITION_MANUAL, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
// --------
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.util
import android.content.Context
import android.content.res.TypedArray
import android.content.res.XmlResourceParser
import android.util.AttributeSet
import kotlin.IntArray
/**
* This class is a helper that can be subclassed in tests to provide a way to parse attributes
* correctly.
*/
open class ResourceHelper(private val context: Context, private val specsFileId: Int) {
open fun getXml(): XmlResourceParser {
return context.resources.getXml(specsFileId)
}
open fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
return context.obtainStyledAttributes(attrs, styleId)
}
}
@@ -17,7 +17,6 @@ package com.android.launcher3.util;
import static android.os.VibrationEffect.createPredefined;
import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
@@ -28,12 +27,17 @@ import android.content.Context;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.Settings;
import androidx.annotation.Nullable;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.anim.PendingAnimation;
import java.util.function.Consumer;
/**
* Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
@@ -52,6 +56,21 @@ public class VibratorWrapper {
public static final VibrationEffect EFFECT_CLICK =
createPredefined(VibrationEffect.EFFECT_CLICK);
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;
@Nullable
private final VibrationEffect mDragEffect;
@Nullable
private final VibrationEffect mCommitEffect;
@Nullable
private final VibrationEffect mBumpEffect;
private long mLastDragTime;
private final int mThresholdUntilNextDragCallMillis;
/**
* Haptic when entering overview.
*/
@@ -62,7 +81,7 @@ public class VibratorWrapper {
private boolean mIsHapticFeedbackEnabled;
public VibratorWrapper(Context context) {
private VibratorWrapper(Context context) {
mVibrator = context.getSystemService(Vibrator.class);
mHasVibrator = mVibrator.hasVibrator();
if (mHasVibrator) {
@@ -75,12 +94,88 @@ public class VibratorWrapper {
}
};
resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
false /* notifyForDescendents */, observer);
false /* notifyForDescendants */, observer);
} else {
mIsHapticFeedbackEnabled = false;
}
if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) {
// 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(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
}
mDragEffect = dragEffect.compose();
mCommitEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
mBumpEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose();
int primitiveDuration = mVibrator.getPrimitiveDurations(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0];
mThresholdUntilNextDragCallMillis =
DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100;
} else {
mDragEffect = null;
mCommitEffect = null;
mBumpEffect = null;
mThresholdUntilNextDragCallMillis = 0;
}
}
/**
* This is called when the user swipes to/from all apps. This is meant to be used in between
* long animation progresses so that it gives a dragging texture effect. For a better
* experience, this should be used in combination with vibrateForDragCommit().
*/
public void vibrateForDragTexture() {
if (mDragEffect == null) {
return;
}
long currentTime = SystemClock.elapsedRealtime();
long elapsedTimeSinceDrag = currentTime - mLastDragTime;
if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) {
vibrate(mDragEffect);
mLastDragTime = currentTime;
}
}
/**
* This is used when user reaches the commit threshold when swiping to/from from all apps.
*/
public void vibrateForDragCommit() {
if (mCommitEffect != null) {
vibrate(mCommitEffect);
}
// resetting dragTexture timestamp to be able to play dragTexture again
mLastDragTime = 0;
}
/**
* The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
* FLING going to/from all apps. Client can just call this method elsewhere just for the
* effect.
*/
public void vibrateForDragBump() {
if (mBumpEffect != null) {
vibrate(mBumpEffect);
}
}
/**
* This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For
* example, when no animation is happening but a vibrator happens to be vibrating still. Need
* boolean parameter for {@link PendingAnimation#addEndListener(Consumer)}.
*/
public void cancelVibrate(boolean unused) {
UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
// reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
mLastDragTime = 0;
}
private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
}
@@ -0,0 +1,252 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.workspace
import android.content.res.TypedArray
import android.content.res.XmlResourceParser
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.util.Xml
import com.android.launcher3.R
import com.android.launcher3.util.ResourceHelper
import java.io.IOException
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
private const val TAG = "WorkspaceSpecs"
class WorkspaceSpecs(resourceHelper: ResourceHelper) {
object XmlTags {
const val WORKSPACE_SPECS = "workspaceSpecs"
const val WORKSPACE_SPEC = "workspaceSpec"
const val START_PADDING = "startPadding"
const val END_PADDING = "endPadding"
const val GUTTER = "gutter"
const val CELL_SIZE = "cellSize"
}
val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
init {
try {
val parser: XmlResourceParser = resourceHelper.getXml()
val depth = parser.depth
var type: Int
while (
(parser.next().also { type = it } != XmlPullParser.END_TAG ||
parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
) {
if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) {
val displayDepth = parser.depth
while (
(parser.next().also { type = it } != XmlPullParser.END_TAG ||
parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
) {
if (
type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name
) {
val attrs =
resourceHelper.obtainStyledAttributes(
Xml.asAttributeSet(parser),
R.styleable.WorkspaceSpec
)
val maxAvailableSize =
attrs.getDimensionPixelSize(
R.styleable.WorkspaceSpec_maxAvailableSize,
0
)
val specType =
WorkspaceSpec.SpecType.values()[
attrs.getInt(
R.styleable.WorkspaceSpec_specType,
WorkspaceSpec.SpecType.HEIGHT.ordinal
)
]
attrs.recycle()
var startPadding: SizeSpec? = null
var endPadding: SizeSpec? = null
var gutter: SizeSpec? = null
var cellSize: SizeSpec? = null
val limitDepth = parser.depth
while (
(parser.next().also { type = it } != XmlPullParser.END_TAG ||
parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
) {
val attr: AttributeSet = Xml.asAttributeSet(parser)
if (type == XmlPullParser.START_TAG) {
when (parser.name) {
XmlTags.START_PADDING -> {
startPadding = SizeSpec(resourceHelper, attr)
}
XmlTags.END_PADDING -> {
endPadding = SizeSpec(resourceHelper, attr)
}
XmlTags.GUTTER -> {
gutter = SizeSpec(resourceHelper, attr)
}
XmlTags.CELL_SIZE -> {
cellSize = SizeSpec(resourceHelper, attr)
}
}
}
}
if (
startPadding == null ||
endPadding == null ||
gutter == null ||
cellSize == null
) {
throw IllegalStateException(
"All attributes in workspaceSpec must be defined"
)
}
val workspaceSpec =
WorkspaceSpec(
maxAvailableSize,
specType,
startPadding,
endPadding,
gutter,
cellSize
)
if (workspaceSpec.isValid()) {
if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
workspaceHeightSpecList.add(workspaceSpec)
else workspaceWidthSpecList.add(workspaceSpec)
} else {
throw IllegalStateException("Invalid workspaceSpec found.")
}
}
}
if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
throw IllegalStateException(
"WorkspaceSpecs is incomplete - " +
"height list size = ${workspaceHeightSpecList.size}; " +
"width list size = ${workspaceWidthSpecList.size}."
)
}
}
}
parser.close()
} catch (e: Exception) {
when (e) {
is IOException,
is XmlPullParserException -> {
throw RuntimeException("Failure parsing workspaces specs file.", e)
}
else -> throw e
}
}
}
}
data class WorkspaceSpec(
val maxAvailableSize: Int,
val specType: SpecType,
val startPadding: SizeSpec,
val endPadding: SizeSpec,
val gutter: SizeSpec,
val cellSize: SizeSpec
) {
enum class SpecType {
HEIGHT,
WIDTH
}
fun isValid(): Boolean {
if (maxAvailableSize <= 0) {
Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
return false
}
// All specs need to be individually valid
if (!allSpecsAreValid()) {
Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
return false
}
return true
}
private fun allSpecsAreValid(): Boolean =
startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
}
class SizeSpec(resourceHelper: ResourceHelper, attrs: AttributeSet) {
val fixedSize: Float
val ofAvailableSpace: Float
val ofRemainderSpace: Float
init {
val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SpecSize)
fixedSize = getValue(styledAttrs, R.styleable.SpecSize_fixedSize)
ofAvailableSpace = getValue(styledAttrs, R.styleable.SpecSize_ofAvailableSpace)
ofRemainderSpace = getValue(styledAttrs, R.styleable.SpecSize_ofRemainderSpace)
styledAttrs.recycle()
}
private fun getValue(a: TypedArray, index: Int): Float {
if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
return a.getDimensionPixelSize(index, 0).toFloat()
} else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
return a.getFloat(index, 0f)
}
return 0f
}
fun isValid(): Boolean {
// All attributes are empty
if (fixedSize <= 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f) {
Log.e(TAG, "SizeSpec#isValid - all attributes are empty")
return false
}
// More than one attribute is filled
val attrCount =
(if (fixedSize > 0) 1 else 0) +
(if (ofAvailableSpace > 0) 1 else 0) +
(if (ofRemainderSpace > 0) 1 else 0)
if (attrCount > 1) {
Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled")
return false
}
// Values should be between 0 and 1
if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) {
Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1")
return false
}
return true
}
override fun toString(): String {
return "SizeSpec(fixedSize=$fixedSize, ofAvailableSpace=$ofAvailableSpace, " +
"ofRemainderSpace=$ofRemainderSpace)"
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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.
*/
-->
<!-- Attributes have to be copied to test for correct parsing of files -->
<resources>
<!-- Responsive grids attributes -->
<declare-styleable name="WorkspaceSpec">
<attr name="specType" format="integer">
<enum name="height" value="0" />
<enum name="width" value="1" />
</attr>
<attr name="maxAvailableSize" format="dimension" />
</declare-styleable>
<declare-styleable name="SpecSize">
<attr name="fixedSize" format="dimension" />
<attr name="ofAvailableSpace" format="float" />
<attr name="ofRemainderSpace" format="float" />
</declare-styleable>
</resources>
@@ -0,0 +1,56 @@
<?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.
-->
<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="648dp">
<!-- missing startPadding -->
<endPadding
launcher:ofAvailableSpace="0.05" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofAvailableSpace="0.0306" />
<endPadding
launcher:ofAvailableSpace="0.068" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<!-- Width spec is always the same -->
<workspaceSpec
launcher:specType="width"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofRemainderSpace="0.21436227" />
<endPadding
launcher:ofRemainderSpace="0.21436227" />
<gutter
launcher:ofRemainderSpace="0.11425509" />
<cellSize
launcher:fixedSize="120dp" />
</workspaceSpec>
</workspaceSpecs>
@@ -0,0 +1,59 @@
<?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.
-->
<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="648dp">
<startPadding
launcher:ofAvailableSpace="0.0125" />
<endPadding
launcher:ofAvailableSpace="0.05" />
<!-- more than 1 value in one tag -->
<gutter
launcher:ofAvailableSpace="0.0125"
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofAvailableSpace="0.0306" />
<endPadding
launcher:ofAvailableSpace="0.068" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<!-- Width spec is always the same -->
<workspaceSpec
launcher:specType="width"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofRemainderSpace="0.21436227" />
<endPadding
launcher:ofRemainderSpace="0.21436227" />
<gutter
launcher:ofRemainderSpace="0.11425509" />
<cellSize
launcher:fixedSize="120dp" />
</workspaceSpec>
</workspaceSpecs>
@@ -0,0 +1,58 @@
<?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.
-->
<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="648dp">
<startPadding
launcher:ofAvailableSpace="0.0125" />
<endPadding
launcher:ofAvailableSpace="0.05" />
<gutter
launcher:fixedSize="16dp" />
<!-- value bigger than 1 -->
<cellSize
launcher:ofRemainderSpace="1.001" />
</workspaceSpec>
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofAvailableSpace="0.0306" />
<endPadding
launcher:ofAvailableSpace="0.068" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<!-- Width spec is always the same -->
<workspaceSpec
launcher:specType="width"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofRemainderSpace="0.21436227" />
<endPadding
launcher:ofRemainderSpace="0.21436227" />
<gutter
launcher:ofRemainderSpace="0.11425509" />
<cellSize
launcher:fixedSize="120dp" />
</workspaceSpec>
</workspaceSpecs>
+57
View File
@@ -0,0 +1,57 @@
<?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.
-->
<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="648dp">
<startPadding
launcher:ofAvailableSpace="0.0125" />
<endPadding
launcher:ofAvailableSpace="0.05" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<workspaceSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofAvailableSpace="0.0306" />
<endPadding
launcher:ofAvailableSpace="0.068" />
<gutter
launcher:fixedSize="16dp" />
<cellSize
launcher:ofRemainderSpace="0.2" />
</workspaceSpec>
<!-- Width spec is always the same -->
<workspaceSpec
launcher:specType="width"
launcher:maxAvailableSize="9999dp">
<startPadding
launcher:ofRemainderSpace="0.21436227" />
<endPadding
launcher:ofRemainderSpace="0.21436227" />
<gutter
launcher:ofRemainderSpace="0.11425509" />
<cellSize
launcher:fixedSize="120dp" />
</workspaceSpec>
</workspaceSpecs>
@@ -0,0 +1,272 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3
import android.content.Context
import android.content.res.Configuration
import android.graphics.Point
import android.graphics.Rect
import android.util.DisplayMetrics
import android.view.Surface
import androidx.test.core.app.ApplicationProvider
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.NavigationMode
import com.android.launcher3.util.WindowBounds
import com.android.launcher3.util.window.CachedDisplayInfo
import com.android.launcher3.util.window.WindowManagerProxy
import kotlin.math.max
import kotlin.math.min
import org.junit.After
import org.junit.Before
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
/**
* This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on
* a real device spec.
*
* For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
*/
abstract class AbstractDeviceProfileTest {
protected var context: Context? = null
protected open val runningContext: Context = ApplicationProvider.getApplicationContext()
private var displayController: DisplayController = mock(DisplayController::class.java)
private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java)
private lateinit var originalDisplayController: DisplayController
private lateinit var originalWindowManagerProxy: WindowManagerProxy
@Before
fun setUp() {
val appContext: Context = ApplicationProvider.getApplicationContext()
originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext)
originalDisplayController = DisplayController.INSTANCE.get(appContext)
WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy)
DisplayController.INSTANCE.initializeForTesting(displayController)
}
@After
fun tearDown() {
WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy)
DisplayController.INSTANCE.initializeForTesting(originalDisplayController)
}
class DeviceSpec(
val naturalSize: Pair<Int, Int>,
val densityDpi: Int,
val statusBarNaturalPx: Int,
val statusBarRotatedPx: Int,
val gesturePx: Int,
val cutoutPx: Int
)
open val deviceSpecs =
mapOf(
"phone" to
DeviceSpec(
Pair(1080, 2400),
densityDpi = 420,
statusBarNaturalPx = 118,
statusBarRotatedPx = 74,
gesturePx = 63,
cutoutPx = 118
),
"tablet" to
DeviceSpec(
Pair(2560, 1600),
densityDpi = 320,
statusBarNaturalPx = 104,
statusBarRotatedPx = 104,
gesturePx = 0,
cutoutPx = 0
),
)
protected fun initializeVarsForPhone(
deviceSpec: DeviceSpec,
isGestureMode: Boolean = true,
isVerticalBar: Boolean = false
) {
val (naturalX, naturalY) = deviceSpec.naturalSize
val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
val displayInfo =
CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
initializeCommonVars(
perDisplayBoundsCache,
displayInfo,
rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
isGestureMode,
densityDpi = deviceSpec.densityDpi
)
}
protected fun initializeVarsForTablet(
deviceSpec: DeviceSpec,
isLandscape: Boolean = false,
isGestureMode: Boolean = true
) {
val (naturalX, naturalY) = deviceSpec.naturalSize
val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
val displayInfo =
CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
initializeCommonVars(
perDisplayBoundsCache,
displayInfo,
rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
isGestureMode,
densityDpi = deviceSpec.densityDpi
)
}
protected fun initializeVarsForTwoPanel(
deviceTabletSpec: DeviceSpec,
deviceSpec: DeviceSpec,
isLandscape: Boolean = false,
isGestureMode: Boolean = true
) {
val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize
val tabletWindowsBounds =
tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY)
val tabletDisplayInfo =
CachedDisplayInfo(
Point(tabletNaturalX, tabletNaturalY),
Surface.ROTATION_0,
Rect(0, 0, 0, 0)
)
val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize
val phoneWindowsBounds =
phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY)
val phoneDisplayInfo =
CachedDisplayInfo(
Point(phoneNaturalX, phoneNaturalY),
Surface.ROTATION_0,
Rect(0, 0, 0, 0)
)
val perDisplayBoundsCache =
mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds)
initializeCommonVars(
perDisplayBoundsCache,
tabletDisplayInfo,
rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
isGestureMode,
densityDpi = deviceTabletSpec.densityDpi
)
}
private fun phoneWindowsBounds(
deviceSpec: DeviceSpec,
isGestureMode: Boolean,
naturalX: Int,
naturalY: Int
): Array<WindowBounds> {
val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
val rotation0Insets =
Rect(
0,
max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
0,
if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
)
val rotation90Insets =
Rect(
deviceSpec.cutoutPx,
deviceSpec.statusBarRotatedPx,
if (isGestureMode) 0 else buttonsNavHeight,
if (isGestureMode) deviceSpec.gesturePx else 0
)
val rotation180Insets =
Rect(
0,
deviceSpec.statusBarNaturalPx,
0,
max(
if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
deviceSpec.cutoutPx
)
)
val rotation270Insets =
Rect(
if (isGestureMode) 0 else buttonsNavHeight,
deviceSpec.statusBarRotatedPx,
deviceSpec.cutoutPx,
if (isGestureMode) deviceSpec.gesturePx else 0
)
return arrayOf(
WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
)
}
private fun tabletWindowsBounds(
deviceSpec: DeviceSpec,
naturalX: Int,
naturalY: Int
): Array<WindowBounds> {
val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
return arrayOf(
WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
)
}
private fun initializeCommonVars(
perDisplayBoundsCache: Map<CachedDisplayInfo, Array<WindowBounds>>,
displayInfo: CachedDisplayInfo,
rotation: Int,
isGestureMode: Boolean = true,
densityDpi: Int
) {
val windowsBounds = perDisplayBoundsCache[displayInfo]!!
val realBounds = windowsBounds[rotation]
whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo)
whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(realBounds)
whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation)
whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any()))
.thenReturn(
if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS
)
val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()
val config =
Configuration(runningContext.resources.configuration).apply {
this.densityDpi = densityDpi
screenWidthDp = (realBounds.bounds.width() / density).toInt()
screenHeightDp = (realBounds.bounds.height() / density).toInt()
smallestScreenWidthDp = min(screenWidthDp, screenHeightDp)
}
context = runningContext.createConfigurationContext(config)
val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache)
whenever(displayController.info).thenReturn(info)
whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode)
}
}
@@ -31,7 +31,13 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
abstract class DeviceProfileBaseTest {
/**
* This is an abstract class for DeviceProfile tests that don't need the real Context and mock an
* InvariantDeviceProfile instead of creating one based on real values.
*
* For an implementation that creates InvariantDeviceProfile, use [AbstractDeviceProfileTest]
*/
abstract class FakeInvariantDeviceProfileTest {
protected var context: Context? = null
protected var inv: InvariantDeviceProfile? = null
@@ -18,7 +18,7 @@ package com.android.launcher3.nonquickstep
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.DeviceProfileBaseTest
import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.WindowBounds
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -26,7 +26,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
/**
* This is a case when after setting the hotseat, the space needs to be recalculated but it
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.util
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import com.android.launcher3.R
import com.android.launcher3.tests.R as TestR
import kotlin.IntArray
class TestResourceHelper(private val context: Context, private val specsFileId: Int) :
ResourceHelper(context, specsFileId) {
override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
var clone = styleId.clone()
if (styleId == R.styleable.SpecSize) clone = TestR.styleable.SpecSize
else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec
return context.obtainStyledAttributes(attrs, clone)
}
}
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.workspace
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class WorkspaceSpecsTest : AbstractDeviceProfileTest() {
override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun setup() {
initializeVarsForPhone(deviceSpecs["phone"]!!)
}
@Test
fun parseValidFile() {
val workspaceSpecs =
WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(2)
assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=1701, " +
"specType=HEIGHT, " +
"startPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0125, " +
"ofRemainderSpace=0.0), " +
"endPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.05, " +
"ofRemainderSpace=0.0), " +
"gutter=SizeSpec(fixedSize=42.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.0), " +
"cellSize=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.2)" +
")"
)
assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=26247, " +
"specType=HEIGHT, " +
"startPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0306, " +
"ofRemainderSpace=0.0), " +
"endPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.068, " +
"ofRemainderSpace=0.0), " +
"gutter=SizeSpec(fixedSize=42.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.0), " +
"cellSize=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.2)" +
")"
)
assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
.isEqualTo(
"WorkspaceSpec(" +
"maxAvailableSize=26247, " +
"specType=WIDTH, " +
"startPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.21436226), " +
"endPadding=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.21436226), " +
"gutter=SizeSpec(fixedSize=0.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.11425509), " +
"cellSize=SizeSpec(fixedSize=315.0, " +
"ofAvailableSpace=0.0, " +
"ofRemainderSpace=0.0)" +
")"
)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2))
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
}
}