Merge "Migrate AppPermissionSummary to flow" into main
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
}
|
||||
|
Reference in New Issue
Block a user