Merge "[TP] Make carousel scale and alpha dimen res" into tm-qpr-dev
diff --git a/res/layout/fragment_clock_settings.xml b/res/layout/fragment_clock_settings.xml
index 088ec2a..5208222 100644
--- a/res/layout/fragment_clock_settings.xml
+++ b/res/layout/fragment_clock_settings.xml
@@ -41,6 +41,11 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
+ <FrameLayout
+ android:id="@+id/clock_host_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center" />
</com.android.wallpaper.picker.DisplayAspectRatioFrameLayout>
diff --git a/res/values/clock_colors.xml b/res/values/clock_colors.xml
new file mode 100644
index 0000000..1539a92
--- /dev/null
+++ b/res/values/clock_colors.xml
@@ -0,0 +1,73 @@
+<?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.
+-->
+<resources>
+ <string-array name="clock_color_ids">
+ <item>RED</item>
+ <item>ORANGE</item>
+ <item>YELLOW</item>
+ <item>GREEN</item>
+ <item>BLUE</item>
+ <item>INDIGO</item>
+ <item>VIOLET</item>
+ <item>GRAY</item>
+ <item>TEAL</item>
+ </string-array>
+ <array name="clock_color_names">
+ <item>@string/clock_color_red</item>
+ <item>@string/clock_color_orange</item>
+ <item>@string/clock_color_yellow</item>
+ <item>@string/clock_color_green</item>
+ <item>@string/clock_color_blue</item>
+ <item>@string/clock_color_indigo</item>
+ <item>@string/clock_color_violet</item>
+ <item>@string/clock_color_gray</item>
+ <item>@string/clock_color_teal</item>
+ </array>
+ <array name="clock_colors">
+ <item>#FFA3A7</item>
+ <item>#F7AC69</item>
+ <item>#FFC951</item>
+ <item>#88D86A</item>
+ <item>#8EC8FF</item>
+ <item>#B9AAFF</item>
+ <item>#F6A2FF</item>
+ <item>#B9B9B9</item>
+ <item>#40D9CF</item>
+ </array>
+ <array name="clock_color_tone_min">
+ <item>20</item>
+ <item>20</item>
+ <item>50</item>
+ <item>20</item>
+ <item>20</item>
+ <item>10</item>
+ <item>20</item>
+ <item>0</item>
+ <item>20</item>
+ </array>
+ <array name="clock_color_tone_max">
+ <item>95</item>
+ <item>95</item>
+ <item>95</item>
+ <item>99</item>
+ <item>95</item>
+ <item>95</item>
+ <item>97</item>
+ <item>100</item>
+ <item>99</item>
+ </array>
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3bd9e84..18c0103 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -45,6 +45,33 @@
<!-- Title of a tab to change the clock color. [CHAR LIMIT=15] -->
<string name="clock_color">Color</string>
+ <!-- Title of a clock color red option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_red">Red</string>
+
+ <!-- Title of a clock color orange option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_orange">Orange</string>
+
+ <!-- Title of a clock color yellow option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_yellow">Yellow</string>
+
+ <!-- Title of a clock color green option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_green">Green</string>
+
+ <!-- Title of a clock color blue option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_blue">Blue</string>
+
+ <!-- Title of a clock color indigo option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_indigo">Indigo</string>
+
+ <!-- Title of a clock color violet option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_violet">Violet</string>
+
+ <!-- Title of a clock color grey option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_gray">Grey</string>
+
+ <!-- Title of a clock color teal option that will appear in the description of the custom clock section. [CHAR LIMIT=15] -->
+ <string name="clock_color_teal">Teal</string>
+
<!-- Title of a tab to change the clock size. [CHAR LIMIT=15] -->
<string name="clock_size">Size</string>
diff --git a/src/com/android/customization/model/grid/GridOptionsManager.java b/src/com/android/customization/model/grid/GridOptionsManager.java
index bff7933..b7ee37f 100644
--- a/src/com/android/customization/model/grid/GridOptionsManager.java
+++ b/src/com/android/customization/model/grid/GridOptionsManager.java
@@ -49,6 +49,7 @@
private final LauncherGridOptionsProvider mProvider;
private final ThemesUserEventLogger mEventLogger;
+ private int mGridOptionSize = -1;
/** Returns the {@link GridOptionsManager} instance. */
public static GridOptionsManager getInstance(Context context) {
@@ -73,16 +74,17 @@
@Override
public boolean isAvailable() {
- int gridOptionSize = 0;
- try {
- gridOptionSize = sExecutorService.submit(() -> {
- List<GridOption> gridOptions = mProvider.fetch(/* reload= */true);
- return gridOptions == null ? 0 : gridOptions.size();
- }).get();
- } catch (InterruptedException | ExecutionException e) {
- Log.w(TAG, "could not get gridOptionSize", e);
+ if (mGridOptionSize < 0) {
+ try {
+ mGridOptionSize = sExecutorService.submit(() -> {
+ List<GridOption> gridOptions = mProvider.fetch(/* reload= */true);
+ return gridOptions == null ? 0 : gridOptions.size();
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.w(TAG, "could not get gridOptionSize", e);
+ }
}
- return gridOptionSize > 1 && mProvider.areGridsAvailable();
+ return mGridOptionSize > 1 && mProvider.areGridsAvailable();
}
@Override
diff --git a/src/com/android/customization/model/grid/GridSectionController.java b/src/com/android/customization/model/grid/GridSectionController.java
index 3e5dba0..c50bfcc 100644
--- a/src/com/android/customization/model/grid/GridSectionController.java
+++ b/src/com/android/customization/model/grid/GridSectionController.java
@@ -96,7 +96,7 @@
@Override
public void release() {
- if (mIsRevampedUiEnabled) {
+ if (mIsRevampedUiEnabled && mGridOptionsManager.isAvailable()) {
mGridOptionsManager.getOptionChangeObservable(/* handler= */ null).removeObserver(
mOptionChangeObserver
);
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
index 5ae283a..4e775c6 100644
--- a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -126,6 +126,7 @@
/**
* Returns an observable that receives a new value each time that the grid options are changed.
+ * Do not call if {@link #areGridsAvailable()} returns false
*/
public LiveData<Object> getOptionChangeObservable(
@Nullable Handler handler) {
diff --git a/src/com/android/customization/model/grid/data/repository/GridRepository.kt b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
index 7c84aec..9a3be0c 100644
--- a/src/com/android/customization/model/grid/data/repository/GridRepository.kt
+++ b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
@@ -35,7 +35,8 @@
import kotlinx.coroutines.withContext
interface GridRepository {
- val optionChanges: Flow<Unit>
+ suspend fun isAvailable(): Boolean
+ fun getOptionChanges(): Flow<Unit>
suspend fun getOptions(): GridOptionItemsModel
}
@@ -45,7 +46,11 @@
private val backgroundDispatcher: CoroutineDispatcher,
) : GridRepository {
- override val optionChanges: Flow<Unit> =
+ override suspend fun isAvailable(): Boolean {
+ return withContext(backgroundDispatcher) { manager.isAvailable }
+ }
+
+ override fun getOptionChanges(): Flow<Unit> =
manager.getOptionChangeObservable(/* handler= */ null).asFlow().map {}
private val selectedOption = MutableStateFlow<GridOption?>(null)
diff --git a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
index 5ab9e1f..cdb679d 100644
--- a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
+++ b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
@@ -24,6 +24,9 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
@@ -34,22 +37,30 @@
private val snapshotRestorer: Provider<GridSnapshotRestorer>,
) {
val options: Flow<GridOptionItemsModel> =
- // this upstream flow tells us each time the options are changed.
- repository.optionChanges
- // when we start, we pretend the options _just_ changed. This way, we load something as
- // soon as possible into the flow so it's ready by the time the first observer starts to
- // observe.
- .onStart { emit(Unit) }
- // each time the options changed, we load them.
- .map { reload() }
- // we place the loaded options in a SharedFlow so downstream observers all
- // share the same flow and don't trigger a new one each time they want to start
- // observing.
- .shareIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- replay = 1,
- )
+ flow { emit(repository.isAvailable()) }
+ .flatMapLatest { isAvailable ->
+ if (isAvailable) {
+ // this upstream flow tells us each time the options are changed.
+ repository
+ .getOptionChanges()
+ // when we start, we pretend the options _just_ changed. This way, we load
+ // something as soon as possible into the flow so it's ready by the time the
+ // first observer starts to observe.
+ .onStart { emit(Unit) }
+ // each time the options changed, we load them.
+ .map { reload() }
+ // we place the loaded options in a SharedFlow so downstream observers all
+ // share the same flow and don't trigger a new one each time they want to
+ // start observing.
+ .shareIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ replay = 1,
+ )
+ } else {
+ emptyFlow()
+ }
+ }
suspend fun setSelectedOption(model: GridOptionItemModel) {
model.onSelected.invoke()
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 66793cd..ae66ce3 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
@@ -16,6 +16,8 @@
*/
package com.android.customization.picker.clock.data.repository
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import kotlinx.coroutines.flow.Flow
@@ -33,7 +35,18 @@
fun setSelectedClock(clockId: String)
- fun setClockColor(color: Int?)
+ /**
+ * Set clock color to the settings.
+ *
+ * @param selectedColor selected color in the color option list.
+ * @param colorToneProgress color tone from 0 to 100 to apply to the selected color
+ * @param seedColor the actual clock color after blending the selected color and color tone
+ */
+ fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ )
suspend 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 a360b6c..880a00b 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
@@ -17,6 +17,8 @@
package com.android.customization.picker.clock.data.repository
import android.provider.Settings
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import com.android.systemui.plugins.ClockMetadata
@@ -34,6 +36,7 @@
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn
+import org.json.JSONObject
/** Implementation of [ClockPickerRepository], using [ClockRegistry]. */
class ClockPickerRepositoryImpl(
@@ -50,7 +53,7 @@
registry
.getClocks()
.filter { "NOT_IN_USE" !in it.clockId }
- .map { it.toModel(null) }
+ .map { it.toModel() }
trySend(allClocks)
}
@@ -72,18 +75,22 @@
allClocks
}
- /** The currently-selected clock. */
+ /** The currently-selected clock. This also emits the clock color information. */
override val selectedClock: Flow<ClockMetadataModel> =
callbackFlow {
fun send() {
val currentClockId = registry.currentClockId
- // It is possible that the model can be null since the full clock list is not
- // initiated.
+ val metadata = registry.settings?.metadata
val model =
registry
.getClocks()
.find { clockMetadata -> clockMetadata.clockId == currentClockId }
- ?.toModel(registry.seedColor)
+ ?.toModel(
+ selectedColorId = metadata?.getSelectedColorId(),
+ colorTone = metadata?.getColorTone()
+ ?: ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = registry.seedColor
+ )
trySend(model)
}
@@ -104,11 +111,26 @@
.mapNotNull { it }
override fun setSelectedClock(clockId: String) {
- registry.currentClockId = clockId
+ registry.mutateSetting { oldSettings ->
+ val newSettings = oldSettings.copy(clockId = clockId)
+ newSettings.metadata = oldSettings.metadata
+ newSettings
+ }
}
- override fun setClockColor(color: Int?) {
- registry.seedColor = color
+ override fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ ) {
+ registry.mutateSetting { oldSettings ->
+ val newSettings = oldSettings.copy(seedColor = seedColor)
+ newSettings.metadata =
+ oldSettings.metadata
+ .put(KEY_METADATA_SELECTED_COLOR_ID, selectedColorId)
+ .put(KEY_METADATA_COLOR_TONE_PROGRESS, colorToneProgress)
+ newSettings
+ }
}
override val selectedClockSize: SharedFlow<ClockSize> =
@@ -131,7 +153,41 @@
)
}
- private fun ClockMetadata.toModel(color: Int?): ClockMetadataModel {
- return ClockMetadataModel(clockId = clockId, name = name, color = color)
+ private fun JSONObject.getSelectedColorId(): String? {
+ return if (this.isNull(KEY_METADATA_SELECTED_COLOR_ID)) {
+ null
+ } else {
+ this.getString(KEY_METADATA_SELECTED_COLOR_ID)
+ }
+ }
+
+ private fun JSONObject.getColorTone(): Int {
+ return this.optInt(
+ KEY_METADATA_COLOR_TONE_PROGRESS,
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+ )
+ }
+
+ /** By default, [ClockMetadataModel] has no color information unless specified. */
+ private fun ClockMetadata.toModel(
+ selectedColorId: String? = null,
+ @IntRange(from = 0, to = 100) colorTone: Int = 0,
+ @ColorInt seedColor: Int? = null,
+ ): ClockMetadataModel {
+ return ClockMetadataModel(
+ clockId = clockId,
+ name = name,
+ selectedColorId = selectedColorId,
+ colorToneProgress = colorTone,
+ seedColor = seedColor,
+ )
+ }
+
+ companion object {
+ // The selected color in the color option list
+ private const val KEY_METADATA_SELECTED_COLOR_ID = "metadataSelectedColorId"
+
+ // The color tone to apply to the selected color
+ private const val KEY_METADATA_COLOR_TONE_PROGRESS = "metadataColorToneProgress"
}
}
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 677fcfa..6f3657a 100644
--- a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
@@ -17,10 +17,13 @@
package com.android.customization.picker.clock.domain.interactor
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
import com.android.customization.picker.clock.data.repository.ClockPickerRepository
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
@@ -31,9 +34,16 @@
val allClocks: Flow<List<ClockMetadataModel>> = repository.allClocks
- val selectedClock: Flow<ClockMetadataModel> = repository.selectedClock
+ val selectedClockId: Flow<String> =
+ repository.selectedClock.map { clock -> clock.clockId }.distinctUntilChanged()
- val selectedClockColor: Flow<Int?> = repository.selectedClock.map { clock -> clock.color }
+ val selectedColorId: Flow<String?> =
+ repository.selectedClock.map { clock -> clock.selectedColorId }.distinctUntilChanged()
+
+ val colorToneProgress: Flow<Int> =
+ repository.selectedClock.map { clock -> clock.colorToneProgress }
+
+ val seedColor: Flow<Int?> = repository.selectedClock.map { clock -> clock.seedColor }
val selectedClockSize: Flow<ClockSize> = repository.selectedClockSize
@@ -41,8 +51,12 @@
repository.setSelectedClock(clockId)
}
- fun setClockColor(color: Int?) {
- repository.setClockColor(color)
+ fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ ) {
+ repository.setClockColor(selectedColorId, colorToneProgress, seedColor)
}
suspend fun setClockSize(size: ClockSize) {
diff --git a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt
index acbc792..bd87ba6 100644
--- a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt
+++ b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt
@@ -17,9 +17,18 @@
package com.android.customization.picker.clock.shared.model
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+
/** Model for clock metadata. */
data class ClockMetadataModel(
val clockId: String,
val name: String,
- val color: Int?,
-)
+ val selectedColorId: String?,
+ @IntRange(from = 0, to = 100) val colorToneProgress: Int,
+ @ColorInt val seedColor: Int?,
+) {
+ companion object {
+ const val DEFAULT_COLOR_TONE_PROGRESS = 75
+ }
+}
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 6c72a5b..9ad735d 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -15,7 +15,6 @@
*/
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
@@ -24,6 +23,7 @@
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.customization.picker.clock.ui.view.ClockCarouselView
+import com.android.customization.picker.clock.ui.view.ClockViewFactory
import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
import com.android.wallpaper.R
import kotlinx.coroutines.launch
@@ -43,7 +43,7 @@
carouselView: ClockCarouselView,
singleClockView: ViewGroup,
viewModel: ClockCarouselViewModel,
- clockViewFactory: (clockId: String) -> View,
+ clockViewFactory: ClockViewFactory,
lifecycleOwner: LifecycleOwner,
): Binding {
val singleClockHostView =
@@ -56,7 +56,7 @@
viewModel.allClockIds.collect { allClockIds ->
carouselView.setUpClockCarouselView(
clockIds = allClockIds,
- onGetClockPreview = clockViewFactory,
+ onGetClockPreview = { clockId -> clockViewFactory.getView(clockId) },
onClockSelected = { clockId -> viewModel.setSelectedClock(clockId) },
)
}
@@ -69,13 +69,17 @@
}
launch {
+ viewModel.seedColor.collect { clockViewFactory.updateColorForAllClocks(it) }
+ }
+
+ launch {
viewModel.isSingleClockViewVisible.collect { singleClockView.isVisible = it }
}
launch {
viewModel.clockId.collect { clockId ->
singleClockHostView.removeAllViews()
- val clockView = clockViewFactory(clockId)
+ val clockView = clockViewFactory.getView(clockId)
// The clock view might still be attached to an existing parent. Detach
// before adding to another parent.
(clockView.parent as? ViewGroup)?.removeView(clockView)
@@ -84,6 +88,15 @@
}
}
}
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ clockViewFactory.registerTimeTicker()
+ }
+ // When paused
+ clockViewFactory.unregisterTimeTicker()
+ }
+
return object : Binding {
override fun show() {
viewModel.showClockCarousel(true)
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 7ea0210..66d9251 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.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 android.widget.SeekBar
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
@@ -28,10 +30,12 @@
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.ui.adapter.ClockSettingsTabAdapter
import com.android.customization.picker.clock.ui.view.ClockSizeRadioButtonGroup
+import com.android.customization.picker.clock.ui.view.ClockViewFactory
import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
import com.android.customization.picker.color.ui.adapter.ColorOptionAdapter
import com.android.customization.picker.common.ui.view.ItemSpacing
import com.android.wallpaper.R
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
/** Bind between the clock settings screen and its view model. */
@@ -39,8 +43,11 @@
fun bind(
view: View,
viewModel: ClockSettingsViewModel,
+ clockViewFactory: ClockViewFactory,
lifecycleOwner: LifecycleOwner,
) {
+ val clockHostView: FrameLayout = view.requireViewById(R.id.clock_host_view)
+
val tabView: RecyclerView = view.requireViewById(R.id.tabs)
val tabAdapter = ClockSettingsTabAdapter()
tabView.adapter = tabAdapter
@@ -63,8 +70,10 @@
}
}
- override fun onStartTrackingTouch(p0: SeekBar?) = Unit
- override fun onStopTrackingTouch(p0: SeekBar?) = Unit
+ override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ seekBar?.progress?.let { viewModel.onSliderProgressStop(it) }
+ }
}
)
@@ -80,6 +89,25 @@
val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container)
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.selectedClockId
+ .mapNotNull { it }
+ .collect { clockId ->
+ val clockView = clockViewFactory.getView(clockId)
+ (clockView.parent as? ViewGroup)?.removeView(clockView)
+ clockHostView.removeAllViews()
+ clockHostView.addView(clockView)
+ }
+ }
+
+ launch {
+ viewModel.seedColor.collect { seedColor ->
+ viewModel.selectedClockId.value?.let { selectedClockId ->
+ clockViewFactory.updateColor(selectedClockId, seedColor)
+ }
+ }
+ }
+
launch { viewModel.tabs.collect { tabAdapter.setItems(it) } }
launch {
@@ -104,6 +132,18 @@
}
launch {
+ viewModel.selectedColorOptionPosition.collect { selectedPosition ->
+ if (selectedPosition != -1) {
+ // We use "post" because we need to give the adapter item a pass to
+ // update the view.
+ colorOptionContainerView.post {
+ colorOptionContainerView.smoothScrollToPosition(selectedPosition)
+ }
+ }
+ }
+ }
+
+ launch {
viewModel.selectedClockSize.collect { size ->
when (size) {
ClockSize.DYNAMIC -> {
@@ -120,16 +160,22 @@
launch {
viewModel.sliderProgress.collect { progress ->
- progress?.let { slider.setProgress(progress, false) }
+ slider.setProgress(progress, true)
}
}
launch {
- viewModel.isSliderEnabled.collect { isEnabled ->
- slider.isInvisible = !isEnabled
- }
+ viewModel.isSliderEnabled.collect { isEnabled -> slider.isEnabled = isEnabled }
}
}
}
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ clockViewFactory.registerTimeTicker()
+ }
+ // When paused
+ clockViewFactory.unregisterTimeTicker()
+ }
}
}
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
index 9b6d737..ef1a5ef 100644
--- a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
@@ -24,6 +24,7 @@
import androidx.lifecycle.get
import com.android.customization.module.ThemePickerInjector
import com.android.customization.picker.clock.ui.binder.ClockSettingsBinder
+import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
import com.android.wallpaper.R
import com.android.wallpaper.module.InjectorProvider
import com.android.wallpaper.picker.AppbarFragment
@@ -95,6 +96,16 @@
onWallpaperColorChanged = { colors ->
colorViewModel.setLockWallpaperColors(colors)
},
+ initialExtrasProvider = {
+ Bundle().apply {
+ // Hide the clock from the system UI rendered preview so we can
+ // place the carousel on top of it.
+ putBoolean(
+ ClockPreviewConstants.KEY_HIDE_CLOCK,
+ true,
+ )
+ }
+ },
),
lifecycleOwner = this,
offsetToStart = displayUtils.isOnWallpaperDisplay(activity),
@@ -111,6 +122,7 @@
),
)
.get(),
+ injector.getClockViewFactory(activity),
this@ClockSettingsFragment,
)
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
index 488dd08..7f480de 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
@@ -17,21 +17,45 @@
import android.app.Activity
import android.view.View
+import androidx.annotation.ColorInt
import com.android.systemui.plugins.ClockController
import com.android.systemui.shared.clocks.ClockRegistry
import com.android.wallpaper.R
import com.android.wallpaper.util.ScreenSizeCalculator
+import com.android.wallpaper.util.TimeUtils.TimeTicker
class ClockViewFactory(
private val activity: Activity,
private val registry: ClockRegistry,
) {
private val clockControllers: HashMap<String, ClockController> = HashMap()
+ private var ticker: TimeTicker? = null
fun getView(clockId: String): View {
return (clockControllers[clockId] ?: initClockController(clockId)).largeClock.view
}
+ fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
+ clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
+ }
+
+ fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
+ return (clockControllers[clockId] ?: initClockController(clockId))
+ .events
+ .onSeedColorChanged(seedColor)
+ }
+
+ fun registerTimeTicker() {
+ ticker =
+ TimeTicker.registerNewReceiver(activity.applicationContext) {
+ clockControllers.values.forEach { it.largeClock.events.onTimeTick() }
+ }
+ }
+
+ fun unregisterTimeTicker() {
+ activity.applicationContext.unregisterReceiver(ticker)
+ }
+
private fun initClockController(clockId: String): ClockController {
val controller =
registry.createExampleClock(clockId).also { it?.initialize(activity.resources, 0f, 0f) }
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 8d614e4..60a9e85 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -42,6 +42,8 @@
allClocks.map { it.clockId }
}
+ val seedColor: Flow<Int?> = interactor.seedColor
+
private val shouldShowCarousel = MutableStateFlow(false)
val isCarouselVisible: Flow<Boolean> =
combine(allClockIds.map { it.size > 1 }.distinctUntilChanged(), shouldShowCarousel) {
@@ -55,8 +57,8 @@
val selectedIndex: Flow<Int> =
allClockIds
.flatMapLatest { allClockIds ->
- interactor.selectedClock.map { selectedClock ->
- val index = allClockIds.indexOf(selectedClock.clockId)
+ interactor.selectedClockId.map { selectedClockId ->
+ val index = allClockIds.indexOf(selectedClockId)
if (index >= 0) {
index
} else {
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt
new file mode 100644
index 0000000..65ebc79
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockColorViewModel.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 android.annotation.ColorInt
+import android.content.res.Resources
+import android.graphics.Color
+import androidx.core.content.res.getStringOrThrow
+import com.android.wallpaper.R
+
+/** The view model that defines custom clock colors. */
+data class ClockColorViewModel(
+ val colorId: String,
+ val colorName: String?,
+ @ColorInt val color: Int,
+ private val colorToneMin: Double,
+ private val colorToneMax: Double,
+) {
+
+ fun getColorTone(progress: Int): Double {
+ return colorToneMin + (progress.toDouble() * (colorToneMax - colorToneMin)) / 100
+ }
+
+ companion object {
+ const val DEFAULT_COLOR_TONE_MIN = 0
+ const val DEFAULT_COLOR_TONE_MAX = 100
+
+ fun getPresetColorMap(resources: Resources): Map<String, ClockColorViewModel> {
+ val ids = resources.obtainTypedArray(R.array.clock_color_ids)
+ val names = resources.obtainTypedArray(R.array.clock_color_names)
+ val colors = resources.obtainTypedArray(R.array.clock_colors)
+ val colorToneMinList = resources.obtainTypedArray(R.array.clock_color_tone_min)
+ val colorToneMaxList = resources.obtainTypedArray(R.array.clock_color_tone_max)
+ return buildList {
+ for (i in 0 until ids.length()) {
+ add(
+ ClockColorViewModel(
+ ids.getStringOrThrow(i),
+ names.getString(i),
+ colors.getColor(i, Color.TRANSPARENT),
+ colorToneMinList.getInt(i, DEFAULT_COLOR_TONE_MIN).toDouble(),
+ colorToneMaxList.getInt(i, DEFAULT_COLOR_TONE_MAX).toDouble(),
+ )
+ )
+ }
+ }
+ .associateBy { it.colorId }
+ .also {
+ ids.recycle()
+ names.recycle()
+ colors.recycle()
+ colorToneMinList.recycle()
+ colorToneMaxList.recycle()
+ }
+ }
+ }
+}
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 9239c0a..008a125 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt
@@ -22,15 +22,20 @@
import com.android.wallpaper.R
import java.util.Locale
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
/** View model for the clock section view on the lockscreen customization surface. */
class ClockSectionViewModel(context: Context, interactor: ClockPickerInteractor) {
val appContext: Context = context.applicationContext
+ val clockColorMap: Map<String, ClockColorViewModel> =
+ ClockColorViewModel.getPresetColorMap(appContext.resources)
val selectedClockColorAndSizeText: Flow<String> =
- interactor.selectedClockSize.map { selectedClockSize ->
- // TODO (b/241966062) Finalize the colors and their names
- val colorText = "Violet"
+ combine(interactor.selectedColorId, interactor.selectedClockSize, ::Pair).map {
+ (selectedColorId, selectedClockSize) ->
+ val colorText =
+ clockColorMap[selectedColorId]?.colorName
+ ?: context.getString(R.string.default_theme_title)
val sizeText =
when (selectedClockSize) {
ClockSize.SMALL -> appContext.getString(R.string.clock_size_small)
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 b36c8eb..23fbc7e 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -24,12 +24,11 @@
import com.android.customization.model.color.ColorSeedOption
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
import com.android.customization.picker.color.shared.model.ColorType
import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
import com.android.wallpaper.R
-import kotlin.math.abs
-import kotlin.math.roundToInt
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -38,9 +37,10 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -57,64 +57,60 @@
SIZE,
}
- private val helperColorHsl: FloatArray by lazy { FloatArray(3) }
+ val colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
+
+ val selectedClockId: StateFlow<String?> =
+ clockPickerInteractor.selectedClockId
+ .distinctUntilChanged()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ val selectedColorId: StateFlow<String?> =
+ clockPickerInteractor.selectedColorId.stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ private val sliderColorToneProgress =
+ MutableStateFlow(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
+ val isSliderEnabled: Flow<Boolean> =
+ clockPickerInteractor.selectedColorId.map { it != null }.distinctUntilChanged()
+ val sliderProgress: Flow<Int> =
+ merge(clockPickerInteractor.colorToneProgress, sliderColorToneProgress)
+
+ private val _seedColor: MutableStateFlow<Int?> = MutableStateFlow(null)
+ val seedColor: Flow<Int?> = merge(clockPickerInteractor.seedColor, _seedColor)
/**
- * Saturation level of the current selected color. Note that this can be null if the selected
- * color is null, which means that the clock color respects the system theme color. In this
- * case, the saturation level is no longer needed since we do not allow changing saturation
- * level of the system theme color.
+ * The slider color tone updates are quick. Do not set color tone and the blended color to the
+ * settings until [onSliderProgressStop] is called. Update to a locally cached temporary
+ * [sliderColorToneProgress] and [_seedColor] instead.
*/
- private val saturationLevel: Flow<Float?> =
- clockPickerInteractor.selectedClockColor
- .map { selectedColor ->
- if (selectedColor == null) {
- null
- } else {
- ColorUtils.colorToHSL(selectedColor, helperColorHsl)
- helperColorHsl[1]
- }
- }
- .shareIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- replay = 1,
- )
-
- /**
- * When the selected clock color is null, it means that the clock will respect the system theme
- * color. And we no longer need the slider, which determines the saturation level of the clock's
- * overridden color.
- */
- val isSliderEnabled: Flow<Boolean> = saturationLevel.map { it != null }
-
- /**
- * Slide progress from 0 to 100. Note that this can be null if the selected color is null, which
- * means that the clock color respects the system theme color. In this case, the saturation
- * level is no longer needed since we do not allow changing saturation level of the system theme
- * color.
- */
- val sliderProgress: Flow<Int?> =
- saturationLevel.map { saturation -> saturation?.let { (it * 100).roundToInt() } }
-
fun onSliderProgressChanged(progress: Int) {
- val saturation = progress / 100f
- val selectedOption = colorOptions.value.find { option -> option.isSelected }
- selectedOption?.let { option ->
- ColorUtils.colorToHSL(option.color0, helperColorHsl)
- helperColorHsl[1] = saturation
- clockPickerInteractor.setClockColor(ColorUtils.HSLToColor(helperColorHsl))
- }
+ sliderColorToneProgress.value = progress
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ _seedColor.value =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ }
+
+ fun onSliderProgressStop(progress: Int) {
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ clockPickerInteractor.setClockColor(
+ selectedColorId = selectedColorId,
+ colorToneProgress = progress,
+ seedColor =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ )
}
@OptIn(ExperimentalCoroutinesApi::class)
val colorOptions: StateFlow<List<ColorOptionViewModel>> =
- combine(
- colorPickerInteractor.colorOptions,
- clockPickerInteractor.selectedClockColor,
- ::Pair,
- )
- .mapLatest { (colorOptions, selectedColor) ->
+ combine(colorPickerInteractor.colorOptions, clockPickerInteractor.selectedColorId, ::Pair)
+ .mapLatest { (colorOptions, selectedColorId) ->
// Use mapLatest and delay(100) here to prevent too many selectedClockColor update
// events from ClockRegistry upstream, caused by sliding the saturation level bar.
delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
@@ -125,55 +121,30 @@
?.colorOption as? ColorSeedOption)
?.toColorOptionViewModel(
context,
- selectedColor,
+ selectedColorId,
)
?: (colorOptions[ColorType.BASIC_COLOR]
?.find { it.isSelected }
?.colorOption as? ColorBundle)
?.toColorOptionViewModel(
context,
- selectedColor,
+ selectedColorId,
)
if (defaultThemeColorOptionViewModel != null) {
add(defaultThemeColorOptionViewModel)
}
- if (selectedColor != null) {
- ColorUtils.colorToHSL(selectedColor, helperColorHsl)
- }
+ val selectedColorPosition = colorMap.keys.indexOf(selectedColorId)
- val selectedColorPosition =
- if (selectedColor != null) {
- getSelectedColorPosition(helperColorHsl)
- } else {
- -1
- }
-
- COLOR_LIST_HSL.forEachIndexed { index, colorHSL ->
- val color = ColorUtils.HSLToColor(colorHSL)
+ colorMap.values.forEachIndexed { index, colorModel ->
val isSelected = selectedColorPosition == index
- val colorToSet: Int by lazy {
- val saturation =
- if (selectedColor != null) {
- helperColorHsl[1]
- } else {
- colorHSL[1]
- }
- ColorUtils.HSLToColor(
- listOf(
- colorHSL[0],
- saturation,
- colorHSL[2],
- )
- .toFloatArray()
- )
- }
+ val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
add(
ColorOptionViewModel(
- color0 = color,
- color1 = color,
- color2 = color,
- color3 = color,
+ color0 = colorModel.color,
+ color1 = colorModel.color,
+ color2 = colorModel.color,
+ color3 = colorModel.color,
contentDescription =
context.getString(
R.string.content_description_color_option,
@@ -184,7 +155,20 @@
if (isSelected) {
null
} else {
- { clockPickerInteractor.setClockColor(colorToSet) }
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = colorModel.colorId,
+ colorToneProgress = colorToneProgress,
+ seedColor =
+ blendColorWithTone(
+ color = colorModel.color,
+ colorTone =
+ colorModel.getColorTone(
+ colorToneProgress,
+ ),
+ ),
+ )
+ }
},
)
)
@@ -197,9 +181,13 @@
initialValue = emptyList(),
)
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val selectedColorOptionPosition: Flow<Int> =
+ colorOptions.mapLatest { it.indexOfFirst { colorOption -> colorOption.isSelected } }
+
private fun ColorSeedOption.toColorOptionViewModel(
context: Context,
- selectedColor: Int?,
+ selectedColorId: String?,
): ColorOptionViewModel {
val colors = previewInfo.resolveColors(context.resources)
return ColorOptionViewModel(
@@ -209,19 +197,25 @@
color3 = colors[3],
contentDescription = getContentDescription(context).toString(),
title = context.getString(R.string.default_theme_title),
- isSelected = selectedColor == null,
+ isSelected = selectedColorId == null,
onClick =
- if (selectedColor == null) {
+ if (selectedColorId == null) {
null
} else {
- { clockPickerInteractor.setClockColor(null) }
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = null,
+ colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = null,
+ )
+ }
},
)
}
private fun ColorBundle.toColorOptionViewModel(
context: Context,
- selectedColor: Int?
+ selectedColorId: String?
): ColorOptionViewModel {
val primaryColor = previewInfo.resolvePrimaryColor(context.resources)
val secondaryColor = previewInfo.resolveSecondaryColor(context.resources)
@@ -232,12 +226,18 @@
color3 = secondaryColor,
contentDescription = getContentDescription(context).toString(),
title = context.getString(R.string.default_theme_title),
- isSelected = selectedColor == null,
+ isSelected = selectedColorId == null,
onClick =
- if (selectedColor == null) {
+ if (selectedColorId == null) {
null
} else {
- { clockPickerInteractor.setClockColor(null) }
+ {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = null,
+ colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = null,
+ )
+ }
},
)
}
@@ -277,21 +277,18 @@
}
companion object {
- // TODO (b/241966062) The color integers here are temporary for dev purposes. We need to
- // finalize the overridden colors.
- val COLOR_LIST_HSL =
- listOf(
- arrayOf(225f, 0.65f, 0.74f).toFloatArray(),
- arrayOf(30f, 0.65f, 0.74f).toFloatArray(),
- arrayOf(249f, 0.65f, 0.74f).toFloatArray(),
- arrayOf(144f, 0.65f, 0.74f).toFloatArray(),
+ private val helperColorLab: DoubleArray by lazy { DoubleArray(3) }
+
+ fun blendColorWithTone(color: Int, colorTone: Double): Int {
+ ColorUtils.colorToLAB(color, helperColorLab)
+ return ColorUtils.LABToColor(
+ colorTone,
+ helperColorLab[1],
+ helperColorLab[2],
)
+ }
const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
-
- fun getSelectedColorPosition(selectedColorHsl: FloatArray): Int {
- return COLOR_LIST_HSL.withIndex().minBy { abs(it.value[0] - selectedColorHsl[0]) }.index
- }
}
class Factory(
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 0b197b4..700439b 100644
--- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
+++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
@@ -79,7 +79,7 @@
carouselView = carouselView,
singleClockView = singleClockView,
viewModel = clockCarouselViewModel,
- clockViewFactory = { clockId -> clockViewFactory.getView(clockId) },
+ clockViewFactory = clockViewFactory,
lifecycleOwner = lifecycleOwner,
)
onScreenSwitched(
diff --git a/tests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt b/tests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
index 6291c21..5953937 100644
--- a/tests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
+++ b/tests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
@@ -32,13 +32,17 @@
class FakeGridRepository(
private val scope: CoroutineScope,
initialOptionCount: Int,
+ var available: Boolean = true
) : GridRepository {
private val _optionChanges =
MutableSharedFlow<Unit>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
- override val optionChanges: Flow<Unit> = _optionChanges.asSharedFlow()
+
+ override suspend fun isAvailable(): Boolean = available
+
+ override fun getOptionChanges(): Flow<Unit> = _optionChanges.asSharedFlow()
private val selectedOptionIndex = MutableStateFlow(0)
private var options: GridOptionItemsModel = createOptions(count = initialOptionCount)
diff --git a/tests/src/com/android/customization/model/grid/domain/interactor/GridInteractorTest.kt b/tests/src/com/android/customization/model/grid/domain/interactor/GridInteractorTest.kt
index 20dd300..f73d5a3 100644
--- a/tests/src/com/android/customization/model/grid/domain/interactor/GridInteractorTest.kt
+++ b/tests/src/com/android/customization/model/grid/domain/interactor/GridInteractorTest.kt
@@ -135,4 +135,12 @@
// External updates do not record a new snapshot with the undo system.
assertThat(store.retrieve()).isEqualTo(storedSnapshot)
}
+
+ @Test
+ fun unavailableRepository_emptyOptions() =
+ testScope.runTest {
+ repository.available = false
+ val options = collectLastValue(underTest.options)
+ assertThat(options()).isNull()
+ }
}
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 b2cb452..2ef4e97 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,9 @@
*/
package com.android.customization.picker.clock.data.repository
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
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
@@ -24,17 +27,30 @@
import kotlinx.coroutines.flow.combine
/** By default [FakeClockPickerRepository] uses [fakeClocks]. */
-open class FakeClockPickerRepository(private val clocks: List<ClockMetadataModel> = fakeClocks) :
+open class FakeClockPickerRepository(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)
+ @ColorInt private val selectedColorId = MutableStateFlow<String?>(null)
+ private val colorTone = MutableStateFlow(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
+ @ColorInt private val seedColor = MutableStateFlow<Int?>(null)
override val selectedClock: Flow<ClockMetadataModel> =
- combine(selectedClockId, clockColor) { selectedClockId, clockColor ->
+ combine(
+ selectedClockId,
+ selectedColorId,
+ colorTone,
+ seedColor,
+ ) { selectedClockId, selectedColor, colorTone, seedColor ->
val selectedClock = fakeClocks.find { clock -> clock.clockId == selectedClockId }
checkNotNull(selectedClock)
- ClockMetadataModel(selectedClock.clockId, selectedClock.name, clockColor)
+ ClockMetadataModel(
+ selectedClock.clockId,
+ selectedClock.name,
+ selectedColor,
+ colorTone,
+ seedColor,
+ )
}
private val _selectedClockSize = MutableStateFlow(ClockSize.SMALL)
@@ -44,8 +60,14 @@
selectedClockId.value = clockId
}
- override fun setClockColor(color: Int?) {
- clockColor.value = color
+ override fun setClockColor(
+ selectedColorId: String?,
+ @IntRange(from = 0, to = 100) colorToneProgress: Int,
+ @ColorInt seedColor: Int?,
+ ) {
+ this.selectedColorId.value = selectedColorId
+ this.colorTone.value = colorToneProgress
+ this.seedColor.value = seedColor
}
override suspend fun setClockSize(size: ClockSize) {
@@ -55,10 +77,13 @@
companion object {
val fakeClocks =
listOf(
- ClockMetadataModel("clock0", "clock0", null),
- ClockMetadataModel("clock1", "clock1", null),
- ClockMetadataModel("clock2", "clock2", null),
- ClockMetadataModel("clock3", "clock3", null),
+ ClockMetadataModel("clock0", "clock0", null, 50, null),
+ ClockMetadataModel("clock1", "clock1", null, 50, null),
+ ClockMetadataModel("clock2", "clock2", null, 50, null),
+ ClockMetadataModel("clock3", "clock3", null, 50, null),
)
+ const val CLOCK_COLOR_ID = "RED"
+ const val CLOCK_COLOR_TONE_PROGRESS = 87
+ const val SEED_COLOR = Color.RED
}
}
diff --git a/tests/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractorTest.kt b/tests/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractorTest.kt
index 883d68b..cd41d7d 100644
--- a/tests/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractorTest.kt
+++ b/tests/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractorTest.kt
@@ -37,6 +37,14 @@
}
@Test
+ fun setSelectedClock() = runTest {
+ val observedSelectedClockId = collectLastValue(underTest.selectedClockId)
+ underTest.setSelectedClock(FakeClockPickerRepository.fakeClocks[1].clockId)
+ Truth.assertThat(observedSelectedClockId())
+ .isEqualTo(FakeClockPickerRepository.fakeClocks[1].clockId)
+ }
+
+ @Test
fun setClockSize() = runTest {
val observedClockSize = collectLastValue(underTest.selectedClockSize)
underTest.setClockSize(ClockSize.DYNAMIC)
@@ -45,4 +53,21 @@
underTest.setClockSize(ClockSize.SMALL)
Truth.assertThat(observedClockSize()).isEqualTo(ClockSize.SMALL)
}
+
+ @Test
+ fun setColor() = runTest {
+ val observedSelectedColor = collectLastValue(underTest.selectedColorId)
+ val observedColorToneProgress = collectLastValue(underTest.colorToneProgress)
+ val observedSeedColor = collectLastValue(underTest.seedColor)
+ underTest.setClockColor(
+ FakeClockPickerRepository.CLOCK_COLOR_ID,
+ FakeClockPickerRepository.CLOCK_COLOR_TONE_PROGRESS,
+ FakeClockPickerRepository.SEED_COLOR,
+ )
+ Truth.assertThat(observedSelectedColor())
+ .isEqualTo(FakeClockPickerRepository.CLOCK_COLOR_ID)
+ Truth.assertThat(observedColorToneProgress())
+ .isEqualTo(FakeClockPickerRepository.CLOCK_COLOR_TONE_PROGRESS)
+ Truth.assertThat(observedSeedColor()).isEqualTo(FakeClockPickerRepository.SEED_COLOR)
+ }
}
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 35c3518..63f77bd 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
@@ -42,7 +42,13 @@
private val repositoryWithSingleClock by lazy {
FakeClockPickerRepository(
listOf(
- ClockMetadataModel("clock0", "clock0", null),
+ ClockMetadataModel(
+ "clock0",
+ "clock0",
+ null,
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ null,
+ ),
)
)
}
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModelTest.kt
new file mode 100644
index 0000000..61976ad
--- /dev/null
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.test.platform.app.InstrumentationRegistry
+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.ClockSize
+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
+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 ClockSectionViewModelTest {
+
+ private lateinit var clockColorMap: Map<String, ClockColorViewModel>
+ private lateinit var interactor: ClockPickerInteractor
+ private lateinit var underTest: ClockSectionViewModel
+
+ @Before
+ fun setUp() {
+ val testDispatcher = StandardTestDispatcher()
+ Dispatchers.setMain(testDispatcher)
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ clockColorMap = ClockColorViewModel.getPresetColorMap(context.resources)
+ interactor = ClockPickerInteractor(FakeClockPickerRepository())
+ underTest =
+ ClockSectionViewModel(
+ context,
+ interactor,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun setSelectedClock() = runTest {
+ val colorRed = clockColorMap.values.first()
+ val observedSelectedClockColorAndSizeText =
+ collectLastValue(underTest.selectedClockColorAndSizeText)
+ interactor.setClockColor(
+ colorRed.colorId,
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ ClockSettingsViewModel.blendColorWithTone(
+ colorRed.color,
+ colorRed.getColorTone(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS),
+ )
+ )
+ interactor.setClockSize(ClockSize.DYNAMIC)
+ assertThat(observedSelectedClockColorAndSizeText()).isEqualTo("Red, dynamic")
+ }
+}
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
similarity index 71%
rename from tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt
rename to tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
index 8f61d8b..d53288d 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
@@ -6,12 +6,12 @@
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.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
import com.android.customization.picker.color.domain.interactor.ColorPickerSnapshotRestorer
import com.android.wallpaper.testing.FakeSnapshotStore
import com.android.wallpaper.testing.collectLastValue
-import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -32,10 +32,10 @@
@RunWith(JUnit4::class)
class ClockSettingsViewModelTest {
- private lateinit var underTest: ClockSettingsViewModel
- private lateinit var colorPickerInteractor: ColorPickerInteractor
- private lateinit var store: FakeSnapshotStore
private lateinit var context: Context
+ private lateinit var colorPickerInteractor: ColorPickerInteractor
+ private lateinit var underTest: ClockSettingsViewModel
+ private lateinit var colorMap: Map<String, ClockColorViewModel>
@Before
fun setUp() {
@@ -47,7 +47,7 @@
repository = FakeColorPickerRepository(context = context),
snapshotRestorer = {
ColorPickerSnapshotRestorer(interactor = colorPickerInteractor).apply {
- runBlocking { setUpSnapshotRestorer(store = store) }
+ runBlocking { setUpSnapshotRestorer(store = FakeSnapshotStore()) }
}
},
)
@@ -58,6 +58,7 @@
colorPickerInteractor = colorPickerInteractor,
)
.create(ClockSettingsViewModel::class.java)
+ colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
}
@After
@@ -66,43 +67,79 @@
}
@Test
- fun setClockColor() = runTest {
+ fun clickOnColorSettingsTab() = runTest {
+ val tabs = collectLastValue(underTest.tabs)
+ assertThat(tabs()?.get(0)?.name).isEqualTo("Color")
+ assertThat(tabs()?.get(0)?.isSelected).isTrue()
+ assertThat(tabs()?.get(1)?.name).isEqualTo("Size")
+ assertThat(tabs()?.get(1)?.isSelected).isFalse()
+
+ tabs()?.get(1)?.onClicked?.invoke()
+ assertThat(tabs()?.get(0)?.isSelected).isFalse()
+ assertThat(tabs()?.get(1)?.isSelected).isTrue()
+ }
+
+ @Test
+ fun setSelectedColor() = runTest {
val observedClockColorOptions = collectLastValue(underTest.colorOptions)
+ val observedSelectedColorOptionPosition =
+ collectLastValue(underTest.selectedColorOptionPosition)
+ val observedSliderProgress = collectLastValue(underTest.sliderProgress)
+ val observedSeedColor = collectLastValue(underTest.seedColor)
// Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
assertThat(observedClockColorOptions()!![0].isSelected).isTrue()
assertThat(observedClockColorOptions()!![0].onClick).isNull()
+ assertThat(observedSelectedColorOptionPosition()).isEqualTo(0)
observedClockColorOptions()!![1].onClick?.invoke()
// Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
assertThat(observedClockColorOptions()!![1].isSelected).isTrue()
assertThat(observedClockColorOptions()!![1].onClick).isNull()
+ assertThat(observedSelectedColorOptionPosition()).isEqualTo(1)
+ assertThat(observedSliderProgress())
+ .isEqualTo(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
+ val expectedSelectedColorModel = colorMap.values.first() // RED
+ assertThat(observedSeedColor())
+ .isEqualTo(
+ ClockSettingsViewModel.blendColorWithTone(
+ expectedSelectedColorModel.color,
+ expectedSelectedColorModel.getColorTone(
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+ ),
+ )
+ )
}
@Test
- fun setClockSaturation() = runTest {
+ fun setColorTone() = runTest {
val observedClockColorOptions = collectLastValue(underTest.colorOptions)
val observedIsSliderEnabled = collectLastValue(underTest.isSliderEnabled)
val observedSliderProgress = collectLastValue(underTest.sliderProgress)
+ val observedSeedColor = collectLastValue(underTest.seedColor)
// Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+ assertThat(observedClockColorOptions()!![0].isSelected).isTrue()
assertThat(observedIsSliderEnabled()).isFalse()
- assertThat(observedSliderProgress()).isNull()
observedClockColorOptions()!![1].onClick?.invoke()
+
// Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
assertThat(observedIsSliderEnabled()).isTrue()
- val targetProgress = 99
- underTest.onSliderProgressChanged(targetProgress)
- advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
- assertThat(observedClockColorOptions()!![1].isSelected).isTrue()
- assertThat(observedSliderProgress())
- .isIn(
- Range.closed(
- targetProgress - 1,
- targetProgress + 1,
+ val targetProgress1 = 99
+ underTest.onSliderProgressChanged(targetProgress1)
+ assertThat(observedSliderProgress()).isEqualTo(targetProgress1)
+ val targetProgress2 = 55
+ underTest.onSliderProgressStop(targetProgress2)
+ assertThat(observedSliderProgress()).isEqualTo(targetProgress2)
+ val expectedSelectedColorModel = colorMap.values.first() // RED
+ assertThat(observedSeedColor())
+ .isEqualTo(
+ ClockSettingsViewModel.blendColorWithTone(
+ expectedSelectedColorModel.color,
+ expectedSelectedColorModel.getColorTone(targetProgress2),
)
)
}
@@ -116,17 +153,4 @@
underTest.setClockSize(ClockSize.SMALL)
assertThat(observedClockSize()).isEqualTo(ClockSize.SMALL)
}
-
- @Test
- fun `Click on a picker tab`() = runTest {
- val tabs = collectLastValue(underTest.tabs)
- assertThat(tabs()?.get(0)?.name).isEqualTo("Color")
- assertThat(tabs()?.get(0)?.isSelected).isTrue()
- assertThat(tabs()?.get(1)?.name).isEqualTo("Size")
- assertThat(tabs()?.get(1)?.isSelected).isFalse()
-
- tabs()?.get(1)?.onClicked?.invoke()
- assertThat(tabs()?.get(0)?.isSelected).isFalse()
- assertThat(tabs()?.get(1)?.isSelected).isTrue()
- }
}