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.template.app
18
19 import android.content.Intent
20 import android.content.IntentFilter
21 import android.os.UserHandle
22 import androidx.compose.foundation.layout.Column
23 import androidx.compose.foundation.layout.PaddingValues
24 import androidx.compose.foundation.layout.fillMaxSize
25 import androidx.compose.foundation.lazy.LazyColumn
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.LaunchedEffect
28 import androidx.compose.runtime.State
29 import androidx.compose.runtime.collectAsState
30 import androidx.compose.runtime.remember
31 import androidx.compose.ui.Modifier
32 import androidx.compose.ui.res.stringResource
33 import androidx.compose.ui.unit.Dp
34 import androidx.lifecycle.viewmodel.compose.viewModel
35 import com.android.settingslib.spa.framework.compose.LifecycleEffect
36 import com.android.settingslib.spa.framework.compose.LogCompositions
37 import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
38 import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
39 import com.android.settingslib.spa.framework.compose.toState
40 import com.android.settingslib.spa.widget.ui.CategoryTitle
41 import com.android.settingslib.spa.widget.ui.PlaceholderTitle
42 import com.android.settingslib.spa.widget.ui.Spinner
43 import com.android.settingslib.spa.widget.ui.SpinnerOption
44 import com.android.settingslib.spaprivileged.R
45 import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
46 import com.android.settingslib.spaprivileged.model.app.AppEntry
47 import com.android.settingslib.spaprivileged.model.app.AppListData
48 import com.android.settingslib.spaprivileged.model.app.AppListModel
49 import com.android.settingslib.spaprivileged.model.app.AppListViewModel
50 import com.android.settingslib.spaprivileged.model.app.AppRecord
51 import com.android.settingslib.spaprivileged.model.app.IAppListViewModel
52 import com.android.settingslib.spaprivileged.model.app.userId
53 import kotlinx.coroutines.Dispatchers
54 import kotlinx.coroutines.flow.MutableStateFlow
55
56 private const val TAG = "AppList"
57 private const val CONTENT_TYPE_HEADER = "header"
58
59 /**
60 * The config used to load the App List.
61 */
62 data class AppListConfig(
63 val userIds: List<Int>,
64 val showInstantApps: Boolean,
65 val matchAnyUserForAdmin: Boolean,
66 )
67
68 data class AppListState(
69 val showSystem: State<Boolean>,
70 val searchQuery: State<String>,
71 )
72
73 data class AppListInput<T : AppRecord>(
74 val config: AppListConfig,
75 val listModel: AppListModel<T>,
76 val state: AppListState,
77 val header: @Composable () -> Unit,
78 val noItemMessage: String? = null,
79 val bottomPadding: Dp,
80 )
81
82 /**
83 * The template to render an App List.
84 *
85 * This UI element will take the remaining space on the screen to show the App List.
86 */
87 @Composable
88 fun <T : AppRecord> AppListInput<T>.AppList() {
89 AppListImpl { rememberViewModel(config, listModel, state) }
90 }
91
92 @Composable
93 internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
94 viewModelSupplier: @Composable () -> IAppListViewModel<T>,
95 ) {
96 LogCompositions(TAG, config.userIds.toString())
97 val viewModel = viewModelSupplier()
<lambda>null98 Column(Modifier.fillMaxSize()) {
99 val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
100 SpinnerOptions(optionsState, viewModel.optionFlow)
101 val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
102 listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
103 }
104 }
105
106 @Composable
SpinnerOptionsnull107 private fun SpinnerOptions(
108 optionsState: State<List<SpinnerOption>?>,
109 optionFlow: MutableStateFlow<Int?>,
110 ) {
111 val options = optionsState.value
112 LaunchedEffect(options) {
113 if (options != null && !options.any { it.id == optionFlow.value }) {
114 // Reset to first option if the available options changed, and the current selected one
115 // does not in the new options.
116 optionFlow.value = options.let { it.firstOrNull()?.id ?: -1 }
117 }
118 }
119 if (options != null) {
120 Spinner(options, optionFlow.collectAsState().value) { optionFlow.value = it }
121 }
122 }
123
124 @Composable
125 private fun <T : AppRecord> AppListModel<T>.AppListWidget(
126 appListData: State<AppListData<T>?>,
127 header: @Composable () -> Unit,
128 bottomPadding: Dp,
129 noItemMessage: String?
130 ) {
131 val timeMeasurer = rememberTimeMeasurer(TAG)
listnull132 appListData.value?.let { (list, option) ->
133 timeMeasurer.logFirst("app list first loaded")
134 if (list.isEmpty()) {
135 header()
136 PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
137 return
138 }
139 LazyColumn(
140 modifier = Modifier.fillMaxSize(),
141 state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
142 contentPadding = PaddingValues(bottom = bottomPadding),
143 ) {
144 item(contentType = CONTENT_TYPE_HEADER) {
145 header()
146 }
147
148 items(count = list.size, key = { list[it].record.itemKey(option) }) {
149 remember(list) { getGroupTitleIfFirst(option, list, it) }
150 ?.let { group -> CategoryTitle(title = group) }
151
152 val appEntry = list[it]
153 val summary = getSummary(option, appEntry.record) ?: "".toState()
154 remember(appEntry) {
155 AppListItemModel(appEntry.record, appEntry.label, summary)
156 }.AppItem()
157 }
158 }
159 }
160 }
161
itemKeynull162 private fun <T : AppRecord> T.itemKey(option: Int) =
163 listOf(option, app.packageName, app.userId)
164
165 /** Returns group title if this is the first item of the group. */
166 private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
167 option: Int,
168 list: List<AppEntry<T>>,
169 index: Int,
170 ): String? = getGroupTitle(option, list[index].record)?.takeIf {
171 index == 0 || it != getGroupTitle(option, list[index - 1].record)
172 }
173
174 @Composable
rememberViewModelnull175 private fun <T : AppRecord> rememberViewModel(
176 config: AppListConfig,
177 listModel: AppListModel<T>,
178 state: AppListState,
179 ): AppListViewModel<T> {
180 val viewModel: AppListViewModel<T> = viewModel(key = config.userIds.toString())
181 viewModel.appListConfig.setIfAbsent(config)
182 viewModel.listModel.setIfAbsent(listModel)
183 viewModel.showSystem.Sync(state.showSystem)
184 viewModel.searchQuery.Sync(state.searchQuery)
185
186 LifecycleEffect(onStart = { viewModel.reloadApps() })
187 val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
188 addAction(Intent.ACTION_PACKAGE_REMOVED)
189 addAction(Intent.ACTION_PACKAGE_CHANGED)
190 addDataScheme("package")
191 }
192 for (userId in config.userIds) {
193 DisposableBroadcastReceiverAsUser(
194 intentFilter = intentFilter,
195 userHandle = UserHandle.of(userId),
196 ) { viewModel.reloadApps() }
197 }
198 return viewModel
199 }
200