diff options
| author | 2024-02-02 21:57:49 +0800 | |
|---|---|---|
| committer | 2024-02-02 23:41:25 +0800 | |
| commit | 3c797ed83ec5ebdcfe73779380d60b8c0042b4d3 (patch) | |
| tree | 76229d6c8ffa61809e188286bad2e13b769db788 | |
| parent | d88ee9c9fb9bfcda61a4c1999ce8297e61b27a2e (diff) | |
Create opModeFlow to always return correct status
Currently we do not watching for the op mode change, cause the status
to be not accurate, watching for the op mode change to fix.
Fix: 322916468
Test: manual - on Toggle Permission page and switch between apps
Test: unit test
Change-Id: Ib3f1e0f024bea80239ccdc23c5ba0f4994f5e3bc
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 |