diff options
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" + } +} |