1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.customization.picker.grid.data.repository 19 20 import androidx.lifecycle.asFlow 21 import com.android.customization.model.CustomizationManager 22 import com.android.customization.model.CustomizationManager.Callback 23 import com.android.customization.model.grid.GridOption 24 import com.android.customization.model.grid.GridOptionsManager 25 import com.android.customization.picker.grid.shared.model.GridOptionItemModel 26 import com.android.customization.picker.grid.shared.model.GridOptionItemsModel 27 import kotlin.coroutines.resume 28 import kotlinx.coroutines.CoroutineDispatcher 29 import kotlinx.coroutines.CoroutineScope 30 import kotlinx.coroutines.flow.Flow 31 import kotlinx.coroutines.flow.MutableStateFlow 32 import kotlinx.coroutines.flow.SharingStarted 33 import kotlinx.coroutines.flow.StateFlow 34 import kotlinx.coroutines.flow.asStateFlow 35 import kotlinx.coroutines.flow.map 36 import kotlinx.coroutines.flow.stateIn 37 import kotlinx.coroutines.suspendCancellableCoroutine 38 import kotlinx.coroutines.withContext 39 40 interface GridRepository { 41 suspend fun isAvailable(): Boolean 42 43 fun getOptionChanges(): Flow<Unit> 44 45 suspend fun getOptions(): GridOptionItemsModel 46 47 fun getSelectedOption(): StateFlow<GridOption?> 48 49 fun applySelectedOption(callback: Callback) 50 51 fun clearSelectedOption() 52 53 fun isSelectedOptionApplied(): Boolean 54 } 55 56 class GridRepositoryImpl( 57 private val applicationScope: CoroutineScope, 58 private val manager: GridOptionsManager, 59 private val backgroundDispatcher: CoroutineDispatcher, 60 private val isGridApplyButtonEnabled: Boolean, 61 ) : GridRepository { 62 isAvailablenull63 override suspend fun isAvailable(): Boolean { 64 return withContext(backgroundDispatcher) { manager.isAvailable } 65 } 66 getOptionChangesnull67 override fun getOptionChanges(): Flow<Unit> = 68 manager.getOptionChangeObservable(/* handler= */ null).asFlow().map {} 69 70 private val selectedOption = MutableStateFlow<GridOption?>(null) 71 72 private var appliedOption: GridOption? = null 73 getSelectedOptionnull74 override fun getSelectedOption() = selectedOption.asStateFlow() 75 76 override suspend fun getOptions(): GridOptionItemsModel { 77 return withContext(backgroundDispatcher) { 78 suspendCancellableCoroutine { continuation -> 79 manager.fetchOptions( 80 object : CustomizationManager.OptionsFetchedListener<GridOption> { 81 override fun onOptionsLoaded(options: MutableList<GridOption>?) { 82 val optionsOrEmpty = options ?: emptyList() 83 // After Apply Button is added, we will rely on onSelected() method 84 // to update selectedOption. 85 if (!isGridApplyButtonEnabled || selectedOption.value == null) { 86 selectedOption.value = optionsOrEmpty.find { it.isActive(manager) } 87 } 88 if (isGridApplyButtonEnabled && appliedOption == null) { 89 appliedOption = selectedOption.value 90 } 91 continuation.resume( 92 GridOptionItemsModel.Loaded( 93 optionsOrEmpty.map { option -> toModel(option) } 94 ) 95 ) 96 } 97 98 override fun onError(throwable: Throwable?) { 99 continuation.resume( 100 GridOptionItemsModel.Error( 101 throwable ?: Exception("Failed to load grid options!") 102 ), 103 ) 104 } 105 }, 106 /* reload= */ true, 107 ) 108 } 109 } 110 } 111 toModelnull112 private fun toModel(option: GridOption): GridOptionItemModel { 113 return GridOptionItemModel( 114 name = option.title, 115 rows = option.rows, 116 cols = option.cols, 117 isSelected = 118 selectedOption 119 .map { it.key() } 120 .map { selectedOptionKey -> option.key() == selectedOptionKey } 121 .stateIn( 122 scope = applicationScope, 123 started = SharingStarted.Eagerly, 124 initialValue = false, 125 ), 126 onSelected = { onSelected(option) }, 127 ) 128 } 129 onSelectednull130 private suspend fun onSelected(option: GridOption) { 131 withContext(backgroundDispatcher) { 132 suspendCancellableCoroutine { continuation -> 133 if (isGridApplyButtonEnabled) { 134 selectedOption.value?.setIsCurrent(false) 135 selectedOption.value = option 136 selectedOption.value?.setIsCurrent(true) 137 manager.preview(option) 138 continuation.resume(true) 139 } else { 140 manager.apply( 141 option, 142 object : CustomizationManager.Callback { 143 override fun onSuccess() { 144 selectedOption.value = option 145 continuation.resume(true) 146 } 147 148 override fun onError(throwable: Throwable?) { 149 continuation.resume(false) 150 } 151 }, 152 ) 153 } 154 } 155 } 156 } 157 applySelectedOptionnull158 override fun applySelectedOption(callback: Callback) { 159 val option = getSelectedOption().value 160 manager.apply( 161 option, 162 if (isGridApplyButtonEnabled) { 163 object : Callback { 164 override fun onSuccess() { 165 callback.onSuccess() 166 appliedOption = option 167 } 168 169 override fun onError(throwable: Throwable?) { 170 callback.onError(throwable) 171 } 172 } 173 } else callback 174 ) 175 } 176 clearSelectedOptionnull177 override fun clearSelectedOption() { 178 if (!isGridApplyButtonEnabled) { 179 return 180 } 181 selectedOption.value?.setIsCurrent(false) 182 selectedOption.value = null 183 } 184 isSelectedOptionAppliednull185 override fun isSelectedOptionApplied() = selectedOption.value?.name == appliedOption?.name 186 187 private fun GridOption?.key(): String? { 188 return if (this != null) "${cols}x${rows}" else null 189 } 190 } 191