diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt index ec1780f89b2..1274eea7316 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.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 = rememberAppPermissionSummary(app), + summaryFlow: Flow = 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 91c3887dd0b..d0bdd6b3eac 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 @@ 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 { 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() { +) { 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, - ) { - if (requestedPermissionCount == 0) { - postValue(noPermissionRequestedState()) - return - } - val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels) - val summary = if (labels.isNotEmpty()) { - ListFormatter.getInstance().format(labels) + grantedGroupLabels: List -> + 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, - ): List = when (additionalGrantedPermissionCount) { - 0 -> grantedGroupLabels - else -> { - grantedGroupLabels + - // N additional permissions. - context.formatString( - R.string.runtime_permissions_additional_count, - "count" to additionalGrantedPermissionCount, - ) - } + ): List = 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 1646851a0f7..11d4b9a7d57 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.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 } } -} \ 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 c82da1a1c15..0735e3b1bef 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 @@ 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() - @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 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 43b7a206efb..a2b479c138a 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)) }