From d493898cb1d8922ec93415878a84167530a4645f Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 20 Sep 2022 15:33:06 +0800 Subject: [PATCH] Add disabled state to ActionButtons Also align the colors to current Settings. And add other utils for the App Buttons usage. Bug: 236346018 Test: Manual with Settings App Change-Id: Ib8026ae7f89cbb88ac93479c5a1996c67dd4976e --- .../DisposableBroadcastReceiverAsUser.kt | 66 +++++++++++++++++++ .../settingslib/spa/framework/util/Flows.kt | 24 ++++--- .../spa/widget/button/ActionButtons.kt | 10 ++- .../spaprivileged/model/app/Apps.kt | 11 ++++ .../model/app/PackageManagers.kt | 2 +- 5 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt new file mode 100644 index 0000000000000..1589b04813cfa --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt @@ -0,0 +1,66 @@ +/* + * 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.spa.framework.compose + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver + +/** + * A [BroadcastReceiver] which registered when on start and unregistered when on stop. + */ +@Composable +fun DisposableBroadcastReceiverAsUser( + userId: Int, + intentFilter: IntentFilter, + onReceive: (Intent) -> Unit, +) { + val broadcastReceiver = remember { + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + onReceive(intent) + } + } + } + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + context.registerReceiverAsUser( + broadcastReceiver, UserHandle.of(userId), intentFilter, null, null) + } else if (event == Lifecycle.Event.ON_STOP) { + context.unregisterReceiver(broadcastReceiver) + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt index 999d8d7dd16a9..d801840565edc 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt @@ -19,25 +19,33 @@ package com.android.settingslib.spa.framework.util import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.snapshotFlow -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +/** + * Returns a [Flow] whose values are a list which containing the results of asynchronously applying + * the given [transform] function to each element in the original flow's list. + */ inline fun Flow>.asyncMapItem(crossinline transform: (T) -> R): Flow> = map { list -> list.asyncMap(transform) } -@OptIn(ExperimentalCoroutinesApi::class) -inline fun Flow.mapState(crossinline block: (T) -> State): Flow = - flatMapLatest { snapshotFlow { block(it).value } } +/** + * Delays the flow a little bit, wait the other flow's first value. + */ +fun Flow.waitFirst(otherFlow: Flow): Flow = + combine(otherFlow.distinctUntilChangedBy {}) { value, _ -> value } -fun Flow.waitFirst(flow: Flow): Flow = - combine(flow.distinctUntilChangedBy {}) { value, _ -> value } +/** + * Returns a [Flow] whose values are generated list by combining the most recently emitted non null + * values by each flow. + */ +inline fun combineToList(vararg flows: Flow): Flow> = combine( + flows.asList(), +) { array: Array -> array.filterNotNull() } class StateFlowBridge { private val stateFlow = MutableStateFlow(null) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 3555ec7467894..a9d1c3735ab76 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -17,6 +17,7 @@ package com.android.settingslib.spa.widget.button import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -39,6 +40,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -54,6 +56,7 @@ import com.android.settingslib.spa.framework.theme.divider data class ActionButton( val text: String, val imageVector: ImageVector, + val enabled: Boolean = true, val onClick: () -> Unit, ) @@ -79,10 +82,15 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { modifier = Modifier .weight(1f) .fillMaxHeight(), + enabled = actionButton.enabled, + // Because buttons could appear, disappear or change positions, reset the interaction source + // to prevent highlight the wrong button. + interactionSource = remember(actionButton) { MutableInteractionSource() }, shape = RectangleShape, colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.surface, + containerColor = SettingsTheme.colorScheme.surface, contentColor = SettingsTheme.colorScheme.categoryTitle, + disabledContainerColor = SettingsTheme.colorScheme.surface, ), contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp), ) { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt index 99a08abad8b7e..96e3e77e93223 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt @@ -17,12 +17,23 @@ package com.android.settingslib.spaprivileged.model.app import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.os.UserHandle +/** The user id for a given application. */ val ApplicationInfo.userId: Int get() = UserHandle.getUserId(uid) +/** The [UserHandle] for a given application. */ val ApplicationInfo.userHandle: UserHandle get() = UserHandle.getUserHandleForUid(uid) +/** Checks whether a flag is associated with the application. */ +fun ApplicationInfo.hasFlag(flag: Int): Boolean = (flags and flag) > 0 + +/** Checks whether the application is disabled until used. */ +fun ApplicationInfo.isDisabledUntilUsed(): Boolean = + enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED + +/** Converts to the route string which used in navigation. */ fun ApplicationInfo.toRoute() = "$packageName/$userId" diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt index ba8af5400e182..5c5caf5987178 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt @@ -55,7 +55,7 @@ object PackageManagers { iPackageManager.isPackageAvailable(it, userId) }.toSet() - private fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? = + fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? = try { PackageManager.getPackageInfoAsUserCached(packageName, flags.toLong(), userId) } catch (e: PackageManager.NameNotFoundException) {