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
This commit is contained in:
Chaohui Wang
2022-10-23 00:10:31 +08:00
parent c84d661e07
commit 00ea7991f5
7 changed files with 325 additions and 20 deletions

View File

@@ -54,11 +54,6 @@ android {
composeOptions {
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {

View File

@@ -51,11 +51,6 @@ android {
composeOptions {
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {

View File

@@ -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 <T> StateFlow<T>.collectAsStateWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> = 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 <T> StateFlow<T>.collectAsStateWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> = 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 <T> Flow<T>.collectAsStateWithLifecycle(
initialValue: T,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> = 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 <T> Flow<T>.collectAsStateWithLifecycle(
initialValue: T,
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> {
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 }
}
}
}
}

View File

@@ -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<T>(flow: Flow<T>) {
private val overrideChannel = Channel<T>()
val flow = merge(overrideChannel.receiveAsFlow(), flow)
fun override(value: T) {
overrideChannel.trySend(value)
}
}

View File

@@ -55,11 +55,6 @@ android {
composeOptions {
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {

View File

@@ -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 <T> collectValues(flow: Flow<T>): List<T> = withTimeout(500) {
val flowValues = mutableListOf<T>()
flow.toList(flowValues)
flowValues
}
}

View File

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