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)