summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PermissionController/res/values/strings.xml6
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt1
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/data/PackagePermissionsVirtualDeviceLiveData.kt118
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java20
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java15
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java7
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt80
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt89
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/utils/MultiDeviceUtils.kt34
-rw-r--r--tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/AppPermissionsTest.kt199
10 files changed, 559 insertions, 10 deletions
diff --git a/PermissionController/res/values/strings.xml b/PermissionController/res/values/strings.xml
index 6e8005ad0..deba7d04a 100644
--- a/PermissionController/res/values/strings.xml
+++ b/PermissionController/res/values/strings.xml
@@ -624,6 +624,9 @@
<!-- Description for showing an app's permission [CHAR LIMIT=60] -->
<string name="app_permission_header"><xliff:g id="perm" example="location">%1$s</xliff:g> access for this app</string>
+ <!-- Description for showing an app's permission along with device name [CHAR LIMIT=NONE] -->
+ <string name="app_permission_header_with_device_name"><xliff:g id="perm" example="camera">%1$s</xliff:g> access for this app on <xliff:g id="device_name" example="tablet">%2$s</xliff:g></string>
+
<!-- Text for linking to the page that shows an app's permissions [CHAR LIMIT=none] -->
<string name="app_permission_footer_app_permissions_link">See all <xliff:g id="app" example="Maps">%1$s</xliff:g> permissions</string>
@@ -799,6 +802,9 @@
<!-- Header for denied permissions/apps [CHAR LIMIT=40] -->
<string name="denied_header">Not allowed</string>
+ <!-- Header to display the Permission group name along with corresponding device name [CHAR LIMIT=None] -->
+ <string name="permission_group_name_with_device_name"><xliff:g id="perm_group_name" example="camera">%1$s</xliff:g> on <xliff:g id="device_name" example="tablet">%2$s</xliff:g></string>
+
<!-- Text of hyperlink shown in storage_footer [CHAR LIMIT=60] -->
<string name="storage_footer_hyperlink_text">See more apps that can access all files</string>
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt
index 09a7bb1e4..bce0cd76f 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt
@@ -162,6 +162,7 @@ object PackageBroadcastReceiver : BroadcastReceiver() {
LightPackageInfoLiveData.invalidateAllForPackage(packageName)
PermStateLiveData.invalidateAllForPackage(packageName)
PackagePermissionsLiveData.invalidateAllForPackage(packageName)
+ PackagePermissionsVirtualDeviceLiveData.invalidateAllForPackage(packageName)
HibernationSettingStateLiveData.invalidateAllForPackage(packageName)
LightAppPermGroupLiveData.invalidateAllForPackage(packageName)
AppPermGroupUiInfoLiveData.invalidateAllForPackage(packageName)
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/PackagePermissionsVirtualDeviceLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/PackagePermissionsVirtualDeviceLiveData.kt
new file mode 100644
index 000000000..79b2c88ed
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/data/PackagePermissionsVirtualDeviceLiveData.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ * 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.permissioncontroller.permission.data
+
+import android.app.Application
+import android.companion.virtual.VirtualDeviceManager
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.UserHandle
+import android.permission.PermissionManager
+import android.permission.PermissionManager.PermissionState
+import androidx.annotation.RequiresApi
+import com.android.modules.utils.build.SdkLevel
+import com.android.permissioncontroller.PermissionControllerApplication
+import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo
+import com.android.permissioncontroller.permission.utils.PermissionMapping
+import kotlinx.coroutines.Job
+
+/**
+ * LiveData that loads all the external device permissions per package. The permissions will be
+ * loaded only if the package has requested the permission. This live data produces the list of
+ * {@link VirtualDeviceGrantInfo} that has group name to which permission belongs to, grant state
+ * and persistentDeviceId
+ *
+ * @param app The current Application
+ * @param packageName The name of the package
+ * @param user The user for whom the packageInfo will be defined
+ */
+class PackagePermissionsVirtualDeviceLiveData
+private constructor(private val app: Application, val packageName: String, val user: UserHandle) :
+ SmartAsyncMediatorLiveData<
+ List<PackagePermissionsVirtualDeviceLiveData.VirtualDeviceGrantInfo>
+ >() {
+ private val permissionManager = app.getSystemService(PermissionManager::class.java)!!
+
+ data class VirtualDeviceGrantInfo(
+ val groupName: String,
+ val permGrantState: AppPermGroupUiInfo.PermGrantState,
+ val persistentDeviceId: String
+ )
+
+ init {
+ update()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override suspend fun loadDataAndPostValue(job: Job) {
+ if (!SdkLevel.isAtLeastV()) {
+ return
+ }
+ val virtualDeviceManager = app.getSystemService(VirtualDeviceManager::class.java)!!
+ val virtualDeviceGrantInfoList =
+ virtualDeviceManager.allPersistentDeviceIds
+ .map { getVirtualDeviceGrantInfoList(it) }
+ .toList()
+ .flatten()
+ postValue(virtualDeviceGrantInfoList)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private fun getVirtualDeviceGrantInfoList(
+ persistentDeviceId: String
+ ): List<VirtualDeviceGrantInfo> {
+ val permissionState =
+ permissionManager.getAllPermissionStates(packageName, persistentDeviceId)
+ return permissionState.mapNotNull { (permissionName, permissionState) ->
+ PermissionMapping.getGroupOfPlatformPermission(permissionName)?.let { groupName ->
+ val grantState = getGrantState(permissionState)
+ VirtualDeviceGrantInfo(groupName, grantState, persistentDeviceId)
+ }
+ }
+ }
+
+ /**
+ * This method returns the GrantState for currently supported virtual device permissions
+ * (Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO).
+ *
+ * TODO: b/328841671 (Unite this with PermGroupUiInfoLiveData#getGrantedIncludingBackground)
+ */
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private fun getGrantState(permissionState: PermissionState): AppPermGroupUiInfo.PermGrantState =
+ if (permissionState.flags and PackageManager.FLAG_PERMISSION_ONE_TIME != 0) {
+ AppPermGroupUiInfo.PermGrantState.PERMS_ASK
+ } else if (permissionState.isGranted) {
+ AppPermGroupUiInfo.PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY
+ } else {
+ AppPermGroupUiInfo.PermGrantState.PERMS_DENIED
+ }
+
+ companion object :
+ DataRepositoryForPackage<
+ Pair<String, UserHandle>, PackagePermissionsVirtualDeviceLiveData
+ >() {
+ override fun newValue(
+ key: Pair<String, UserHandle>
+ ): PackagePermissionsVirtualDeviceLiveData {
+ return PackagePermissionsVirtualDeviceLiveData(
+ PermissionControllerApplication.get(),
+ key.first,
+ key.second
+ )
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
index 7fa51dd8a..97b336ab6 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java
@@ -82,6 +82,7 @@ import com.android.permissioncontroller.permission.ui.model.AppPermissionViewMod
import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModelFactory;
import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs;
import com.android.permissioncontroller.permission.utils.KotlinUtils;
+import com.android.permissioncontroller.permission.utils.MultiDeviceUtils;
import com.android.permissioncontroller.permission.utils.Utils;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
@@ -106,6 +107,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader
private static final long EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS = 200L;
static final String GRANT_CATEGORY = "grant_category";
+ static final String PERSISTENT_DEVICE_ID = "persistent_device_id";
private @NonNull AppPermissionViewModel mViewModel;
private @NonNull ViewGroup mAppPermissionRationaleContainer;
@@ -135,6 +137,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader
// This prevents the user from clicking the photo picker button multiple times in succession
private boolean mPhotoPickerTriggered;
private long mSessionId;
+ private String mPersistentDeviceId;
private @NonNull String mPackageLabel;
private @NonNull String mPermGroupLabel;
@@ -198,8 +201,12 @@ public class AppPermissionFragment extends SettingsWithLargeHeader
mPackageName, mUser);
mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
+ mPersistentDeviceId = getArguments().getString(PERSISTENT_DEVICE_ID,
+ MultiDeviceUtils.getDefaultDevicePersistentDeviceId());
+
AppPermissionViewModelFactory factory = new AppPermissionViewModelFactory(
- getActivity().getApplication(), mPackageName, mPermGroupName, mUser, mSessionId);
+ getActivity().getApplication(), mPackageName, mPermGroupName, mUser, mSessionId,
+ mPersistentDeviceId);
mViewModel = new ViewModelProvider(this, factory).get(AppPermissionViewModel.class);
Handler delayHandler = new Handler(Looper.getMainLooper());
mViewModel.getButtonStateLiveData().observe(this, buttonState -> {
@@ -230,8 +237,15 @@ public class AppPermissionFragment extends SettingsWithLargeHeader
setHeader(mPackageIcon, mPackageLabel, null, null, false);
updateHeader(root.requireViewById(R.id.large_header));
- ((TextView) root.requireViewById(R.id.permission_message)).setText(
- context.getString(R.string.app_permission_header, mPermGroupLabel));
+ String text = null;
+ if (MultiDeviceUtils.isDefaultDeviceId(mPersistentDeviceId)) {
+ text = context.getString(R.string.app_permission_header, mPermGroupLabel);
+ } else {
+ final String deviceName = MultiDeviceUtils.getDeviceName(context, mPersistentDeviceId);
+ text = context.getString(R.string.app_permission_header_with_device_name,
+ mPermGroupLabel, deviceName);
+ }
+ ((TextView) root.requireViewById(R.id.permission_message)).setText(text);
String caller = getArguments().getString(EXTRA_CALLER_NAME);
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
index 24b6439b5..1eea2da87 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionGroupsFragment.java
@@ -75,6 +75,7 @@ import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsV
import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.GroupUiInfo;
import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory;
import com.android.permissioncontroller.permission.utils.KotlinUtils;
+import com.android.permissioncontroller.permission.utils.MultiDeviceUtils;
import com.android.permissioncontroller.permission.utils.StringUtils;
import com.android.permissioncontroller.permission.utils.Utils;
import com.android.settingslib.HelpUtils;
@@ -350,7 +351,19 @@ public final class AppPermissionGroupsFragment extends SettingsWithLargeHeader i
PermissionControlPreference preference = new PermissionControlPreference(context,
mPackageName, groupName, mUser, AppPermissionGroupsFragment.class.getName(),
sessionId, grantCategory.getCategoryName(), true);
- preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName));
+
+ CharSequence permissionGroupName = KotlinUtils.INSTANCE.getPermGroupLabel(context,
+ groupName);
+ if (MultiDeviceUtils.isDefaultDeviceId(groupInfo.getPersistentDeviceId())) {
+ preference.setTitle(permissionGroupName);
+ } else {
+ final String deviceName = MultiDeviceUtils.getDeviceName(context,
+ groupInfo.getPersistentDeviceId());
+ preference.setTitle(context.getString(
+ R.string.permission_group_name_with_device_name,
+ permissionGroupName, deviceName));
+ preference.setPersistentDeviceId(groupInfo.getPersistentDeviceId());
+ }
preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName));
preference.setKey(groupName);
String summary = mViewModel.getPreferenceSummary(groupInfo, context,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java
index 3d47909e8..039aca39d 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionControlPreference.java
@@ -21,6 +21,7 @@ import static android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP;
import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME;
import static com.android.permissioncontroller.permission.ui.handheld.AppPermissionFragment.GRANT_CATEGORY;
+import static com.android.permissioncontroller.permission.ui.handheld.AppPermissionFragment.PERSISTENT_DEVICE_ID;
import static com.android.permissioncontroller.permission.utils.KotlinUtilsKt.navigateSafe;
import android.Manifest;
@@ -69,6 +70,7 @@ public class PermissionControlPreference extends Preference {
private @NonNull long mSessionId;
private boolean mHasNavGraph;
private @NonNull UserHandle mUser;
+ private @Nullable String mPersistentDeviceId;
public PermissionControlPreference(@NonNull Context context,
@NonNull AppPermissionGroup group, @NonNull String caller) {
@@ -237,6 +239,7 @@ public class PermissionControlPreference extends Preference {
args.putString(EXTRA_CALLER_NAME, mCaller);
args.putLong(EXTRA_SESSION_ID, mSessionId);
args.putString(GRANT_CATEGORY, mGranted);
+ args.putString(PERSISTENT_DEVICE_ID, mPersistentDeviceId);
navigateSafe(Navigation.findNavController(holder.itemView), R.id.perm_groups_to_app,
args);
} else {
@@ -254,6 +257,10 @@ public class PermissionControlPreference extends Preference {
});
}
+ public void setPersistentDeviceId(String persistentDeviceId) {
+ this.mPersistentDeviceId = persistentDeviceId;
+ }
+
private void setIcons(PreferenceViewHolder holder, @Nullable List<Integer> icons, int frameId) {
ViewGroup frame = (ViewGroup) holder.findViewById(frameId);
if (icons != null && !icons.isEmpty()) {
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
index 5ecab1527..626ee2433 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionGroupsViewModel.kt
@@ -44,6 +44,7 @@ import com.android.permissioncontroller.permission.data.HibernationSettingStateL
import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData
import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData
import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS
+import com.android.permissioncontroller.permission.data.PackagePermissionsVirtualDeviceLiveData
import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
import com.android.permissioncontroller.permission.data.get
import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState
@@ -51,6 +52,7 @@ import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage
import com.android.permissioncontroller.permission.ui.Category
import com.android.permissioncontroller.permission.utils.IPC
import com.android.permissioncontroller.permission.utils.KotlinUtils
+import com.android.permissioncontroller.permission.utils.MultiDeviceUtils
import com.android.permissioncontroller.permission.utils.PermissionMapping
import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.permission.utils.Utils.AppPermsLastAccessType
@@ -93,12 +95,29 @@ class AppPermissionGroupsViewModel(
data class GroupUiInfo(
val groupName: String,
val isSystem: Boolean = false,
- val subtitle: PermSubtitle
+ val subtitle: PermSubtitle,
+ val persistentDeviceId: String,
) {
constructor(
groupName: String,
isSystem: Boolean
- ) : this(groupName, isSystem, PermSubtitle.NONE)
+ ) : this(
+ groupName,
+ isSystem,
+ PermSubtitle.NONE,
+ MultiDeviceUtils.getDefaultDevicePersistentDeviceId()
+ )
+
+ constructor(
+ groupName: String,
+ isSystem: Boolean,
+ subtitle: PermSubtitle,
+ ) : this(
+ groupName,
+ isSystem,
+ subtitle,
+ MultiDeviceUtils.getDefaultDevicePersistentDeviceId()
+ )
}
// Auto-revoke and hibernation share the same settings
@@ -107,6 +126,8 @@ class AppPermissionGroupsViewModel(
private val packagePermsLiveData = PackagePermissionsLiveData[packageName, user]
private val appPermGroupUiInfoLiveDatas = mutableMapOf<String, AppPermGroupUiInfoLiveData>()
private val fullStoragePermsLiveData = FullStoragePermissionAppsLiveData
+ private val packagePermsVirtualDeviceLiveData =
+ PackagePermissionsVirtualDeviceLiveData[packageName, user]
/**
* LiveData whose data is a map of grant category (either allowed or denied) to a list of
@@ -217,6 +238,61 @@ class AppPermissionGroupsViewModel(
}
}
+ packagePermsVirtualDeviceLiveData.value?.forEach { virtualDeviceGrantInfo ->
+ val groupName = virtualDeviceGrantInfo.groupName
+ val isSystem =
+ PermissionMapping.getPlatformPermissionGroups().contains(groupName)
+ val persistentDeviceId = virtualDeviceGrantInfo.persistentDeviceId
+ when (virtualDeviceGrantInfo.permGrantState) {
+ PermGrantState.PERMS_ALLOWED -> {
+ groupGrantStates[Category.ALLOWED]!!.add(
+ GroupUiInfo(
+ groupName,
+ isSystem,
+ PermSubtitle.NONE,
+ persistentDeviceId
+ )
+ )
+ }
+ PermGrantState.PERMS_ALLOWED_ALWAYS ->
+ groupGrantStates[Category.ALLOWED]!!.add(
+ GroupUiInfo(
+ groupName,
+ isSystem,
+ PermSubtitle.BACKGROUND,
+ persistentDeviceId
+ )
+ )
+ PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY ->
+ groupGrantStates[Category.ALLOWED]!!.add(
+ GroupUiInfo(
+ groupName,
+ isSystem,
+ PermSubtitle.FOREGROUND_ONLY,
+ persistentDeviceId
+ )
+ )
+ PermGrantState.PERMS_DENIED ->
+ groupGrantStates[Category.DENIED]!!.add(
+ GroupUiInfo(
+ groupName,
+ isSystem,
+ PermSubtitle.NONE,
+ persistentDeviceId
+ )
+ )
+ PermGrantState.PERMS_ASK ->
+ groupGrantStates[Category.ASK]!!.add(
+ GroupUiInfo(
+ groupName,
+ isSystem,
+ PermSubtitle.NONE,
+ persistentDeviceId
+ )
+ )
+ }
+ }
+
value = groupGrantStates
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
index 971542e2b..33639835b 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt
@@ -52,9 +52,11 @@ import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState
import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData
+import com.android.permissioncontroller.permission.data.PackagePermissionsVirtualDeviceLiveData
import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
import com.android.permissioncontroller.permission.data.get
import com.android.permissioncontroller.permission.data.v34.SafetyLabelInfoLiveData
+import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo
import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup
import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission
import com.android.permissioncontroller.permission.service.PermissionChangeStorageImpl
@@ -76,6 +78,7 @@ import com.android.permissioncontroller.permission.utils.KotlinUtils.isLocationA
import com.android.permissioncontroller.permission.utils.KotlinUtils.isPhotoPickerPromptEnabled
import com.android.permissioncontroller.permission.utils.KotlinUtils.openPhotoPickerForApp
import com.android.permissioncontroller.permission.utils.LocationUtils
+import com.android.permissioncontroller.permission.utils.MultiDeviceUtils
import com.android.permissioncontroller.permission.utils.PermissionMapping
import com.android.permissioncontroller.permission.utils.PermissionMapping.getPartialStorageGrantPermissionsForGroup
import com.android.permissioncontroller.permission.utils.SafetyNetLogger
@@ -96,15 +99,17 @@ import kotlin.collections.component2
* @param permGroupName The name of the permission group this ViewModel represents
* @param user The user of the package
* @param sessionId A session ID used in logs to identify this particular session
+ * @param persistentDeviceId Indicates the device in the context of virtual devices, can be null
+ * indicating default device
*/
class AppPermissionViewModel(
private val app: Application,
private val packageName: String,
private val permGroupName: String,
private val user: UserHandle,
- private val sessionId: Long
+ private val sessionId: Long,
+ private val persistentDeviceId: String?
) : ViewModel() {
-
companion object {
private val LOG_TAG = AppPermissionViewModel::class.java.simpleName
private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role"
@@ -223,6 +228,7 @@ class AppPermissionViewModel(
value = null
}
}
+
override fun onUpdate() {
for (state in FullStoragePermissionAppsLiveData.value ?: return) {
if (state.packageName == packageName && state.user == user) {
@@ -302,8 +308,58 @@ class AppPermissionViewModel(
}
}
+ // TODO: b/328839130 (Merge this with default device implementation)
+ private fun getVirtualDeviceButtonStates(): Map<ButtonType, ButtonState> {
+ val allowedForegroundState = ButtonState()
+ allowedForegroundState.isShown = true
+
+ val askState = ButtonState()
+ askState.isShown = true
+
+ val deniedState = ButtonState()
+ deniedState.isShown = true
+
+ val virtualDeviceGrantInfoList =
+ PackagePermissionsVirtualDeviceLiveData[packageName, user].value
+
+ virtualDeviceGrantInfoList!!
+ .filter {
+ it.groupName == permGroupName && it.persistentDeviceId == persistentDeviceId
+ }
+ .map { it.permGrantState }
+ .forEach {
+ when (it) {
+ AppPermGroupUiInfo.PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY ->
+ allowedForegroundState.isChecked = true
+ AppPermGroupUiInfo.PermGrantState.PERMS_ASK -> askState.isChecked = true
+ AppPermGroupUiInfo.PermGrantState.PERMS_DENIED ->
+ deniedState.isChecked = true
+ else -> {
+ Log.e(LOG_TAG, "Unsupported PermGrantState=$it")
+ }
+ }
+ }
+ return mapOf(
+ ALLOW to ButtonState(),
+ ALLOW_ALWAYS to ButtonState(),
+ ALLOW_FOREGROUND to allowedForegroundState,
+ ASK_ONCE to ButtonState(),
+ ASK to askState,
+ DENY to deniedState,
+ DENY_FOREGROUND to ButtonState(),
+ LOCATION_ACCURACY to ButtonState(),
+ SELECT_PHOTOS to ButtonState()
+ )
+ }
+
override fun onUpdate() {
+ if (!MultiDeviceUtils.isDefaultDeviceId(persistentDeviceId)) {
+ value = getVirtualDeviceButtonStates()
+ return
+ }
+
val group = appPermGroupLiveData.value ?: return
+
for (mediaGroupLiveData in mediaStorageSupergroupLiveData.values) {
if (!mediaGroupLiveData.isInitialized) {
return
@@ -1315,16 +1371,41 @@ class AppPermissionViewModel(
* @param permGroupName The name of the permission group this ViewModel represents
* @param user The user of the package
* @param sessionId A session ID used in logs to identify this particular session
+ * @param persistentDeviceId Indicates the device in the context of virtual devices
*/
class AppPermissionViewModelFactory(
private val app: Application,
private val packageName: String,
private val permGroupName: String,
private val user: UserHandle,
- private val sessionId: Long
+ private val sessionId: Long,
+ private val persistentDeviceId: String
) : ViewModelProvider.Factory {
+ constructor(
+ app: Application,
+ packageName: String,
+ permGroupName: String,
+ user: UserHandle,
+ sessionId: Long
+ ) : this(
+ app,
+ packageName,
+ permGroupName,
+ user,
+ sessionId,
+ MultiDeviceUtils.getDefaultDevicePersistentDeviceId()
+ )
+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
- return AppPermissionViewModel(app, packageName, permGroupName, user, sessionId) as T
+ return AppPermissionViewModel(
+ app,
+ packageName,
+ permGroupName,
+ user,
+ sessionId,
+ persistentDeviceId
+ )
+ as T
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/MultiDeviceUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/MultiDeviceUtils.kt
index ba5ba1c23..fcffc50e8 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/MultiDeviceUtils.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/MultiDeviceUtils.kt
@@ -47,4 +47,38 @@ object MultiDeviceUtils {
}
throw IllegalArgumentException("No device name for device: $deviceId")
}
+
+ @JvmStatic
+ @ChecksSdkIntAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun isDefaultDeviceId(persistentDeviceId: String?) =
+ !SdkLevel.isAtLeastV() ||
+ persistentDeviceId.isNullOrBlank() ||
+ persistentDeviceId == VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT
+
+ @JvmStatic
+ @ChecksSdkIntAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun getDeviceName(context: Context, persistentDeviceId: String): String {
+ if (
+ !SdkLevel.isAtLeastV() ||
+ persistentDeviceId == VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT
+ ) {
+ return Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
+ }
+ val vdm: VirtualDeviceManager =
+ context.getSystemService(VirtualDeviceManager::class.java)
+ ?: throw RuntimeException("Device manager not found")
+ val deviceName =
+ vdm.getDisplayNameForPersistentDeviceId(persistentDeviceId)
+ ?: DEFAULT_REMOTE_DEVICE_NAME
+ return deviceName.toString()
+ }
+
+ @JvmStatic
+ @ChecksSdkIntAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun getDefaultDevicePersistentDeviceId(): String =
+ if (!SdkLevel.isAtLeastV()) {
+ "default: ${ContextCompat.DEVICE_ID_DEFAULT}"
+ } else {
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT
+ }
}
diff --git a/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/AppPermissionsTest.kt b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/AppPermissionsTest.kt
new file mode 100644
index 000000000..73a0819c9
--- /dev/null
+++ b/tests/cts/permissionmultidevice/src/android/permissionmultidevice/cts/AppPermissionsTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ * 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 android.permissionmultidevice.cts
+
+import android.Manifest
+import android.app.Instrumentation
+import android.companion.virtual.VirtualDeviceManager
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.UserHandle
+import android.permission.flags.Flags
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject
+import androidx.test.uiautomator.UiSelector
+import com.android.compatibility.common.util.AdoptShellPermissionsRule
+import com.android.compatibility.common.util.SystemUtil
+import com.android.compatibility.common.util.UiAutomatorUtils2
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName = "VanillaIceCream")
+class AppPermissionsTest {
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val defaultDeviceContext = instrumentation.targetContext
+
+ private lateinit var virtualDevice: VirtualDeviceManager.VirtualDevice
+ private lateinit var virtualDeviceContext: Context
+ private lateinit var persistentDeviceId: String
+ private lateinit var deviceName: String
+
+ @get:Rule(order = 0) var mFakeVirtualDeviceRule = FakeVirtualDeviceRule()
+
+ @get:Rule(order = 1)
+ val mAdoptShellPermissionsRule =
+ AdoptShellPermissionsRule(
+ instrumentation.uiAutomation,
+ Manifest.permission.CREATE_VIRTUAL_DEVICE,
+ Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+ Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS,
+ Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
+ )
+
+ @Rule @JvmField val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Before
+ fun setup() {
+ virtualDevice = mFakeVirtualDeviceRule.virtualDevice
+ virtualDeviceContext = defaultDeviceContext.createDeviceContext(virtualDevice.deviceId)
+ persistentDeviceId = virtualDevice.persistentDeviceId!!
+ deviceName = mFakeVirtualDeviceRule.deviceDisplayName
+
+ PackageManagementUtils.installPackage(APP_APK_PATH_STREAMING)
+ }
+
+ @After
+ fun cleanup() {
+ PackageManagementUtils.uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false)
+ virtualDevice.close()
+ }
+
+ @RequiresFlagsEnabled(
+ Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED,
+ Flags.FLAG_DEVICE_AWARE_PERMISSIONS_ENABLED
+ )
+ @Test
+ fun virtualDevicePermissionGrantTest() {
+ grantRunTimePermission()
+
+ openAppPermissionsScreen()
+
+ val grantInfoMap = getGrantInfoMap()
+
+ val virtualDeviceCameraText = "Camera on $deviceName"
+
+ assertEquals(1, grantInfoMap["Allowed"]!!.size)
+ assertEquals(true, grantInfoMap["Allowed"]!!.contains(virtualDeviceCameraText))
+
+ assertEquals(1, grantInfoMap["Not allowed"]!!.size)
+ assertEquals(true, grantInfoMap["Not allowed"]!!.contains("Camera"))
+
+ clickPermissionItem(virtualDeviceCameraText)
+
+ val foregroundOnlyRadioButton =
+ UiAutomatorUtils2.waitFindObject(By.res(ALLOW_FOREGROUND_ONLY_RADIO_BUTTON))
+ val askRadioButton = UiAutomatorUtils2.waitFindObject(By.res(ASK_RADIO_BUTTON))
+ val denyRadioButton = UiAutomatorUtils2.waitFindObject(By.res(DENY_RADIO_BUTTON))
+ assertEquals(foregroundOnlyRadioButton.isChecked, true)
+ assertEquals(askRadioButton.isChecked, false)
+ assertEquals(denyRadioButton.isChecked, false)
+ }
+
+ private fun getGrantInfoMap(): Map<String, List<String>> {
+ val recyclerView = getAppPermissionsRecyclerView()
+
+ val grantInfoMap =
+ mapOf(
+ "Allowed" to mutableListOf<String>(),
+ "Ask every time" to mutableListOf(),
+ "Not allowed" to mutableListOf(),
+ "Unused app settings" to mutableListOf(),
+ "Manage app if unused" to mutableListOf()
+ )
+
+ val childItemSelector = UiSelector().resourceId(TITLE)
+ var grantText = ""
+
+ for (i in 0..recyclerView.childCount) {
+ val child = recyclerView.getChild(UiSelector().index(i)).getChild(childItemSelector)
+ if (!child.exists()) {
+ break
+ }
+ if (child.text in grantInfoMap) {
+ grantText = child.text
+ } else if (!child.text.startsWith("No permissions")) {
+ grantInfoMap[grantText]!!.add(child.text)
+ }
+ }
+ return grantInfoMap
+ }
+
+ private fun getAppPermissionsRecyclerView(): UiObject {
+ val uiObject =
+ UiAutomatorUtils2.getUiDevice().findObject(UiSelector().resourceId(RECYCLER_VIEW))
+ uiObject.waitForExists(5000)
+ return uiObject
+ }
+
+ private fun openAppPermissionsScreen() {
+ instrumentation.context.startActivity(
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.fromParts("package", APP_PACKAGE_NAME, null)
+ addCategory(Intent.CATEGORY_DEFAULT)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+ )
+ SystemUtil.eventually { UiAutomatorUtils.click(By.text("Permissions")) }
+ }
+
+ private fun clickPermissionItem(permissionItemName: String) {
+ val childItemSelector = UiSelector().resourceId(TITLE)
+ getAppPermissionsRecyclerView().getChild(childItemSelector.text(permissionItemName)).let {
+ it.waitForExists(5000)
+ it.click()
+ }
+ }
+
+ private fun grantRunTimePermission() {
+ virtualDeviceContext.packageManager.grantRuntimePermission(
+ APP_PACKAGE_NAME,
+ DEVICE_AWARE_PERMISSION,
+ UserHandle.of(virtualDeviceContext.userId)
+ )
+ }
+
+ companion object {
+ private const val APK_DIRECTORY = "/data/local/tmp/cts-permissionmultidevice"
+ private const val APP_APK_PATH_STREAMING =
+ "${APK_DIRECTORY}/CtsAccessRemoteDeviceCamera.apk"
+ private const val APP_PACKAGE_NAME =
+ "android.permissionmultidevice.cts.accessremotedevicecamera"
+ private const val DEVICE_AWARE_PERMISSION = Manifest.permission.CAMERA
+
+ private const val ALLOW_FOREGROUND_ONLY_RADIO_BUTTON =
+ "com.android.permissioncontroller:id/allow_foreground_only_radio_button"
+ private const val ASK_RADIO_BUTTON = "com.android.permissioncontroller:id/ask_radio_button"
+ private const val DENY_RADIO_BUTTON =
+ "com.android.permissioncontroller:id/deny_radio_button"
+ private const val TITLE = "android:id/title"
+ private const val RECYCLER_VIEW = "com.android.permissioncontroller:id/recycler_view"
+ }
+}