Merge "[TP] Handle case for single default clock" into tm-qpr-dev am: a11d857a92 am: 1208b415b5 am: 2646a4871c

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

Change-Id: I149ed150f9ee0ed0a0dbed9a07384c4566f5662f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/layout/single_clock_view.xml b/res/layout/single_clock_view.xml
new file mode 100644
index 0000000..e7ac518
--- /dev/null
+++ b/res/layout/single_clock_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.wallpaper.picker.DisplayAspectRatioFrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/clock_carousel_item_height">
+    <FrameLayout
+        android:id="@+id/single_clock_host_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+</com.android.wallpaper.picker.DisplayAspectRatioFrameLayout>
\ No newline at end of file
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index 35a78cb..48b37ba 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -16,6 +16,8 @@
 package com.android.customization.picker.clock.ui.binder
 
 import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -23,7 +25,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.customization.picker.clock.ui.view.ClockCarouselView
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
-import kotlinx.coroutines.flow.collect
+import com.android.wallpaper.R
 import kotlinx.coroutines.launch
 
 object ClockCarouselViewBinder {
@@ -39,12 +41,17 @@
     @JvmStatic
     fun bind(
         view: ClockCarouselView,
+        singleClockView: ViewGroup,
         viewModel: ClockCarouselViewModel,
         clockViewFactory: (clockId: String) -> View,
         lifecycleOwner: LifecycleOwner,
     ): Binding {
+        val singleClockHostView =
+            singleClockView.requireViewById<FrameLayout>(R.id.single_clock_host_view)
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch { viewModel.isCarouselVisible.collect { view.isVisible = it } }
+
                 launch {
                     viewModel.allClockIds.collect { allClockIds ->
                         view.setUpClockCarouselView(
@@ -54,20 +61,34 @@
                         )
                     }
                 }
+
                 launch {
                     viewModel.selectedIndex.collect { selectedIndex ->
                         view.setSelectedClockIndex(selectedIndex)
                     }
                 }
+
+                launch { viewModel.isSingleClockViewVisible.collect { view.isVisible = it } }
+
+                launch {
+                    viewModel.clockId.collect { clockId ->
+                        singleClockHostView.removeAllViews()
+                        val clockView = clockViewFactory(clockId)
+                        // The clock view might still be attached to an existing parent. Detach
+                        // before adding to another parent.
+                        (clockView.parent as? ViewGroup)?.removeView(clockView)
+                        singleClockHostView.addView(clockView)
+                    }
+                }
             }
         }
         return object : Binding {
             override fun show() {
-                view.isVisible = true
+                viewModel.showClockCarousel(true)
             }
 
             override fun hide() {
-                view.isVisible = false
+                viewModel.showClockCarousel(false)
             }
         }
     }
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
index ecbb901..7ea0210 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
@@ -83,7 +83,7 @@
                 launch { viewModel.tabs.collect { tabAdapter.setItems(it) } }
 
                 launch {
-                    viewModel.selectedTabPosition.collect { tab ->
+                    viewModel.selectedTab.collect { tab ->
                         when (tab) {
                             ClockSettingsViewModel.Tab.COLOR -> {
                                 colorOptionContainer.isVisible = true
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
index 751661f..8d614e4 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -19,23 +19,38 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
 
+/**
+ * Clock carousel view model that provides data for the carousel of clock previews. When there is
+ * only one item, we should show a single clock preview instead of a carousel.
+ */
 class ClockCarouselViewModel(
     private val interactor: ClockPickerInteractor,
 ) {
-
     @OptIn(ExperimentalCoroutinesApi::class)
     val allClockIds: Flow<List<String>> =
-        interactor.allClocks.mapLatest { clockArray ->
+        interactor.allClocks.mapLatest { allClocks ->
             // Delay to avoid the case that the full list of clocks is not initiated.
             delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
-            clockArray.map { it.clockId }
+            allClocks.map { it.clockId }
         }
 
+    private val shouldShowCarousel = MutableStateFlow(false)
+    val isCarouselVisible: Flow<Boolean> =
+        combine(allClockIds.map { it.size > 1 }.distinctUntilChanged(), shouldShowCarousel) {
+                hasMoreThanOneClock,
+                shouldShowCarousel ->
+                hasMoreThanOneClock && shouldShowCarousel
+            }
+            .distinctUntilChanged()
+
     @OptIn(ExperimentalCoroutinesApi::class)
     val selectedIndex: Flow<Int> =
         allClockIds
@@ -51,10 +66,30 @@
             }
             .mapNotNull { it }
 
+    // Handle the case when there is only one clock in the carousel
+    private val shouldShowSingleClock = MutableStateFlow(false)
+    val isSingleClockViewVisible: Flow<Boolean> =
+        combine(allClockIds.map { it.size == 1 }.distinctUntilChanged(), shouldShowSingleClock) {
+                hasOneClock,
+                shouldShowSingleClock ->
+                hasOneClock && shouldShowSingleClock
+            }
+            .distinctUntilChanged()
+
+    val clockId: Flow<String> =
+        allClockIds
+            .map { allClockIds -> if (allClockIds.size == 1) allClockIds[0] else null }
+            .mapNotNull { it }
+
     fun setSelectedClock(clockId: String) {
         interactor.setSelectedClock(clockId)
     }
 
+    fun showClockCarousel(shouldShow: Boolean) {
+        shouldShowCarousel.value = shouldShow
+        shouldShowSingleClock.value = shouldShow
+    }
+
     companion object {
         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
     }
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
index c15bc67..41b4010 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -194,9 +194,9 @@
     }
 
     private val _selectedTabPosition = MutableStateFlow(Tab.COLOR)
-    val selectedTabPosition: StateFlow<Tab> = _selectedTabPosition.asStateFlow()
+    val selectedTab: StateFlow<Tab> = _selectedTabPosition.asStateFlow()
     val tabs: Flow<List<ClockSettingsTabViewModel>> =
-        selectedTabPosition.map {
+        selectedTab.map {
             listOf(
                 ClockSettingsTabViewModel(
                     name = context.resources.getString(R.string.clock_color),
diff --git a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
index f7fa9a5..2ca1f3b 100644
--- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
+++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
@@ -19,8 +19,8 @@
 
 import android.app.Activity
 import android.content.Context
+import android.view.ViewGroup
 import android.view.ViewStub
-import androidx.core.view.isGone
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import com.android.customization.picker.clock.ui.binder.ClockCarouselViewBinder
@@ -67,12 +67,17 @@
         val view = super.createView(context)
         val carouselViewStub: ViewStub = view.requireViewById(R.id.clock_carousel_view_stub)
         carouselViewStub.layoutResource = R.layout.clock_carousel_view
-        val carouselView: ClockCarouselView = carouselViewStub.inflate() as ClockCarouselView
-        carouselView.isGone = true
+        val carouselView = carouselViewStub.inflate() as ClockCarouselView
+
+        // TODO (b/270716937) We should handle the single clock case in the clock carousel itself
+        val singleClockViewStub: ViewStub = view.requireViewById(R.id.single_clock_view_stub)
+        singleClockViewStub.layoutResource = R.layout.single_clock_view
+        val singleClockView = singleClockViewStub.inflate() as ViewGroup
         lifecycleOwner.lifecycleScope.launch {
             clockCarouselBinding =
                 ClockCarouselViewBinder.bind(
                     view = carouselView,
+                    singleClockView = singleClockView,
                     viewModel = clockCarouselViewModel,
                     clockViewFactory = { clockId -> clockViewFactory.getView(clockId) },
                     lifecycleOwner = lifecycleOwner,
diff --git a/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt b/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
index d2a9efc..b2cb452 100644
--- a/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
+++ b/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
@@ -15,6 +15,7 @@
  */
 package com.android.customization.picker.clock.data.repository
 
+import com.android.customization.picker.clock.data.repository.FakeClockPickerRepository.Companion.fakeClocks
 import com.android.customization.picker.clock.shared.ClockSize
 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
 import kotlinx.coroutines.flow.Flow
@@ -22,9 +23,10 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 
-class FakeClockPickerRepository : ClockPickerRepository {
-    override val allClocks: Flow<List<ClockMetadataModel>> =
-        MutableStateFlow(fakeClocks).asStateFlow()
+/** By default [FakeClockPickerRepository] uses [fakeClocks]. */
+open class FakeClockPickerRepository(private val clocks: List<ClockMetadataModel> = fakeClocks) :
+    ClockPickerRepository {
+    override val allClocks: Flow<List<ClockMetadataModel>> = MutableStateFlow(clocks).asStateFlow()
 
     private val selectedClockId = MutableStateFlow(fakeClocks[0].clockId)
     private val clockColor = MutableStateFlow<Int?>(null)
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
index 8d7ec30..35c3518 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
@@ -18,6 +18,7 @@
 import androidx.test.filters.SmallTest
 import com.android.customization.picker.clock.data.repository.FakeClockPickerRepository
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
 import com.android.wallpaper.testing.collectLastValue
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
@@ -37,14 +38,20 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class ClockCarouselViewModelTest {
-
+    private val repositoryWithMultipleClocks by lazy { FakeClockPickerRepository() }
+    private val repositoryWithSingleClock by lazy {
+        FakeClockPickerRepository(
+            listOf(
+                ClockMetadataModel("clock0", "clock0", null),
+            )
+        )
+    }
     private lateinit var underTest: ClockCarouselViewModel
 
     @Before
     fun setUp() {
         val testDispatcher = StandardTestDispatcher()
         Dispatchers.setMain(testDispatcher)
-        underTest = ClockCarouselViewModel(ClockPickerInteractor(FakeClockPickerRepository()))
     }
 
     @After
@@ -54,9 +61,54 @@
 
     @Test
     fun setSelectedClock() = runTest {
+        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithMultipleClocks))
         val observedSelectedIndex = collectLastValue(underTest.selectedIndex)
         advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
         underTest.setSelectedClock(FakeClockPickerRepository.fakeClocks[2].clockId)
         assertThat(observedSelectedIndex()).isEqualTo(2)
     }
+
+    @Test
+    fun setShouldShowCarousel() = runTest {
+        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithMultipleClocks))
+        val observedIsCarouselVisible = collectLastValue(underTest.isCarouselVisible)
+        advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+        underTest.showClockCarousel(false)
+        assertThat(observedIsCarouselVisible()).isFalse()
+        underTest.showClockCarousel(true)
+        assertThat(observedIsCarouselVisible()).isTrue()
+    }
+
+    @Test
+    fun shouldNotShowCarouselWhenSingleClock() = runTest {
+        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithSingleClock))
+        val observedIsCarouselVisible = collectLastValue(underTest.isCarouselVisible)
+        advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+        underTest.showClockCarousel(false)
+        assertThat(observedIsCarouselVisible()).isFalse()
+        underTest.showClockCarousel(true)
+        assertThat(observedIsCarouselVisible()).isFalse()
+    }
+
+    @Test
+    fun setShouldShowSingleClock() = runTest {
+        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithSingleClock))
+        val observedIsSingleClockViewVisible = collectLastValue(underTest.isSingleClockViewVisible)
+        advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+        underTest.showClockCarousel(false)
+        assertThat(observedIsSingleClockViewVisible()).isFalse()
+        underTest.showClockCarousel(true)
+        assertThat(observedIsSingleClockViewVisible()).isTrue()
+    }
+
+    @Test
+    fun shouldNotShowSingleClockWhenMultipleClocks() = runTest {
+        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithMultipleClocks))
+        val observedIsSingleClockViewVisible = collectLastValue(underTest.isSingleClockViewVisible)
+        advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+        underTest.showClockCarousel(false)
+        assertThat(observedIsSingleClockViewVisible()).isFalse()
+        underTest.showClockCarousel(true)
+        assertThat(observedIsSingleClockViewVisible()).isFalse()
+    }
 }