Migrate AppPermissionSummary to flow

Bug: 321163306
Test: manual - on App Info
Test: unit test
Change-Id: I36f6a479d530fc646a55f68fbaf681b72eff00dd
diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionPreference.kt
index ec1780f..1274eea 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.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 @@
 @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 {
diff --git a/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt b/src/com/android/settings/spa/app/appinfo/AppPermissionSummary.kt
index 91c3887..d0bdd6b 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 @@
 
 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 @@
 )
 
 @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 @@
     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,
+            )
     }
 }
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 1646851..11d4b9a 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.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 @@
 
     @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 @@
             CompositionLocalProvider(LocalContext provides context) {
                 AppPermissionPreference(
                     app = APP,
-                    summaryLiveData = MutableLiveData(
+                    summaryFlow = flowOf(
                         AppPermissionSummaryState(summary = SUMMARY, enabled = true)
                     ),
                 )
@@ -103,4 +98,4 @@
             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 c82da1a..0735e3b 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 @@
 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.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 @@
     }
 
     @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 @@
     }
 
     @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 @@
     }
 
     @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 43b7a20..a2b479c 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))
 }