Merge "Import translations. DO NOT MERGE ANYWHERE" into tm-qpr-dev
diff --git a/res/layout/clock_carousel_view.xml b/res/layout/clock_carousel_view.xml
new file mode 100644
index 0000000..996eaa3
--- /dev/null
+++ b/res/layout/clock_carousel_view.xml
@@ -0,0 +1,102 @@
+<?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.
+-->
+<androidx.constraintlayout.motion.widget.MotionLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/motion_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layoutDescription="@xml/carousel_scene">
+
+ <FrameLayout
+ android:id="@+id/item_view_0"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_marginEnd="16dp"
+ android:scaleType="centerCrop"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/item_view_1"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <FrameLayout
+ android:id="@+id/item_view_1"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_marginEnd="16dp"
+ android:scaleType="centerCrop"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/item_view_2"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <FrameLayout
+ android:id="@+id/item_view_2"
+ android:layout_width="150dp"
+ android:layout_height="150dp"
+ android:scaleType="centerCrop"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <FrameLayout
+ android:id="@+id/item_view_3"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_marginStart="16dp"
+ android:scaleType="centerCrop"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/item_view_2"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <FrameLayout
+ android:id="@+id/item_view_4"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:layout_marginStart="16dp"
+ android:scaleType="centerCrop"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/item_view_3"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.constraintlayout.helper.widget.Carousel
+ android:id="@+id/carousel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:carousel_backwardTransition="@+id/backward"
+ app:carousel_firstView="@+id/item_view_2"
+ app:carousel_forwardTransition="@+id/forward"
+ app:carousel_infinite="true"
+ app:carousel_nextState="@+id/next"
+ app:carousel_previousState="@+id/previous"
+ app:constraint_referenced_ids="item_view_0,item_view_1,item_view_2,item_view_3,item_view_4" />
+
+ <!-- The guidelines make sure that only the view in the middle show between the lines -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="100dp" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="100dp" />
+</androidx.constraintlayout.motion.widget.MotionLayout>
\ No newline at end of file
diff --git a/res/layout/fragment_clock_carousel_demo.xml b/res/layout/fragment_clock_carousel_demo.xml
new file mode 100644
index 0000000..6a54bcb
--- /dev/null
+++ b/res/layout/fragment_clock_carousel_demo.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/section_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <include layout="@layout/section_header" />
+ </FrameLayout>
+
+ <com.android.customization.picker.clock.ui.view.ClockCarouselView
+ android:id="@+id/image_carousel_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/xml/carousel_scene.xml b/res/xml/carousel_scene.xml
new file mode 100644
index 0000000..05007f4
--- /dev/null
+++ b/res/xml/carousel_scene.xml
@@ -0,0 +1,133 @@
+<?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.
+-->
+<MotionScene
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:motion="http://schemas.android.com/apk/res-auto">
+
+ <Transition
+ motion:constraintSetStart="@id/start"
+ motion:constraintSetEnd="@+id/next"
+ motion:duration="1000"
+ android:id="@+id/forward">
+ <OnSwipe
+ motion:dragDirection="dragLeft"
+ motion:touchAnchorSide="left" />
+ </Transition>
+
+ <Transition
+ motion:constraintSetStart="@+id/start"
+ motion:constraintSetEnd="@+id/previous"
+ android:id="@+id/backward">
+ <OnSwipe
+ motion:dragDirection="dragRight"
+ motion:touchAnchorSide="right" />
+ </Transition>
+
+ <ConstraintSet android:id="@+id/previous">
+ <Constraint
+ android:id="@+id/item_view_0"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintEnd_toStartOf="@id/guideline_start"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_1"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ motion:layout_constraintDimensionRatio="1:1"
+ motion:layout_constraintHorizontal_bias="0.5"
+ motion:layout_constraintStart_toStartOf="@id/guideline_start"
+ motion:layout_constraintEnd_toEndOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_2"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintStart_toStartOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp" />
+ </ConstraintSet>
+
+ <ConstraintSet android:id="@+id/start">
+ <Constraint
+ android:id="@+id/item_view_1"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintEnd_toStartOf="@id/guideline_start"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ motion:layout_constraintDimensionRatio="1:1"
+ motion:layout_constraintHorizontal_bias="0.5"
+ motion:layout_constraintStart_toStartOf="@id/guideline_start"
+ motion:layout_constraintEnd_toEndOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_3"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintStart_toStartOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp" />
+ </ConstraintSet>
+
+ <ConstraintSet android:id="@+id/next">
+ <Constraint
+ android:id="@+id/item_view_2"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintEnd_toStartOf="@id/guideline_start"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_3"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ motion:layout_constraintDimensionRatio="1:1"
+ motion:layout_constraintHorizontal_bias="0.5"
+ motion:layout_constraintStart_toStartOf="@id/guideline_start"
+ motion:layout_constraintEnd_toEndOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp" />
+ <Constraint
+ android:id="@+id/item_view_4"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ motion:layout_constraintStart_toStartOf="@id/guideline_end"
+ motion:layout_constraintTop_toTopOf="parent"
+ motion:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="16dp" />
+ </ConstraintSet>
+
+</MotionScene>
\ No newline at end of file
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
index 8f163b7..c7fa2c5 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
@@ -25,9 +25,13 @@
* clocks.
*/
interface ClockPickerRepository {
- val selectedClock: Flow<ClockMetadataModel?>
+ val allClocks: Array<ClockMetadataModel>
+
+ val selectedClock: Flow<ClockMetadataModel>
val selectedClockSize: Flow<ClockSize>
+ fun setSelectedClock(clockId: String)
+
fun setClockSize(size: ClockSize)
}
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
index 1c50517..d5830e2 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
@@ -16,7 +16,6 @@
*/
package com.android.customization.picker.clock.data.repository
-import android.util.Log
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import com.android.systemui.plugins.ClockMetadata
@@ -28,19 +27,24 @@
import kotlinx.coroutines.flow.callbackFlow
/** Implementation of [ClockPickerRepository], using [ClockRegistry]. */
-class ClockPickerRepositoryImpl(registry: ClockRegistry) : ClockPickerRepository {
+class ClockPickerRepositoryImpl(private val registry: ClockRegistry) : ClockPickerRepository {
+
+ override val allClocks: Array<ClockMetadataModel> =
+ registry
+ .getClocks()
+ .filter { "NOT_IN_USE" !in it.clockId }
+ .map { it.toModel() }
+ .toTypedArray()
/** The currently-selected clock. */
- override val selectedClock: Flow<ClockMetadataModel?> = callbackFlow {
+ override val selectedClock: Flow<ClockMetadataModel> = callbackFlow {
fun send() {
val model =
registry
.getClocks()
.find { clockMetadata -> clockMetadata.clockId == registry.currentClockId }
?.toModel()
- if (model == null) {
- Log.e(TAG, "Currently selected clock ID is not one of the available clocks.")
- }
+ checkNotNull(model)
trySend(model)
}
@@ -50,6 +54,10 @@
awaitClose { registry.unregisterClockChangeListener(listener) }
}
+ override fun setSelectedClock(clockId: String) {
+ registry.currentClockId = clockId
+ }
+
// TODO(b/262924055): Use the shared system UI component to query the clock size
private val _selectedClockSize = MutableStateFlow(ClockSize.DYNAMIC)
override val selectedClockSize: Flow<ClockSize> = _selectedClockSize.asStateFlow()
@@ -61,8 +69,4 @@
private fun ClockMetadata.toModel(): ClockMetadataModel {
return ClockMetadataModel(clockId = clockId, name = name)
}
-
- companion object {
- private const val TAG = "ClockPickerRepositoryImpl"
- }
}
diff --git a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
index a0bf14e..627c4a9 100644
--- a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
@@ -27,10 +27,16 @@
* clocks.
*/
class ClockPickerInteractor(private val repository: ClockPickerRepository) {
- val selectedClock: Flow<ClockMetadataModel?> = repository.selectedClock
+ val allClocks: Array<ClockMetadataModel> = repository.allClocks
+
+ val selectedClock: Flow<ClockMetadataModel> = repository.selectedClock
val selectedClockSize: Flow<ClockSize> = repository.selectedClockSize
+ fun setSelectedClock(clockId: String) {
+ repository.setSelectedClock(clockId)
+ }
+
fun setClockSize(size: ClockSize) {
repository.setClockSize(size)
}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
new file mode 100644
index 0000000..595057b
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.binder
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+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.launch
+
+object ClockCarouselViewBinder {
+ fun bind(
+ view: ClockCarouselView,
+ viewModel: ClockCarouselViewModel,
+ clockViewFactory: (clockId: String) -> View,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ view.setUpImageCarouselView(
+ clockIds = viewModel.allClockIds,
+ onGetClockPreview = clockViewFactory,
+ onClockSelected = { clockId -> viewModel.setSelectedClock(clockId) }
+ )
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch { viewModel.selectedClockId.collect { view.setSelectedClockId(it) } }
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt
new file mode 100644
index 0000000..a4b1c1e
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.lifecycleScope
+import com.android.customization.module.ThemePickerInjector
+import com.android.customization.picker.clock.ui.binder.ClockCarouselViewBinder
+import com.android.customization.picker.clock.ui.view.ClockCarouselView
+import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
+import com.android.wallpaper.R
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class ClockCarouselDemoFragment : AppbarFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val injector = InjectorProvider.getInjector() as ThemePickerInjector
+ val view = inflater.inflate(R.layout.fragment_clock_carousel_demo, container, false)
+ setUpToolbar(view)
+ val carouselView = view.requireViewById<ClockCarouselView>(R.id.image_carousel_view)
+ lifecycleScope.launch {
+ val registry =
+ withContext(Dispatchers.IO) {
+ injector.getClockRegistryProvider(requireContext()).get()
+ }
+ ClockCarouselViewBinder.bind(
+ view = carouselView,
+ viewModel =
+ ClockCarouselViewModel(
+ injector.getClockPickerInteractor(requireContext(), registry)
+ ),
+ clockViewFactory = { clockId ->
+ registry.createExampleClock(clockId)?.largeClock?.view!!
+ },
+ lifecycleOwner = this@ClockCarouselDemoFragment,
+ )
+ }
+
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return "Clock H-scroll Demo"
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
new file mode 100644
index 0000000..68ec014
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.constraintlayout.helper.widget.Carousel
+import com.android.wallpaper.R
+
+class ClockCarouselView(
+ context: Context,
+ attrs: AttributeSet,
+) :
+ FrameLayout(
+ context,
+ attrs,
+ ) {
+
+ private val carousel: Carousel
+ private lateinit var adapter: ClockCarouselAdapter
+
+ init {
+ val view = LayoutInflater.from(context).inflate(R.layout.clock_carousel_view, this)
+ carousel = view.requireViewById(R.id.carousel)
+ }
+
+ fun setUpImageCarouselView(
+ clockIds: Array<String>,
+ onGetClockPreview: (clockId: String) -> View,
+ onClockSelected: (clockId: String) -> Unit,
+ ) {
+ adapter = ClockCarouselAdapter(clockIds, onGetClockPreview, onClockSelected)
+ carousel.setAdapter(adapter)
+ }
+
+ fun setSelectedClockId(
+ selectedClockId: String,
+ ) {
+ carousel.jumpToIndex(adapter.clockIds.indexOf(selectedClockId))
+ }
+
+ class ClockCarouselAdapter(
+ val clockIds: Array<String>,
+ val onGetClockPreview: (clockId: String) -> View,
+ val onClockSelected: (clockId: String) -> Unit,
+ ) : Carousel.Adapter {
+
+ override fun count(): Int {
+ return clockIds.size
+ }
+
+ override fun populate(view: View?, index: Int) {
+ val viewGroup = view as ViewGroup
+ viewGroup.removeAllViews()
+ viewGroup.addView(onGetClockPreview(clockIds[index]))
+ }
+
+ override fun onNewItem(index: Int) {
+ onClockSelected.invoke(clockIds[index])
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
new file mode 100644
index 0000000..b126a73
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.viewmodel
+
+import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class ClockCarouselViewModel(private val interactor: ClockPickerInteractor) {
+ val selectedClockId: Flow<String> = interactor.selectedClock.map { it.clockId }
+
+ val allClockIds: Array<String> = interactor.allClocks.map { it.clockId }.toTypedArray()
+
+ fun setSelectedClock(clockId: String) {
+ interactor.setSelectedClock(clockId)
+ }
+}
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
index f50bb9c..26fbf63 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
@@ -14,7 +14,6 @@
* limitations under the License.
*
*/
-
package com.android.customization.picker.clock.ui.viewmodel
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
@@ -24,5 +23,5 @@
/** View model for the clock section view on the lockscreen customization surface. */
class ClockSectionViewModel(interactor: ClockPickerInteractor) {
- val selectedClockName: Flow<String?> = interactor.selectedClock.map { it?.name }
+ val selectedClockName: Flow<String> = interactor.selectedClock.map { it.name }
}
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 0ff0cab..af045d5 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
@@ -23,12 +23,31 @@
class FakeClockPickerRepository : ClockPickerRepository {
- override val selectedClock: Flow<ClockMetadataModel?> = MutableStateFlow(null)
+ override val allClocks: Array<ClockMetadataModel> = fakeClocks
+
+ private val _selectedClock = MutableStateFlow(fakeClocks[0])
+ override val selectedClock: Flow<ClockMetadataModel> = _selectedClock.asStateFlow()
private val _selectedClockSize = MutableStateFlow(ClockSize.LARGE)
override val selectedClockSize: Flow<ClockSize> = _selectedClockSize.asStateFlow()
+ override fun setSelectedClock(clockId: String) {
+ val clock = fakeClocks.find { it.clockId == clockId }
+ checkNotNull(clock)
+ _selectedClock.value = clock
+ }
+
override fun setClockSize(size: ClockSize) {
_selectedClockSize.value = size
}
+
+ companion object {
+ val fakeClocks =
+ arrayOf(
+ ClockMetadataModel("clock0", "clock0"),
+ ClockMetadataModel("clock1", "clock1"),
+ ClockMetadataModel("clock2", "clock2"),
+ ClockMetadataModel("clock3", "clock3"),
+ )
+ }
}
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
new file mode 100644
index 0000000..776663e
--- /dev/null
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.viewmodel
+
+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.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+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 ClockCarouselViewModelTest {
+
+ private lateinit var underTest: ClockCarouselViewModel
+
+ @Before
+ fun setUp() {
+ val testDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(testDispatcher)
+ underTest = ClockCarouselViewModel(ClockPickerInteractor(FakeClockPickerRepository()))
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun setSelectedClock() = runTest {
+ val observedSelectedClock = collectLastValue(underTest.selectedClockId)
+
+ underTest.setSelectedClock(FakeClockPickerRepository.fakeClocks[2].clockId)
+ assertThat(observedSelectedClock())
+ .isEqualTo(FakeClockPickerRepository.fakeClocks[2].clockId)
+ }
+}