Merge "Migrate AppPermissionSummary to flow" into main

This commit is contained in:
Chaohui Wang
2024-01-19 17:39:24 +00:00
committed by Android (Google) Code Review
5 changed files with 93 additions and 136 deletions

View File

@@ -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.Intent
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 @@ private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"
@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 {

View File

@@ -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 @@ package com.android.settings.spa.app.appinfo
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 @@ data class AppPermissionSummaryState(
)
@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 @@ class AppPermissionSummaryLiveData(
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,
)
}
}

View File

@@ -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.junit4.createComposeRule
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 @@ class AppPermissionPreferenceTest {
@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 @@ class AppPermissionPreferenceTest {
CompositionLocalProvider(LocalContext provides context) {
AppPermissionPreference(
app = APP,
summaryLiveData = MutableLiveData(
summaryFlow = flowOf(
AppPermissionSummaryState(summary = SUMMARY, enabled = true)
),
)
@@ -103,4 +98,4 @@ class AppPermissionPreferenceTest {
packageName = PACKAGE_NAME
}
}
}
}

View File

@@ -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 @@ package com.android.settings.spa.app.appinfo
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.R
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 @@ class AppPermissionSummaryTest {
}
@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 @@ class AppPermissionSummaryTest {
}
@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 @@ class AppPermissionSummaryTest {
}
@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

View File

@@ -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))
}