Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6c7dbdd6 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Migrate AppPermissionSummary to flow" into main

parents b9ee95e4 52558679
Loading
Loading
Loading
Loading
+7 −10
Original line number Diff line number Diff line
/*
 * 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 {
+39 −54
Original line number Diff line number Diff line
/*
 * 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
            }
            grantedGroupLabels: List<CharSequence> ->
            val summaryState = if (requestedPermissionCount == 0) {
                noPermissionRequestedState()
            } else {
                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)
                }
            postValue(AppPermissionSummaryState(summary = summary, enabled = true))
                AppPermissionSummaryState(summary = summary, enabled = true)
            }
            continuation.resume(summaryState)
        }
    }

@@ -100,9 +86,9 @@ class AppPermissionSummaryLiveData(
    private fun getDisplayLabels(
        additionalGrantedPermissionCount: Int,
        grantedGroupLabels: List<CharSequence>,
    ): List<CharSequence> = when (additionalGrantedPermissionCount) {
        0 -> grantedGroupLabels
        else -> {
    ): List<CharSequence> = if (additionalGrantedPermissionCount == 0) {
        grantedGroupLabels
    } else {
        grantedGroupLabels +
            // N additional permissions.
            context.formatString(
@@ -111,4 +97,3 @@ class AppPermissionSummaryLiveData(
            )
    }
}
}
+17 −22
Original line number Diff line number Diff line
/*
 * 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)
                    ),
                )
+24 −45
Original line number Diff line number Diff line
/*
 * 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
+6 −5
Original line number Diff line number Diff line
/*
 * 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))
}