diff options
7 files changed, 250 insertions, 23 deletions
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt new file mode 100644 index 000000000000..1d3eb51ebaa4 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/lifecycle/FlowExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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.lifecycle + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.Flow + +@Composable +fun <T> Flow<T>.collectAsCallbackWithLifecycle(): () -> T? { + val value by collectAsStateWithLifecycle(initialValue = null) + return { value } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt new file mode 100644 index 000000000000..de915ef662cf --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/lifecycle/FlowExtTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 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.lifecycle + +import androidx.compose.material3.Text +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.waitUntilExists +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FlowExtTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun collectAsCallbackWithLifecycle() { + val flow = flowOf(TEXT) + + composeTestRule.setContent { + val callback = flow.collectAsCallbackWithLifecycle() + Text(callback() ?: "") + } + + composeTestRule.waitUntilExists(hasText(TEXT)) + } + + @Test + fun collectAsCallbackWithLifecycle_changed() { + val flow = MutableStateFlow(TEXT) + + composeTestRule.setContent { + val callback = flow.collectAsCallbackWithLifecycle() + Text(callback() ?: "") + } + flow.value = NEW_TEXT + + composeTestRule.waitUntilExists(hasText(NEW_TEXT)) + } + + private companion object { + const val TEXT = "Text" + const val NEW_TEXT = "New Text" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt index 9f33fcb0052b..6e9bde45e061 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 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. @@ -16,7 +16,7 @@ package com.android.settingslib.spaprivileged.model.app -import android.app.AppOpsManager; +import android.app.AppOpsManager import android.app.AppOpsManager.MODE_ALLOWED import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.Mode @@ -24,14 +24,13 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.UserHandle -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.map import com.android.settingslib.spaprivileged.framework.common.appOpsManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map interface IAppOpsController { - val mode: LiveData<Int> - val isAllowed: LiveData<Boolean> + val mode: Flow<Int> + val isAllowed: Flow<Boolean> get() = mode.map { it == MODE_ALLOWED } fun setAllowed(allowed: Boolean) @@ -48,9 +47,7 @@ class AppOpsController( ) : IAppOpsController { private val appOpsManager = context.appOpsManager private val packageManager = context.packageManager - - override val mode: LiveData<Int> - get() = _mode + override val mode = appOpsManager.opModeFlow(op, app) override fun setAllowed(allowed: Boolean) { val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed @@ -68,15 +65,7 @@ class AppOpsController( PackageManager.FLAG_PERMISSION_USER_SET, UserHandle.getUserHandleForUid(app.uid)) } - _mode.postValue(mode) } - @Mode override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName) - - private val _mode = - object : MutableLiveData<Int>() { - override fun onActive() { - postValue(getMode()) - } - } + @Mode override fun getMode(): Int = appOpsManager.getOpMode(op, app) } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt new file mode 100644 index 000000000000..0b5604eb74c1 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 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.model.app + +import android.app.AppOpsManager +import android.content.pm.ApplicationInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +fun AppOpsManager.getOpMode(op: Int, app: ApplicationInfo) = + checkOpNoThrow(op, app.uid, app.packageName) + +fun AppOpsManager.opModeFlow(op: Int, app: ApplicationInfo) = + opChangedFlow(op, app).map { getOpMode(op, app) }.flowOn(Dispatchers.Default) + +private fun AppOpsManager.opChangedFlow(op: Int, app: ApplicationInfo) = callbackFlow { + val listener = object : AppOpsManager.OnOpChangedListener { + override fun onOpChanged(op: String, packageName: String) {} + + override fun onOpChanged(op: String, packageName: String, userId: Int) { + if (userId == app.userId) trySend(Unit) + } + } + startWatchingMode(op, app.packageName, listener) + trySend(Unit) + + awaitClose { stopWatchingMode(listener) } +}.conflate().flowOn(Dispatchers.Default) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt index 25c3bc541249..5db5eae22745 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt @@ -20,7 +20,7 @@ import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.runtime.Composable -import androidx.compose.runtime.livedata.observeAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.spa.framework.util.asyncMapItem import com.android.settingslib.spa.framework.util.filterItem import com.android.settingslib.spaprivileged.model.app.AppOpsController @@ -166,7 +166,7 @@ internal fun isAllowed( return { true } } - val mode = appOpsController.mode.observeAsState() + val mode = appOpsController.mode.collectAsStateWithLifecycle(initialValue = null) return { when (mode.value) { null -> null diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt new file mode 100644 index 000000000000..97c74411d945 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 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.model.app + +import android.app.AppOpsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.UserHandle +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.android.settingslib.spaprivileged.framework.common.appOpsManager +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class AppOpsRepositoryTest { + + private var listener: AppOpsManager.OnOpChangedListener? = null + + private val mockAppOpsManager = mock<AppOpsManager> { + on { + checkOpNoThrow(AppOpsManager.OP_MANAGE_MEDIA, UID, PACKAGE_NAME) + } doReturn AppOpsManager.MODE_ERRORED + + on { + startWatchingMode(eq(AppOpsManager.OP_MANAGE_MEDIA), eq(PACKAGE_NAME), any()) + } doAnswer { listener = it.arguments[2] as AppOpsManager.OnOpChangedListener } + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { appOpsManager } doReturn mockAppOpsManager + } + + @Test + fun getOpMode() { + val mode = context.appOpsManager.getOpMode(AppOpsManager.OP_MANAGE_MEDIA, APP) + + assertThat(mode).isEqualTo(AppOpsManager.MODE_ERRORED) + } + + @Test + fun opModeFlow() = runBlocking { + val flow = context.appOpsManager.opModeFlow(AppOpsManager.OP_MANAGE_MEDIA, APP) + + val mode = flow.first() + + assertThat(mode).isEqualTo(AppOpsManager.MODE_ERRORED) + } + + @Test + fun opModeFlow_changed() = runBlocking { + val listDeferred = async { + context.appOpsManager.opModeFlow(AppOpsManager.OP_MANAGE_MEDIA, APP).toListWithTimeout() + } + delay(100) + + mockAppOpsManager.stub { + on { checkOpNoThrow(AppOpsManager.OP_MANAGE_MEDIA, UID, PACKAGE_NAME) } doReturn + AppOpsManager.MODE_IGNORED + } + listener?.onOpChanged("", "", UserHandle.getUserId(UID)) + + assertThat(listDeferred.await()).contains(AppOpsManager.MODE_IGNORED) + } + + private companion object { + const val UID = 110000 + const val PACKAGE_NAME = "package.name" + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index d158a2414f85..bb25cf3b6d48 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import androidx.compose.ui.test.junit4.createComposeRule -import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull @@ -330,7 +329,7 @@ class AppOpPermissionAppListTest { private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { var setAllowedCalledWith: Boolean? = null - override val mode = MutableLiveData(fakeMode) + override val mode = flowOf(fakeMode) override fun setAllowed(allowed: Boolean) { setAllowedCalledWith = allowed |