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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.content.pm.ApplicationInfo
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.LiveData
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel 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 com.android.settingslib.spaprivileged.model.app.userHandle
import kotlinx.coroutines.flow.Flow
private const val TAG = "AppPermissionPreference" private const val TAG = "AppPermissionPreference"
private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton" private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"
@@ -38,14 +38,11 @@ private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"
@Composable @Composable
fun AppPermissionPreference( fun AppPermissionPreference(
app: ApplicationInfo, app: ApplicationInfo,
summaryLiveData: LiveData<AppPermissionSummaryState> = rememberAppPermissionSummary(app), summaryFlow: Flow<AppPermissionSummaryState> = rememberAppPermissionSummary(app),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val summaryState = summaryLiveData.observeAsState( val summaryState = summaryFlow.collectAsStateWithLifecycle(
initial = AppPermissionSummaryState( initialValue = AppPermissionSummaryState(summary = placeholder(), enabled = false),
summary = stringResource(R.string.summary_placeholder),
enabled = false,
)
) )
Preference( Preference(
model = remember { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager.OnPermissionsChangedListener
import android.icu.text.ListFormatter import android.icu.text.ListFormatter
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.LiveData
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.applications.PermissionsSummaryHelper 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.spa.framework.util.formatString
import com.android.settingslib.spaprivileged.framework.common.asUser 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 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( data class AppPermissionSummaryState(
val summary: String, val summary: String,
@@ -37,58 +41,40 @@ data class AppPermissionSummaryState(
) )
@Composable @Composable
fun rememberAppPermissionSummary(app: ApplicationInfo): AppPermissionSummaryLiveData { fun rememberAppPermissionSummary(app: ApplicationInfo): Flow<AppPermissionSummaryState> {
val context = LocalContext.current 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 context: Context,
private val app: ApplicationInfo, private val app: ApplicationInfo,
) : LiveData<AppPermissionSummaryState>() { ) {
private val userContext = context.asUser(app.userHandle) private val userContext = context.asUser(app.userHandle)
private val userPackageManager = userContext.packageManager
private val onPermissionsChangedListener = OnPermissionsChangedListener { uid -> val flow = context.permissionsChangedFlow(app)
if (uid == app.uid) update() .map { getPermissionSummary() }
} .flowOn(Dispatchers.Default)
override fun onActive() { private suspend fun getPermissionSummary() = suspendCancellableCoroutine { continuation ->
userPackageManager.addOnPermissionsChangeListener(onPermissionsChangedListener)
if (app.isArchived) {
postValue(noPermissionRequestedState())
} else {
update()
}
}
override fun onInactive() {
userPackageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener)
}
private fun update() {
PermissionsSummaryHelper.getPermissionSummary( PermissionsSummaryHelper.getPermissionSummary(
userContext, app.packageName, permissionsCallback userContext,
) app.packageName,
} ) { requestedPermissionCount: Int,
private val permissionsCallback = object : PermissionsResultCallback {
override fun onPermissionSummaryResult(
requestedPermissionCount: Int,
additionalGrantedPermissionCount: Int, additionalGrantedPermissionCount: Int,
grantedGroupLabels: List<CharSequence>, grantedGroupLabels: List<CharSequence> ->
) { val summaryState = if (requestedPermissionCount == 0) {
if (requestedPermissionCount == 0) { noPermissionRequestedState()
postValue(noPermissionRequestedState()) } else {
return
}
val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels) val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels)
val summary = if (labels.isNotEmpty()) { val summary = if (labels.isNotEmpty()) {
ListFormatter.getInstance().format(labels) ListFormatter.getInstance().format(labels)
} else { } else {
context.getString(R.string.runtime_permissions_summary_no_permissions_granted) context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
} }
postValue(AppPermissionSummaryState(summary = summary, enabled = true)) AppPermissionSummaryState(summary = summary, enabled = true)
}
continuation.resume(summaryState)
} }
} }
@@ -100,9 +86,9 @@ class AppPermissionSummaryLiveData(
private fun getDisplayLabels( private fun getDisplayLabels(
additionalGrantedPermissionCount: Int, additionalGrantedPermissionCount: Int,
grantedGroupLabels: List<CharSequence>, grantedGroupLabels: List<CharSequence>,
): List<CharSequence> = when (additionalGrantedPermissionCount) { ): List<CharSequence> = if (additionalGrantedPermissionCount == 0) {
0 -> grantedGroupLabels grantedGroupLabels
else -> { } else {
grantedGroupLabels + grantedGroupLabels +
// N additional permissions. // N additional permissions.
context.formatString( context.formatString(
@@ -110,5 +96,4 @@ class AppPermissionSummaryLiveData(
"count" to additionalGrantedPermissionCount, "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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.onNodeWithText
import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spa.testutils.delay
import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userHandle
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.kotlin.any
import org.mockito.Mockito.any import org.mockito.kotlin.argumentCaptor
import org.mockito.Mockito.doNothing import org.mockito.kotlin.doNothing
import org.mockito.Mockito.eq import org.mockito.kotlin.eq
import org.mockito.Mockito.verify import org.mockito.kotlin.spy
import org.mockito.Spy import org.mockito.kotlin.verify
import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.whenever
import org.mockito.junit.MockitoRule
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AppPermissionPreferenceTest { class AppPermissionPreferenceTest {
@get:Rule @get:Rule
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()
@get:Rule private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
val mockito: MockitoRule = MockitoJUnit.rule() doNothing().whenever(mock).startActivityAsUser(any(), any())
}
@Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Test @Test
fun title_display() { fun title_display() {
@@ -66,15 +63,13 @@ class AppPermissionPreferenceTest {
@Test @Test
fun whenClick_startActivity() { fun whenClick_startActivity() {
doNothing().`when`(context).startActivityAsUser(any(), any())
setContent() setContent()
composeTestRule.onRoot().performClick() composeTestRule.onRoot().performClick()
composeTestRule.delay() composeTestRule.delay()
val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) val intent = argumentCaptor {
verify(context).startActivityAsUser(intentCaptor.capture(), eq(APP.userHandle)) verify(context).startActivityAsUser(capture(), eq(APP.userHandle))
val intent = intentCaptor.value }.firstValue
assertThat(intent.action).isEqualTo(Intent.ACTION_MANAGE_APP_PERMISSIONS) assertThat(intent.action).isEqualTo(Intent.ACTION_MANAGE_APP_PERMISSIONS)
assertThat(intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE_NAME) assertThat(intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE_NAME)
assertThat(intent.getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)).isEqualTo(true) assertThat(intent.getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)).isEqualTo(true)
@@ -85,7 +80,7 @@ class AppPermissionPreferenceTest {
CompositionLocalProvider(LocalContext provides context) { CompositionLocalProvider(LocalContext provides context) {
AppPermissionPreference( AppPermissionPreference(
app = APP, app = APP,
summaryLiveData = MutableLiveData( summaryFlow = flowOf(
AppPermissionSummaryState(summary = SUMMARY, enabled = true) AppPermissionSummaryState(summary = SUMMARY, enabled = true)
), ),
) )

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession 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.settings.testutils.mockAsUser
import com.android.settingslib.applications.PermissionsSummaryHelper import com.android.settingslib.applications.PermissionsSummaryHelper
import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback
import com.android.settingslib.spa.testutils.getOrAwaitValue
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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.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.quality.Strictness
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AppPermissionSummaryTest { class AppPermissionSummaryTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var mockSession: MockitoSession private lateinit var mockSession: MockitoSession
@Spy private val mockPackageManager = mock<PackageManager>()
private var context: Context = ApplicationProvider.getApplicationContext()
@Mock private var context: Context = spy(ApplicationProvider.getApplicationContext()) {
private lateinit var packageManager: PackageManager mock.mockAsUser()
on { packageManager } doReturn mockPackageManager
}
private lateinit var summaryLiveData: AppPermissionSummaryLiveData private val summaryRepository = AppPermissionSummaryRepository(context, APP)
@Before @Before
fun setUp() { fun setUp() {
mockSession = mockitoSession() mockSession = mockitoSession()
.initMocks(this)
.mockStatic(PermissionsSummaryHelper::class.java) .mockStatic(PermissionsSummaryHelper::class.java)
.strictness(Strictness.LENIENT) .strictness(Strictness.LENIENT)
.startMocking() .startMocking()
context.mockAsUser()
whenever(context.packageManager).thenReturn(packageManager)
summaryLiveData = AppPermissionSummaryLiveData(context, APP)
} }
private fun mockGetPermissionSummary( private fun mockGetPermissionSummary(
@@ -95,22 +86,10 @@ class AppPermissionSummaryTest {
} }
@Test @Test
fun permissionsChangeListener() { fun summary_noPermissionsRequested() = runBlocking {
mockGetPermissionSummary()
summaryLiveData.getOrAwaitValue {
verify(packageManager).addOnPermissionsChangeListener(any())
verify(packageManager, never()).removeOnPermissionsChangeListener(any())
}
verify(packageManager).removeOnPermissionsChangeListener(any())
}
@Test
fun summary_noPermissionsRequested() {
mockGetPermissionSummary(requestedPermissionCount = 0) mockGetPermissionSummary(requestedPermissionCount = 0)
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!! val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo( assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_requested) context.getString(R.string.runtime_permissions_summary_no_permissions_requested)
@@ -119,10 +98,10 @@ class AppPermissionSummaryTest {
} }
@Test @Test
fun summary_noPermissionsGranted() { fun summary_noPermissionsGranted() = runBlocking {
mockGetPermissionSummary(requestedPermissionCount = 1, grantedGroupLabels = emptyList()) mockGetPermissionSummary(requestedPermissionCount = 1, grantedGroupLabels = emptyList())
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!! val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo( assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_granted) context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
@@ -131,34 +110,34 @@ class AppPermissionSummaryTest {
} }
@Test @Test
fun onPermissionSummaryResult_hasRuntimePermission_shouldSetPermissionAsSummary() { fun summary_hasRuntimePermission_usePermissionAsSummary() = runBlocking {
mockGetPermissionSummary( mockGetPermissionSummary(
requestedPermissionCount = 1, requestedPermissionCount = 1,
grantedGroupLabels = listOf(PERMISSION), grantedGroupLabels = listOf(PERMISSION),
) )
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!! val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo(PERMISSION) assertThat(summary).isEqualTo(PERMISSION)
assertThat(enabled).isTrue() assertThat(enabled).isTrue()
} }
@Test @Test
fun onPermissionSummaryResult_hasAdditionalPermission_shouldSetAdditionalSummary() { fun summary_hasAdditionalPermission_containsAdditionalSummary() = runBlocking {
mockGetPermissionSummary( mockGetPermissionSummary(
requestedPermissionCount = 5, requestedPermissionCount = 5,
additionalGrantedPermissionCount = 2, additionalGrantedPermissionCount = 2,
grantedGroupLabels = listOf(PERMISSION), grantedGroupLabels = listOf(PERMISSION),
) )
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!! val (summary, enabled) = summaryRepository.flow.first()
assertThat(summary).isEqualTo("Storage and 2 additional permissions") assertThat(summary).isEqualTo("Storage and 2 additional permissions")
assertThat(enabled).isTrue() assertThat(enabled).isTrue()
} }
private companion object { private companion object {
const val PACKAGE_NAME = "packageName" const val PACKAGE_NAME = "package.name"
const val PERMISSION = "Storage" const val PERMISSION = "Storage"
val APP = ApplicationInfo().apply { val APP = ApplicationInfo().apply {
packageName = PACKAGE_NAME 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -17,10 +17,11 @@
package com.android.settings.testutils package com.android.settings.testutils
import android.content.Context import android.content.Context
import org.mockito.Mockito.any import org.mockito.kotlin.any
import org.mockito.Mockito.doReturn import org.mockito.kotlin.doReturn
import org.mockito.Mockito.eq import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
fun Context.mockAsUser() { fun Context.mockAsUser() {
doReturn(this).`when`(this).createContextAsUser(any(), eq(0)) doReturn(this).whenever(this).createContextAsUser(any(), eq(0))
} }