diff options
author | 2024-03-07 22:03:07 +0000 | |
---|---|---|
committer | 2024-03-12 16:41:57 +0000 | |
commit | 5bfda27abafe8e0b167184d4e874c134b4e1ba6a (patch) | |
tree | a40214074d807a409ca74f4482de4db76332a048 | |
parent | 58de5c2bc18fe099c7e11a88c683b1a69b60cf3c (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
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" + } +} |