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