blob: 3f6394beffa2d599e28d672b923e35f59af0ae8d [file] [log] [blame]
/*
* 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.content.res.Resources
import android.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.customization.module.logging.ThemesUserEventLogger
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
import com.android.customization.picker.clock.shared.ClockSize
import com.android.customization.picker.clock.ui.view.ClockViewFactory
import com.android.wallpaper.R
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* Clock carousel view model that provides data for the carousel of clock previews. When there is
* only one item, we should show a single clock preview instead of a carousel.
*/
class ClockCarouselViewModel(
private val interactor: ClockPickerInteractor,
private val backgroundDispatcher: CoroutineDispatcher,
private val clockViewFactory: ClockViewFactory,
private val resources: Resources,
private val logger: ThemesUserEventLogger,
) : ViewModel() {
@OptIn(ExperimentalCoroutinesApi::class)
val allClocks: StateFlow<List<ClockCarouselItemViewModel>> =
interactor.allClocks
.mapLatest { allClocks ->
// Delay to avoid the case that the full list of clocks is not initiated.
delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
allClocks.map {
val contentDescription =
resources.getString(
R.string.select_clock_action_description,
clockViewFactory.getController(it.clockId).config.description
)
ClockCarouselItemViewModel(it.clockId, it.isSelected, contentDescription)
}
}
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize
val seedColor: Flow<Int?> = interactor.seedColor
fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> {
return interactor.seedColor.map {
it.let { seedColor ->
// if seedColor is null, default clock color is selected
if (seedColor == null) {
if (isDarkThemeEnabled) {
// In dark mode, use darkest surface container color
R.color.system_surface_container_high
} else {
// In light mode, use lightest surface container color
R.color.system_surface_bright
}
} else {
val luminance = Color.luminance(seedColor)
if (isDarkThemeEnabled) {
if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) {
R.color.system_surface_bright
} else {
R.color.system_surface_container_high
}
} else {
if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) {
R.color.system_surface_bright
} else {
R.color.system_surface_container_highest
}
}
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
val selectedIndex: Flow<Int> =
allClocks
.flatMapLatest { allClockIds ->
interactor.selectedClockId.map { selectedClockId ->
val index = allClockIds.indexOfFirst { it.clockId == selectedClockId }
/** Making sure there is no active [setSelectedClockJob] */
val isSetClockIdJobActive = setSelectedClockJob?.isActive == true
if (index >= 0 && !isSetClockIdJobActive) {
index
} else {
null
}
}
}
.mapNotNull { it }
private var setSelectedClockJob: Job? = null
fun setSelectedClock(clockId: String) {
setSelectedClockJob?.cancel()
setSelectedClockJob =
viewModelScope.launch(backgroundDispatcher) {
interactor.setSelectedClock(clockId)
logger.logClockApplied(clockId)
}
}
class Factory(
private val interactor: ClockPickerInteractor,
private val backgroundDispatcher: CoroutineDispatcher,
private val clockViewFactory: ClockViewFactory,
private val resources: Resources,
private val logger: ThemesUserEventLogger,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ClockCarouselViewModel(
interactor = interactor,
backgroundDispatcher = backgroundDispatcher,
clockViewFactory = clockViewFactory,
resources = resources,
logger = logger,
)
as T
}
}
companion object {
const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f
const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f
}
}