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