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: Id8af47d6479a237b1837f4c76bb4d107b578cb62
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()
+ }
}