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)
+    }
+}