blob: 105dcff09d831b5b5f7a18f36a064a04271d47bc [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.wallpaper.picker.customization.ui.viewmodel
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.savedstate.SavedStateRegistryOwner
import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
/** Models UI state for views that can render wallpaper quick switching. */
@OptIn(ExperimentalCoroutinesApi::class)
class WallpaperQuickSwitchViewModel
@VisibleForTesting
constructor(
private val interactor: WallpaperInteractor,
maxOptions: Int,
) : ViewModel() {
private val isLockScreenSelected = MutableStateFlow(false)
private val selectedWallpaperId: Flow<String> =
isLockScreenSelected
.flatMapLatest { isOnLockScreen ->
interactor.selectedWallpaperId(
destination =
if (isOnLockScreen) {
WallpaperDestination.LOCK
} else {
WallpaperDestination.HOME
},
)
}
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
private val selectingWallpaperId: Flow<String?> =
isLockScreenSelected
.flatMapLatest { isOnLockScreen ->
interactor.selectingWallpaperId(
destination =
if (isOnLockScreen) {
WallpaperDestination.LOCK
} else {
WallpaperDestination.HOME
},
)
}
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
val options: Flow<List<WallpaperQuickSwitchOptionViewModel>> =
isLockScreenSelected
.flatMapLatest { isOnLockScreen ->
interactor
.previews(
destination =
if (isOnLockScreen) {
WallpaperDestination.LOCK
} else {
WallpaperDestination.HOME
},
maxResults = maxOptions,
)
.distinctUntilChangedBy { previews ->
// Produce a key that's the same if the same set of wallpapers is available,
// even if in a different order. This is so that the view can keep from
// moving the wallpaper options around when the sort order changes as the
// user selects different wallpapers.
previews.map { preview -> preview.wallpaperId }.sorted().joinToString(",")
}
.map { previews ->
// True if any option is becoming selected following user click.
val isSomethingBecomingSelectedFlow: Flow<Boolean> =
selectingWallpaperId.distinctUntilChanged().map { it != null }
previews.map { preview ->
// True if this option is currently selected.
val isSelectedFlow: Flow<Boolean> =
selectedWallpaperId.distinctUntilChanged().map {
it == preview.wallpaperId
}
// True if this option is becoming the selected one following user
// click.
val isBecomingSelectedFlow: Flow<Boolean> =
selectingWallpaperId.distinctUntilChanged().map {
it == preview.wallpaperId
}
WallpaperQuickSwitchOptionViewModel(
wallpaperId = preview.wallpaperId,
placeholderColor = preview.placeholderColor,
thumbnail = {
interactor.loadThumbnail(
wallpaperId = preview.wallpaperId,
)
},
isLarge =
combine(
isSelectedFlow,
isBecomingSelectedFlow,
isSomethingBecomingSelectedFlow,
) { isSelected, isBecomingSelected, isSomethingBecomingSelected
->
// The large option is the one that's currently selected or
// the one that is becoming the selected one following user
// click.
(isSelected && !isSomethingBecomingSelected) ||
isBecomingSelected
},
// We show the progress indicator if the option is in the process of
// becoming the selected one following user click.
isProgressIndicatorVisible = isBecomingSelectedFlow,
isSelectionBorderVisible =
combine(
isSelectedFlow,
isBecomingSelectedFlow,
isSomethingBecomingSelectedFlow,
) { isSelected, isBeingSelected, isSomethingBecomingSelected ->
// The selection border is shown for the option that is the
// one that's currently selected or the one that is becoming
// the selected one following user click.
(isSelected && !isSomethingBecomingSelected) ||
isBeingSelected
},
isSelectionIconVisible =
combine(
isSelectedFlow,
isSomethingBecomingSelectedFlow,
) { isSelected, isSomethingBecomingSelected ->
// The selection icon is shown for the option that is
// currently selected but only if nothing else is becoming
// selected. If anything is being selected following user
// click, the selection icon is not shown on any option.
isSelected && !isSomethingBecomingSelected
},
onSelected =
combine(
isSelectedFlow,
isBecomingSelectedFlow,
isSomethingBecomingSelectedFlow,
) { isSelected, isBeingSelected, isSomethingBecomingSelected
->
// An option is selectable if it is not itself becoming
// selected following user click or if nothing else is
// becoming selected but this option is not the selected
// one.
(isSomethingBecomingSelected && !isBeingSelected) ||
(!isSomethingBecomingSelected && !isSelected)
}
.distinctUntilChanged()
.map { isSelectable ->
if (isSelectable) {
{
// A selectable option can become selected.
viewModelScope.launch {
interactor.setWallpaper(
destination =
if (isOnLockScreen) {
WallpaperDestination.LOCK
} else {
WallpaperDestination.HOME
},
wallpaperId = preview.wallpaperId,
)
}
}
} else {
// A non-selectable option cannot become selected.
null
}
}
)
}
}
}
.shareIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
replay = 1,
)
fun setOnLockScreen(isLockScreenSelected: Boolean) {
this.isLockScreenSelected.value = isLockScreenSelected
}
companion object {
@JvmStatic
fun newFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
interactor: WallpaperInteractor,
): AbstractSavedStateViewModelFactory =
object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle,
): T {
return WallpaperQuickSwitchViewModel(
interactor = interactor,
maxOptions = MAX_OPTIONS,
)
as T
}
}
/** The maximum number of options to show, including the currently-selected one. */
private const val MAX_OPTIONS = 5
}
}