From 00ea7991f53818228fb80d5971e6718c056e200a Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Sun, 23 Oct 2022 00:10:31 +0800 Subject: [PATCH] Add OverridableFlow for Spa One typical use case is that when a switch state comes from a flow, and the user toggle the switch to change the value, we can modify the flow to show the latest state. Also temporary copy the FlowExt from AndroidX lifecycle, to use the collectAsStateWithLifecycle(), so we can show the latest data from flow when each on start lifecycle event. Bug: 236346018 Test: Unit test Change-Id: Ib34ae9c5ce0b9122d11032aeed173033df5cc2d1 --- packages/SettingsLib/Spa/gallery/build.gradle | 5 - packages/SettingsLib/Spa/spa/build.gradle | 5 - .../spa/framework/compose/FlowExt.kt | 189 ++++++++++++++++++ .../spa/framework/compose/OverridableFlow.kt | 35 ++++ packages/SettingsLib/Spa/tests/build.gradle | 5 - .../spa/framework/OverridableFlowTest.kt | 63 ++++++ .../framework/common/Contexts.kt | 43 +++- 7 files changed, 325 insertions(+), 20 deletions(-) create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt create mode 100644 packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt create mode 100644 packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle index c1ce7d96702df..20dd707de4821 100644 --- a/packages/SettingsLib/Spa/gallery/build.gradle +++ b/packages/SettingsLib/Spa/gallery/build.gradle @@ -54,11 +54,6 @@ android { composeOptions { kotlinCompilerExtensionVersion jetpack_compose_compiler_version } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } } dependencies { diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index c5874113ef321..3bafcf27fed99 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -51,11 +51,6 @@ android { composeOptions { kotlinCompilerExtensionVersion jetpack_compose_compiler_version } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } } dependencies { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt new file mode 100644 index 0000000000000..dbf8836796efd --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt @@ -0,0 +1,189 @@ +/* + * Copyright 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.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * ************************************************************************************************* + * This file was forked from AndroidX: + * lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt + * TODO: Replace with AndroidX when it's usable. + */ + +/** + * Collects values from this [StateFlow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * The [StateFlow.value] is used as an initial value. Every time there would be new value posted + * into the [StateFlow] the returned [State] will be updated causing recomposition of every + * [State.value] usage whenever the [lifecycleOwner]'s lifecycle is at least [minActiveState]. + * + * This [StateFlow] is collected every time the [lifecycleOwner]'s lifecycle reaches the + * [minActiveState] Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle + * falls below [minActiveState]. + * + * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this` + * flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@SuppressLint("StateFlowValueCalledInComposition") +@Composable +fun StateFlow.collectAsStateWithLifecycle( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext +): State = collectAsStateWithLifecycle( + initialValue = this.value, + lifecycle = lifecycleOwner.lifecycle, + minActiveState = minActiveState, + context = context +) + +/** + * Collects values from this [StateFlow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * The [StateFlow.value] is used as an initial value. Every time there would be new value posted + * into the [StateFlow] the returned [State] will be updated causing recomposition of every + * [State.value] usage whenever the [lifecycle] is at least [minActiveState]. + * + * This [StateFlow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle + * state. The collection stops when [lifecycle] falls below [minActiveState]. + * + * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param lifecycle [Lifecycle] used to restart collecting `this` flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@SuppressLint("StateFlowValueCalledInComposition") +@Composable +fun StateFlow.collectAsStateWithLifecycle( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext +): State = collectAsStateWithLifecycle( + initialValue = this.value, + lifecycle = lifecycle, + minActiveState = minActiveState, + context = context +) + +/** + * Collects values from this [Flow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * Every time there would be new value posted into the [Flow] the returned [State] will be updated + * causing recomposition of every [State.value] usage whenever the [lifecycleOwner]'s lifecycle is + * at least [minActiveState]. + * + * This [Flow] is collected every time the [lifecycleOwner]'s lifecycle reaches the [minActiveState] + * Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle falls below + * [minActiveState]. + * + * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param initialValue The initial value given to the returned [State.value]. + * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this` + * flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +fun Flow.collectAsStateWithLifecycle( + initialValue: T, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext +): State = collectAsStateWithLifecycle( + initialValue = initialValue, + lifecycle = lifecycleOwner.lifecycle, + minActiveState = minActiveState, + context = context +) + +/** + * Collects values from this [Flow] and represents its latest value via [State] in a + * lifecycle-aware manner. + * + * Every time there would be new value posted into the [Flow] the returned [State] will be updated + * causing recomposition of every [State.value] usage whenever the [lifecycle] is at + * least [minActiveState]. + * + * This [Flow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle + * state. The collection stops when [lifecycle] falls below [minActiveState]. + * + * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle + * + * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a + * parameter will throw an [IllegalArgumentException]. + * + * @param initialValue The initial value given to the returned [State.value]. + * @param lifecycle [Lifecycle] used to restart collecting `this` flow. + * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The + * collection will stop if the lifecycle falls below that state, and will restart if it's in that + * state again. + * @param context [CoroutineContext] to use for collecting. + */ +@Composable +fun Flow.collectAsStateWithLifecycle( + initialValue: T, + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + context: CoroutineContext = EmptyCoroutineContext +): State { + return produceState(initialValue, this, lifecycle, minActiveState, context) { + lifecycle.repeatOnLifecycle(minActiveState) { + if (context == EmptyCoroutineContext) { + this@collectAsStateWithLifecycle.collect { this@produceState.value = it } + } else withContext(context) { + this@collectAsStateWithLifecycle.collect { this@produceState.value = it } + } + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt new file mode 100644 index 0000000000000..1b33dd6d177e6 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt @@ -0,0 +1,35 @@ +/* + * Copyright 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 kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.receiveAsFlow + +/** + * A flow which result is overridable. + */ +class OverridableFlow(flow: Flow) { + private val overrideChannel = Channel() + + val flow = merge(overrideChannel.receiveAsFlow(), flow) + + fun override(value: T) { + overrideChannel.trySend(value) + } +} diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle index b43bf1854c268..4b4c6a35e92dc 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -55,11 +55,6 @@ android { composeOptions { kotlinCompilerExtensionVersion jetpack_compose_compiler_version } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } } dependencies { diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt new file mode 100644 index 0000000000000..8f460f30cf364 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/com/android/settingslib/spa/framework/OverridableFlowTest.kt @@ -0,0 +1,63 @@ +/* + * 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.com.android.settingslib.spa.framework + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.framework.compose.OverridableFlow +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class OverridableFlowTest { + + @Test + fun noOverride() = runTest { + val overridableFlow = OverridableFlow(flowOf(true)) + + launch { + val values = collectValues(overridableFlow.flow) + assertThat(values).containsExactly(true) + } + } + + @Test + fun whenOverride() = runTest { + val overridableFlow = OverridableFlow(flowOf(true)) + + overridableFlow.override(false) + + launch { + val values = collectValues(overridableFlow.flow) + assertThat(values).containsExactly(true, false).inOrder() + } + } + + private suspend fun collectValues(flow: Flow): List = withTimeout(500) { + val flowValues = mutableListOf() + flow.toList(flowValues) + flowValues + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt index 99649263c76aa..fd723dd067d7b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt @@ -1,24 +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.framework.common +import android.app.AlarmManager +import android.app.AppOpsManager import android.app.admin.DevicePolicyManager import android.app.usage.StorageStatsManager +import android.apphibernation.AppHibernationManager import android.content.Context import android.content.pm.verify.domain.DomainVerificationManager import android.os.UserHandle import android.os.UserManager +import android.permission.PermissionControllerManager -/** The [UserManager] instance. */ -val Context.userManager get() = getSystemService(UserManager::class.java)!! +/** The [AlarmManager] instance. */ +val Context.alarmManager get() = getSystemService(AlarmManager::class.java)!! + +/** The [AppHibernationManager] instance. */ +val Context.appHibernationManager get() = getSystemService(AppHibernationManager::class.java)!! + +/** The [AppOpsManager] instance. */ +val Context.appOpsManager get() = getSystemService(AppOpsManager::class.java)!! /** The [DevicePolicyManager] instance. */ val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::class.java)!! -/** The [StorageStatsManager] instance. */ -val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!! - /** The [DomainVerificationManager] instance. */ val Context.domainVerificationManager get() = getSystemService(DomainVerificationManager::class.java)!! +/** The [PermissionControllerManager] instance. */ +val Context.permissionControllerManager + get() = getSystemService(PermissionControllerManager::class.java)!! + +/** The [StorageStatsManager] instance. */ +val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!! + +/** The [UserManager] instance. */ +val Context.userManager get() = getSystemService(UserManager::class.java)!! + /** Gets a new [Context] for the given [UserHandle]. */ fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0)