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