From 5a8be5b16f8f440cfc946bf0d140237d1380689f Mon Sep 17 00:00:00 2001 From: Nate Myren Date: Tue, 10 Oct 2023 09:47:47 -0700 Subject: Add "Edit Photos" button in settings When clicked, this button will open the photo picker, and allow users to change which photos the app in question has access to. This also consolidates the photo picker launching logic. Bug: 303834669 Test: atest PhotoPickerPermissionTest Relnote: add a button that allows users to select photos in settings Change-Id: I8ca75795644915428db8e1b7fda5f15672818720 Merged-In: I8ca75795644915428db8e1b7fda5f15672818720 --- .../ui/auto/AutoAppPermissionFragment.java | 2 +- .../ui/handheld/AppPermissionFragment.java | 47 ++++++++++++++++++++-- .../permission/ui/model/AppPermissionViewModel.kt | 44 ++++---------------- .../ui/model/GrantPermissionsViewModel.kt | 21 ++-------- .../ui/television/AppPermissionFragment.java | 4 +- .../permission/utils/KotlinUtils.kt | 25 ++++++++++++ 6 files changed, 82 insertions(+), 61 deletions(-) (limited to 'PermissionController/src/com/android/permissioncontroller/permission') diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/AutoAppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/AutoAppPermissionFragment.java index 2de936469..d3a89c3ed 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/AutoAppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/auto/AutoAppPermissionFragment.java @@ -418,7 +418,7 @@ public class AutoAppPermissionFragment extends AutoSettingsFrameFragment // TODO(b/229024576): This code is duplicated, refactor ConfirmDialog for easier // NFF sharing boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST) - == ChangeRequest.GRANT_All_FILE_ACCESS; + == ChangeRequest.GRANT_ALL_FILE_ACCESS; boolean isGrantStorageSupergroup = getArguments().getSerializable(CHANGE_REQUEST) == ChangeRequest.GRANT_STORAGE_SUPERGROUP; int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway; 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 cab0de15e..7fa51dd8a 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java @@ -103,6 +103,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader implements AppPermissionViewModel.ConfirmDialogShowingFragment { private static final String LOG_TAG = "AppPermissionFragment"; private static final long POST_DELAY_MS = 20; + private static final long EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS = 200L; static final String GRANT_CATEGORY = "grant_category"; @@ -117,6 +118,9 @@ public class AppPermissionFragment extends SettingsWithLargeHeader private @NonNull RadioButton mSelectPhotosButton; private @NonNull RadioButton mDenyButton; private @NonNull RadioButton mDenyForegroundButton; + private @NonNull ImageView mEditPhotosButton; + private @NonNull View mSelectPhotosLayout; + private @NonNull View mEditPhotosDivider; private @NonNull View mLocationAccuracy; private @NonNull Switch mLocationAccuracySwitch; private @NonNull View mDivider; @@ -128,6 +132,8 @@ public class AppPermissionFragment extends SettingsWithLargeHeader private @NonNull UserHandle mUser; private boolean mIsStorageGroup; private boolean mIsInitialLoad; + // This prevents the user from clicking the photo picker button multiple times in succession + private boolean mPhotoPickerTriggered; private long mSessionId; private @NonNull String mPackageLabel; @@ -209,7 +215,6 @@ public class AppPermissionFragment extends SettingsWithLargeHeader if (mIsStorageGroup) { mViewModel.getFullStorageStateLiveData().observe(this, this::setSpecialStorageState); } - mViewModel.registerPhotoPickerResultIfNeeded(this); mRoleManager = Utils.getSystemServiceSafe(getContext(), RoleManager.class); } @@ -261,12 +266,15 @@ public class AppPermissionFragment extends SettingsWithLargeHeader mSelectPhotosButton = root.requireViewById(R.id.select_radio_button); mDenyButton = root.requireViewById(R.id.deny_radio_button); mDenyForegroundButton = root.requireViewById(R.id.deny_foreground_radio_button); + mDivider = root.requireViewById(R.id.two_target_divider); mWidgetFrame = root.requireViewById(R.id.widget_frame); mPermissionDetails = root.requireViewById(R.id.permission_details); mLocationAccuracy = root.requireViewById(R.id.location_accuracy); mLocationAccuracySwitch = root.requireViewById(R.id.location_accuracy_switch); - + mSelectPhotosLayout = root.requireViewById(R.id.radio_select_layout); + mEditPhotosButton = root.requireViewById(R.id.edit_selected_button); + mEditPhotosDivider = root.requireViewById(R.id.edit_photos_divider); mNestedScrollView = root.requireViewById(R.id.nested_scroll_view); if (mViewModel.getButtonStateLiveData().getValue() != null) { @@ -280,6 +288,9 @@ public class AppPermissionFragment extends SettingsWithLargeHeader mDenyButton.setVisibility(View.GONE); mDenyForegroundButton.setVisibility(View.GONE); mLocationAccuracy.setVisibility(View.GONE); + mSelectPhotosLayout.setVisibility(View.GONE); + mEditPhotosDivider.setAlpha(0f); + mEditPhotosButton.setAlpha(0f); } if (mViewModel.getFullStorageStateLiveData().isInitialized() && mIsStorageGroup) { @@ -302,6 +313,12 @@ public class AppPermissionFragment extends SettingsWithLargeHeader return root; } + public void onResume() { + super.onResume(); + // If we're returning to the fragment, photo picker hasn't been triggered + mPhotoPickerTriggered = false; + } + private void showPermissionRationaleDialog(boolean showPermissionRationale) { if (!showPermissionRationale) { mAppPermissionRationaleContainer.setVisibility(View.GONE); @@ -380,7 +397,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader }); mAllowAlwaysButton.setOnClickListener((v) -> { if (mIsStorageGroup) { - showConfirmDialog(ChangeRequest.GRANT_All_FILE_ACCESS, + showConfirmDialog(ChangeRequest.GRANT_ALL_FILE_ACCESS, R.string.special_file_access_dialog, -1, false); } else { mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH, @@ -412,6 +429,13 @@ public class AppPermissionFragment extends SettingsWithLargeHeader mViewModel.requestChange(false, this, this, ChangeRequest.PHOTOS_SELECTED, buttonPressed); }); + mEditPhotosButton.setOnClickListener((v) -> { + ButtonState selectState = states.get(ButtonType.SELECT_PHOTOS); + if (selectState != null && selectState.isChecked() && !mPhotoPickerTriggered) { + mPhotoPickerTriggered = true; + mViewModel.openPhotoPicker(this); + } + }); mDenyButton.setOnClickListener((v) -> { if (mViewModel.getFullStorageStateLiveData().getValue() != null && !mViewModel.getFullStorageStateLiveData().getValue().isLegacy()) { @@ -485,6 +509,21 @@ public class AppPermissionFragment extends SettingsWithLargeHeader if (mIsInitialLoad) { button.jumpDrawablesToCurrentState(); } + + if (button == mSelectPhotosButton) { + mSelectPhotosLayout.setVisibility(visible); + float endOpacity = state.isChecked() ? 1f : 0f; + // On initial load, do not show the fade in/out animation + if (mIsInitialLoad) { + mEditPhotosDivider.setAlpha(endOpacity); + mEditPhotosButton.setAlpha(endOpacity); + return; + } + mEditPhotosButton.animate().alpha(endOpacity) + .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS); + mEditPhotosDivider.animate().alpha(endOpacity) + .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS); + } } private void setSpecialStorageState(FullStoragePackageState storageState, View v) { @@ -630,7 +669,7 @@ public class AppPermissionFragment extends SettingsWithLargeHeader // NFF sharing AppPermissionFragment fragment = (AppPermissionFragment) getParentFragment(); boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST) - == ChangeRequest.GRANT_All_FILE_ACCESS; + == ChangeRequest.GRANT_ALL_FILE_ACCESS; int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway; if (isGrantFileAccess) { positiveButtonStringResId = R.string.grant_dialog_button_allow; 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 d6197b15f..cc29acbd7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/AppPermissionViewModel.kt @@ -29,19 +29,14 @@ import android.app.AppOpsManager.MODE_ALLOWED import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE import android.app.Application -import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.os.UserHandle -import android.provider.MediaStore import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi import androidx.annotation.StringRes -import androidx.core.util.Consumer import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -80,6 +75,7 @@ import com.android.permissioncontroller.permission.utils.KotlinUtils import com.android.permissioncontroller.permission.utils.KotlinUtils.getDefaultPrecision import com.android.permissioncontroller.permission.utils.KotlinUtils.isLocationAccuracyEnabled 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.PermissionMapping import com.android.permissioncontroller.permission.utils.PermissionMapping.getPartialStorageGrantPermissionsForGroup @@ -113,7 +109,6 @@ class AppPermissionViewModel( companion object { private val LOG_TAG = AppPermissionViewModel::class.java.simpleName private const val DEVICE_PROFILE_ROLE_PREFIX = "android.app.role" - const val PHOTO_PICKER_REQUEST_CODE = 1 } interface ConfirmDialogShowingFragment { @@ -135,7 +130,7 @@ class AppPermissionViewModel( GRANT_BOTH(GRANT_FOREGROUND.value or GRANT_BACKGROUND.value), REVOKE_BOTH(REVOKE_FOREGROUND.value or REVOKE_BACKGROUND.value), GRANT_FOREGROUND_ONLY(GRANT_FOREGROUND.value or REVOKE_BACKGROUND.value), - GRANT_All_FILE_ACCESS(1 shl 4), + GRANT_ALL_FILE_ACCESS(1 shl 4), GRANT_FINE_LOCATION(1 shl 5), REVOKE_FINE_LOCATION(1 shl 6), GRANT_STORAGE_SUPERGROUP(1 shl 7), @@ -167,8 +162,6 @@ class AppPermissionViewModel( permGroupName == Manifest.permission_group.STORAGE && !SdkLevel.isAtLeastT() private var hasConfirmedRevoke = false private var lightAppPermGroup: LightAppPermGroup? = null - private var photoPickerLauncher: ActivityResultLauncher? = null - private var photoPickerResultConsumer: Consumer? = null private val mediaStorageSupergroupPermGroups = mutableMapOf() @@ -513,32 +506,6 @@ class AppPermissionViewModel( return !userSelectedPerm.isImplicit } - fun registerPhotoPickerResultIfNeeded(fragment: Fragment) { - if (permGroupName != READ_MEDIA_VISUAL) { - return - } - photoPickerLauncher = - fragment.registerForActivityResult( - object : ActivityResultContract() { - override fun parseResult(resultCode: Int, intent: Intent?): Int { - return resultCode - } - - override fun createIntent(context: Context, input: Unit): Intent { - return Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) - .putExtra(Intent.EXTRA_UID, lightAppPermGroup?.packageInfo?.uid) - .setType( - KotlinUtils.getMimeTypeForPermissions( - lightAppPermGroup?.foregroundPermNames ?: emptyList() - ) - ) - } - } - ) { result -> - photoPickerResultConsumer?.accept(result) - } - } - private fun isFineLocationChecked(group: LightAppPermGroup): Boolean { if (shouldShowLocationAccuracy == true) { val coarseLocation = group.permissions[ACCESS_COARSE_LOCATION]!! @@ -687,6 +654,12 @@ class AppPermissionViewModel( fragment.findNavController().navigateSafe(actionId, args) } + fun openPhotoPicker(fragment: Fragment) { + val appPermGroup = lightAppPermGroup ?: return + openPhotoPickerForApp(fragment.requireActivity(), appPermGroup.packageInfo.uid, + appPermGroup.foregroundPermNames, 0) + } + /** * Request to grant/revoke permissions group. * @@ -991,7 +964,6 @@ class AppPermissionViewModel( groupName: String, targetSdk: Int ) { - val aural = groupName == Manifest.permission_group.READ_MEDIA_AURAL val visual = groupName == Manifest.permission_group.READ_MEDIA_VISUAL val allow = changeRequest === ChangeRequest.GRANT_STORAGE_SUPERGROUP diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt index d7bb351b4..3f19db475 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -39,9 +39,7 @@ import android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP import android.os.Build import android.os.Bundle import android.os.Process -import android.os.UserManager import android.permission.PermissionManager -import android.provider.MediaStore import android.util.Log import androidx.core.util.Consumer import androidx.lifecycle.ViewModel @@ -125,6 +123,7 @@ import com.android.permissioncontroller.permission.utils.KotlinUtils.grantForegr import com.android.permissioncontroller.permission.utils.KotlinUtils.isLocationAccuracyEnabled import com.android.permissioncontroller.permission.utils.KotlinUtils.isPhotoPickerPromptEnabled import com.android.permissioncontroller.permission.utils.KotlinUtils.isPhotoPickerPromptSupported +import com.android.permissioncontroller.permission.utils.KotlinUtils.openPhotoPickerForApp import com.android.permissioncontroller.permission.utils.KotlinUtils.revokeBackgroundRuntimePermissions import com.android.permissioncontroller.permission.utils.KotlinUtils.revokeForegroundRuntimePermissions import com.android.permissioncontroller.permission.utils.PermissionMapping @@ -1623,22 +1622,8 @@ class GrantPermissionsViewModel( } requestInfosLiveData.update() } - // A clone profile doesn't have a MediaProvider. If this user is a clone profile, open - // the photo picker in the parent profile - val userManager = activity.getSystemService(UserManager::class.java)!! - val user = - if (userManager.isCloneProfile) { - userManager.getProfileParent(Process.myUserHandle()) ?: Process.myUserHandle() - } else { - Process.myUserHandle() - } - activity.startActivityForResultAsUser( - Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) - .putExtra(Intent.EXTRA_UID, packageInfo.uid) - .setType(KotlinUtils.getMimeTypeForPermissions(unfilteredAffectedPermissions)), - PHOTO_PICKER_REQUEST_CODE, - user - ) + openPhotoPickerForApp(activity, packageInfo.uid, unfilteredAffectedPermissions, + PHOTO_PICKER_REQUEST_CODE) } /** diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/television/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/AppPermissionFragment.java index e2df47009..399a694d0 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/television/AppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/AppPermissionFragment.java @@ -289,7 +289,7 @@ public class AppPermissionFragment extends SettingsWithHeader }); mAllowAlwaysButton.setOnPreferenceClickListener((v) -> { if (mIsStorageGroup) { - showConfirmDialog(ChangeRequest.GRANT_All_FILE_ACCESS, + showConfirmDialog(ChangeRequest.GRANT_ALL_FILE_ACCESS, R.string.special_file_access_dialog, -1, false); } else { mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH, @@ -464,7 +464,7 @@ public class AppPermissionFragment extends SettingsWithHeader public Dialog onCreateDialog(Bundle savedInstanceState) { AppPermissionFragment fragment = (AppPermissionFragment) getTargetFragment(); boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST) - == ChangeRequest.GRANT_All_FILE_ACCESS; + == ChangeRequest.GRANT_ALL_FILE_ACCESS; int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway; if (isGrantFileAccess) { positiveButtonStringResId = R.string.grant_dialog_button_allow; diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt index 5d18881ec..f2aa0ae66 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt @@ -25,6 +25,7 @@ import android.Manifest.permission.POST_NOTIFICATIONS import android.Manifest.permission.READ_MEDIA_IMAGES import android.Manifest.permission.READ_MEDIA_VIDEO import android.Manifest.permission_group.NOTIFICATIONS +import android.app.Activity import android.app.ActivityManager import android.app.AppOpsManager import android.app.AppOpsManager.MODE_ALLOWED @@ -59,8 +60,10 @@ import android.health.connect.HealthConnectManager import android.os.Build import android.os.Bundle import android.os.UserHandle +import android.os.UserManager import android.permission.PermissionManager import android.provider.DeviceConfig +import android.provider.MediaStore import android.provider.Settings import android.safetylabel.SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED import android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED @@ -647,6 +650,28 @@ object KotlinUtils { } } + fun openPhotoPickerForApp( + activity: Activity, + uid: Int, + requestedPermissions: List, + requestCode: Int + ) { + // A clone profile doesn't have a MediaProvider. If the app's user is a clone profile, open + // the photo picker in the parent profile + val appUser = UserHandle.getUserHandleForUid(uid) + val userManager = + activity.createContextAsUser(appUser, 0).getSystemService(UserManager::class.java)!! + val user = if (userManager.isCloneProfile) { + userManager.getProfileParent(appUser) ?: appUser + } else { + appUser + } + val pickerIntent = Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) + .putExtra(Intent.EXTRA_UID, uid) + .setType(getMimeTypeForPermissions(requestedPermissions)) + activity.startActivityForResultAsUser(pickerIntent, requestCode, user) + } + /** Return a specific MIME type, if a set of permissions is associated with one */ fun getMimeTypeForPermissions(permissions: List): String? { if (permissions.contains(READ_MEDIA_IMAGES) && !permissions.contains(READ_MEDIA_VIDEO)) { -- cgit v1.2.3