Migrate AppPermissionSummary to flow
Bug: 321163306
Test: manual - on App Info
Test: unit test
Change-Id: I36f6a479d530fc646a55f68fbaf681b72eff00dd
diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
index ec1780f..1274eea 100644
--- a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.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.
@@ -22,15 +22,15 @@
import android.content.pm.ApplicationInfo
import android.util.Log
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.LiveData
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spaprivileged.framework.compose.placeholder
import com.android.settingslib.spaprivileged.model.app.userHandle
+import kotlinx.coroutines.flow.Flow
private const val TAG = "AppPermissionPreference"
private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"
@@ -38,14 +38,11 @@
@Composable
fun AppPermissionPreference(
app: ApplicationInfo,
- summaryLiveData: LiveData<AppPermissionSummaryState> = rememberAppPermissionSummary(app),
+ summaryFlow: Flow<AppPermissionSummaryState> = rememberAppPermissionSummary(app),
) {
val context = LocalContext.current
- val summaryState = summaryLiveData.observeAsState(
- initial = AppPermissionSummaryState(
- summary = stringResource(R.string.summary_placeholder),
- enabled = false,
- )
+ val summaryState = summaryFlow.collectAsStateWithLifecycle(
+ initialValue = AppPermissionSummaryState(summary = placeholder(), enabled = false),
)
Preference(
model = remember {
diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt
index 91c3887..d0bdd6b 100644
--- a/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.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.
@@ -18,18 +18,22 @@
import android.content.Context
import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager.OnPermissionsChangedListener
import android.icu.text.ListFormatter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
-import androidx.lifecycle.LiveData
import com.android.settings.R
import com.android.settingslib.applications.PermissionsSummaryHelper
-import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback
import com.android.settingslib.spa.framework.util.formatString
import com.android.settingslib.spaprivileged.framework.common.asUser
+import com.android.settingslib.spaprivileged.model.app.permissionsChangedFlow
import com.android.settingslib.spaprivileged.model.app.userHandle
+import kotlin.coroutines.resume
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.suspendCancellableCoroutine
data class AppPermissionSummaryState(
val summary: String,
@@ -37,58 +41,40 @@
)
@Composable
-fun rememberAppPermissionSummary(app: ApplicationInfo): AppPermissionSummaryLiveData {
+fun rememberAppPermissionSummary(app: ApplicationInfo): Flow<AppPermissionSummaryState> {
val context = LocalContext.current
- return remember(app) { AppPermissionSummaryLiveData(context, app) }
+ return remember(app) { AppPermissionSummaryRepository(context, app).flow }
}
-class AppPermissionSummaryLiveData(
+class AppPermissionSummaryRepository(
private val context: Context,
private val app: ApplicationInfo,
-) : LiveData<AppPermissionSummaryState>() {
+) {
private val userContext = context.asUser(app.userHandle)
- private val userPackageManager = userContext.packageManager
- private val onPermissionsChangedListener = OnPermissionsChangedListener { uid ->
- if (uid == app.uid) update()
- }
+ val flow = context.permissionsChangedFlow(app)
+ .map { getPermissionSummary() }
+ .flowOn(Dispatchers.Default)
- override fun onActive() {
- userPackageManager.addOnPermissionsChangeListener(onPermissionsChangedListener)
- if (app.isArchived) {
- postValue(noPermissionRequestedState())
- } else {
- update()
- }
- }
-
- override fun onInactive() {
- userPackageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener)
- }
-
- private fun update() {
+ private suspend fun getPermissionSummary() = suspendCancellableCoroutine { continuation ->
PermissionsSummaryHelper.getPermissionSummary(
- userContext, app.packageName, permissionsCallback
- )
- }
-
- private val permissionsCallback = object : PermissionsResultCallback {
- override fun onPermissionSummaryResult(
- requestedPermissionCount: Int,
+ userContext,
+ app.packageName,
+ ) { requestedPermissionCount: Int,
additionalGrantedPermissionCount: Int,
- grantedGroupLabels: List<CharSequence>,
- ) {
- if (requestedPermissionCount == 0) {
- postValue(noPermissionRequestedState())
- return
- }
- val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels)
- val summary = if (labels.isNotEmpty()) {
- ListFormatter.getInstance().format(labels)
+ grantedGroupLabels: List<CharSequence> ->
+ val summaryState = if (requestedPermissionCount == 0) {
+ noPermissionRequestedState()
} else {
- context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
+ val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels)
+ val summary = if (labels.isNotEmpty()) {
+ ListFormatter.getInstance().format(labels)
+ } else {
+ context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
+ }
+ AppPermissionSummaryState(summary = summary, enabled = true)
}
- postValue(AppPermissionSummaryState(summary = summary, enabled = true))
+ continuation.resume(summaryState)
}
}
@@ -100,15 +86,14 @@
private fun getDisplayLabels(
additionalGrantedPermissionCount: Int,
grantedGroupLabels: List<CharSequence>,
- ): List<CharSequence> = when (additionalGrantedPermissionCount) {
- 0 -> grantedGroupLabels
- else -> {
- grantedGroupLabels +
- // N additional permissions.
- context.formatString(
- R.string.runtime_permissions_additional_count,
- "count" to additionalGrantedPermissionCount,
- )
- }
+ ): List<CharSequence> = if (additionalGrantedPermissionCount == 0) {
+ grantedGroupLabels
+ } else {
+ grantedGroupLabels +
+ // N additional permissions.
+ context.formatString(
+ R.string.runtime_permissions_additional_count,
+ "count" to additionalGrantedPermissionCount,
+ )
}
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionPreferenceTest.kt
index 1646851..11d4b9a 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionPreferenceTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionPreferenceTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 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.
@@ -26,35 +26,32 @@
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
-import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settingslib.spa.testutils.delay
import com.android.settingslib.spaprivileged.model.app.userHandle
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.any
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.verify
-import org.mockito.Spy
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class AppPermissionPreferenceTest {
@get:Rule
val composeTestRule = createComposeRule()
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
-
- @Spy
- private val context: Context = ApplicationProvider.getApplicationContext()
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ doNothing().whenever(mock).startActivityAsUser(any(), any())
+ }
@Test
fun title_display() {
@@ -66,15 +63,13 @@
@Test
fun whenClick_startActivity() {
- doNothing().`when`(context).startActivityAsUser(any(), any())
-
setContent()
composeTestRule.onRoot().performClick()
composeTestRule.delay()
- val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
- verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle))
- val intent = intentCaptor.value
+ val intent = argumentCaptor {
+ verify(context).startActivityAsUser(capture(), eq(APP.userHandle))
+ }.firstValue
assertThat(intent.action).isEqualTo(Intent.ACTION_MANAGE_APP_PERMISSIONS)
assertThat(intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE_NAME)
assertThat(intent.getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)).isEqualTo(true)
@@ -85,7 +80,7 @@
CompositionLocalProvider(LocalContext provides context) {
AppPermissionPreference(
app = APP,
- summaryLiveData = MutableLiveData(
+ summaryFlow = flowOf(
AppPermissionSummaryState(summary = SUMMARY, enabled = true)
),
)
@@ -103,4 +98,4 @@
packageName = PACKAGE_NAME
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionSummaryTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionSummaryTest.kt
index c82da1a..0735e3b 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionSummaryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppPermissionSummaryTest.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.
@@ -19,7 +19,6 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
@@ -27,50 +26,42 @@
import com.android.settings.testutils.mockAsUser
import com.android.settingslib.applications.PermissionsSummaryHelper
import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback
-import com.android.settingslib.spa.testutils.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
import org.mockito.MockitoSession
-import org.mockito.Spy
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
-import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class)
class AppPermissionSummaryTest {
- @get:Rule
- val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var mockSession: MockitoSession
- @Spy
- private var context: Context = ApplicationProvider.getApplicationContext()
+ private val mockPackageManager = mock<PackageManager>()
- @Mock
- private lateinit var packageManager: PackageManager
+ private var context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ mock.mockAsUser()
+ on { packageManager } doReturn mockPackageManager
+ }
- private lateinit var summaryLiveData: AppPermissionSummaryLiveData
+ private val summaryRepository = AppPermissionSummaryRepository(context, APP)
@Before
fun setUp() {
mockSession = mockitoSession()
- .initMocks(this)
.mockStatic(PermissionsSummaryHelper::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
- context.mockAsUser()
- whenever(context.packageManager).thenReturn(packageManager)
-
- summaryLiveData = AppPermissionSummaryLiveData(context, APP)
}
private fun mockGetPermissionSummary(
@@ -95,22 +86,10 @@
}
@Test
- fun permissionsChangeListener() {
- mockGetPermissionSummary()
-
- summaryLiveData.getOrAwaitValue {
- verify(packageManager).addOnPermissionsChangeListener(any())
- verify(packageManager, never()).removeOnPermissionsChangeListener(any())
- }
-
- verify(packageManager).removeOnPermissionsChangeListener(any())
- }
-
- @Test
- fun summary_noPermissionsRequested() {
+ fun summary_noPermissionsRequested() = runBlocking {
mockGetPermissionSummary(requestedPermissionCount = 0)
- val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
+ val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_requested)
@@ -119,10 +98,10 @@
}
@Test
- fun summary_noPermissionsGranted() {
+ fun summary_noPermissionsGranted() = runBlocking {
mockGetPermissionSummary(requestedPermissionCount = 1, grantedGroupLabels = emptyList())
- val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
+ val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
@@ -131,34 +110,34 @@
}
@Test
- fun onPermissionSummaryResult_hasRuntimePermission_shouldSetPermissionAsSummary() {
+ fun summary_hasRuntimePermission_usePermissionAsSummary() = runBlocking {
mockGetPermissionSummary(
requestedPermissionCount = 1,
grantedGroupLabels = listOf(PERMISSION),
)
- val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
+ val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo(PERMISSION)
assertThat(enabled).isTrue()
}
@Test
- fun onPermissionSummaryResult_hasAdditionalPermission_shouldSetAdditionalSummary() {
+ fun summary_hasAdditionalPermission_containsAdditionalSummary() = runBlocking {
mockGetPermissionSummary(
requestedPermissionCount = 5,
additionalGrantedPermissionCount = 2,
grantedGroupLabels = listOf(PERMISSION),
)
- val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
+ val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo("Storage and 2 additional permissions")
assertThat(enabled).isTrue()
}
private companion object {
- const val PACKAGE_NAME = "packageName"
+ const val PACKAGE_NAME = "package.name"
const val PERMISSION = "Storage"
val APP = ApplicationInfo().apply {
packageName = PACKAGE_NAME
diff --git a/tests/spa_unit/src/com/android/settings/testutils/ContextTestUtil.kt b/tests/spa_unit/src/com/android/settings/testutils/ContextTestUtil.kt
index 43b7a20..a2b479c 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/ContextTestUtil.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/ContextTestUtil.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.
@@ -17,10 +17,11 @@
package com.android.settings.testutils
import android.content.Context
-import org.mockito.Mockito.any
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.eq
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
fun Context.mockAsUser() {
- doReturn(this).`when`(this).createContextAsUser(any(), eq(0))
+ doReturn(this).whenever(this).createContextAsUser(any(), eq(0))
}