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:
@@ -54,11 +54,6 @@ android {
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -51,11 +51,6 @@ android {
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -55,11 +55,6 @@ android {
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion jetpack_compose_compiler_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user