Add AppPermissionPreference for Spa

This is the permission preference in the App Settings page.
The summary is single line.

Add the first Kotlin Robolectric Test for Settings, since Kotlin is not
directly supported by the Robolectric test, using a Java class as a
wrapper.

Bug: 236346018
Test: Manual with App Settings page
Test: Robolectric Test
Change-Id: Ic5a4f7d965885a9cd143428a8cd1900981e316a9
This commit is contained in:
Chaohui Wang
2022-10-09 17:52:36 +08:00
parent fcf10fa0a5
commit 2b11c1fe12
6 changed files with 383 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2022 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.app.appsettings
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.model.app.userHandle
private const val TAG = "AppPermissionPreference"
private const val EXTRA_HIDE_INFO_BUTTON = "hideInfoButton"
@Composable
fun AppPermissionPreference(app: ApplicationInfo) {
val context = LocalContext.current
val summaryLiveData = remember { AppPermissionSummaryLiveData(context, app) }
val summaryState = summaryLiveData.observeAsState(initial = AppPermissionSummaryState(
summary = stringResource(R.string.summary_placeholder),
enabled = false,
))
Preference(
model = remember {
object : PreferenceModel {
override val title = context.getString(R.string.permissions_label)
override val summary = derivedStateOf { summaryState.value.summary }
override val enabled = derivedStateOf { summaryState.value.enabled }
override val onClick = { startManagePermissionsActivity(context, app) }
}
},
singleLineSummary = true,
)
}
/** Starts new activity to manage app permissions */
private fun startManagePermissionsActivity(context: Context, app: ApplicationInfo) {
val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS).apply {
putExtra(Intent.EXTRA_PACKAGE_NAME, app.packageName)
putExtra(EXTRA_HIDE_INFO_BUTTON, true)
}
try {
context.startActivityAsUser(intent, app.userHandle)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No app can handle android.intent.action.MANAGE_APP_PERMISSIONS")
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2022 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.app.appsettings
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager.OnPermissionsChangedListener
import android.icu.text.ListFormatter
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.spaprivileged.model.app.userHandle
data class AppPermissionSummaryState(
val summary: String,
val enabled: Boolean,
)
class AppPermissionSummaryLiveData(
private val context: Context,
private val app: ApplicationInfo,
) : LiveData<AppPermissionSummaryState>() {
private val contextAsUser = context.createContextAsUser(app.userHandle, 0)
private val packageManager = contextAsUser.packageManager
private val onPermissionsChangedListener = OnPermissionsChangedListener { uid ->
if (uid == app.uid) update()
}
override fun onActive() {
packageManager.addOnPermissionsChangeListener(onPermissionsChangedListener)
update()
}
override fun onInactive() {
packageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener)
}
private fun update() {
PermissionsSummaryHelper.getPermissionSummary(
contextAsUser, app.packageName, permissionsCallback
)
}
private val permissionsCallback = object : PermissionsResultCallback {
override fun onPermissionSummaryResult(
requestedPermissionCount: Int,
additionalGrantedPermissionCount: Int,
grantedGroupLabels: List<CharSequence>,
) {
if (requestedPermissionCount == 0) {
postValue(noPermissionRequestedState())
return
}
val labels = getDisplayLabels(additionalGrantedPermissionCount, grantedGroupLabels)
val summary = when {
labels.isEmpty() -> {
context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
}
else -> ListFormatter.getInstance().format(labels)
}
postValue(AppPermissionSummaryState(summary = summary, enabled = true))
}
}
private fun noPermissionRequestedState() = AppPermissionSummaryState(
summary = context.getString(R.string.runtime_permissions_summary_no_permissions_requested),
enabled = false,
)
private fun getDisplayLabels(
additionalGrantedPermissionCount: Int,
grantedGroupLabels: List<CharSequence>,
): List<CharSequence> = when (additionalGrantedPermissionCount) {
0 -> grantedGroupLabels
else -> {
grantedGroupLabels +
// N additional permissions.
context.resources.getQuantityString(
R.plurals.runtime_permissions_additional_count,
additionalGrantedPermissionCount,
additionalGrantedPermissionCount,
)
}
}
}

View File

