Merge "Add AppPermissionPreference for Spa"
This commit is contained in:
committed by
Android (Google) Code Review
commit
2d539a83da
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,8 @@ private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) {
|
||||
|
||||
AppButtons(packageInfoPresenter)
|
||||
|
||||
AppPermissionPreference(app)
|
||||
|
||||
Category(title = stringResource(R.string.advanced_apps)) {
|
||||
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
|
||||
ModifySystemSettingsAppListProvider.InfoPageEntryItem(app)
|
||||
|
||||
@@ -73,6 +73,7 @@ android_robolectric_test {
|
||||
name: "SettingsRoboTests",
|
||||
srcs: [
|
||||
"src/**/*.java",
|
||||
"src/**/*.kt",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user