summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Kiran Ramachandra <kiranmr@google.com> 2024-03-07 22:03:07 +0000
committer Kiran Ramachandra <kiranmr@google.com> 2024-03-12 16:41:57 +0000
commit5bfda27abafe8e0b167184d4e874c134b4e1ba6a (patch)
treea40214074d807a409ca74f4482de4db76332a048
parent58de5c2bc18fe099c7e11a88c683b1a69b60cf3c (diff)
Enhancement to display virtual devices on the App Permissions screen
1) Added new live data that gets the permission from system API and returns a list of VirtualDeviceGrantInfo 2) Modified ViewModel and other objects to carry this info and display on the App Permissions UI 3) Added new CTS test to verify this Note on tests: There will be further test enhancement and more use-case coverage in the upcoming CL where users will have the ability to alter the permissions in App Permissions flow. Bug: 322875936 Bug: 322875937 Test: atest CtsPermissionMultiDeviceTestCases:android.permissionmultidevice.cts.AppPermissionsTest LOW_COVERAGE_REASON=Next set of changes enable to alter permissions (b/322876988). This enables to write additional tests and coverage will be taken of during the process. Screenshot1: https://screenshot.googleplex.com/744eQJbBMGTKykp.png Screenshot2: https://screenshot.googleplex.com/39MLRygiCb8HejS.png Change-Id: Iee54daf727f28fd08dfd78f8920d91be7fb64dca
-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"
+ }
+}