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