fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
)
}

View File

@@ -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 {}
}
}

View File

@@ -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)
}
}

View File

@@ -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
},
)
}

View File

@@ -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,
)
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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,
),
)
}
}