Customizable lock screen affordance entry-point. am: abda67b66e

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/20601630

Change-Id: I6078aef1e634239fe6f61ad68468a397f2f45c7e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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..4e14fc8
--- /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_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/values/strings.xml b/res/values/strings.xml
index f645dc4..1bb5f3c 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -313,4 +313,28 @@
     [CHAR LIMIT=10].
     -->
     <string name="keyguard_affordance_enablement_dialog_dismiss_button">Done</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_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/src/com/android/customization/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt b/robolectric_tests/src/com/android/customization/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt
deleted file mode 100644
index 771fd3b..0000000
--- a/robolectric_tests/src/com/android/customization/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepositoryTest.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.quickaffordance.data.repository
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-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 client: FakeKeyguardQuickAffordanceProviderClient
-
-    @Before
-    fun setUp() {
-        client = FakeKeyguardQuickAffordanceProviderClient()
-
-        underTest =
-            KeyguardQuickAffordancePickerRepository(
-                client = client,
-            )
-    }
-
-    @Test
-    fun `isFeatureEnabled - enabled`() = runTest {
-        client.setFlag(
-            com.android.systemui.shared.quickaffordance.data.content
-                .KeyguardQuickAffordanceProviderContract
-                .FlagsTable
-                .FLAG_NAME_FEATURE_ENABLED,
-            true,
-        )
-        val values = mutableListOf<Boolean>()
-        val job = launch(UnconfinedTestDispatcher()) { underTest.isFeatureEnabled.toList(values) }
-
-        assertThat(values.last()).isTrue()
-
-        job.cancel()
-    }
-
-    @Test
-    fun `isFeatureEnabled - not enabled`() = runTest {
-        client.setFlag(
-            com.android.systemui.shared.quickaffordance.data.content
-                .KeyguardQuickAffordanceProviderContract
-                .FlagsTable
-                .FLAG_NAME_FEATURE_ENABLED,
-            false,
-        )
-        val values = mutableListOf<Boolean>()
-        val job = launch(UnconfinedTestDispatcher()) { underTest.isFeatureEnabled.toList(values) }
-
-        assertThat(values.last()).isFalse()
-
-        job.cancel()
-    }
-}
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/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
similarity index 72%
rename from src/com/android/customization/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
rename to src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
index 480e113..5846107 100644
--- a/src/com/android/customization/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
+++ b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt
@@ -15,30 +15,29 @@
  *
  */
 
