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