@@ -90,6 +90,8 @@ private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) {
AppButtons(packageInfoPresenter) AppButtons(packageInfoPresenter)
AppPermissionPreference(app)
Category(title = stringResource(R.string.advanced_apps)) { Category(title = stringResource(R.string.advanced_apps)) {
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) ModifySystemSettingsAppListProvider.InfoPageEntryItem(app)

View File

@@ -73,6 +73,7 @@ android_robolectric_test {
name: "SettingsRoboTests", name: "SettingsRoboTests",
srcs: [ srcs: [
"src/**/*.java", "src/**/*.java",
"src/**/*.kt",
], ],
static_libs: [ static_libs: [

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.app.appsettings;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
// TODO: Remove this class when Kotlin is supported by the Robolectric test.
@RunWith(RobolectricTestRunner.class)
public class AppPermissionSummaryJavaTest extends AppPermissionSummaryTest {
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (C) 2022 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.app.appsettings
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import com.android.settings.R
import com.android.settingslib.applications.PermissionsSummaryHelper
import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
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.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.mockito.Mockito.`when` as whenever
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowPermissionsSummaryHelper::class])
open class AppPermissionSummaryTest {
@JvmField
@Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@Spy
private var context: Context = ApplicationProvider.getApplicationContext()
@Mock
private lateinit var packageManager: PackageManager
private lateinit var summaryLiveData: AppPermissionSummaryLiveData
@Before
fun setUp() {
doReturn(context).`when`(context).createContextAsUser(any(), eq(0))
whenever(context.packageManager).thenReturn(packageManager)
val app = ApplicationInfo().apply {
packageName = PACKAGE_NAME
}
summaryLiveData = AppPermissionSummaryLiveData(context, app)
}
@Test
open fun permissionsChangeListener() {
summaryLiveData.getOrAwaitValue() {
verify(packageManager).addOnPermissionsChangeListener(any())
verify(packageManager, never()).removeOnPermissionsChangeListener(any())
}
verify(packageManager).removeOnPermissionsChangeListener(any())
}
@Test
fun summary_noPermissionsRequested() {
ShadowPermissionsSummaryHelper.requestedPermissionCount = 0
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_requested)
)
assertThat(enabled).isFalse()
}
@Test
fun summary_noPermissionsGranted() {
ShadowPermissionsSummaryHelper.requestedPermissionCount = 1
ShadowPermissionsSummaryHelper.grantedGroupLabels = emptyList()
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
assertThat(summary).isEqualTo(
context.getString(R.string.runtime_permissions_summary_no_permissions_granted)
)
assertThat(enabled).isTrue()
}
@Test
open fun onPermissionSummaryResult_hasRuntimePermission_shouldSetPermissionAsSummary() {
ShadowPermissionsSummaryHelper.requestedPermissionCount = 1
ShadowPermissionsSummaryHelper.grantedGroupLabels = listOf(PERMISSION)
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
assertThat(summary).isEqualTo(PERMISSION)
assertThat(enabled).isTrue()
}
@Test
open fun onPermissionSummaryResult_hasAdditionalPermission_shouldSetAdditionalSummary() {
ShadowPermissionsSummaryHelper.requestedPermissionCount = 5
ShadowPermissionsSummaryHelper.additionalGrantedPermissionCount = 2
ShadowPermissionsSummaryHelper.grantedGroupLabels = listOf(PERMISSION)
val (summary, enabled) = summaryLiveData.getOrAwaitValue()!!
assertThat(summary).isEqualTo("Storage and 2 additional permissions")
assertThat(enabled).isTrue()
}
companion object {
private const val PACKAGE_NAME = "packageName"
private const val PERMISSION = "Storage"
}
}
@Implements(PermissionsSummaryHelper::class)
private object ShadowPermissionsSummaryHelper {
var requestedPermissionCount = 0
var additionalGrantedPermissionCount = 0
var grantedGroupLabels: List<CharSequence> = emptyList()
@Implementation
@JvmStatic
@Suppress("UNUSED_PARAMETER")
fun getPermissionSummary(context: Context, pkg: String, callback: PermissionsResultCallback) {
callback.onPermissionSummaryResult(
requestedPermissionCount,
additionalGrantedPermissionCount,
grantedGroupLabels,
)
}
}
private fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {},
): T? {
var data: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> { o ->
data = o
latch.countDown()
}
this.observeForever(observer)
afterObserve()
try {
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
return data
}