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.map 34 import kotlinx.coroutines.flow.stateIn 35 import kotlinx.coroutines.suspendCancellableCoroutine 36 import kotlinx.coroutines.withContext 37 38 interface GridRepository { 39 suspend fun isAvailable(): Boolean 40 fun getOptionChanges(): Flow<Unit> 41 suspend fun getOptions(): GridOptionItemsModel 42 fun getSelectedOption(): GridOption? 43 fun applySelectedOption(callback: Callback) 44 fun clearSelectedOption() 45 fun isSelectedOptionApplied(): Boolean 46 } 47 48 class GridRepositoryImpl( 49 private val applicationScope: CoroutineScope, 50 private val manager: GridOptionsManager, 51 private val backgroundDispatcher: CoroutineDispatcher, 52 private val isGridApplyButtonEnabled: Boolean, 53 ) : GridRepository { 54 isAvailablenull55 override suspend fun isAvailable(): Boolean { 56 return withContext(backgroundDispatcher) { manager.isAvailable } 57 } 58 getOptionChangesnull59 override fun getOptionChanges(): Flow<Unit> = 60 manager.getOptionChangeObservable(/* handler= */ null).asFlow().map {} 61 62 private val selectedOption = MutableStateFlow<GridOption?>(null) 63 64 private var appliedOption: GridOption? = null 65 getSelectedOptionnull66 override fun getSelectedOption() = selectedOption.value 67 68 override suspend fun getOptions(): GridOptionItemsModel { 69 return withContext(backgroundDispatcher) { 70 suspendCancellableCoroutine { continuation -> 71 manager.fetchOptions( 72 object : CustomizationManager.OptionsFetchedListener<GridOption> { 73 override fun onOptionsLoaded(options: MutableList<GridOption>?) { 74 val optionsOrEmpty = options ?: emptyList() 75 // After Apply Button is added, we will rely on onSelected() method 76 // to update selectedOption. 77 if (!isGridApplyButtonEnabled || selectedOption.value == null) { 78 selectedOption.value = optionsOrEmpty.find { it.isActive(manager) } 79 } 80 if (isGridApplyButtonEnabled && appliedOption == null) { 81 appliedOption = selectedOption.value 82 } 83 continuation.resume( 84 GridOptionItemsModel.Loaded( 85 optionsOrEmpty.map { option -> toModel(option) } 86 ) 87 ) 88 } 89 90 override fun onError(throwable: Throwable?) { 91 continuation.resume( 92 GridOptionItemsModel.Error( 93 throwable ?: Exception("Failed to load grid options!") 94 ), 95 ) 96 } 97 }, 98 /* reload= */ true, 99 ) 100 } 101 } 102 } 103 toModelnull104 private fun toModel(option: GridOption): GridOptionItemModel { 105 return GridOptionItemModel( 106 name = option.title, 107 rows = option.rows, 108 cols = option.cols, 109 isSelected = 110 selectedOption 111 .map { it.key() } 112 .map { selectedOptionKey -> option.key() == selectedOptionKey } 113 .stateIn( 114 scope = applicationScope, 115 started = SharingStarted.Eagerly, 116 initialValue = false, 117 ), 118 onSelected = { onSelected(option) }, 119 ) 120 } 121 onSelectednull122 private suspend fun onSelected(option: GridOption) { 123 withContext(backgroundDispatcher) { 124 suspendCancellableCoroutine { continuation -> 125 if (isGridApplyButtonEnabled) { 126 selectedOption.value?.setIsCurrent(false) 127 selectedOption.value = option 128 selectedOption.value?.setIsCurrent(true) 129 manager.preview(option) 130 continuation.resume(true) 131 } else { 132 manager.apply( 133 option, 134 object : CustomizationManager.Callback { 135 override fun onSuccess() { 136 continuation.resume(true) 137 } 138 139 override fun onError(throwable: Throwable?) { 140 continuation.resume(false) 141 } 142 }, 143 ) 144 } 145 } 146 } 147 } 148 applySelectedOptionnull149 override fun applySelectedOption(callback: Callback) { 150 val option = getSelectedOption() 151 manager.apply( 152 option, 153 if (isGridApplyButtonEnabled) { 154 object : Callback { 155 override fun onSuccess() { 156 callback.onSuccess() 157 appliedOption = option 158 } 159 160 override fun onError(throwable: Throwable?) { 161 callback.onError(throwable) 162 } 163 } 164 } else callback 165 ) 166 } 167 clearSelectedOptionnull168 override fun clearSelectedOption() { 169 if (!isGridApplyButtonEnabled) { 170 return 171 } 172 selectedOption.value?.setIsCurrent(false) 173 selectedOption.value = null 174 } 175 isSelectedOptionAppliednull176 override fun isSelectedOptionApplied() = selectedOption.value?.name == appliedOption?.name 177 178 private fun GridOption?.key(): String? { 179 return if (this != null) "${cols}x${rows}" else null 180 } 181 } 182