-package com.android.customization.quickaffordance.data.repository
+package com.android.customization.picker.quickaffordance.data.repository
 
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerAffordanceModel as AffordanceModel
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel as SelectionModel
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerSlotModel as SlotModel
+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(
-    client: Client,
+    private val client: Client,
+    private val backgroundDispatcher: CoroutineDispatcher,
 ) {
     /** Whether the feature is enabled. */
     val isFeatureEnabled: Flow<Boolean> =
-        client.observeFlags().map { flags ->
-            flags
-                .find { flag -> flag.name == Contract.FlagsTable.FLAG_NAME_FEATURE_ENABLED }
-                ?.value == true
-        }
+        client.observeFlags().map { flags -> flags.isFeatureEnabled() }
 
     /** List of slots available on the device. */
     val slots: Flow<List<SlotModel>> =
@@ -56,6 +55,15 @@
             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,
diff --git a/src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
similarity index 78%
rename from src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
rename to src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
index f60fc12..87cedf5 100644
--- a/src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
+++ b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt
@@ -15,14 +15,14 @@
  *
  */
 
-package com.android.customization.quickaffordance.domain.interactor
+package com.android.customization.picker.quickaffordance.domain.interactor
 
 import android.graphics.drawable.Drawable
 import androidx.annotation.DrawableRes
-import com.android.customization.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerAffordanceModel as AffordanceModel
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel as SelectionModel
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerSlotModel as SlotModel
+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
 
@@ -31,7 +31,7 @@
  * the lock screen.
  */
 class KeyguardQuickAffordancePickerInteractor(
-    repository: KeyguardQuickAffordancePickerRepository,
+    private val repository: KeyguardQuickAffordancePickerRepository,
     private val client: Client,
 ) {
     /** Whether the feature is enabled. */
@@ -83,4 +83,9 @@
     ): 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/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
similarity index 95%
rename from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
rename to src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
index 0a61cc2..1b18af7 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerAffordanceModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+package com.android.customization.picker.quickaffordance.shared.model
 
 import androidx.annotation.DrawableRes
 
diff --git a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
similarity index 91%
rename from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
rename to src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
index d72bc71..eea8b2a 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+package com.android.customization.picker.quickaffordance.shared.model
 
 /** Models a selection of an affordance on a slot. */
 data class KeyguardQuickAffordancePickerSelectionModel(
diff --git a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
similarity index 92%
rename from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
rename to src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
index 7044e06..7e662e0 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+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(
diff --git a/src/com/android/customization/quickaffordance/ui/adapter/AffordancesAdapter.kt b/src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt
similarity index 93%
rename from src/com/android/customization/quickaffordance/ui/adapter/AffordancesAdapter.kt
rename to src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt
index f63fa7a..609c6ae 100644
--- a/src/com/android/customization/quickaffordance/ui/adapter/AffordancesAdapter.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/AffordancesAdapter.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.adapter
+package com.android.customization.picker.quickaffordance.ui.adapter
 
 import android.view.LayoutInflater
 import android.view.View
@@ -23,7 +23,7 @@
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
-import com.android.customization.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceViewModel
 import com.android.wallpaper.R
 
 /** Adapts between lock screen quick affordance items and views. */
diff --git a/src/com/android/customization/quickaffordance/ui/adapter/SlotTabAdapter.kt b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt
similarity index 92%
rename from src/com/android/customization/quickaffordance/ui/adapter/SlotTabAdapter.kt
rename to src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt
index 953b632..acafef4 100644
--- a/src/com/android/customization/quickaffordance/ui/adapter/SlotTabAdapter.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt
@@ -15,14 +15,14 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.adapter
+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.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceSlotViewModel
+import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordanceSlotViewModel
 import com.android.wallpaper.R
 
 /** Adapts between lock screen quick affordance slot items and views. */
diff --git a/src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
similarity index 93%
rename from src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
rename to src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
index 4486e86..605bd7b 100644
--- a/src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.binder
+package com.android.customization.picker.quickaffordance.ui.binder
 
 import android.app.AlertDialog
 import android.app.Dialog
@@ -28,9 +28,9 @@
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
-import com.android.customization.quickaffordance.ui.adapter.AffordancesAdapter
-import com.android.customization.quickaffordance.ui.adapter.SlotTabAdapter
-import com.android.customization.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+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
diff --git a/src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
similarity index 93%
rename from src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
rename to src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
index 399c033..13ee553 100644
--- a/src/com/android/customization/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerPreviewBinder.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.binder
+package com.android.customization.picker.quickaffordance.ui.binder
 
 import android.view.View
 import android.widget.ImageView
@@ -23,7 +23,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.customization.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel
+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
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/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
similarity index 60%
copy from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
copy to src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
index 7044e06..e910488 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSlotModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt
@@ -15,11 +15,16 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+package com.android.customization.picker.quickaffordance.ui.fragment
 
-/** 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,
-)
+import com.android.wallpaper.picker.AppbarFragment
+
+class KeyguardQuickAffordancePickerFragment : AppbarFragment() {
+    companion object {
+        fun newInstance(): KeyguardQuickAffordancePickerFragment {
+            return KeyguardQuickAffordancePickerFragment()
+        }
+    }
+
+    // TODO(b/254858701): implement this UI.
+}
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/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt b/src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt
similarity index 65%
copy from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
copy to src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt
index d72bc71..daace7d 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/view/KeyguardQuickAffordanceSectionView.kt
@@ -15,10 +15,17 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+package com.android.customization.picker.quickaffordance.ui.view
 
-/** Models a selection of an affordance on a slot. */
-data class KeyguardQuickAffordancePickerSelectionModel(
-    val slotId: String,
-    val affordanceId: String,
-)
+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/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
similarity index 81%
rename from src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
rename to src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
index e947c9a..e69c639 100644
--- a/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.viewmodel
+package com.android.customization.picker.quickaffordance.ui.viewmodel
 
 import android.annotation.SuppressLint
 import android.content.Context
@@ -25,7 +25,7 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
-import com.android.customization.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+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
@@ -34,6 +34,7 @@
 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. */
@@ -161,6 +162,34 @@
                 }
         }
 
+    @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.
@@ -257,6 +286,36 @@
         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,
diff --git a/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
similarity index 95%
rename from src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
rename to src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
index a0b77fa..bb9b29b 100644
--- a/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.viewmodel
+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(
diff --git a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt
similarity index 68%
copy from src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
copy to src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt
index d72bc71..3860a9f 100644
--- a/src/com/android/customization/quickaffordance/shared/model/KeyguardQuickAffordancePickerSelectionModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSummaryViewModel.kt
@@ -15,10 +15,13 @@
  *
  */
 
-package com.android.customization.quickaffordance.shared.model
+package com.android.customization.picker.quickaffordance.ui.viewmodel
 
-/** Models a selection of an affordance on a slot. */
-data class KeyguardQuickAffordancePickerSelectionModel(
-    val slotId: String,
-    val affordanceId: String,
+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/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
similarity index 96%
rename from src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
rename to src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 8f24145..d720b0c 100644
--- a/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -15,7 +15,7 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.viewmodel
+package com.android.customization.picker.quickaffordance.ui.viewmodel
 
 import android.annotation.SuppressLint
 import android.content.Context
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/robolectric_tests/src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
similarity index 88%
rename from robolectric_tests/src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
rename to tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
index 227fd6a..d8a136d 100644
--- a/robolectric_tests/src/com/android/customization/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractorTest.kt
@@ -15,11 +15,12 @@
  *
  */
 
-package com.android.customization.quickaffordance.domain.interactor
+package com.android.customization.model.picker.quickaffordance.domain.interactor
 
 import androidx.test.filters.SmallTest
-import com.android.customization.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
-import com.android.customization.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel
+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
@@ -59,6 +60,7 @@
                 repository =
                     KeyguardQuickAffordancePickerRepository(
                         client = client,
+                        backgroundDispatcher = coroutineDispatcher,
                     ),
                 client = client,
             )
@@ -73,7 +75,7 @@
     fun select() =
         testScope.runTest {
             val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
-            val job = launch(UnconfinedTestDispatcher()) { underTest.selections.toList(selections) }
+            val job = launch { underTest.selections.toList(selections) }
 
             underTest.select(
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
@@ -110,7 +112,7 @@
     fun unselect() =
         testScope.runTest {
             val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
-            val job = launch(UnconfinedTestDispatcher()) { underTest.selections.toList(selections) }
+            val job = launch { underTest.selections.toList(selections) }
             underTest.select(
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
@@ -131,7 +133,7 @@
         testScope.runTest {
             client.setSlotCapacity(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, 3)
             val selections = mutableListOf<List<KeyguardQuickAffordancePickerSelectionModel>>()
-            val job = launch(UnconfinedTestDispatcher()) { underTest.selections.toList(selections) }
+            val job = launch { underTest.selections.toList(selections) }
             underTest.select(
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
                 affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
diff --git a/robolectric_tests/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
similarity index 65%
rename from robolectric_tests/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
rename to tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
index 126b22b..756ffb4 100644
--- a/robolectric_tests/src/com/android/customization/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
@@ -15,14 +15,20 @@
  *
  */
 
-package com.android.customization.quickaffordance.ui.viewmodel
+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.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
-import com.android.customization.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
+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
@@ -47,11 +53,13 @@
 
     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)
@@ -59,12 +67,13 @@
 
         underTest =
             KeyguardQuickAffordancePickerViewModel.Factory(
-                    context = InstrumentationRegistry.getInstrumentation().targetContext,
+                    context = context,
                     interactor =
                         KeyguardQuickAffordancePickerInteractor(
                             repository =
                                 KeyguardQuickAffordancePickerRepository(
                                     client = client,
+                                    backgroundDispatcher = coroutineDispatcher,
                                 ),
                             client = client,
                         ),
@@ -84,12 +93,8 @@
             val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
 
             val jobs = buildList {
-                add(launch(UnconfinedTestDispatcher()) { underTest.slots.toList(slots) })
-                add(
-                    launch(UnconfinedTestDispatcher()) {
-                        underTest.quickAffordances.toList(quickAffordances)
-                    }
-                )
+                add(launch { underTest.slots.toList(slots) })
+                add(launch { underTest.quickAffordances.toList(quickAffordances) })
             }
 
             // Initially, the first slot is selected with the "none" affordance selected.
@@ -177,12 +182,8 @@
             val quickAffordances = mutableListOf<List<KeyguardQuickAffordanceViewModel>>()
 
             val jobs = buildList {
-                add(launch(UnconfinedTestDispatcher()) { underTest.slots.toList(slots) })
-                add(
-                    launch(UnconfinedTestDispatcher()) {
-                        underTest.quickAffordances.toList(quickAffordances)
-                    }
-                )
+                add(launch { underTest.slots.toList(slots) })
+                add(launch { underTest.quickAffordances.toList(quickAffordances) })
             }
 
             // Select "affordance 1" for the first slot.
@@ -225,13 +226,9 @@
             val dialog = mutableListOf<KeyguardQuickAffordancePickerViewModel.DialogViewModel?>()
 
             val jobs = buildList {
-                add(launch(UnconfinedTestDispatcher()) { underTest.slots.toList(slots) })
-                add(
-                    launch(UnconfinedTestDispatcher()) {
-                        underTest.quickAffordances.toList(quickAffordances)
-                    }
-                )
-                add(launch(UnconfinedTestDispatcher()) { underTest.dialog.toList(dialog) })
+                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"
@@ -241,17 +238,15 @@
             // Lets add a disabled affordance to the picker:
             val affordanceIndex =
                 client.addAffordance(
-                    com.android.systemui.shared.quickaffordance.data.content
-                        .KeyguardQuickAffordanceProviderClient
-                        .Affordance(
-                            id = "disabled",
-                            name = "disabled",
-                            iconResourceId = 0,
-                            isEnabled = false,
-                            enablementInstructions = enablementInstructions,
-                            enablementActionText = enablementActionText,
-                            enablementActionComponentName = enablementActionComponentName,
-                        )
+                    KeyguardQuickAffordanceProviderClient.Affordance(
+                        id = "disabled",
+                        name = "disabled",
+                        iconResourceId = 0,
+                        isEnabled = false,
+                        enablementInstructions = enablementInstructions,
+                        enablementActionText = enablementActionText,
+                        enablementActionComponentName = enablementActionComponentName,
+                    )
                 )
 
             // Lets try to select that disabled affordance:
@@ -274,6 +269,116 @@
             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.
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;
+    }
 }