1 /*
<lambda>null2  * Copyright (C) 2022 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 package com.android.settingslib.spaprivileged.model.app
18 
19 import android.app.Application
20 import android.content.Context
21 import android.content.pm.ApplicationInfo
22 import android.icu.text.Collator
23 import androidx.lifecycle.AndroidViewModel
24 import androidx.lifecycle.viewModelScope
25 import com.android.settingslib.spa.framework.util.StateFlowBridge
26 import com.android.settingslib.spa.framework.util.asyncMapItem
27 import com.android.settingslib.spa.framework.util.waitFirst
28 import com.android.settingslib.spa.widget.ui.SpinnerOption
29 import com.android.settingslib.spaprivileged.template.app.AppListConfig
30 import java.util.concurrent.ConcurrentHashMap
31 import kotlinx.coroutines.Dispatchers
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.flow.Flow
34 import kotlinx.coroutines.flow.MutableStateFlow
35 import kotlinx.coroutines.flow.SharingStarted
36 import kotlinx.coroutines.flow.combine
37 import kotlinx.coroutines.flow.filterNotNull
38 import kotlinx.coroutines.flow.flatMapLatest
39 import kotlinx.coroutines.flow.flowOf
40 import kotlinx.coroutines.flow.launchIn
41 import kotlinx.coroutines.flow.map
42 import kotlinx.coroutines.flow.shareIn
43 import kotlinx.coroutines.launch
44 import kotlinx.coroutines.plus
45 
46 internal data class AppListData<T : AppRecord>(
47     val appEntries: List<AppEntry<T>>,
48     val option: Int,
49 ) {
50     fun filter(predicate: (AppEntry<T>) -> Boolean) =
51         AppListData(appEntries.filter(predicate), option)
52 }
53 
54 internal interface IAppListViewModel<T : AppRecord> {
55     val optionFlow: MutableStateFlow<Int?>
56     val spinnerOptionsFlow: Flow<List<SpinnerOption>>
57     val appListDataFlow: Flow<AppListData<T>>
58 }
59 
60 internal class AppListViewModel<T : AppRecord>(
61     application: Application,
62 ) : AppListViewModelImpl<T>(application)
63 
64 @OptIn(ExperimentalCoroutinesApi::class)
65 internal open class AppListViewModelImpl<T : AppRecord>(
66     application: Application,
67     appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl,
68     appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl,
69 ) : AndroidViewModel(application), IAppListViewModel<T> {
70     val appListConfig = StateFlowBridge<AppListConfig>()
71     val listModel = StateFlowBridge<AppListModel<T>>()
72     val showSystem = StateFlowBridge<Boolean>()
73     final override val optionFlow = MutableStateFlow<Int?>(null)
74     val searchQuery = StateFlowBridge<String>()
75 
76     private val appListRepository = appListRepositoryFactory(application)
77     private val appRepository = appRepositoryFactory(application)
78     private val collator = Collator.getInstance().freeze()
79     private val labelMap = ConcurrentHashMap<String, String>()
80     private val scope = viewModelScope + Dispatchers.IO
81 
confignull82     private val userSubGraphsFlow = appListConfig.flow.map { config ->
83         config.userIds.map { userId ->
84             UserSubGraph(userId, config.showInstantApps, config.matchAnyUserForAdmin)
85         }
86     }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
87 
88     private inner class UserSubGraph(
89         private val userId: Int,
90         private val showInstantApps: Boolean,
91         private val matchAnyUserForAdmin: Boolean,
92     ) {
93         private val userIdFlow = flowOf(userId)
94 
95         private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)
96 
97         val recordListFlow = listModel.flow
<lambda>null98             .flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
99             .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
100 
101         private val systemFilteredFlow =
102             appListRepository.showSystemPredicate(userIdFlow, showSystem.flow)
recordListnull103                 .combine(recordListFlow) { showAppPredicate, recordList ->
104                     recordList.filter { showAppPredicate(it.app) }
105                 }
106                 .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
107 
optionnull108         val listModelFilteredFlow = optionFlow.filterNotNull().flatMapLatest { option ->
109             listModel.flow.flatMapLatest { listModel ->
110                 listModel.filter(this.userIdFlow, option, this.systemFilteredFlow)
111             }
112         }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
113 
reloadAppsnull114         fun reloadApps() {
115             scope.launch {
116                 appsStateFlow.value =
117                     appListRepository.loadApps(userId, showInstantApps, matchAnyUserForAdmin)
118             }
119         }
120     }
121 
userSubGraphListnull122     private val combinedRecordListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
123         combine(userSubGraphList.map { it.recordListFlow }) { it.toList().flatten() }
124     }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
125 
126     override val spinnerOptionsFlow =
listModelnull127         combinedRecordListFlow.combine(listModel.flow) { recordList, listModel ->
128             listModel.getSpinnerOptions(recordList)
129         }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
130 
userSubGraphListnull131     private val appEntryListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
132         combine(userSubGraphList.map { it.listModelFilteredFlow }) { it.toList().flatten() }
133     }.asyncMapItem { record ->
134         val label = getLabel(record.app)
135         AppEntry(
136             record = record,
137             label = label,
138             labelCollationKey = collator.getCollationKey(label),
139         )
140     }
141 
142     override val appListDataFlow =
143         combine(
144             appEntryListFlow,
145             listModel.flow,
146             optionFlow.filterNotNull(),
listModelnull147         ) { appEntries, listModel, option ->
148             AppListData(
149                 appEntries = appEntries.sortedWith(listModel.getComparator(option)),
150                 option = option,
151             )
152         }.combine(searchQuery.flow) { appListData, searchQuery ->
<lambda>null153             appListData.filter {
154                 it.label.contains(other = searchQuery, ignoreCase = true)
155             }
156         }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
157 
158     init {
159         scheduleOnFirstLoaded()
160     }
161 
reloadAppsnull162     fun reloadApps() {
163         scope.launch {
164             userSubGraphsFlow.collect { userSubGraphList ->
165                 for (userSubGraph in userSubGraphList) {
166                     userSubGraph.reloadApps()
167                 }
168             }
169         }
170     }
171 
scheduleOnFirstLoadednull172     private fun scheduleOnFirstLoaded() {
173         combinedRecordListFlow
174             .waitFirst(appListDataFlow)
175             .combine(listModel.flow) { recordList, listModel ->
176                 if (listModel.onFirstLoaded(recordList)) {
177                     preFetchLabels(recordList)
178                 }
179             }
180             .launchIn(scope)
181     }
182 
preFetchLabelsnull183     private fun preFetchLabels(recordList: List<T>) {
184         for (record in recordList) {
185             getLabel(record.app)
186         }
187     }
188 
<lambda>null189     private fun getLabel(app: ApplicationInfo) = labelMap.computeIfAbsent(app.packageName) {
190         appRepository.loadLabel(app)
191     }
192 }
193