fix: 首次提交
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.text.BidiFormatter
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.android.settingslib.development.DevelopmentSettingsEnabler
|
||||
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.widget.ui.CopyableBody
|
||||
import com.android.settingslib.spa.widget.ui.SettingsBody
|
||||
import com.android.settingslib.spa.widget.ui.SettingsTitle
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.model.app.rememberAppRepository
|
||||
|
||||
class AppInfoProvider(private val packageInfo: PackageInfo) {
|
||||
@Composable
|
||||
fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsDimension.itemPaddingStart,
|
||||
vertical = SettingsDimension.itemPaddingVertical,
|
||||
)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val app = checkNotNull(packageInfo.applicationInfo)
|
||||
Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
|
||||
AppIcon(app = app, size = SettingsDimension.appIconInfoSize)
|
||||
}
|
||||
AppLabel(app, isClonedAppPage)
|
||||
InstallType(app)
|
||||
if (displayVersion) AppVersion()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstallType(app: ApplicationInfo) {
|
||||
if (!app.isInstantApp) return
|
||||
Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
|
||||
SettingsBody(
|
||||
stringResource(
|
||||
com.android.settingslib.widget.preference.app.R.string.install_type_instant
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppVersion() {
|
||||
val versionName = packageInfo.versionNameBidiWrapped ?: return
|
||||
Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
|
||||
SettingsBody(versionName)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
|
||||
val context = LocalContext.current
|
||||
val footer = remember(packageInfo, showPackageName) {
|
||||
val list = mutableListOf<String>()
|
||||
packageInfo.versionNameBidiWrapped?.let {
|
||||
list += context.getString(R.string.version_text, it)
|
||||
}
|
||||
if (showPackageName) {
|
||||
list += packageInfo.packageName
|
||||
}
|
||||
list.joinToString(separator = System.lineSeparator())
|
||||
}
|
||||
if (footer.isBlank()) return
|
||||
HorizontalDivider()
|
||||
Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
|
||||
CopyableBody(footer)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberIsDevelopmentSettingsEnabled(): Boolean {
|
||||
val context = LocalContext.current
|
||||
return remember {
|
||||
DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Wrapped the version name, so its directionality still keep same when RTL. */
|
||||
val PackageInfo.versionNameBidiWrapped: String?
|
||||
get() = BidiFormatter.getInstance().unicodeWrap(versionName)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun AppIcon(app: ApplicationInfo, size: Dp) {
|
||||
val appRepository = rememberAppRepository()
|
||||
Image(
|
||||
painter = rememberDrawablePainter(appRepository.produceIcon(app).value),
|
||||
contentDescription = appRepository.produceIconContentDescription(app).value,
|
||||
modifier = Modifier.size(size),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun AppLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false) {
|
||||
val appRepository = rememberAppRepository()
|
||||
SettingsTitle(appRepository.produceLabel(app, isClonedAppPage).value, useMediumWeight = true)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
|
||||
import com.android.settingslib.spa.widget.ui.Footer
|
||||
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
|
||||
|
||||
@Composable
|
||||
fun AppInfoPage(
|
||||
title: String,
|
||||
packageName: String,
|
||||
userId: Int,
|
||||
footerContent: @Composable () -> Unit,
|
||||
packageManagers: IPackageManagers,
|
||||
content: @Composable PackageInfo.() -> Unit,
|
||||
) {
|
||||
val packageInfo = remember(packageName, userId) {
|
||||
packageManagers.getPackageInfoAsUser(packageName, userId)
|
||||
} ?: return
|
||||
RegularScaffold(title = title) {
|
||||
remember(packageInfo) { AppInfoProvider(packageInfo) }.AppInfo(displayVersion = true)
|
||||
|
||||
packageInfo.content()
|
||||
|
||||
Footer(footerContent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.UserHandle
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.settingslib.spa.framework.compose.LifecycleEffect
|
||||
import com.android.settingslib.spa.framework.compose.LogCompositions
|
||||
import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
|
||||
import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
|
||||
import com.android.settingslib.spa.widget.ui.CategoryTitle
|
||||
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
|
||||
import com.android.settingslib.spa.widget.ui.Spinner
|
||||
import com.android.settingslib.spa.widget.ui.SpinnerOption
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
|
||||
import com.android.settingslib.spaprivileged.model.app.AppEntry
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListData
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListViewModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.model.app.IAppListViewModel
|
||||
import com.android.settingslib.spaprivileged.model.app.userId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
private const val TAG = "AppList"
|
||||
private const val CONTENT_TYPE_HEADER = "header"
|
||||
|
||||
/**
|
||||
* The config used to load the App List.
|
||||
*/
|
||||
data class AppListConfig(
|
||||
val userIds: List<Int>,
|
||||
val showInstantApps: Boolean,
|
||||
val matchAnyUserForAdmin: Boolean,
|
||||
)
|
||||
|
||||
data class AppListState(
|
||||
val showSystem: () -> Boolean,
|
||||
val searchQuery: () -> String,
|
||||
)
|
||||
|
||||
data class AppListInput<T : AppRecord>(
|
||||
val config: AppListConfig,
|
||||
val listModel: AppListModel<T>,
|
||||
val state: AppListState,
|
||||
val header: @Composable () -> Unit,
|
||||
val noItemMessage: String? = null,
|
||||
val bottomPadding: Dp,
|
||||
)
|
||||
|
||||
/**
|
||||
* The template to render an App List.
|
||||
*
|
||||
* This UI element will take the remaining space on the screen to show the App List.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListInput<T>.AppList() {
|
||||
AppListImpl { rememberViewModel(config, listModel, state) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
|
||||
viewModelSupplier: @Composable () -> IAppListViewModel<T>,
|
||||
) {
|
||||
LogCompositions(TAG, config.userIds.toString())
|
||||
val viewModel = viewModelSupplier()
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
|
||||
SpinnerOptions(optionsState, viewModel.optionFlow)
|
||||
val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
|
||||
listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpinnerOptions(
|
||||
optionsState: State<List<SpinnerOption>?>,
|
||||
optionFlow: MutableStateFlow<Int?>,
|
||||
) {
|
||||
val options = optionsState.value
|
||||
LaunchedEffect(options) {
|
||||
if (options != null && !options.any { it.id == optionFlow.value }) {
|
||||
// Reset to first option if the available options changed, and the current selected one
|
||||
// does not in the new options.
|
||||
optionFlow.value = options.let { it.firstOrNull()?.id ?: -1 }
|
||||
}
|
||||
}
|
||||
if (options != null) {
|
||||
Spinner(options, optionFlow.collectAsState().value) { optionFlow.value = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : AppRecord> AppListModel<T>.AppListWidget(
|
||||
appListData: State<AppListData<T>?>,
|
||||
header: @Composable () -> Unit,
|
||||
bottomPadding: Dp,
|
||||
noItemMessage: String?
|
||||
) {
|
||||
val timeMeasurer = rememberTimeMeasurer(TAG)
|
||||
appListData.value?.let { (list, option) ->
|
||||
timeMeasurer.logFirst("app list first loaded")
|
||||
if (list.isEmpty()) {
|
||||
header()
|
||||
PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
|
||||
return
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
|
||||
contentPadding = PaddingValues(bottom = bottomPadding),
|
||||
) {
|
||||
item(contentType = CONTENT_TYPE_HEADER) {
|
||||
header()
|
||||
}
|
||||
|
||||
items(count = list.size, key = { list[it].record.itemKey(option) }) {
|
||||
remember(list) { getGroupTitleIfFirst(option, list, it) }
|
||||
?.let { group -> CategoryTitle(title = group) }
|
||||
|
||||
val appEntry = list[it]
|
||||
val summary = getSummary(option, appEntry.record) ?: { "" }
|
||||
remember(appEntry) {
|
||||
AppListItemModel(appEntry.record, appEntry.label, summary)
|
||||
}.AppItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : AppRecord> T.itemKey(option: Int) =
|
||||
listOf(option, app.packageName, app.userId)
|
||||
|
||||
/** Returns group title if this is the first item of the group. */
|
||||
private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
|
||||
option: Int,
|
||||
list: List<AppEntry<T>>,
|
||||
index: Int,
|
||||
): String? = getGroupTitle(option, list[index].record)?.takeIf {
|
||||
index == 0 || it != getGroupTitle(option, list[index - 1].record)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : AppRecord> rememberViewModel(
|
||||
config: AppListConfig,
|
||||
listModel: AppListModel<T>,
|
||||
state: AppListState,
|
||||
): AppListViewModel<T> {
|
||||
val viewModel: AppListViewModel<T> = viewModel(key = config.userIds.toString())
|
||||
viewModel.appListConfig.setIfAbsent(config)
|
||||
viewModel.listModel.setIfAbsent(listModel)
|
||||
viewModel.showSystem.Sync(state.showSystem)
|
||||
viewModel.searchQuery.Sync(state.searchQuery)
|
||||
|
||||
LifecycleEffect(onStart = { viewModel.reloadApps() })
|
||||
val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
for (userId in config.userIds) {
|
||||
DisposableBroadcastReceiverAsUser(
|
||||
intentFilter = intentFilter,
|
||||
userHandle = UserHandle.of(userId),
|
||||
) { viewModel.reloadApps() }
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spa.widget.preference.TwoTargetButtonPreference
|
||||
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListItemModel<T>.AppListButtonItem (
|
||||
onClick: () -> Unit,
|
||||
onButtonClick: () -> Unit,
|
||||
buttonIcon: ImageVector,
|
||||
buttonIconDescription: String,
|
||||
) {
|
||||
TwoTargetButtonPreference(
|
||||
title = label,
|
||||
summary = this@AppListButtonItem.summary,
|
||||
icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
|
||||
onClick = onClick,
|
||||
buttonIcon = buttonIcon,
|
||||
buttonIconDescription = buttonIconDescription,
|
||||
onButtonClick = onButtonClick
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.framework.theme.SettingsTheme
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
|
||||
data class AppListItemModel<T : AppRecord>(
|
||||
val record: T,
|
||||
val label: String,
|
||||
val summary: () -> String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListItemModel<T>.AppListItem(onClick: () -> Unit) {
|
||||
Preference(remember {
|
||||
object : PreferenceModel {
|
||||
override val title = label
|
||||
override val summary = this@AppListItem.summary
|
||||
override val icon = @Composable {
|
||||
AppIcon(app = record.app, size = SettingsDimension.appIconItemSize)
|
||||
}
|
||||
override val onClick = onClick
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppListItemPreview() {
|
||||
SettingsTheme {
|
||||
val record = object : AppRecord {
|
||||
override val app = LocalContext.current.applicationInfo
|
||||
}
|
||||
AppListItemModel<AppRecord>(record, "Chrome", { "Allowed" }).AppListItem {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
|
||||
import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
|
||||
import com.android.settingslib.spa.widget.scaffold.SearchScaffold
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.template.common.UserProfilePager
|
||||
|
||||
/**
|
||||
* The full screen template for an App List page.
|
||||
*
|
||||
* @param noMoreOptions default false. If true, then do not display more options action button,
|
||||
* including the "Show System" / "Hide System" action.
|
||||
* @param header the description header appears before all the applications.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListPage(
|
||||
title: String,
|
||||
listModel: AppListModel<T>,
|
||||
showInstantApps: Boolean = false,
|
||||
noMoreOptions: Boolean = false,
|
||||
matchAnyUserForAdmin: Boolean = false,
|
||||
noItemMessage: String? = null,
|
||||
moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
|
||||
header: @Composable () -> Unit = {},
|
||||
appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
|
||||
) {
|
||||
var showSystem by rememberSaveable { mutableStateOf(false) }
|
||||
SearchScaffold(
|
||||
title = title,
|
||||
actions = {
|
||||
if (!noMoreOptions) {
|
||||
MoreOptionsAction {
|
||||
ShowSystemAction(showSystem) { showSystem = it }
|
||||
moreOptions()
|
||||
}
|
||||
}
|
||||
},
|
||||
) { bottomPadding, searchQuery ->
|
||||
UserProfilePager { userGroup ->
|
||||
val appListInput = AppListInput(
|
||||
config = AppListConfig(
|
||||
userIds = userGroup.userInfos.map { it.id },
|
||||
showInstantApps = showInstantApps,
|
||||
matchAnyUserForAdmin = matchAnyUserForAdmin,
|
||||
),
|
||||
listModel = listModel,
|
||||
state = AppListState(
|
||||
showSystem = { showSystem },
|
||||
searchQuery = searchQuery,
|
||||
),
|
||||
header = header,
|
||||
bottomPadding = bottomPadding,
|
||||
noItemMessage = noItemMessage,
|
||||
)
|
||||
appList(appListInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoreOptionsScope.ShowSystemAction(
|
||||
showSystem: Boolean,
|
||||
setShowSystem: (showSystem: Boolean) -> Unit,
|
||||
) {
|
||||
val menuText = if (showSystem) R.string.menu_hide_system else R.string.menu_show_system
|
||||
MenuItem(text = stringResource(menuText)) {
|
||||
setShowSystem(!showSystem)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListItemModel<T>.AppListSwitchItem(
|
||||
checked: () -> Boolean?,
|
||||
changeable: () -> Boolean,
|
||||
onCheckedChange: ((newChecked: Boolean) -> Unit)?,
|
||||
) {
|
||||
SwitchPreference(
|
||||
model = object : SwitchPreferenceModel {
|
||||
override val title = label
|
||||
override val summary = this@AppListSwitchItem.summary
|
||||
override val icon = @Composable {
|
||||
AppIcon(record.app, SettingsDimension.appIconItemSize)
|
||||
}
|
||||
override val checked = checked
|
||||
override val changeable = changeable
|
||||
override val onCheckedChange = onCheckedChange
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
|
||||
@Composable
|
||||
fun <T : AppRecord> AppListItemModel<T>.AppListTwoTargetSwitchItem(
|
||||
onClick: () -> Unit,
|
||||
checked: () -> Boolean?,
|
||||
changeable: () -> Boolean,
|
||||
onCheckedChange: ((newChecked: Boolean) -> Unit)?,
|
||||
) {
|
||||
TwoTargetSwitchPreference(
|
||||
model = object : SwitchPreferenceModel {
|
||||
override val title = label
|
||||
override val summary = this@AppListTwoTargetSwitchItem.summary
|
||||
override val checked = checked
|
||||
override val changeable = changeable
|
||||
override val onCheckedChange = onCheckedChange
|
||||
},
|
||||
icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.app.AppOpsManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.android.settingslib.spa.framework.util.asyncMapItem
|
||||
import com.android.settingslib.spa.framework.util.filterItem
|
||||
import com.android.settingslib.spaprivileged.model.app.AppOpsController
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
|
||||
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
|
||||
import com.android.settingslib.spaprivileged.model.app.PackageManagers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
data class AppOpPermissionRecord(
|
||||
override val app: ApplicationInfo,
|
||||
val hasRequestBroaderPermission: Boolean,
|
||||
val hasRequestPermission: Boolean,
|
||||
var appOpsController: IAppOpsController,
|
||||
) : AppRecord
|
||||
|
||||
abstract class AppOpPermissionListModel(
|
||||
protected val context: Context,
|
||||
private val packageManagers: IPackageManagers = PackageManagers,
|
||||
) : TogglePermissionAppListModel<AppOpPermissionRecord> {
|
||||
|
||||
abstract val appOp: Int
|
||||
abstract val permission: String
|
||||
|
||||
override val enhancedConfirmationKey: String?
|
||||
get() = AppOpsManager.opToPublicName(appOp)
|
||||
|
||||
/**
|
||||
* When set, specifies the broader permission who trumps the [permission].
|
||||
*
|
||||
* When trumped, the [permission] is not changeable and model shows the [permission] as allowed.
|
||||
*/
|
||||
open val broaderPermission: String? = null
|
||||
|
||||
/**
|
||||
* Indicates whether [permission] has protection level appop flag.
|
||||
*
|
||||
* If true, it uses getAppOpPermissionPackages() to fetch bits to decide whether the permission
|
||||
* is requested.
|
||||
*/
|
||||
open val permissionHasAppOpFlag: Boolean = true
|
||||
|
||||
open val modeForNotAllowed: Int = AppOpsManager.MODE_ERRORED
|
||||
|
||||
/**
|
||||
* Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
|
||||
*
|
||||
* Security or privacy related app-ops should be set with setUidMode() instead of setMode().
|
||||
*/
|
||||
open val setModeByUid = false
|
||||
|
||||
/** These not changeable packages will also be hidden from app list. */
|
||||
private val notChangeablePackages =
|
||||
setOf("android", "com.android.systemui", context.packageName)
|
||||
|
||||
private fun createAppOpsController(app: ApplicationInfo) =
|
||||
AppOpsController(
|
||||
context = context,
|
||||
app = app,
|
||||
op = appOp,
|
||||
setModeByUid = setModeByUid,
|
||||
modeForNotAllowed = modeForNotAllowed,
|
||||
)
|
||||
|
||||
private fun createRecord(
|
||||
app: ApplicationInfo,
|
||||
hasRequestPermission: Boolean
|
||||
): AppOpPermissionRecord =
|
||||
with(packageManagers) {
|
||||
AppOpPermissionRecord(
|
||||
app = app,
|
||||
hasRequestBroaderPermission = broaderPermission?.let {
|
||||
app.hasRequestPermission(it)
|
||||
} ?: false,
|
||||
hasRequestPermission = hasRequestPermission,
|
||||
appOpsController = createAppOpsController(app),
|
||||
)
|
||||
}
|
||||
|
||||
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
|
||||
if (permissionHasAppOpFlag) {
|
||||
userIdFlow
|
||||
.map { userId -> packageManagers.getAppOpPermissionPackages(userId, permission) }
|
||||
.combine(appListFlow) { packageNames, appList ->
|
||||
appList.map { app ->
|
||||
createRecord(
|
||||
app = app,
|
||||
hasRequestPermission = app.packageName in packageNames,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appListFlow.asyncMapItem { app ->
|
||||
with(packageManagers) { createRecord(app, app.hasRequestPermission(permission)) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun transformItem(app: ApplicationInfo) =
|
||||
with(packageManagers) {
|
||||
createRecord(
|
||||
app = app,
|
||||
hasRequestPermission = app.hasRequestPermission(permission),
|
||||
)
|
||||
}
|
||||
|
||||
override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
|
||||
recordListFlow.filterItem(::isChangeable)
|
||||
|
||||
@Composable
|
||||
override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? =
|
||||
isAllowed(
|
||||
record = record,
|
||||
appOpsController = record.appOpsController,
|
||||
permission = permission,
|
||||
packageManagers = packageManagers,
|
||||
)
|
||||
|
||||
override fun isChangeable(record: AppOpPermissionRecord) =
|
||||
record.hasRequestPermission &&
|
||||
!record.hasRequestBroaderPermission &&
|
||||
record.app.packageName !in notChangeablePackages
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
record.appOpsController.setAllowed(newAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defining the default behavior as permissible as long as the package requested this permission
|
||||
* (This means pre-M gets approval during install time; M apps gets approval during runtime).
|
||||
*/
|
||||
@Composable
|
||||
internal fun isAllowed(
|
||||
record: AppOpPermissionRecord,
|
||||
appOpsController: IAppOpsController,
|
||||
permission: String,
|
||||
packageManagers: IPackageManagers = PackageManagers,
|
||||
): () -> Boolean? {
|
||||
if (record.hasRequestBroaderPermission) {
|
||||
// Broader permission trumps the specific permission.
|
||||
return { true }
|
||||
}
|
||||
|
||||
val mode = appOpsController.mode.collectAsStateWithLifecycle(initialValue = null)
|
||||
return {
|
||||
when (mode.value) {
|
||||
null -> null
|
||||
AppOpsManager.MODE_ALLOWED -> true
|
||||
AppOpsManager.MODE_DEFAULT -> {
|
||||
with(packageManagers) { record.app.hasGrantPermission(permission) }
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
|
||||
import com.android.settingslib.spaprivileged.framework.compose.placeholder
|
||||
import com.android.settingslib.spaprivileged.model.app.userHandle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
private const val TAG = "AppStorageSize"
|
||||
|
||||
@Composable
|
||||
fun ApplicationInfo.getStorageSize(): State<String> {
|
||||
val context = LocalContext.current
|
||||
return remember(this) {
|
||||
flow {
|
||||
val sizeBytes = calculateSizeBytes(context)
|
||||
this.emit(if (sizeBytes != null) Formatter.formatFileSize(context, sizeBytes) else "")
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}.collectAsStateWithLifecycle(initialValue = placeholder())
|
||||
}
|
||||
|
||||
fun ApplicationInfo.calculateSizeBytes(context: Context): Long? {
|
||||
val storageStatsManager = context.storageStatsManager
|
||||
return try {
|
||||
val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle)
|
||||
stats.codeBytes + stats.dataBytes
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to query stats: $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntry
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
|
||||
import com.android.settingslib.spa.framework.common.SettingsPage
|
||||
import com.android.settingslib.spa.framework.common.SettingsPageProvider
|
||||
import com.android.settingslib.spa.framework.compose.navigator
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spa.widget.ui.AnnotatedText
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
|
||||
import com.android.settingslib.spaprivileged.model.app.PackageManagers
|
||||
import com.android.settingslib.spaprivileged.model.app.toRoute
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.EnhancedConfirmation
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
internal class TogglePermissionAppInfoPageProvider(
|
||||
private val appListTemplate: TogglePermissionAppListTemplate,
|
||||
) : SettingsPageProvider {
|
||||
override val name = PAGE_NAME
|
||||
|
||||
override val parameter = PAGE_PARAMETER
|
||||
|
||||
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
|
||||
val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments)
|
||||
return listOf(SettingsEntryBuilder.create("AllowControl", owner).build())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Page(arguments: Bundle?) {
|
||||
val permissionType = arguments?.getString(PERMISSION)!!
|
||||
val packageName = arguments.getString(PACKAGE_NAME)!!
|
||||
val userId = arguments.getInt(USER_ID)
|
||||
appListTemplate.rememberModel(permissionType)
|
||||
.TogglePermissionAppInfoPage(packageName, userId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_NAME = "TogglePermissionAppInfoPage"
|
||||
private const val PERMISSION = "permission"
|
||||
private const val PACKAGE_NAME = "rt_packageName"
|
||||
private const val USER_ID = "rt_userId"
|
||||
|
||||
private val PAGE_PARAMETER = listOf(
|
||||
navArgument(PERMISSION) { type = NavType.StringType },
|
||||
navArgument(PACKAGE_NAME) { type = NavType.StringType },
|
||||
navArgument(USER_ID) { type = NavType.IntType },
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the route prefix to this page.
|
||||
*
|
||||
* Expose route prefix to enable enter from non-SPA pages.
|
||||
*/
|
||||
fun getRoutePrefix(permissionType: String) = "$PAGE_NAME/$permissionType"
|
||||
|
||||
@Composable
|
||||
fun navigator(permissionType: String, app: ApplicationInfo) =
|
||||
navigator(route = "$PAGE_NAME/$permissionType/${app.toRoute()}")
|
||||
|
||||
fun buildPageData(permissionType: String): SettingsPage {
|
||||
return SettingsPage.create(
|
||||
name = PAGE_NAME,
|
||||
parameter = PAGE_PARAMETER,
|
||||
arguments = bundleOf(PERMISSION to permissionType)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppInfoPageEntryItem(
|
||||
permissionType: String,
|
||||
app: ApplicationInfo,
|
||||
) {
|
||||
val record = remember { transformItem(app) }
|
||||
if (!remember { isChangeable(record) }) return
|
||||
val context = LocalContext.current
|
||||
val internalListModel = remember {
|
||||
TogglePermissionInternalAppListModel(
|
||||
context = context,
|
||||
permissionType = permissionType,
|
||||
listModel = this,
|
||||
restrictionsProviderFactory = ::RestrictionsProviderImpl,
|
||||
)
|
||||
}
|
||||
Preference(
|
||||
object : PreferenceModel {
|
||||
override val title = stringResource(pageTitleResId)
|
||||
override val summary = internalListModel.getSummary(record)
|
||||
override val onClick =
|
||||
TogglePermissionAppInfoPageProvider.navigator(permissionType, app)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppInfoPage(
|
||||
packageName: String,
|
||||
userId: Int,
|
||||
packageManagers: IPackageManagers = PackageManagers,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
|
||||
) {
|
||||
AppInfoPage(
|
||||
title = stringResource(pageTitleResId),
|
||||
packageName = packageName,
|
||||
userId = userId,
|
||||
footerContent = { AnnotatedText(footerResId) },
|
||||
packageManagers = packageManagers,
|
||||
) {
|
||||
val app = applicationInfo ?: return@AppInfoPage
|
||||
val record = rememberRecord(app).value ?: return@AppInfoPage
|
||||
val isAllowed = isAllowed(record)
|
||||
val isChangeable by rememberIsChangeable(record)
|
||||
val switchModel = object : SwitchPreferenceModel {
|
||||
override val title = stringResource(switchTitleResId)
|
||||
override val checked = isAllowed
|
||||
override val changeable = { isChangeable }
|
||||
override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) }
|
||||
}
|
||||
val restrictions = Restrictions(userId = userId,
|
||||
keys = switchRestrictionKeys,
|
||||
enhancedConfirmation = enhancedConfirmationKey?.let { EnhancedConfirmation(
|
||||
key = it,
|
||||
packageName = packageName) })
|
||||
RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory)
|
||||
InfoPageAdditionalContent(record, isAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberRecord(app: ApplicationInfo) =
|
||||
remember(app) {
|
||||
flow {
|
||||
emit(transformItem(app))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.collectAsStateWithLifecycle(initialValue = null)
|
||||
|
||||
|
||||
@Composable
|
||||
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberIsChangeable(record: T) =
|
||||
remember(record) {
|
||||
flow {
|
||||
emit(isChangeable(record))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
}.collectAsStateWithLifecycle(initialValue = false)
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
|
||||
import com.android.settingslib.spa.framework.common.SettingsPageProvider
|
||||
import com.android.settingslib.spa.framework.compose.rememberContext
|
||||
import com.android.settingslib.spa.framework.util.asyncMapItem
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Implement this interface to build an App List which toggles a permission on / off.
|
||||
*/
|
||||
interface TogglePermissionAppListModel<T : AppRecord> {
|
||||
val pageTitleResId: Int
|
||||
val switchTitleResId: Int
|
||||
val footerResId: Int
|
||||
val switchRestrictionKeys: List<String>
|
||||
get() = emptyList()
|
||||
|
||||
val enhancedConfirmationKey: String?
|
||||
get() = null
|
||||
|
||||
/**
|
||||
* Loads the extra info for the App List, and generates the [AppRecord] List.
|
||||
*
|
||||
* Default is implemented by [transformItem]
|
||||
*/
|
||||
fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>): Flow<List<T>> =
|
||||
appListFlow.asyncMapItem(::transformItem)
|
||||
|
||||
/**
|
||||
* Loads the extra info for one app, and generates the [AppRecord].
|
||||
*
|
||||
* This must be implemented, because when show the App Info page for single app, this will be
|
||||
* used instead of [transform].
|
||||
*/
|
||||
fun transformItem(app: ApplicationInfo): T
|
||||
|
||||
/**
|
||||
* Filters the [AppRecord] list.
|
||||
*
|
||||
* @return the [AppRecord] list which will be displayed.
|
||||
*/
|
||||
fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<T>>): Flow<List<T>>
|
||||
|
||||
/**
|
||||
* Gets whether the permission is allowed for the given app.
|
||||
*/
|
||||
@Composable
|
||||
fun isAllowed(record: T): () -> Boolean?
|
||||
|
||||
/**
|
||||
* Gets whether the permission on / off is changeable for the given app.
|
||||
*/
|
||||
fun isChangeable(record: T): Boolean
|
||||
|
||||
/**
|
||||
* Sets whether the permission is allowed for the given app.
|
||||
*/
|
||||
fun setAllowed(record: T, newAllowed: Boolean)
|
||||
|
||||
@Composable
|
||||
fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?){}
|
||||
}
|
||||
|
||||
interface TogglePermissionAppListProvider {
|
||||
val permissionType: String
|
||||
|
||||
fun createModel(context: Context): TogglePermissionAppListModel<out AppRecord>
|
||||
|
||||
fun buildAppListInjectEntry(): SettingsEntryBuilder =
|
||||
TogglePermissionAppListPageProvider.buildInjectEntry(permissionType) { createModel(it) }
|
||||
|
||||
/**
|
||||
* Gets the route to the toggle permission App List page.
|
||||
*
|
||||
* Expose route to enable enter from non-SPA pages.
|
||||
*/
|
||||
fun getAppListRoute(): String =
|
||||
TogglePermissionAppListPageProvider.getRoute(permissionType)
|
||||
|
||||
/**
|
||||
* Gets the route prefix to the toggle permission App Info page.
|
||||
*
|
||||
* Expose route prefix to enable enter from non-SPA pages.
|
||||
*/
|
||||
fun getAppInfoRoutePrefix(): String =
|
||||
TogglePermissionAppInfoPageProvider.getRoutePrefix(permissionType)
|
||||
|
||||
@Composable
|
||||
fun InfoPageEntryItem(app: ApplicationInfo) {
|
||||
val listModel = rememberContext(::createModel)
|
||||
listModel.TogglePermissionAppInfoPageEntryItem(permissionType, app)
|
||||
}
|
||||
}
|
||||
|
||||
class TogglePermissionAppListTemplate(
|
||||
allProviders: List<TogglePermissionAppListProvider>,
|
||||
) {
|
||||
private val listModelProviderMap = allProviders.associateBy { it.permissionType }
|
||||
|
||||
fun createPageProviders(): List<SettingsPageProvider> = listOf(
|
||||
TogglePermissionAppListPageProvider(this),
|
||||
TogglePermissionAppInfoPageProvider(this),
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun rememberModel(permissionType: String) = rememberContext { context ->
|
||||
listModelProviderMap.getValue(permissionType).createModel(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntry
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
|
||||
import com.android.settingslib.spa.framework.common.SettingsPage
|
||||
import com.android.settingslib.spa.framework.common.SettingsPageProvider
|
||||
import com.android.settingslib.spa.framework.compose.navigator
|
||||
import com.android.settingslib.spa.framework.compose.rememberContext
|
||||
import com.android.settingslib.spa.framework.util.getStringArg
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.model.app.userId
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.EnhancedConfirmation
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
|
||||
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
private const val ENTRY_NAME = "AppList"
|
||||
private const val PERMISSION = "permission"
|
||||
private const val PAGE_NAME = "TogglePermissionAppList"
|
||||
private val PAGE_PARAMETER = listOf(
|
||||
navArgument(PERMISSION) { type = NavType.StringType },
|
||||
)
|
||||
|
||||
internal class TogglePermissionAppListPageProvider(
|
||||
private val appListTemplate: TogglePermissionAppListTemplate,
|
||||
) : SettingsPageProvider {
|
||||
override val name = PAGE_NAME
|
||||
|
||||
override val parameter = PAGE_PARAMETER
|
||||
|
||||
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
|
||||
val permissionType = parameter.getStringArg(PERMISSION, arguments)!!
|
||||
val appListPage = SettingsPage.create(name, parameter = parameter, arguments = arguments)
|
||||
val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType)
|
||||
// TODO: add more categories, such as personal, work, cloned, etc.
|
||||
return listOf("personal").map { category ->
|
||||
SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
|
||||
.setLink(toPage = appInfoPage)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Page(arguments: Bundle?) {
|
||||
val permissionType = arguments?.getString(PERMISSION)!!
|
||||
appListTemplate.rememberModel(permissionType).TogglePermissionAppList(permissionType)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Gets the route to this page.
|
||||
*
|
||||
* Expose route to enable enter from non-SPA pages.
|
||||
*/
|
||||
fun getRoute(permissionType: String) = "$PAGE_NAME/$permissionType"
|
||||
|
||||
fun buildInjectEntry(
|
||||
permissionType: String,
|
||||
listModelSupplier: (Context) -> TogglePermissionAppListModel<out AppRecord>,
|
||||
): SettingsEntryBuilder {
|
||||
val appListPage = SettingsPage.create(
|
||||
name = PAGE_NAME,
|
||||
parameter = PAGE_PARAMETER,
|
||||
arguments = bundleOf(PERMISSION to permissionType)
|
||||
)
|
||||
return SettingsEntryBuilder.createInject(owner = appListPage)
|
||||
.setUiLayoutFn {
|
||||
val listModel = rememberContext(listModelSupplier)
|
||||
Preference(
|
||||
object : PreferenceModel {
|
||||
override val title = stringResource(listModel.pageTitleResId)
|
||||
override val onClick = navigator(route = getRoute(permissionType))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppList(
|
||||
permissionType: String,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
|
||||
appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
AppListPage(
|
||||
title = stringResource(pageTitleResId),
|
||||
listModel = remember {
|
||||
TogglePermissionInternalAppListModel(
|
||||
context = context,
|
||||
permissionType = permissionType,
|
||||
listModel = this,
|
||||
restrictionsProviderFactory = restrictionsProviderFactory,
|
||||
)
|
||||
},
|
||||
appList = appList,
|
||||
)
|
||||
}
|
||||
|
||||
internal class TogglePermissionInternalAppListModel<T : AppRecord>(
|
||||
private val context: Context,
|
||||
private val permissionType: String,
|
||||
private val listModel: TogglePermissionAppListModel<T>,
|
||||
private val restrictionsProviderFactory: RestrictionsProviderFactory,
|
||||
) : AppListModel<T> {
|
||||
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
|
||||
listModel.transform(userIdFlow, appListFlow)
|
||||
|
||||
override fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>) =
|
||||
listModel.filter(userIdFlow, recordListFlow)
|
||||
|
||||
@Composable
|
||||
override fun getSummary(option: Int, record: T) = getSummary(record)
|
||||
|
||||
@Composable
|
||||
fun getSummary(record: T): () -> String {
|
||||
val restrictions = remember(record.app.userId, record.app.packageName) {
|
||||
Restrictions(
|
||||
userId = record.app.userId,
|
||||
keys = listModel.switchRestrictionKeys,
|
||||
enhancedConfirmation = listModel.enhancedConfirmationKey?.let {
|
||||
EnhancedConfirmation(
|
||||
key = it,
|
||||
packageName = record.app.packageName)
|
||||
})
|
||||
}
|
||||
val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions)
|
||||
val allowed = listModel.isAllowed(record)
|
||||
return RestrictedSwitchPreferenceModel.getSummary(
|
||||
context = context,
|
||||
restrictedModeSupplier = { restrictedMode },
|
||||
summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) },
|
||||
checked = allowed,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSummaryIfNoRestricted(allowed: Boolean?): String = when (allowed) {
|
||||
true -> context.getString(R.string.app_permission_summary_allowed)
|
||||
false -> context.getString(R.string.app_permission_summary_not_allowed)
|
||||
null -> context.getPlaceholder()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun AppListItemModel<T>.AppItem() {
|
||||
AppListItem(
|
||||
onClick = TogglePermissionAppInfoPageProvider.navigator(
|
||||
permissionType = permissionType,
|
||||
app = record.app,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.common
|
||||
|
||||
import android.content.pm.UserInfo
|
||||
import android.content.pm.UserProperties
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.android.settingslib.spa.widget.scaffold.SettingsPager
|
||||
import com.android.settingslib.spaprivileged.framework.common.userManager
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.EnterpriseRepository
|
||||
|
||||
/**
|
||||
* Info about how to group multiple profiles for Settings.
|
||||
*
|
||||
* @see [UserProperties.ShowInSettings]
|
||||
*/
|
||||
data class UserGroup(
|
||||
/** The users in this user group, if multiple users, the first one is parent user. */
|
||||
val userInfos: List<UserInfo>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun UserProfilePager(content: @Composable (userGroup: UserGroup) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val userGroups = remember { context.userManager.getUserGroups() }
|
||||
val titles = remember {
|
||||
val enterpriseRepository = EnterpriseRepository(context)
|
||||
userGroups.map { userGroup ->
|
||||
enterpriseRepository.getProfileTitle(
|
||||
userGroup.userInfos.first(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsPager(titles) { page ->
|
||||
content(userGroups[page])
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserManager.getUserGroups(): List<UserGroup> {
|
||||
val userGroupList = mutableListOf<UserGroup>()
|
||||
val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) }
|
||||
|
||||
showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let {
|
||||
userGroupList += UserGroup(it)
|
||||
}
|
||||
|
||||
showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach {
|
||||
userGroupList += UserGroup(listOf(it))
|
||||
}
|
||||
|
||||
return userGroupList
|
||||
}
|
||||
|
||||
private fun UserManager.showInSettings(userInfo: UserInfo): Int {
|
||||
val userProperties = getUserProperties(userInfo.userHandle)
|
||||
return if (userInfo.isQuietModeEnabled && userProperties.showInQuietMode
|
||||
== UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
|
||||
UserProperties.SHOW_IN_SETTINGS_NO
|
||||
} else {
|
||||
userProperties.showInSettings
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.preference
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.widget.preference.MainSwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
|
||||
|
||||
@Composable
|
||||
fun RestrictedMainSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) {
|
||||
RestrictedMainSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun RestrictedMainSwitchPreference(
|
||||
model: SwitchPreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory,
|
||||
) {
|
||||
if (restrictions.keys.isEmpty()) {
|
||||
MainSwitchPreference(model)
|
||||
return
|
||||
}
|
||||
restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
|
||||
MainSwitchPreference(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.preference
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
|
||||
|
||||
@Composable
|
||||
fun RestrictedPreference(
|
||||
model: PreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
) {
|
||||
RestrictedPreference(model, restrictions, ::RestrictionsProviderImpl)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun RestrictedPreference(
|
||||
model: PreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory,
|
||||
) {
|
||||
if (restrictions.keys.isEmpty()) {
|
||||
Preference(model)
|
||||
return
|
||||
}
|
||||
val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
|
||||
val restrictedSwitchModel = remember(restrictedMode) {
|
||||
RestrictedPreferenceModel(model, restrictedMode)
|
||||
}
|
||||
restrictedSwitchModel.RestrictionWrapper {
|
||||
Preference(restrictedSwitchModel)
|
||||
}
|
||||
}
|
||||
|
||||
private class RestrictedPreferenceModel(
|
||||
model: PreferenceModel,
|
||||
private val restrictedMode: RestrictedMode?,
|
||||
) : PreferenceModel {
|
||||
override val title = model.title
|
||||
override val summary = model.summary
|
||||
override val icon = model.icon
|
||||
|
||||
override val enabled = when (restrictedMode) {
|
||||
NoRestricted -> model.enabled
|
||||
else -> ({ false })
|
||||
}
|
||||
|
||||
override val onClick = when (restrictedMode) {
|
||||
NoRestricted -> model.onClick
|
||||
// Need to passthrough onClick for clickable semantics, although since enabled is false so
|
||||
// this will not be called.
|
||||
BaseUserRestricted -> model.onClick
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RestrictionWrapper(content: @Composable () -> Unit) {
|
||||
if (restrictedMode !is BlockedByAdmin) {
|
||||
content()
|
||||
return
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
|
||||
)
|
||||
) { content() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.preference
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
|
||||
|
||||
@Composable
|
||||
fun RestrictedSwitchPreference(
|
||||
model: SwitchPreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
) {
|
||||
RestrictedSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun RestrictedSwitchPreference(
|
||||
model: SwitchPreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory,
|
||||
) {
|
||||
if (restrictions.isEmpty()) {
|
||||
SwitchPreference(model)
|
||||
return
|
||||
}
|
||||
restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
|
||||
SwitchPreference(it)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.toggleableState
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByEcm
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
|
||||
|
||||
internal class RestrictedSwitchPreferenceModel(
|
||||
context: Context,
|
||||
model: SwitchPreferenceModel,
|
||||
private val restrictedMode: RestrictedMode?,
|
||||
) : SwitchPreferenceModel {
|
||||
override val title = model.title
|
||||
|
||||
override val summary = getSummary(
|
||||
context = context,
|
||||
restrictedModeSupplier = { restrictedMode },
|
||||
summaryIfNoRestricted = model.summary,
|
||||
checked = model.checked,
|
||||
)
|
||||
|
||||
override val checked = when (restrictedMode) {
|
||||
null -> ({ null })
|
||||
is NoRestricted -> model.checked
|
||||
is BaseUserRestricted -> ({ false })
|
||||
is BlockedByAdmin -> model.checked
|
||||
is BlockedByEcm -> model.checked
|
||||
}
|
||||
|
||||
override val changeable = if (restrictedMode is NoRestricted) model.changeable else ({ false })
|
||||
|
||||
override val onCheckedChange = when (restrictedMode) {
|
||||
null -> null
|
||||
is NoRestricted -> model.onCheckedChange
|
||||
// Need to passthrough onCheckedChange for toggleable semantics, although since changeable
|
||||
// is false so this will not be called.
|
||||
is BaseUserRestricted -> model.onCheckedChange
|
||||
// Pass null since semantics ToggleableState is provided in RestrictionWrapper.
|
||||
is BlockedByAdmin -> null
|
||||
is BlockedByEcm -> null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RestrictionWrapper(content: @Composable () -> Unit) {
|
||||
when (restrictedMode) {
|
||||
is BlockedByAdmin -> {
|
||||
Box(
|
||||
Modifier
|
||||
.clickable(
|
||||
role = Role.Switch,
|
||||
onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
|
||||
)
|
||||
.semantics {
|
||||
this.toggleableState = ToggleableState(checked())
|
||||
},
|
||||
) { content() }
|
||||
}
|
||||
|
||||
is BlockedByEcm -> {
|
||||
Box(
|
||||
Modifier
|
||||
.clickable(
|
||||
role = Role.Switch,
|
||||
onClick = { restrictedMode.showRestrictedSettingsDetails() },
|
||||
)
|
||||
.semantics {
|
||||
this.toggleableState = ToggleableState(checked())
|
||||
},
|
||||
) { content() }
|
||||
}
|
||||
|
||||
else -> {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ToggleableState(value: Boolean?) = when (value) {
|
||||
true -> ToggleableState.On
|
||||
false -> ToggleableState.Off
|
||||
null -> ToggleableState.Indeterminate
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun RestrictionsProviderFactory.RestrictedSwitchWrapper(
|
||||
model: SwitchPreferenceModel,
|
||||
restrictions: Restrictions,
|
||||
content: @Composable (SwitchPreferenceModel) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val restrictedMode = rememberRestrictedMode(restrictions).value
|
||||
val restrictedSwitchPreferenceModel = remember(restrictedMode) {
|
||||
RestrictedSwitchPreferenceModel(context, model, restrictedMode)
|
||||
}
|
||||
restrictedSwitchPreferenceModel.RestrictionWrapper {
|
||||
content(restrictedSwitchPreferenceModel)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSummary(
|
||||
context: Context,
|
||||
restrictedModeSupplier: () -> RestrictedMode?,
|
||||
summaryIfNoRestricted: () -> String,
|
||||
checked: () -> Boolean?,
|
||||
): () -> String = {
|
||||
when (val restrictedMode = restrictedModeSupplier()) {
|
||||
is NoRestricted -> summaryIfNoRestricted()
|
||||
is BaseUserRestricted ->
|
||||
context.getString(com.android.settingslib.R.string.disabled)
|
||||
|
||||
is BlockedByAdmin -> restrictedMode.getSummary(checked())
|
||||
is BlockedByEcm ->
|
||||
context.getString(com.android.settingslib.R.string.disabled)
|
||||
|
||||
null -> context.getPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.scaffold
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByEcm
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
|
||||
|
||||
@Composable
|
||||
fun MoreOptionsScope.RestrictedMenuItem(
|
||||
text: String,
|
||||
restrictions: Restrictions,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
RestrictedMenuItemImpl(text, restrictions, onClick, ::RestrictionsProviderImpl)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Composable
|
||||
internal fun MoreOptionsScope.RestrictedMenuItemImpl(
|
||||
text: String,
|
||||
restrictions: Restrictions,
|
||||
onClick: () -> Unit,
|
||||
restrictionsProviderFactory: RestrictionsProviderFactory,
|
||||
) {
|
||||
val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
|
||||
MenuItem(text = text, enabled = restrictedMode !== BaseUserRestricted) {
|
||||
when (restrictedMode) {
|
||||
is BlockedByAdmin -> restrictedMode.sendShowAdminSupportDetailsIntent()
|
||||
is BlockedByEcm -> restrictedMode.showRestrictedSettingsDetails()
|
||||
else -> onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user