Merge "Import translations. DO NOT MERGE ANYWHERE"
diff --git a/Android.bp b/Android.bp
index 7bbbb10..64fc327 100644
--- a/Android.bp
+++ b/Android.bp
@@ -55,11 +55,15 @@
static_libs: [
"guava",
"monet",
- "renderscript_toolkit",
+ "renderscript_toolkit",
"wallpaper-common-deps",
"SettingsLibSettingsTheme",
"SystemUI-statsd",
"styleprotoslite",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.lifecycle_lifecycle-viewmodel-ktx",
+ "androidx.recyclerview_recyclerview",
+ "SystemUICustomizationLib",
],
jni_libs: [
diff --git a/res/color/keyguard_quick_affordance_slot_tab_background_color.xml b/res/color/keyguard_quick_affordance_slot_tab_background_color.xml
new file mode 100644
index 0000000..4708cef
--- /dev/null
+++ b/res/color/keyguard_quick_affordance_slot_tab_background_color.xml
@@ -0,0 +1,21 @@
+<?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:state_selected="true" android:color="@color/color_accent_primary" />
+ <item android:color="@android:color/transparent" />
+</selector>
diff --git a/res/color/keyguard_quick_affordance_slot_tab_text_color.xml b/res/color/keyguard_quick_affordance_slot_tab_text_color.xml
new file mode 100644
index 0000000..84502d4
--- /dev/null
+++ b/res/color/keyguard_quick_affordance_slot_tab_text_color.xml
@@ -0,0 +1,21 @@
+<?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:state_selected="true" android:color="@color/text_color_on_accent" />
+ <item android:color="@color/text_color_primary" />
+</selector>
diff --git a/res/drawable/keyguard_quick_affordance_icon_container_background.xml b/res/drawable/keyguard_quick_affordance_icon_container_background.xml
new file mode 100644
index 0000000..8bd8af4
--- /dev/null
+++ b/res/drawable/keyguard_quick_affordance_icon_container_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2021 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">
+ <corners android:radius="20dp" />
+ <solid android:color="@color/color_surface_variant" />
+</shape>
diff --git a/res/drawable/keyguard_quick_affordance_icon_container_background_selected.xml b/res/drawable/keyguard_quick_affordance_icon_container_background_selected.xml
new file mode 100644
index 0000000..93a80eb
--- /dev/null
+++ b/res/drawable/keyguard_quick_affordance_icon_container_background_selected.xml
@@ -0,0 +1,34 @@
+<!--
+ Copyright (C) 2021 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" >
+
+ <stroke
+ android:width="2dp"
+ android:color="@color/text_color_primary" />
+
+ <solid android:color="@color/color_surface_variant" />
+
+ <corners android:radius="20dp" />
+
+ <padding
+ android:left="5dp"
+ android:top="5dp"
+ android:right="5dp"
+ android:bottom="5dp" />
+
+</shape>
diff --git a/res/drawable/keyguard_quick_affordance_picker_background.xml b/res/drawable/keyguard_quick_affordance_picker_background.xml
new file mode 100644
index 0000000..3a49d7a
--- /dev/null
+++ b/res/drawable/keyguard_quick_affordance_picker_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2021 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">
+ <corners android:radius="28dp" />
+ <solid android:color="@color/color_surface" />
+</shape>
diff --git a/res/drawable/keyguard_quick_affordance_slot_tab_background.xml b/res/drawable/keyguard_quick_affordance_slot_tab_background.xml
new file mode 100644
index 0000000..3fbced3
--- /dev/null
+++ b/res/drawable/keyguard_quick_affordance_slot_tab_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2021 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">
+ <corners android:radius="50dp" />
+ <solid android:color="@color/keyguard_quick_affordance_slot_tab_background_color" />
+</shape>
diff --git a/res/drawable/link_off.xml b/res/drawable/link_off.xml
new file mode 100644
index 0000000..f16a63f
--- /dev/null
+++ b/res/drawable/link_off.xml
@@ -0,0 +1,26 @@
+<?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.
+ ~
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/keyguard_quick_affordance_icon_size"
+ android:height="@dimen/keyguard_quick_affordance_icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M14.39,11L16,12.61V11zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1 0,1.27 -0.77,2.37 -1.87,2.84l1.4,1.4C21.05,15.36 22,13.79 22,12c0,-2.76 -2.24,-5 -5,-5zM2,4.27l3.11,3.11C3.29,8.12 2,9.91 2,12c0,2.76 2.24,5 5,5h4v-1.9H7c-1.71,0 -3.1,-1.39 -3.1,-3.1 0,-1.59 1.21,-2.9 2.76,-3.07L8.73,11H8v2h2.73L13,15.27V17h1.73l4.01,4.01 1.41,-1.41L3.41,2.86 2,4.27z"/>
+</vector>
diff --git a/res/drawable/selectable.xml b/res/drawable/selectable.xml
new file mode 100644
index 0000000..1364d68
--- /dev/null
+++ b/res/drawable/selectable.xml
@@ -0,0 +1,21 @@
+<?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:state_selected="true" android:drawable="@drawable/selectable_selected" />
+ <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/drawable/selectable_selected.xml b/res/drawable/selectable_selected.xml
new file mode 100644
index 0000000..2ba8948
--- /dev/null
+++ b/res/drawable/selectable_selected.xml
@@ -0,0 +1,22 @@
+<?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.
+ ~
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="8dp" />
+ <stroke android:width="2dp" android:color="@android:color/white" />
+</shape>
diff --git a/res/layout/fragment_lock_screen_quick_affordances.xml b/res/layout/fragment_lock_screen_quick_affordances.xml
new file mode 100644
index 0000000..9927e6a
--- /dev/null
+++ b/res/layout/fragment_lock_screen_quick_affordances.xml
@@ -0,0 +1,69 @@
+<?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.
+ ~
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/section_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <include layout="@layout/section_header" />
+
+ </FrameLayout>
+
+ <!-- TODO(b/254858701): plug in the preview here. -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="24dp"
+ android:layout_marginBottom="28dp"
+ android:background="@drawable/keyguard_quick_affordance_picker_background"
+ android:paddingTop="22dp"
+ android:paddingBottom="62dp">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@id/slot_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingHorizontal="16dp" />
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="22dp" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@id/affordances"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingHorizontal="16dp" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/keyguard_quick_affordance.xml b/res/layout/keyguard_quick_affordance.xml
new file mode 100644
index 0000000..90ba68e
--- /dev/null
+++ b/res/layout/keyguard_quick_affordance.xml
@@ -0,0 +1,53 @@
+<?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.
+ ~
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="@dimen/keyguard_quick_affordance_picker_item_width"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+
+ <FrameLayout
+ android:id="@+id/icon_container"
+ android:layout_width="@dimen/keyguard_quick_affordance_icon_container_size"
+ android:layout_height="@dimen/keyguard_quick_affordance_icon_container_size" >
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/keyguard_quick_affordance_icon_size"
+ android:layout_height="@dimen/keyguard_quick_affordance_icon_size"
+ android:layout_gravity="center"
+ android:tint="@color/text_color_primary" />
+
+ </FrameLayout>
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="8dp" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/text_color_primary"
+ android:singleLine="true"
+ android:ellipsize="end"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/keyguard_quick_affordance_section_view.xml b/res/layout/keyguard_quick_affordance_section_view.xml
new file mode 100644
index 0000000..fc2a1ba
--- /dev/null
+++ b/res/layout/keyguard_quick_affordance_section_view.xml
@@ -0,0 +1,75 @@
+<?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.
+-->
+<com.android.customization.picker.quickaffordance.ui.view.KeyguardQuickAffordanceSectionView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?selectableItemBackground"
+ android:clickable="true"
+ android:paddingVertical="@dimen/section_top_padding"
+ android:paddingHorizontal="@dimen/section_horizontal_padding"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/keyguard_quick_affordance_section_title"
+ style="@style/SectionTitleTextStyle" />
+
+ <TextView
+ android:id="@+id/keyguard_quick_affordance_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/SectionSubtitleTextStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="@dimen/option_tile_width"
+ android:layout_height="@dimen/option_tile_width"
+ android:orientation="horizontal"
+ android:background="@drawable/option_border_color"
+ android:importantForAccessibility="noHideDescendants"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/icon_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <View
+ android:id="@+id/icon_spacer"
+ android:layout_width="14dp"
+ android:layout_height="0dp"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@+id/icon_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+
+</com.android.customization.picker.quickaffordance.ui.view.KeyguardQuickAffordanceSectionView>
\ No newline at end of file
diff --git a/res/layout/keyguard_quick_affordance_slot_tab.xml b/res/layout/keyguard_quick_affordance_slot_tab.xml
new file mode 100644
index 0000000..ba233cd
--- /dev/null
+++ b/res/layout/keyguard_quick_affordance_slot_tab.xml
@@ -0,0 +1,29 @@
+<?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.
+ ~
+ -->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/keyguard_quick_affordance_slot_tab_text_color"
+ android:paddingVertical="8dp"
+ android:paddingHorizontal="16dp"
+ android:minWidth="48dp"
+ android:minHeight="48dp"
+ android:gravity="center"
+ android:background="@drawable/keyguard_quick_affordance_slot_tab_background" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0ad221e..225d7b0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -135,4 +135,12 @@
<dimen name="color_seed_option_tile_padding">10dp</dimen>
<dimen name="color_seed_option_tile_padding_selected">6dp</dimen>
<dimen name="color_seed_chip_margin">14dp</dimen>
+
+ <!-- Keyguard quick affordances -->
+ <!-- Size for the container for the icon of a quick affordance for the lock screen in the picker experience. -->
+ <dimen name="keyguard_quick_affordance_icon_container_size">74dp</dimen>
+ <!-- Size for the icon of a quick affordance for the lock screen in the picker experience. -->
+ <dimen name="keyguard_quick_affordance_icon_size">24dp</dimen>
+ <!-- Width of a single selectable item in the lock screen quick affordance picker. -->
+ <dimen name="keyguard_quick_affordance_picker_item_width">74dp</dimen>
</resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..1ed004d
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,23 @@
+<?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.
+ ~
+ -->
+
+<resources>
+ <item name="start_affordance" type="id" />
+ <item name="end_affordance" type="id" />
+ <item name="slot_tabs" type="id" />
+ <item name="affordances" type="id" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7aef401..63fb560 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -267,4 +267,81 @@
<!-- Title of a section of color selection option that obtains colors automatically from the
wallpaper instead of a set color [CHAR LIMIT=15] -->
<string name="adaptive_color_title">Dynamic</string>
+
+ <!--
+ Name of the slot on the "start" side of the bottom of the lock screen, where quick affordance
+ buttons can be added to the lock screen. In left-to-right languages, this is the left-hand side
+ button. In right-to-left languages, this is the right-hand side button. [CHAR LIMIT=16].
+ -->
+ <string name="keyguard_slot_name_bottom_start">Left button</string>
+
+ <!--
+ Name of the slot on the "end" side of the bottom of the lock screen, where quick affordance
+ buttons can be added to the lock screen. In left-to-right languages, this is the right-hand side
+ button. In right-to-left languages, this is the left-hand side button. [CHAR LIMIT=16].
+ -->
+ <string name="keyguard_slot_name_bottom_end">Right button</string>
+
+ <!--
+ Name for an option to have no quick affordance selected for one of the sides of the lock
+ screen. We show this as an option in a settings experience, where users get to choose which
+ quick affordances (or buttons) are available on their device's lock screen. [CHAR LIMIT=10].
+ -->
+ <string name="keyguard_affordance_none">None</string>
+
+ <!--
+ Title for a popup dialog shown when the user attempts to select an option that is not currently
+ enabled. The dialog contains a list of instructions that the user needs to take in order to
+ enable the option before it can be selected again. [CHAR LIMIT=NONE].
+ -->
+ <string name="keyguard_affordance_enablement_dialog_title">Additional setup needed</string>
+
+ <!--
+ Template for an action that opens a specific app. [CHAR LIMIT=16]
+ -->
+ <string name="keyguard_affordance_enablement_dialog_action_template">Open <xliff:g id="appName" example="Wallet">%1$s</xliff:g></string>
+
+ <!--
+ Template for a message shown right before a list of instructions that tell the user what to do
+ in order to enable a shortcut to a specific app. [CHAR LIMIT=NONE]
+ -->
+ <string name="keyguard_affordance_enablement_dialog_message">To add the <xliff:g id="appName" example="Wallet">%1$s</xliff:g> app as a shortcut, make sure</string>
+
+ <!--
+ Label for button in a dialog shown to the user with a list of instructions that the user should
+ follow in order to make a piece of functionality available as a lock screen quick affordance.
+ [CHAR LIMIT=10].
+ -->
+ <string name="keyguard_affordance_enablement_dialog_dismiss_button">Done</string>
+
+ <!--
+ Title for a screen where the user can configure the lock screen shortcut buttons that appear on
+ the device without unlocking.
+ [CHAR LIMIT=32].
+ -->
+ <string name="keyguard_quick_affordance_title">Shortcuts</string>
+
+ <!--
+ Label for a menu item on a settings screen that helps the user open a new screen where they can
+ configure the lock screen shortcut buttons that appear on the device without unlocking.
+ [CHAR LIMIT=16].
+ -->
+ <string name="keyguard_quick_affordance_section_title">Shortcuts</string>
+
+ <!--
+ Template for text that shows the names of two currently-selected lock screen shortcuts on the
+ lock screen. For example, it may say "Camera, Wallet", if the first selected shortcut opens the
+ camera app and the second one opens the tap-to-pay wallet experience.
+ [CHAR LIMIT=60].
+ -->
+ <string name="keyguard_quick_affordance_two_selected_template"><xliff:g id="first">%1$s</xliff:g>, <xliff:g id="second">%2$s</xliff:g></string>
+
+ <!--
+ Placeholder text that shows when no lock screen shortcuts are currently selected on the lock
+ screen. When selected, "None" is replaced by another string that shows what is currently
+ selected. For example, it may say "Camera, Wallet", if the first selected shortcut opens the
+ camera app and the second one opens the tap-to-pay wallet experience.
+ [CHAR LIMIT=60].
+ -->
+ <string name="keyguard_quick_affordance_none_selected">None</string>
</resources>
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
deleted file mode 100644
index f5ab901..0000000
--- a/robolectric_tests/Android.mk
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (C) 2019 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.
-
-#############################################
-# ThenePicker Robolectric test target. #
-#############################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := ThemePickerRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_SDK_VERSION := system_current
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.test.core \
- androidx.test.runner \
- androidx.test.rules \
- mockito-robolectric-prebuilt \
- truth-prebuilt
-LOCAL_JAVA_LIBRARIES := \
- platform-robolectric-4.8.2-prebuilt
-
-LOCAL_JAVA_RESOURCE_DIRS := config
-
-LOCAL_INSTRUMENTATION_FOR := ThemePicker
-LOCAL_MODULE_TAGS := optional
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-############################################
-# Target to run the previous target. #
-############################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunThemePickerRoboTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_SDK_VERSION := system_current
-LOCAL_JAVA_LIBRARIES := \
- ThemePickerRoboTests
-
-LOCAL_TEST_PACKAGE := ThemePicker
-
-LOCAL_INSTRUMENT_SOURCE_DIRS := packages/apps/ThemePicker/src \
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include prebuilts/misc/common/robolectric/4.8.2/run_robotests.mk
diff --git a/src/com/android/customization/module/CustomizationInjector.java b/src/com/android/customization/module/CustomizationInjector.java
index 85853de..2cc1245 100644
--- a/src/com/android/customization/module/CustomizationInjector.java
+++ b/src/com/android/customization/module/CustomizationInjector.java
@@ -22,6 +22,7 @@
import com.android.customization.model.theme.OverlayManagerCompat;
import com.android.customization.model.theme.ThemeBundleProvider;
import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor;
import com.android.wallpaper.module.Injector;
public interface CustomizationInjector extends Injector {
@@ -30,4 +31,11 @@
ThemeManager getThemeManager(ThemeBundleProvider provider, FragmentActivity activity,
OverlayManagerCompat overlayManagerCompat, ThemesUserEventLogger logger);
+
+
+ /**
+ * Get {@link KeyguardQuickAffordancePickerInteractor}
+ */
+ KeyguardQuickAffordancePickerInteractor getKeyguardQuickAffordancePickerInteractor(
+ Context context);
}
diff --git a/src/com/android/customization/module/DefaultCustomizationSections.java b/src/com/android/customization/module/DefaultCustomizationSections.java
index 21f2c84..7eb8865 100644
--- a/src/com/android/customization/module/DefaultCustomizationSections.java
+++ b/src/com/android/customization/module/DefaultCustomizationSections.java
@@ -1,9 +1,9 @@
package com.android.customization.module;
-import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
import com.android.customization.model.color.ColorSectionController;
@@ -28,9 +28,12 @@
public final class DefaultCustomizationSections implements CustomizationSections {
@Override
- public List<CustomizationSectionController<?>> getAllSectionControllers(Activity activity,
- LifecycleOwner lifecycleOwner, WallpaperColorsViewModel wallpaperColorsViewModel,
- WorkspaceViewModel workspaceViewModel, PermissionRequester permissionRequester,
+ public List<CustomizationSectionController<?>> getAllSectionControllers(
+ FragmentActivity activity,
+ LifecycleOwner lifecycleOwner,
+ WallpaperColorsViewModel wallpaperColorsViewModel,
+ WorkspaceViewModel workspaceViewModel,
+ PermissionRequester permissionRequester,
WallpaperPreviewNavigator wallpaperPreviewNavigator,
CustomizationSectionNavigationController sectionNavigationController,
@Nullable Bundle savedInstanceState) {
diff --git a/src/com/android/customization/module/ThemePickerInjector.java b/src/com/android/customization/module/ThemePickerInjector.java
index ef2b60a..4069b50 100644
--- a/src/com/android/customization/module/ThemePickerInjector.java
+++ b/src/com/android/customization/module/ThemePickerInjector.java
@@ -21,6 +21,7 @@
import static com.android.wallpaper.picker.PreviewFragment.ARG_VIEW_AS_HOME;
import static com.android.wallpaper.picker.PreviewFragment.ARG_WALLPAPER;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -32,6 +33,11 @@
import com.android.customization.model.theme.OverlayManagerCompat;
import com.android.customization.model.theme.ThemeBundleProvider;
import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository;
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor;
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel;
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient;
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl;
import com.android.wallpaper.model.LiveWallpaperInfo;
import com.android.wallpaper.model.WallpaperInfo;
import com.android.wallpaper.module.CustomizationSections;
@@ -42,6 +48,8 @@
import com.android.wallpaper.picker.LivePreviewFragment;
import com.android.wallpaper.picker.PreviewFragment;
+import kotlinx.coroutines.Dispatchers;
+
/**
* A concrete, real implementation of the dependency provider.
*/
@@ -50,9 +58,12 @@
private CustomizationSections mCustomizationSections;
private ThemesUserEventLogger mUserEventLogger;
private WallpaperPreferences mPrefs;
+ private KeyguardQuickAffordancePickerInteractor mKeyguardQuickAffordancePickerInteractor;
+ private KeyguardQuickAffordancePickerViewModel.Factory
+ mKeyguardQuickAffordancePickerViewModelFactory;
@Override
- public CustomizationSections getCustomizationSections() {
+ public CustomizationSections getCustomizationSections(Activity activity) {
if (mCustomizationSections == null) {
mCustomizationSections = new DefaultCustomizationSections();
}
@@ -122,4 +133,31 @@
OverlayManagerCompat overlayManagerCompat, ThemesUserEventLogger logger) {
return new ThemeManager(provider, activity, overlayManagerCompat, logger);
}
+
+ @Override
+ public KeyguardQuickAffordancePickerInteractor getKeyguardQuickAffordancePickerInteractor(
+ Context context) {
+ if (mKeyguardQuickAffordancePickerInteractor == null) {
+ final KeyguardQuickAffordanceProviderClient client =
+ new KeyguardQuickAffordanceProviderClientImpl(context, Dispatchers.getIO());
+ mKeyguardQuickAffordancePickerInteractor = new KeyguardQuickAffordancePickerInteractor(
+ new KeyguardQuickAffordancePickerRepository(client, Dispatchers.getIO()),
+ client);
+ }
+ return mKeyguardQuickAffordancePickerInteractor;
+ }
+
+ /**
+ * Returns a {@link KeyguardQuickAffordancePickerViewModel.Factory}.
+ */
+ public KeyguardQuickAffordancePickerViewModel.Factory
+ getKeyguardQuickAffordancePickerViewModelFactory(Context context) {
+ if (mKeyguardQuickAffordancePickerViewModelFactory == null) {
+ mKeyguardQuickAffordancePickerViewModelFactory =
+ new KeyguardQuickAffordancePickerViewModel.Factory(
+ context,
+ getKeyguardQuickAffordancePickerInteractor(context));
+ }
+ return mKeyguardQuickAffordancePickerViewModelFactory;
+ }
}
diff --git a/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
new file mode 100644
index 0000000..5846107
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.data.repository
+
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerAffordanceModel as AffordanceModel
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel as SelectionModel
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSlotModel as SlotModel
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient as Client
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/**
+ * Abstracts access to application state related to functionality for selecting, picking, or setting
+ * lock screen quick affordances.
+ */
+class KeyguardQuickAffordancePickerRepository(
+ private val client: Client,
+ private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ /** Whether the feature is enabled. */
+ val isFeatureEnabled: Flow<Boolean> =
+ client.observeFlags().map { flags -> flags.isFeatureEnabled() }
+
+ /** List of slots available on the device. */
+ val slots: Flow<List<SlotModel>> =
+ client.observeSlots().map { slots -> slots.map { slot -> slot.toModel() } }
+
+ /** List of all available quick affordances. */
+ val affordances: Flow<List<AffordanceModel>> =
+ client.observeAffordances().map { affordances ->
+ affordances.map { affordance -> affordance.toModel() }
+ }
+
+ /** List of slot-affordance pairs, modeling what the user has currently chosen for each slot. */
+ val selections: Flow<List<SelectionModel>> =
+ client.observeSelections().map { selections ->
+ selections.map { selection -> selection.toModel() }
+ }
+
+ suspend fun isFeatureEnabled(): Boolean {
+ return withContext(backgroundDispatcher) { client.queryFlags().isFeatureEnabled() }
+ }
+
+ private fun List<Client.Flag>.isFeatureEnabled(): Boolean {
+ return find { flag -> flag.name == Contract.FlagsTable.FLAG_NAME_FEATURE_ENABLED }?.value ==
+ true
+ }
+
+ private fun Client.Slot.toModel(): SlotModel {
+ return SlotModel(
+ id = id,
+ maxSelectedQuickAffordances = capacity,
+ )
+ }
+
+ private fun Client.Affordance.toModel(): AffordanceModel {
+ return AffordanceModel(
+ id = id,
+ name = name,
+ iconResourceId = iconResourceId,
+ isEnabled = isEnabled,
+ enablementInstructions = enablementInstructions ?: emptyList(),
+ enablementActionText = enablementActionText,
+ enablementActionComponentName = enablementActionComponentName,
+ )
+ }
+
+ private fun Client.Selection.toModel(): SelectionModel {
+ return SelectionModel(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
new file mode 100644
index 0000000..87cedf5
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.domain.interactor
+
+import android.graphics.drawable.Drawable
+import androidx.annotation.DrawableRes
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerAffordanceModel as AffordanceModel
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel as SelectionModel
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSlotModel as SlotModel
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient as Client
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Single entry-point for all application state and business logic related to quick affordances on
+ * the lock screen.
+ */
+class KeyguardQuickAffordancePickerInteractor(
+ private val repository: KeyguardQuickAffordancePickerRepository,
+ private val client: Client,
+) {
+ /** Whether the feature is enabled. */
+ val isFeatureEnabled: Flow<Boolean> = repository.isFeatureEnabled
+
+ /** List of slots available on the device. */
+ val slots: Flow<List<SlotModel>> = repository.slots
+
+ /** List of all available quick affordances. */
+ val affordances: Flow<List<AffordanceModel>> = repository.affordances
+
+ /** List of slot-affordance pairs, modeling what the user has currently chosen for each slot. */
+ val selections: Flow<List<SelectionModel>> = repository.selections
+
+ /**
+ * Selects an affordance with the given ID for a slot with the given ID.
+ *
+ * Note that the maximum affordance per slot is automatically managed. If trying to select an
+ * affordance for a slot that's already full, the oldest affordance is removed to make room.
+ *
+ * Note that if an affordance with the given ID is already selected on the slot with the given
+ * ID, that affordance is moved to the newest position on the slot.
+ */
+ suspend fun select(slotId: String, affordanceId: String) {
+ client.insertSelection(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ }
+
+ /** Unselects an affordance with the given ID from the slot with the given ID. */
+ suspend fun unselect(slotId: String, affordanceId: String) {
+ client.deleteSelection(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ }
+
+ /** Unselects all affordances from the slot with the given ID. */
+ suspend fun unselectAll(slotId: String) {
+ client.deleteAllSelections(
+ slotId = slotId,
+ )
+ }
+
+ /** Returns a [Drawable] for the given resource ID, from the system UI package. */
+ suspend fun getAffordanceIcon(
+ @DrawableRes iconResourceId: Int,
+ ): Drawable {
+ return client.getAffordanceIcon(iconResourceId)
+ }
+
+ /** Returns `true` if the feature is enabled; `false` otherwise. */
+ suspend fun isFeatureEnabled(): Boolean {
+ return repository.isFeatureEnabled()
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
new file mode 100644
index 0000000..1b18af7
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.shared.model
+
+import androidx.annotation.DrawableRes
+
+/** Models a quick affordance. */
+data class KeyguardQuickAffordancePickerAffordanceModel(
+ val id: String,
+ val name: String,
+ /**
+ * The resource ID for a drawable of the icon. This is in the namespace of system UI so it
+ * should be queries from that package.
+ */
+ @DrawableRes val iconResourceId: Int,
+ /** Whether this quick affordance is enabled. */
+ val isEnabled: Boolean,
+ /** If not enabled, the list of user-visible steps to re-enable it. */
+ val enablementInstructions: List<String>,
+ /**
+ * If not enabled, an optional label for a button that takes the user to a destination where
+ * they can re-enable it.
+ */
+ val enablementActionText: String?,
+ /**
+ * If not enabled, an optional component name (package and action) for a button that takes the
+ * user to a destination where they can re-enable it.
+ */
+ val enablementActionComponentName: String?,
+)
diff --git a/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
new file mode 100644
index 0000000..eea8b2a
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.shared.model
+
+/** Models a selection of an affordance on a slot. */
+data class KeyguardQuickAffordancePickerSelectionModel(
+ val slotId: String,
+ val affordanceId: String,
+)
diff --git a/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
new file mode 100644
index 0000000..7e662e0
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.shared.model
+
+/** Models a lock screen quick affordance slot (or position) where affordances can be displayed. */
+data class KeyguardQuickAffordancePickerSlotModel(
+ val id: String,
+ /** Maximum number of affordances allowed to be set on this slot. */
+ val maxSelectedQuickAffordances: Int,
+)
diff --git a/src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt b/src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt
new file mode 100644
index 0000000..b0dc350
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.wallpaper.R
+
+/** Adapts between lock screen quick affordance items and views. */
+class AffordancesAdapter : RecyclerView.Adapter<AffordancesAdapter.ViewHolder>() {
+
+ private val items = mutableListOf<KeyguardQuickAffordanceViewModel>()
+
+ fun setItems(items: List<KeyguardQuickAffordanceViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val iconContainerView: View = itemView.requireViewById(R.id.icon_container)
+ val iconView: ImageView = itemView.requireViewById(R.id.icon)
+ val nameView: TextView = itemView.requireViewById(R.id.name)
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(
+ R.layout.keyguard_quick_affordance,
+ parent,
+ false,
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.itemView.alpha =
+ if (item.isEnabled) {
+ ALPHA_ENABLED
+ } else {
+ ALPHA_DISABLED
+ }
+
+ holder.itemView.setOnClickListener(
+ if (item.onClicked != null) {
+ View.OnClickListener { item.onClicked.invoke() }
+ } else {
+ null
+ }
+ )
+ holder.iconContainerView.setBackgroundResource(
+ if (item.isSelected) {
+ R.drawable.keyguard_quick_affordance_icon_container_background_selected
+ } else {
+ R.drawable.keyguard_quick_affordance_icon_container_background
+ }
+ )
+ holder.iconView.isSelected = item.isSelected
+ holder.nameView.isSelected = item.isSelected
+ holder.iconView.setImageDrawable(item.icon)
+ holder.nameView.text = item.contentDescription
+ holder.nameView.isSelected = item.isSelected
+ }
+
+ companion object {
+ private const val ALPHA_ENABLED = 1f
+ private const val ALPHA_DISABLED = 0.3f
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt
new file mode 100644
index 0000000..acafef4
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceSlotViewModel
+import com.android.wallpaper.R
+
+/** Adapts between lock screen quick affordance slot items and views. */
+class SlotTabAdapter : RecyclerView.Adapter<SlotTabAdapter.ViewHolder>() {
+
+ private val items = mutableListOf<KeyguardQuickAffordanceSlotViewModel>()
+
+ fun setItems(items: List<KeyguardQuickAffordanceSlotViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(
+ R.layout.keyguard_quick_affordance_slot_tab,
+ parent,
+ false,
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.itemView.isSelected = item.isSelected
+ holder.textView.text = item.name
+ holder.textView.setOnClickListener(
+ if (item.onClicked != null) {
+ View.OnClickListener { item.onClicked.invoke() }
+ } else {
+ null
+ }
+ )
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val textView: TextView = itemView.requireViewById(R.id.text)
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
new file mode 100644
index 0000000..62f2e26
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.binder
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.Rect
+import android.view.View
+import androidx.core.view.ViewCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.quickaffordance.ui.adapter.AffordancesAdapter
+import com.android.customization.picker.quickaffordance.ui.adapter.SlotTabAdapter
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+object KeyguardQuickAffordancePickerBinder {
+
+ /** Binds view with view-model for a lock screen quick affordance picker experience. */
+ @JvmStatic
+ fun bind(
+ view: View,
+ viewModel: KeyguardQuickAffordancePickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ val slotTabView: RecyclerView = view.requireViewById(R.id.slot_tabs)
+ val affordancesView: RecyclerView = view.requireViewById(R.id.affordances)
+
+ val slotTabAdapter = SlotTabAdapter()
+ slotTabView.adapter = slotTabAdapter
+ slotTabView.layoutManager =
+ LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ slotTabView.addItemDecoration(ItemSpacing())
+ val affordancesAdapter = AffordancesAdapter()
+ affordancesView.adapter = affordancesAdapter
+ affordancesView.layoutManager =
+ LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ affordancesView.addItemDecoration(ItemSpacing())
+
+ var dialog: Dialog? = null
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.slots
+ .map { slotById -> slotById.values }
+ .collect { slots -> slotTabAdapter.setItems(slots.toList()) }
+ }
+
+ launch {
+ viewModel.quickAffordances.collect { affordances ->
+ affordancesAdapter.setItems(affordances)
+ }
+ }
+
+ launch {
+ viewModel.dialog.distinctUntilChanged().collect { dialogRequest ->
+ dialog?.dismiss()
+ dialog =
+ if (dialogRequest != null) {
+ showDialog(
+ context = view.context,
+ request = dialogRequest,
+ onDismissed = viewModel::onDialogDismissed
+ )
+ } else {
+ dialog?.dismiss()
+ null
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun showDialog(
+ context: Context,
+ request: KeyguardQuickAffordancePickerViewModel.DialogViewModel,
+ onDismissed: () -> Unit,
+ ): Dialog {
+ // TODO(b/254858701): make this dialog prettier and probably use a DialogFragment.
+ return AlertDialog.Builder(context, context.themeResId)
+ .setTitle(context.getString(R.string.keyguard_affordance_enablement_dialog_title))
+ .setMessage(
+ buildString {
+ append(request.instructionHeader)
+ if (request.instructions.isNotEmpty()) {
+ append("\n")
+ }
+ request.instructions.forEachIndexed { index, instruction ->
+ append(instruction)
+ if (index < request.instructions.size - 1) {
+ append("\n")
+ }
+ }
+ }
+ )
+ .setOnDismissListener { onDismissed.invoke() }
+ .setPositiveButton(
+ request.actionText,
+ if (request.intent != null) {
+ DialogInterface.OnClickListener { _, _ ->
+ context.startActivity(request.intent)
+ }
+ } else {
+ DialogInterface.OnClickListener { _, _ -> onDismissed() }
+ },
+ )
+ .show()
+ }
+
+ private class ItemSpacing : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {
+ val addSpacingToStart = itemPosition > 0
+ val addSpacingToEnd = itemPosition < (parent.adapter?.itemCount ?: 0) - 1
+ val isRtl = parent.layoutManager?.layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL
+ val density = parent.context.resources.displayMetrics.density
+ if (!isRtl) {
+ outRect.left = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+ outRect.right = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+ } else {
+ outRect.left = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+ outRect.right = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+ }
+ }
+
+ private fun Int.toPx(density: Float): Int {
+ return (this * density).toInt()
+ }
+
+ companion object {
+ private const val ITEM_SPACING_DP = 8
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
new file mode 100644
index 0000000..13ee553
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+
+object KeyguardQuickAffordancePickerPreviewBinder {
+
+ /** Binds view with view-model for a lock screen quick affordance preview experience. */
+ @JvmStatic
+ fun bind(
+ view: View,
+ viewModel: KeyguardQuickAffordancePickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ val startView: ImageView = view.requireViewById(R.id.start_affordance)
+ val endView: ImageView = view.requireViewById(R.id.end_affordance)
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ updateView(
+ view = startView,
+ viewModel = viewModel,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ )
+ }
+
+ launch {
+ updateView(
+ view = endView,
+ viewModel = viewModel,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun updateView(
+ view: ImageView,
+ viewModel: KeyguardQuickAffordancePickerViewModel,
+ slotId: String,
+ ) {
+ viewModel.slots
+ .mapNotNull { slotById -> slotById[slotId] }
+ .map { slot -> slot.selectedQuickAffordances.firstOrNull() }
+ .collect { affordance ->
+ view.setImageDrawable(affordance?.icon)
+ view.contentDescription = affordance?.contentDescription
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt
new file mode 100644
index 0000000..e832cc2
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+object KeyguardQuickAffordanceSectionViewBinder {
+ fun bind(
+ view: View,
+ viewModel: KeyguardQuickAffordancePickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ onClicked: () -> Unit,
+ ) {
+ view.setOnClickListener { onClicked() }
+
+ val descriptionView: TextView =
+ view.requireViewById(R.id.keyguard_quick_affordance_description)
+ val icon1: ImageView = view.requireViewById(R.id.icon_1)
+ val icon2: ImageView = view.requireViewById(R.id.icon_2)
+ val iconSpacer: View = view.requireViewById(R.id.icon_spacer)
+
+ lifecycleOwner.lifecycleScope.launch {
+ viewModel.summary
+ .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.RESUMED)
+ .collectLatest { summary ->
+ descriptionView.text = summary.description
+
+ icon1.setImageDrawable(summary.icon1)
+ icon1.isVisible = summary.icon1 != null
+
+ icon2.setImageDrawable(summary.icon2)
+ icon2.isVisible = summary.icon2 != null
+
+ iconSpacer.isVisible = summary.isIconSpacingVisible
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
new file mode 100644
index 0000000..89235d7
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
+import com.android.customization.module.ThemePickerInjector
+import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordancePickerBinder
+import com.android.wallpaper.R
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+
+class KeyguardQuickAffordancePickerFragment : AppbarFragment() {
+ companion object {
+ fun newInstance(): KeyguardQuickAffordancePickerFragment {
+ return KeyguardQuickAffordancePickerFragment()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view =
+ inflater.inflate(
+ R.layout.fragment_lock_screen_quick_affordances,
+ container,
+ false,
+ )
+ setUpToolbar(view)
+ val injector = InjectorProvider.getInjector() as ThemePickerInjector
+ KeyguardQuickAffordancePickerBinder.bind(
+ view = view,
+ viewModel =
+ ViewModelProvider(
+ requireActivity(),
+ injector.getKeyguardQuickAffordancePickerViewModelFactory(requireContext()),
+ )
+ .get(),
+ lifecycleOwner = this,
+ )
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return requireContext().getString(R.string.keyguard_quick_affordance_title)
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt b/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt
new file mode 100644
index 0000000..6b35d7c
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.section
+
+import android.content.Context
+import android.view.LayoutInflater
+import androidx.lifecycle.LifecycleOwner
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordanceSectionViewBinder
+import com.android.customization.picker.quickaffordance.ui.fragment.KeyguardQuickAffordancePickerFragment
+import com.android.customization.picker.quickaffordance.ui.view.KeyguardQuickAffordanceSectionView
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.wallpaper.R
+import com.android.wallpaper.model.CustomizationSectionController
+import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController as NavigationController
+import kotlinx.coroutines.runBlocking
+
+class KeyguardQuickAffordanceSectionController(
+ private val navigationController: NavigationController,
+ private val interactor: KeyguardQuickAffordancePickerInteractor,
+ private val viewModel: KeyguardQuickAffordancePickerViewModel,
+ private val lifecycleOwner: LifecycleOwner,
+) : CustomizationSectionController<KeyguardQuickAffordanceSectionView> {
+
+ private val isFeatureEnabled: Boolean = runBlocking { interactor.isFeatureEnabled() }
+
+ override fun isAvailable(context: Context?): Boolean {
+ return isFeatureEnabled
+ }
+
+ override fun createView(context: Context?): KeyguardQuickAffordanceSectionView {
+ val view =
+ LayoutInflater.from(context)
+ .inflate(
+ R.layout.keyguard_quick_affordance_section_view,
+ null,
+ ) as KeyguardQuickAffordanceSectionView
+ KeyguardQuickAffordanceSectionViewBinder.bind(
+ view = view,
+ viewModel = viewModel,
+ lifecycleOwner = lifecycleOwner,
+ ) {
+ navigationController.navigateTo(KeyguardQuickAffordancePickerFragment.newInstance())
+ }
+ return view
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt b/src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt
new file mode 100644
index 0000000..daace7d
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.wallpaper.picker.SectionView
+
+class KeyguardQuickAffordanceSectionView(
+ context: Context?,
+ attrs: AttributeSet?,
+) :
+ SectionView(
+ context,
+ attrs,
+ )
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
new file mode 100644
index 0000000..e69c639
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
@@ -0,0 +1,332 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import androidx.annotation.DrawableRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import com.android.wallpaper.R
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Models UI state for a lock screen quick affordance picker experience. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class KeyguardQuickAffordancePickerViewModel
+private constructor(
+ context: Context,
+ private val interactor: KeyguardQuickAffordancePickerInteractor,
+) : ViewModel() {
+
+ @SuppressLint("StaticFieldLeak") private val applicationContext = context.applicationContext
+
+ private val selectedSlotId = MutableStateFlow<String?>(null)
+
+ /** View-models for each slot, keyed by slot ID. */
+ val slots: Flow<Map<String, KeyguardQuickAffordanceSlotViewModel>> =
+ combine(
+ interactor.slots,
+ interactor.affordances,
+ interactor.selections,
+ selectedSlotId,
+ ) { slots, affordances, selections, selectedSlotIdOrNull ->
+ slots
+ .mapIndexed { index, slot ->
+ val selectedAffordanceIds =
+ selections
+ .filter { selection -> selection.slotId == slot.id }
+ .map { selection -> selection.affordanceId }
+ .toSet()
+ val selectedAffordances =
+ affordances.filter { affordance ->
+ selectedAffordanceIds.contains(affordance.id)
+ }
+ val isSelected =
+ (selectedSlotIdOrNull == null && index == 0) ||
+ selectedSlotIdOrNull == slot.id
+ slot.id to
+ KeyguardQuickAffordanceSlotViewModel(
+ name = getSlotName(slot.id),
+ isSelected = isSelected,
+ selectedQuickAffordances =
+ selectedAffordances.map { affordanceModel ->
+ KeyguardQuickAffordanceViewModel(
+ icon = getAffordanceIcon(affordanceModel.iconResourceId),
+ contentDescription = affordanceModel.name,
+ isSelected = true,
+ onClicked = null,
+ isEnabled = affordanceModel.isEnabled,
+ )
+ },
+ maxSelectedQuickAffordances = slot.maxSelectedQuickAffordances,
+ onClicked =
+ if (isSelected) {
+ null
+ } else {
+ { this.selectedSlotId.value = slot.id }
+ },
+ )
+ }
+ .toMap()
+ }
+
+ /** The list of all available quick affordances for the selected slot. */
+ val quickAffordances: Flow<List<KeyguardQuickAffordanceViewModel>> =
+ combine(
+ interactor.slots,
+ interactor.affordances,
+ interactor.selections,
+ selectedSlotId,
+ ) { slots, affordances, selections, selectedSlotIdOrNull ->
+ val selectedSlot =
+ selectedSlotIdOrNull?.let { slots.find { slot -> slot.id == it } } ?: slots.first()
+ val selectedAffordanceIds =
+ selections
+ .filter { selection -> selection.slotId == selectedSlot.id }
+ .map { selection -> selection.affordanceId }
+ .toSet()
+ listOf(
+ none(
+ slotId = selectedSlot.id,
+ isSelected = selectedAffordanceIds.isEmpty(),
+ )
+ ) +
+ affordances.map { affordance ->
+ val isSelected = selectedAffordanceIds.contains(affordance.id)
+ KeyguardQuickAffordanceViewModel(
+ icon = getAffordanceIcon(affordance.iconResourceId),
+ contentDescription = affordance.name,
+ isSelected = isSelected,
+ onClicked =
+ if (affordance.isEnabled) {
+ {
+ viewModelScope.launch {
+ if (isSelected) {
+ interactor.unselect(
+ slotId = selectedSlot.id,
+ affordanceId = affordance.id,
+ )
+ } else {
+ interactor.select(
+ slotId = selectedSlot.id,
+ affordanceId = affordance.id,
+ )
+ }
+ }
+ }
+ } else {
+ {
+ showEnablementDialog(
+ instructionHeader =
+ affordance.enablementInstructions.first(),
+ instructions =
+ affordance.enablementInstructions.subList(
+ 1,
+ affordance.enablementInstructions.size
+ ),
+ actionText = affordance.enablementActionText,
+ actionComponentName =
+ affordance.enablementActionComponentName,
+ )
+ }
+ },
+ isEnabled = affordance.isEnabled,
+ )
+ }
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ val summary: Flow<KeyguardQuickAffordanceSummaryViewModel> =
+ slots.map { slots ->
+ val icon2 =
+ slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
+ ?.selectedQuickAffordances
+ ?.firstOrNull()
+ ?.icon
+ val icon1 =
+ slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
+ ?.selectedQuickAffordances
+ ?.firstOrNull()
+ ?.icon
+
+ val isIconSpacingVisible = icon1 != null && icon2 != null
+ KeyguardQuickAffordanceSummaryViewModel(
+ description = toDescriptionText(context, slots),
+ icon1 = icon1
+ ?: if (icon2 == null) {
+ context.getDrawable(R.drawable.link_off)
+ } else {
+ null
+ },
+ icon2 = icon2,
+ isIconSpacingVisible = isIconSpacingVisible,
+ )
+ }
+
+ private val _dialog = MutableStateFlow<DialogViewModel?>(null)
+ /**
+ * The current dialog to show. If `null`, no dialog should be shown.
+ *
+ * When the dialog is dismissed, [onDialogDismissed] must be called.
+ */
+ val dialog: Flow<DialogViewModel?> = _dialog.asStateFlow()
+
+ /** Notifies that the dialog has been dismissed in the UI. */
+ fun onDialogDismissed() {
+ _dialog.value = null
+ }
+
+ private fun showEnablementDialog(
+ instructionHeader: String,
+ instructions: List<String>,
+ actionText: String?,
+ actionComponentName: String?,
+ ) {
+ _dialog.value =
+ DialogViewModel(
+ instructionHeader = instructionHeader,
+ instructions = instructions,
+ actionText = actionText
+ ?: applicationContext.getString(
+ R.string.keyguard_affordance_enablement_dialog_dismiss_button
+ ),
+ intent = actionComponentName.toIntent(),
+ )
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private fun none(
+ slotId: String,
+ isSelected: Boolean,
+ ): KeyguardQuickAffordanceViewModel {
+ return KeyguardQuickAffordanceViewModel.none(
+ context = applicationContext,
+ isSelected = isSelected,
+ onSelected = { viewModelScope.launch { interactor.unselectAll(slotId) } },
+ )
+ }
+
+ private fun getSlotName(slotId: String): String {
+ return applicationContext.getString(
+ when (slotId) {
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START ->
+ R.string.keyguard_slot_name_bottom_start
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END ->
+ R.string.keyguard_slot_name_bottom_end
+ else -> error("No name for slot with ID of \"$slotId\"!")
+ }
+ )
+ }
+
+ private suspend fun getAffordanceIcon(@DrawableRes iconResourceId: Int): Drawable {
+ return interactor.getAffordanceIcon(iconResourceId)
+ }
+
+ private fun String?.toIntent(): Intent? {
+ if (isNullOrEmpty()) {
+ return null
+ }
+
+ val splitUp = split(Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR)
+ check(splitUp.size == 1 || splitUp.size == 2) {
+ "Illegal component name \"$this\". Must be either just an action or a package and an" +
+ " action separated by a" +
+ " \"${Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR}\"!"
+ }
+
+ return Intent(splitUp.last()).apply {
+ if (splitUp.size > 1) {
+ setPackage(splitUp[0])
+ }
+ }
+ }
+
+ /** Encapsulates a request to show a dialog. */
+ data class DialogViewModel(
+ /** The header for the instructions section. */
+ val instructionHeader: String,
+
+ /** The set of instructions to show below the header. */
+ val instructions: List<String>,
+
+ /** Label for the dialog button. */
+ val actionText: String,
+
+ /**
+ * Optional [Intent] to use to start an activity when the dialog button is clicked. If
+ * `null`, the dialog should be dismissed.
+ */
+ val intent: Intent?,
+ )
+
+ private fun toDescriptionText(
+ context: Context,
+ slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
+ ): String {
+ val bottomStartAffordanceName =
+ slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
+ ?.selectedQuickAffordances
+ ?.firstOrNull()
+ ?.contentDescription
+ val bottomEndAffordanceName =
+ slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
+ ?.selectedQuickAffordances
+ ?.firstOrNull()
+ ?.contentDescription
+
+ return when {
+ !bottomStartAffordanceName.isNullOrEmpty() &&
+ !bottomEndAffordanceName.isNullOrEmpty() -> {
+ context.getString(
+ R.string.keyguard_quick_affordance_two_selected_template,
+ bottomStartAffordanceName,
+ bottomEndAffordanceName,
+ )
+ }
+ !bottomStartAffordanceName.isNullOrEmpty() -> bottomStartAffordanceName
+ !bottomEndAffordanceName.isNullOrEmpty() -> bottomEndAffordanceName
+ else -> context.getString(R.string.keyguard_quick_affordance_none_selected)
+ }
+ }
+
+ class Factory(
+ private val context: Context,
+ private val interactor: KeyguardQuickAffordancePickerInteractor,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ @Suppress("UNCHECKED_CAST")
+ return KeyguardQuickAffordancePickerViewModel(
+ context = context,
+ interactor = interactor,
+ )
+ as T
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
new file mode 100644
index 0000000..bb9b29b
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.viewmodel
+
+/** Models UI state for a single lock screen quick affordance slot in a picker experience. */
+data class KeyguardQuickAffordanceSlotViewModel(
+ /** User-visible name for the slot. */
+ val name: String,
+
+ /** Whether this is the currently-selected slot in the picker. */
+ val isSelected: Boolean,
+
+ /**
+ * The list of quick affordances selected for this slot.
+ *
+ * Useful for preview.
+ */
+ val selectedQuickAffordances: List<KeyguardQuickAffordanceViewModel>,
+
+ /**
+ * The maximum number of quick affordances that can be selected for this slot.
+ *
+ * Useful for picker and preview.
+ */
+ val maxSelectedQuickAffordances: Int,
+
+ /** Notifies that the slot has been clicked by the user. */
+ val onClicked: (() -> Unit)?,
+)
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt
new file mode 100644
index 0000000..3860a9f
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class KeyguardQuickAffordanceSummaryViewModel(
+ val description: String,
+ val icon1: Drawable?,
+ val icon2: Drawable?,
+ val isIconSpacingVisible: Boolean,
+)
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
new file mode 100644
index 0000000..d720b0c
--- /dev/null
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.picker.quickaffordance.ui.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.android.wallpaper.R
+
+/** Models UI state for a single lock screen quick affordance in a picker experience. */
+data class KeyguardQuickAffordanceViewModel(
+ /** An icon for the quick affordance. */
+ val icon: Drawable,
+
+ /** A content description for the icon. */
+ val contentDescription: String,
+
+ /** Whether this quick affordance is selected in its slot. */
+ val isSelected: Boolean,
+
+ /** Whether this quick affordance is enabled. */
+ val isEnabled: Boolean,
+
+ /** Notifies that the quick affordance has been clicked by the user. */
+ val onClicked: (() -> Unit)?,
+) {
+ companion object {
+ @SuppressLint("UseCompatLoadingForDrawables")
+ fun none(
+ context: Context,
+ isSelected: Boolean,
+ onSelected: () -> Unit,
+ ): KeyguardQuickAffordanceViewModel {
+ return KeyguardQuickAffordanceViewModel(
+ icon = checkNotNull(context.getDrawable(R.drawable.link_off)),
+ contentDescription = context.getString(R.string.keyguard_affordance_none),
+ isSelected = isSelected,
+ onClicked =
+ if (isSelected) {
+ null
+ } else {
+ onSelected
+ },
+ isEnabled = true,
+ )
+ }
+ }
+}
diff --git a/tests/src/com/android/customization/model/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt
new file mode 100644
index 0000000..4a88f3b
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.model.picker.quickaffordance.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordancePickerRepositoryTest {
+
+ private lateinit var underTest: KeyguardQuickAffordancePickerRepository
+
+ private lateinit var testScope: TestScope
+ private lateinit var client: FakeKeyguardQuickAffordanceProviderClient
+
+ @Before
+ fun setUp() {
+ client = FakeKeyguardQuickAffordanceProviderClient()
+ val coroutineDispatcher = UnconfinedTestDispatcher()
+ testScope = TestScope(coroutineDispatcher)
+ Dispatchers.setMain(coroutineDispatcher)
+
+ underTest =
+ KeyguardQuickAffordancePickerRepository(
+ client = client,
+ backgroundDispatcher = coroutineDispatcher,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `isFeatureEnabled - enabled`() =
+ testScope.runTest {
+ client.setFlag(
+ com.android.systemui.shared.quickaffordance.data.content
+ .KeyguardQuickAffordanceProviderContract
+ .FlagsTable
+ .FLAG_NAME_FEATURE_ENABLED,
+ true,
+ )
+ val values = mutableListOf<Boolean>()
+ val job = launch { underTest.isFeatureEnabled.toList(values) }
+
+ assertThat(values.last()).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFeatureEnabled - not enabled`() =
+ testScope.runTest {
+ client.setFlag(
+ com.android.systemui.shared.quickaffordance.data.content
+ .KeyguardQuickAffordanceProviderContract
+ .FlagsTable
+ .FLAG_NAME_FEATURE_ENABLED,
+ false,
+ )
+ val values = mutableListOf<Boolean>()
+ val job = launch { underTest.isFeatureEnabled.toList(values) }
+
+ assertThat(values.last()).isFalse()
+
+ job.cancel()
+ }
+}
diff --git a/tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
new file mode 100644
index 0000000..d8a136d
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.model.picker.quickaffordance.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordancePickerInteractorTest {
+
+ private lateinit var underTest: KeyguardQuickAffordancePickerInteractor
+
+ private lateinit var testScope: TestScope
+ private lateinit var client: FakeKeyguardQuickAffordanceProviderClient
+
+ @Before
+ fun setUp() {
+ val coroutineDispatcher = UnconfinedTestDispatcher()
+ testScope = TestScope(coroutineDispatcher)
+ Dispatchers.setMain(coroutineDispatcher)
+ client = FakeKeyguardQuickAffordanceProviderClient()
+ underTest =
+ KeyguardQuickAffordancePickerInteractor(
+ repository =
+ KeyguardQuickAffordancePickerRepository(
+ client = client,
+ backgroundDispatcher = coroutineDispatcher,
+ ),
+ client = client,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun select() =
+ testScope.runTest {
+ val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
+ val job = launch { underTest.selections.toList(selections) }
+
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ )
+ assertThat(selections.last())
+ .isEqualTo(
+ listOf(
+ KeyguardQuickAffordancePickerSelectionModel(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ ),
+ )
+ )
+
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+ )
+ assertThat(selections.last())
+ .isEqualTo(
+ listOf(
+ KeyguardQuickAffordancePickerSelectionModel(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+ ),
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun unselect() =
+ testScope.runTest {
+ val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
+ val job = launch { underTest.selections.toList(selections) }
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ )
+
+ underTest.unselect(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ )
+
+ assertThat(selections.last()).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun unselectAll() =
+ testScope.runTest {
+ client.setSlotCapacity(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, 3)
+ val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
+ val job = launch { underTest.selections.toList(selections) }
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ )
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+ )
+ underTest.select(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3,
+ )
+
+ underTest.unselectAll(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ )
+
+ assertThat(selections.last()).isEmpty()
+
+ job.cancel()
+ }
+}
diff --git a/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
new file mode 100644
index 0000000..756ffb4
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
@@ -0,0 +1,461 @@
+/*
+ * 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.
+ *
+ */
+
+package com.android.customization.model.picker.quickaffordance.ui.viewmodel
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceSlotViewModel
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceSummaryViewModel
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordancePickerViewModelTest {
+
+ private lateinit var underTest: KeyguardQuickAffordancePickerViewModel
+
+ private lateinit var context: Context
+ private lateinit var testScope: TestScope
+ private lateinit var client: FakeKeyguardQuickAffordanceProviderClient
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ val coroutineDispatcher = UnconfinedTestDispatcher()
+ testScope = TestScope(coroutineDispatcher)
+ Dispatchers.setMain(coroutineDispatcher)
+ client = FakeKeyguardQuickAffordanceProviderClient()
+
+ underTest =
+ KeyguardQuickAffordancePickerViewModel.Factory(
+ context = context,
+ interactor =
+ KeyguardQuickAffordancePickerInteractor(
+ repository =
+ KeyguardQuickAffordancePickerRepository(
+ client = client,
+ backgroundDispatcher = coroutineDispatcher,
+ ),
+ client = client,
+ ),
+ )
+ .create(KeyguardQuickAffordancePickerViewModel::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `Select an affordance for each side`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ }
+
+ // Initially, the first slot is selected with the "none" affordance selected.
+ assertPickerUiState(
+ slots = slots.last(),
+ affordances = quickAffordances.last(),
+ selectedSlotText = "Left button",
+ selectedAffordanceText = "None",
+ )
+ assertPreviewUiState(
+ slots = slots.last(),
+ expectedAffordanceNameBySlotId =
+ mapOf(
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to null,
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to null,
+ ),
+ )
+
+ // Select "affordance 1" for the first slot.
+ quickAffordances.last()[1].onClicked?.invoke()
+ assertPickerUiState(
+ slots = slots.last(),
+ affordances = quickAffordances.last(),
+ selectedSlotText = "Left button",
+ selectedAffordanceText = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ )
+ assertPreviewUiState(
+ slots = slots.last(),
+ expectedAffordanceNameBySlotId =
+ mapOf(
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to null,
+ ),
+ )
+
+ // Select an affordance for the second slot.
+ // First, switch to the second slot:
+ slots.last()[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]?.onClicked?.invoke()
+ // Second, select the "affordance 3" affordance:
+ quickAffordances.last()[3].onClicked?.invoke()
+ assertPickerUiState(
+ slots = slots.last(),
+ affordances = quickAffordances.last(),
+ selectedSlotText = "Right button",
+ selectedAffordanceText = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3,
+ )
+ assertPreviewUiState(
+ slots = slots.last(),
+ expectedAffordanceNameBySlotId =
+ mapOf(
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3,
+ ),
+ )
+
+ // Select a different affordance for the second slot.
+ quickAffordances.last()[2].onClicked?.invoke()
+ assertPickerUiState(
+ slots = slots.last(),
+ affordances = quickAffordances.last(),
+ selectedSlotText = "Right button",
+ selectedAffordanceText = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+ )
+ assertPreviewUiState(
+ slots = slots.last(),
+ expectedAffordanceNameBySlotId =
+ mapOf(
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+ ),
+ )
+
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `Unselect - AKA selecting the none affordance - on one side`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ }
+
+ // Select "affordance 1" for the first slot.
+ quickAffordances.last()[1].onClicked?.invoke()
+ // Select an affordance for the second slot.
+ // First, switch to the second slot:
+ slots.last()[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]?.onClicked?.invoke()
+ // Second, select the "affordance 3" affordance:
+ quickAffordances.last()[3].onClicked?.invoke()
+
+ // Switch back to the first slot:
+ slots.last()[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]?.onClicked?.invoke()
+ // Select the "none" affordance, which is always in position 0:
+ quickAffordances.last()[0].onClicked?.invoke()
+
+ assertPickerUiState(
+ slots = slots.last(),
+ affordances = quickAffordances.last(),
+ selectedSlotText = "Left button",
+ selectedAffordanceText = "None",
+ )
+ assertPreviewUiState(
+ slots = slots.last(),
+ expectedAffordanceNameBySlotId =
+ mapOf(
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to null,
+ KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+ FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3,
+ ),
+ )
+
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `Show enablement dialog when selecting a disabled affordance`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+ val dialog = mutableListOf<KeyguardQuickAffordancePickerViewModel.DialogViewModel?>()
+
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ add(launch { underTest.dialog.toList(dialog) })
+ }
+ val enablementInstructions = listOf("header", "enablementInstructions")
+ val enablementActionText = "enablementActionText"
+ val packageName = "packageName"
+ val action = "action"
+ val enablementActionComponentName = "$packageName/$action"
+ // Lets add a disabled affordance to the picker:
+ val affordanceIndex =
+ client.addAffordance(
+ KeyguardQuickAffordanceProviderClient.Affordance(
+ id = "disabled",
+ name = "disabled",
+ iconResourceId = 0,
+ isEnabled = false,
+ enablementInstructions = enablementInstructions,
+ enablementActionText = enablementActionText,
+ enablementActionComponentName = enablementActionComponentName,
+ )
+ )
+
+ // Lets try to select that disabled affordance:
+ quickAffordances.last()[affordanceIndex + 1].onClicked?.invoke()
+
+ // We expect there to be a dialog that should be shown:
+ assertThat(dialog.last()?.instructionHeader).isEqualTo(enablementInstructions[0])
+ assertThat(dialog.last()?.instructions)
+ .isEqualTo(enablementInstructions.subList(1, enablementInstructions.size))
+ assertThat(dialog.last()?.actionText).isEqualTo(enablementActionText)
+ assertThat(dialog.last()?.intent?.`package`).isEqualTo(packageName)
+ assertThat(dialog.last()?.intent?.action).isEqualTo(action)
+
+ // Once we report that the dialog has been dismissed by the user, we expect there to be
+ // no
+ // dialog to be shown:
+ underTest.onDialogDismissed()
+ assertThat(dialog.last()).isNull()
+
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `summary - affordance selected in both bottom-start and bottom-end`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+ val summary = mutableListOf<KeyguardQuickAffordanceSummaryViewModel>()
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ add(launch { underTest.summary.toList(summary) })
+ }
+
+ // Select "affordance 1" for the first slot.
+ quickAffordances.last()[1].onClicked?.invoke()
+ // Select an affordance for the second slot.
+ // First, switch to the second slot:
+ slots.last()[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]?.onClicked?.invoke()
+ // Second, select the "affordance 3" affordance:
+ quickAffordances.last()[3].onClicked?.invoke()
+
+ assertThat(summary.last())
+ .isEqualTo(
+ KeyguardQuickAffordanceSummaryViewModel(
+ description =
+ "${FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1}," +
+ " ${FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3}",
+ icon1 = FakeKeyguardQuickAffordanceProviderClient.ICON_1,
+ icon2 = FakeKeyguardQuickAffordanceProviderClient.ICON_3,
+ isIconSpacingVisible = true,
+ )
+ )
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `summary - affordance selected only on bottom-start`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+ val summary = mutableListOf<KeyguardQuickAffordanceSummaryViewModel>()
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ add(launch { underTest.summary.toList(summary) })
+ }
+
+ // Select "affordance 1" for the first slot.
+ quickAffordances.last()[1].onClicked?.invoke()
+
+ assertThat(summary.last())
+ .isEqualTo(
+ KeyguardQuickAffordanceSummaryViewModel(
+ description = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+ icon1 = FakeKeyguardQuickAffordanceProviderClient.ICON_1,
+ icon2 = null,
+ isIconSpacingVisible = false,
+ )
+ )
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `summary - affordance selected only on bottom-end`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+ val summary = mutableListOf<KeyguardQuickAffordanceSummaryViewModel>()
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ add(launch { underTest.summary.toList(summary) })
+ }
+
+ // Select an affordance for the second slot.
+ // First, switch to the second slot:
+ slots.last()[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]?.onClicked?.invoke()
+ // Second, select the "affordance 3" affordance:
+ quickAffordances.last()[3].onClicked?.invoke()
+
+ assertThat(summary.last())
+ .isEqualTo(
+ KeyguardQuickAffordanceSummaryViewModel(
+ description = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_3,
+ icon1 = null,
+ icon2 = FakeKeyguardQuickAffordanceProviderClient.ICON_3,
+ isIconSpacingVisible = false,
+ )
+ )
+ jobs.forEach { it.cancel() }
+ }
+
+ @Test
+ fun `summary - no affordances selected`() =
+ testScope.runTest {
+ val slots = mutableListOf<Map<String, KeyguardQuickAffordanceSlotViewModel>>()
+ val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
+ val summary = mutableListOf<KeyguardQuickAffordanceSummaryViewModel>()
+ val jobs = buildList {
+ add(launch { underTest.slots.toList(slots) })
+ add(launch { underTest.quickAffordances.toList(quickAffordances) })
+ add(launch { underTest.summary.toList(summary) })
+ }
+
+ assertThat(summary.last().description).isEqualTo("None")
+ assertThat(summary.last().icon1).isNotNull()
+ assertThat(summary.last().icon2).isNull()
+ assertThat(summary.last().isIconSpacingVisible).isFalse()
+ jobs.forEach { it.cancel() }
+ }
+
+ /**
+ * Asserts the entire picker UI state is what is expected. This includes the slot tabs and the
+ * affordance list.
+ *
+ * @param slots The observed slot view-models, keyed by slot ID
+ * @param affordances The observed affordances
+ * @param selectedSlotText The text of the slot that's expected to be selected
+ * @param selectedAffordanceText The text of the affordance that's expected to be selected
+ */
+ private fun assertPickerUiState(
+ slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
+ affordances: List<KeyguardQuickAffordanceViewModel>,
+ selectedSlotText: String,
+ selectedAffordanceText: String,
+ ) {
+ assertSlotTabUiState(
+ slots = slots,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ isSelected = "Left button" == selectedSlotText,
+ )
+ assertSlotTabUiState(
+ slots = slots,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ isSelected = "Right button" == selectedSlotText,
+ )
+
+ var foundSelectedAffordance = false
+ affordances.forEach { affordance ->
+ val nameMatchesSelectedName = affordance.contentDescription == selectedAffordanceText
+ assertWithMessage(
+ "Expected affordance with name \"${affordance.contentDescription}\" to have" +
+ " isSelected=$nameMatchesSelectedName but it was ${affordance.isSelected}"
+ )
+ .that(affordance.isSelected)
+ .isEqualTo(nameMatchesSelectedName)
+ foundSelectedAffordance = foundSelectedAffordance || nameMatchesSelectedName
+ }
+ assertWithMessage("No affordance is selected!").that(foundSelectedAffordance).isTrue()
+ }
+
+ /**
+ * Asserts that a slot tab has the correct UI state.
+ *
+ * @param slots The observed slot view-models, keyed by slot ID
+ * @param slotId the ID of the slot to assert
+ * @param isSelected Whether that slot should be selected
+ */
+ private fun assertSlotTabUiState(
+ slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
+ slotId: String,
+ isSelected: Boolean,
+ ) {
+ val viewModel = slots[slotId] ?: error("No slot with ID \"$slotId\"!")
+ assertThat(viewModel.isSelected).isEqualTo(isSelected)
+ }
+
+ /**
+ * Asserts the UI state of the preview.
+ *
+ * @param slots The observed slot view-models, keyed by slot ID
+ * @param expectedAffordanceNameBySlotId The expected name of the selected affordance for each
+ * slot ID or `null` if it's expected for there to be no affordance for that slot in the preview
+ */
+ private fun assertPreviewUiState(
+ slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
+ expectedAffordanceNameBySlotId: Map<String, String?>,
+ ) {
+ slots.forEach { (slotId, slotViewModel) ->
+ val expectedAffordanceName = expectedAffordanceNameBySlotId[slotId]
+ val actualAffordanceName =
+ slotViewModel.selectedQuickAffordances.firstOrNull()?.contentDescription
+ assertWithMessage(
+ "At slotId=\"$slotId\", expected affordance=\"$expectedAffordanceName\" but" +
+ " was \"$actualAffordanceName\"!"
+ )
+ .that(actualAffordanceName)
+ .isEqualTo(expectedAffordanceName)
+ }
+ }
+}
diff --git a/tests/src/com/android/customization/testing/TestCustomizationInjector.java b/tests/src/com/android/customization/testing/TestCustomizationInjector.java
index dbbdb74..15898c1 100644
--- a/tests/src/com/android/customization/testing/TestCustomizationInjector.java
+++ b/tests/src/com/android/customization/testing/TestCustomizationInjector.java
@@ -10,11 +10,18 @@
import com.android.customization.module.CustomizationInjector;
import com.android.customization.module.CustomizationPreferences;
import com.android.customization.module.ThemesUserEventLogger;
+import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository;
+import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor;
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel;
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient;
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl;
import com.android.wallpaper.module.DrawableLayerResolver;
import com.android.wallpaper.module.PackageStatusNotifier;
import com.android.wallpaper.module.UserEventLogger;
import com.android.wallpaper.testing.TestInjector;
+import kotlinx.coroutines.Dispatchers;
+
/**
* Test implementation of the dependency injector.
*/
@@ -24,6 +31,9 @@
private PackageStatusNotifier mPackageStatusNotifier;
private DrawableLayerResolver mDrawableLayerResolver;
private UserEventLogger mUserEventLogger;
+ private KeyguardQuickAffordancePickerInteractor mKeyguardQuickAffordancePickerInteractor;
+ private KeyguardQuickAffordancePickerViewModel.Factory
+ mKeyguardQuickAffordancePickerViewModelFactory;
@Override
public CustomizationPreferences getCustomizationPreferences(Context context) {
@@ -68,4 +78,17 @@
}
return mUserEventLogger;
}
+
+ @Override
+ public KeyguardQuickAffordancePickerInteractor getKeyguardQuickAffordancePickerInteractor(
+ Context context) {
+ if (mKeyguardQuickAffordancePickerInteractor == null) {
+ final KeyguardQuickAffordanceProviderClient client =
+ new KeyguardQuickAffordanceProviderClientImpl(context, Dispatchers.getIO());
+ mKeyguardQuickAffordancePickerInteractor = new KeyguardQuickAffordancePickerInteractor(
+ new KeyguardQuickAffordancePickerRepository(client, Dispatchers.getIO()),
+ client);
+ }
+ return mKeyguardQuickAffordancePickerInteractor;
+ }
}