1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.permissioncontroller.permission.ui.handheld; 18 19 import static android.Manifest.permission_group.STORAGE; 20 import static android.app.Activity.RESULT_OK; 21 22 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 23 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW; 25 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS; 26 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND; 27 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ASK_EVERY_TIME; 28 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY; 29 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY_FOREGROUND; 30 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__GRANT_FINE_LOCATION; 31 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PHOTOS_SELECTED; 32 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__REVOKE_FINE_LOCATION; 33 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED; 34 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN; 35 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ALWAYS; 36 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY; 37 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; 38 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED; 39 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT; 40 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; 41 42 import android.app.ActionBar; 43 import android.app.AlertDialog; 44 import android.app.Dialog; 45 import android.app.role.RoleManager; 46 import android.content.Context; 47 import android.content.DialogInterface; 48 import android.content.Intent; 49 import android.graphics.drawable.Drawable; 50 import android.os.Bundle; 51 import android.os.Handler; 52 import android.os.Looper; 53 import android.os.UserHandle; 54 import android.text.BidiFormatter; 55 import android.util.Log; 56 import android.view.LayoutInflater; 57 import android.view.MenuItem; 58 import android.view.View; 59 import android.view.ViewGroup; 60 import android.widget.CompoundButton; 61 import android.widget.FrameLayout; 62 import android.widget.ImageView; 63 import android.widget.RadioButton; 64 import android.widget.Switch; 65 import android.widget.TextView; 66 import android.widget.Toast; 67 68 import androidx.annotation.NonNull; 69 import androidx.annotation.Nullable; 70 import androidx.annotation.StringRes; 71 import androidx.core.widget.NestedScrollView; 72 import androidx.fragment.app.DialogFragment; 73 import androidx.lifecycle.ViewModelProvider; 74 75 import com.android.modules.utils.build.SdkLevel; 76 import com.android.permissioncontroller.R; 77 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState; 78 import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler; 79 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel; 80 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState; 81 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType; 82 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ChangeRequest; 83 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModelFactory; 84 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs; 85 import com.android.permissioncontroller.permission.utils.KotlinUtils; 86 import com.android.permissioncontroller.permission.utils.Utils; 87 import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils; 88 import com.android.settingslib.RestrictedLockUtils; 89 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 90 import com.android.settingslib.widget.ActionBarShadowController; 91 92 import kotlin.Pair; 93 94 import java.util.Map; 95 import java.util.Objects; 96 import java.util.Optional; 97 import java.util.Set; 98 99 /** 100 * Show and manage a single permission group for an app. 101 * 102 * <p>Allows the user to control whether the app is granted the permission. 103 */ 104 public class AppPermissionFragment extends SettingsWithLargeHeader 105 implements AppPermissionViewModel.ConfirmDialogShowingFragment { 106 private static final String LOG_TAG = "AppPermissionFragment"; 107 private static final long POST_DELAY_MS = 20; 108 private static final long EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS = 200L; 109 110 static final String GRANT_CATEGORY = "grant_category"; 111 static final String PERSISTENT_DEVICE_ID = "persistent_device_id"; 112 113 private @NonNull AppPermissionViewModel mViewModel; 114 private @NonNull ViewGroup mAppPermissionRationaleContainer; 115 private @NonNull ViewGroup mAppPermissionRationaleContent; 116 private @NonNull FrameLayout mAllowButtonFrame; 117 private @NonNull RadioButton mAllowButton; 118 private @NonNull RadioButton mAllowAlwaysButton; 119 private @NonNull RadioButton mAllowForegroundButton; 120 private @NonNull RadioButton mAskOneTimeButton; 121 private @NonNull RadioButton mAskButton; 122 private @NonNull RadioButton mAllowLimitedButton; 123 private @NonNull RadioButton mDenyButton; 124 private @NonNull RadioButton mDenyForegroundButton; 125 private @NonNull ImageView mSelectPhotosButton; 126 private @NonNull View mAllowLimitedPhotosLayout; 127 private @NonNull View mSelectPhotosDivider; 128 private @NonNull View mLocationAccuracy; 129 private @NonNull Switch mLocationAccuracySwitch; 130 private @NonNull View mDivider; 131 private @NonNull ViewGroup mWidgetFrame; 132 private @NonNull TextView mPermissionDetails; 133 private @NonNull NestedScrollView mNestedScrollView; 134 private @NonNull String mPackageName; 135 private @NonNull String mPermGroupName; 136 private @NonNull UserHandle mUser; 137 private boolean mIsStorageGroup; 138 private boolean mIsInitialLoad; 139 // This prevents the user from clicking the photo picker button multiple times in succession 140 private boolean mPhotoPickerTriggered; 141 private long mSessionId; 142 private String mPersistentDeviceId; 143 144 private @NonNull String mPackageLabel; 145 private @NonNull String mPermGroupLabel; 146 private Drawable mPackageIcon; 147 private @NonNull RoleManager mRoleManager; 148 149 /** 150 * Create a bundle with the arguments needed by this fragment 151 * 152 * @param packageName The name of the package 153 * @param permName The name of the permission whose group this fragment is for (optional) 154 * @param groupName The name of the permission group (required if permName not specified) 155 * @param userHandle The user of the app permission group 156 * @param caller The name of the fragment we called from 157 * @param sessionId The current session ID 158 * @param grantCategory The grant status of this app permission group. Used to initially set 159 * the button state 160 * @return A bundle with all of the args placed 161 */ createArgs(@onNull String packageName, @Nullable String permName, @Nullable String groupName, @NonNull UserHandle userHandle, @Nullable String caller, long sessionId, @Nullable String grantCategory)162 public static Bundle createArgs(@NonNull String packageName, 163 @Nullable String permName, @Nullable String groupName, 164 @NonNull UserHandle userHandle, @Nullable String caller, long sessionId, @Nullable 165 String grantCategory) { 166 Bundle arguments = new Bundle(); 167 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 168 if (groupName == null) { 169 arguments.putString(Intent.EXTRA_PERMISSION_NAME, permName); 170 } else { 171 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName); 172 } 173 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 174 arguments.putString(EXTRA_CALLER_NAME, caller); 175 arguments.putLong(EXTRA_SESSION_ID, sessionId); 176 arguments.putString(GRANT_CATEGORY, grantCategory); 177 return arguments; 178 } 179 180 @Override onCreate(Bundle savedInstanceState)181 public void onCreate(Bundle savedInstanceState) { 182 super.onCreate(savedInstanceState); 183 184 setHasOptionsMenu(true); 185 ActionBar ab = getActivity().getActionBar(); 186 if (ab != null) { 187 ab.setDisplayHomeAsUpEnabled(true); 188 } 189 190 mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 191 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 192 if (mPermGroupName == null) { 193 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); 194 } 195 mIsStorageGroup = Objects.equals(mPermGroupName, STORAGE); 196 mUser = getArguments().getParcelable(Intent.EXTRA_USER); 197 mPackageLabel = BidiFormatter.getInstance().unicodeWrap( 198 KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), mPackageName, 199 mUser)); 200 mPermGroupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), 201 mPermGroupName).toString(); 202 mPackageIcon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(), 203 mPackageName, mUser); 204 mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 205 206 mPersistentDeviceId = getArguments().getString(PERSISTENT_DEVICE_ID, 207 MultiDeviceUtils.getDefaultDevicePersistentDeviceId()); 208 209 AppPermissionViewModelFactory factory = new AppPermissionViewModelFactory( 210 getActivity().getApplication(), mPackageName, mPermGroupName, mUser, mSessionId, 211 mPersistentDeviceId); 212 mViewModel = new ViewModelProvider(this, factory).get(AppPermissionViewModel.class); 213 Handler delayHandler = new Handler(Looper.getMainLooper()); 214 mViewModel.getButtonStateLiveData().observe(this, buttonState -> { 215 if (mIsInitialLoad) { 216 setRadioButtonsState(buttonState); 217 } else { 218 delayHandler.removeCallbacksAndMessages(null); 219 delayHandler.postDelayed(() -> setRadioButtonsState(buttonState), POST_DELAY_MS); 220 } 221 }); 222 mViewModel.getDetailResIdLiveData().observe(this, this::setDetail); 223 mViewModel.getShowAdminSupportLiveData().observe(this, this::setAdminSupportDetail); 224 if (mIsStorageGroup) { 225 mViewModel.getFullStorageStateLiveData().observe(this, this::setSpecialStorageState); 226 } 227 228 mRoleManager = Utils.getSystemServiceSafe(getContext(), RoleManager.class); 229 } 230 231 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)232 public View onCreateView(LayoutInflater inflater, ViewGroup container, 233 Bundle savedInstanceState) { 234 Context context = getContext(); 235 ViewGroup root = (ViewGroup) inflater.inflate(R.layout.app_permission, container, false); 236 237 mIsInitialLoad = true; 238 239 setHeader(mPackageIcon, mPackageLabel, null, null, false); 240 updateHeader(root.requireViewById(R.id.large_header)); 241 242 String text = null; 243 if (MultiDeviceUtils.isDefaultDeviceId(mPersistentDeviceId)) { 244 text = context.getString(R.string.app_permission_header, mPermGroupLabel); 245 } else { 246 final String deviceName = MultiDeviceUtils.getDeviceName(context, mPersistentDeviceId); 247 text = context.getString(R.string.app_permission_header_with_device_name, 248 mPermGroupLabel, deviceName); 249 } 250 ((TextView) root.requireViewById(R.id.permission_message)).setText(text); 251 252 String caller = getArguments().getString(EXTRA_CALLER_NAME); 253 254 TextView footer1Link = root.requireViewById(R.id.footer_link_1); 255 footer1Link.setText(context.getString(R.string.app_permission_footer_app_permissions_link, 256 mPackageLabel)); 257 setBottomLinkState(footer1Link, caller, Intent.ACTION_MANAGE_APP_PERMISSIONS); 258 259 TextView footer2Link = root.requireViewById(R.id.footer_link_2); 260 footer2Link.setText(context.getString(R.string.app_permission_footer_permission_apps_link)); 261 setBottomLinkState(footer2Link, caller, Intent.ACTION_MANAGE_PERMISSION_APPS); 262 263 Set<String> exemptedPackages = Utils.getExemptedPackages(mRoleManager); 264 ImageView footerInfoIcon = root.requireViewById(R.id.app_additional_info_icon); 265 TextView footerInfoText = root.requireViewById(R.id.app_additional_info_text); 266 if (exemptedPackages.contains(mPackageName)) { 267 int additional_info_label = Utils.isStatusBarIndicatorPermission(mPermGroupName) 268 ? R.string.exempt_mic_camera_info_label : R.string.exempt_info_label; 269 footerInfoText.setText(context.getString(additional_info_label, mPackageLabel)); 270 footerInfoIcon.setVisibility(View.VISIBLE); 271 footerInfoText.setVisibility(View.VISIBLE); 272 } else { 273 footerInfoIcon.setVisibility(View.GONE); 274 footerInfoText.setVisibility(View.GONE); 275 } 276 277 mAllowButtonFrame = root.requireViewById(R.id.allow_radio_button_frame); 278 mAllowButton = root.requireViewById(R.id.allow_radio_button); 279 mAllowAlwaysButton = root.requireViewById(R.id.allow_always_radio_button); 280 mAllowForegroundButton = root.requireViewById(R.id.allow_foreground_only_radio_button); 281 mAskOneTimeButton = root.requireViewById(R.id.ask_one_time_radio_button); 282 mAskButton = root.requireViewById(R.id.ask_radio_button); 283 mAllowLimitedButton = root.requireViewById(R.id.select_radio_button); 284 mDenyButton = root.requireViewById(R.id.deny_radio_button); 285 mDenyForegroundButton = root.requireViewById(R.id.deny_foreground_radio_button); 286 287 mDivider = root.requireViewById(R.id.two_target_divider); 288 mWidgetFrame = root.requireViewById(R.id.widget_frame); 289 mPermissionDetails = root.requireViewById(R.id.permission_details); 290 mLocationAccuracy = root.requireViewById(R.id.location_accuracy); 291 mLocationAccuracySwitch = root.requireViewById(R.id.location_accuracy_switch); 292 mAllowLimitedPhotosLayout = root.requireViewById(R.id.radio_select_layout); 293 mSelectPhotosButton = root.requireViewById(R.id.edit_selected_button); 294 mSelectPhotosDivider = root.requireViewById(R.id.edit_photos_divider); 295 mNestedScrollView = root.requireViewById(R.id.nested_scroll_view); 296 297 if (mViewModel.getButtonStateLiveData().getValue() != null) { 298 setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue()); 299 } else { 300 mAllowButton.setVisibility(View.GONE); 301 mAllowAlwaysButton.setVisibility(View.GONE); 302 mAllowForegroundButton.setVisibility(View.GONE); 303 mAskOneTimeButton.setVisibility(View.GONE); 304 mAskButton.setVisibility(View.GONE); 305 mDenyButton.setVisibility(View.GONE); 306 mDenyForegroundButton.setVisibility(View.GONE); 307 mLocationAccuracy.setVisibility(View.GONE); 308 mAllowLimitedPhotosLayout.setVisibility(View.GONE); 309 mSelectPhotosDivider.setAlpha(0f); 310 mSelectPhotosButton.setAlpha(0f); 311 } 312 313 if (mViewModel.getFullStorageStateLiveData().isInitialized() && mIsStorageGroup) { 314 setSpecialStorageState(mViewModel.getFullStorageStateLiveData().getValue(), root); 315 } else { 316 TextView storageFooter = root.requireViewById(R.id.footer_storage_special_app_access); 317 storageFooter.setVisibility(View.GONE); 318 } 319 mAppPermissionRationaleContainer = 320 root.requireViewById(R.id.app_permission_rationale_container); 321 mAppPermissionRationaleContent = 322 root.requireViewById(R.id.app_permission_rationale_content); 323 mViewModel.getShowPermissionRationaleLiveData().observe(this, show -> { 324 showPermissionRationaleDialog(Optional.ofNullable(show).orElse(false)); 325 }); 326 327 getActivity().setTitle( 328 getPreferenceManager().getContext().getString(R.string.app_permission_title, 329 mPermGroupLabel)); 330 return root; 331 } 332 onResume()333 public void onResume() { 334 super.onResume(); 335 // If we're returning to the fragment, photo picker hasn't been triggered 336 mPhotoPickerTriggered = false; 337 } 338 showPermissionRationaleDialog(boolean showPermissionRationale)339 private void showPermissionRationaleDialog(boolean showPermissionRationale) { 340 if (!showPermissionRationale) { 341 mAppPermissionRationaleContainer.setVisibility(View.GONE); 342 } else { 343 mAppPermissionRationaleContainer.setVisibility(View.VISIBLE); 344 mAppPermissionRationaleContent.setOnClickListener((v) -> { 345 if (!SdkLevel.isAtLeastU()) { 346 return; 347 } 348 mViewModel.showPermissionRationaleActivity(getActivity(), mPermGroupName); 349 }); 350 } 351 } 352 setBottomLinkState(TextView view, String caller, String action)353 private void setBottomLinkState(TextView view, String caller, String action) { 354 if ((Objects.equals(caller, AppPermissionGroupsFragment.class.getName()) 355 && action.equals(Intent.ACTION_MANAGE_APP_PERMISSIONS)) 356 || (Objects.equals(caller, PermissionAppsFragment.class.getName()) 357 && action.equals(Intent.ACTION_MANAGE_PERMISSION_APPS))) { 358 view.setVisibility(View.GONE); 359 } else { 360 view.setOnClickListener((v) -> { 361 Bundle args; 362 if (action.equals(Intent.ACTION_MANAGE_APP_PERMISSIONS)) { 363 args = AppPermissionGroupsFragment.createArgs(mPackageName, mUser, 364 mSessionId, true); 365 } else { 366 args = PermissionAppsFragment.createArgs(mPermGroupName, mSessionId); 367 } 368 mViewModel.showBottomLinkPage(this, action, args); 369 }); 370 } 371 } 372 setSpecialStorageState(FullStoragePackageState storageState)373 private void setSpecialStorageState(FullStoragePackageState storageState) { 374 setSpecialStorageState(storageState, getView()); 375 } 376 377 @Override onStart()378 public void onStart() { 379 super.onStart(); 380 381 ActionBar ab = getActivity().getActionBar(); 382 if (ab != null) { 383 ab.setElevation(0); 384 } 385 386 ActionBarShadowController.attachToView(getActivity(), getLifecycle(), mNestedScrollView); 387 } 388 389 @Override onOptionsItemSelected(MenuItem item)390 public boolean onOptionsItemSelected(MenuItem item) { 391 if (item.getItemId() == android.R.id.home) { 392 pressBack(this); 393 return true; 394 } 395 return super.onOptionsItemSelected(item); 396 } 397 setRadioButtonsState(Map<ButtonType, ButtonState> states)398 private void setRadioButtonsState(Map<ButtonType, ButtonState> states) { 399 if (states == null && !mViewModel.getButtonStateLiveData().isStale()) { 400 pressBack(this); 401 Log.w(LOG_TAG, "invalid package " + mPackageName + " or perm group " 402 + mPermGroupName); 403 Toast.makeText( 404 getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 405 return; 406 } else if (states == null) { 407 return; 408 } 409 mAllowButtonFrame.setOnClickListener((v) -> allowButtonFrameClickListener()); 410 mAllowAlwaysButton.setOnClickListener((v) -> { 411 if (mIsStorageGroup) { 412 showConfirmDialog(ChangeRequest.GRANT_ALL_FILE_ACCESS, 413 R.string.special_file_access_dialog, -1, false); 414 } else { 415 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH, 416 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS); 417 } 418 setResult(GRANTED_ALWAYS); 419 }); 420 mAllowForegroundButton.setOnClickListener((v) -> { 421 if (mIsStorageGroup) { 422 mViewModel.setAllFilesAccess(false); 423 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH, 424 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW); 425 setResult(GRANTED_ALWAYS); 426 } else { 427 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FOREGROUND_ONLY, 428 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND); 429 setResult(GRANTED_FOREGROUND_ONLY); 430 } 431 }); 432 // mAskOneTimeButton only shows if checked hence should do nothing 433 mAskButton.setOnClickListener((v) -> { 434 mViewModel.requestChange(true, this, this, ChangeRequest.REVOKE_BOTH, 435 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ASK_EVERY_TIME); 436 setResult(DENIED); 437 }); 438 mAllowLimitedButton.setOnClickListener((v) -> { 439 int buttonPressed = 440 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PHOTOS_SELECTED; 441 mViewModel.requestChange(false, this, this, ChangeRequest.PHOTOS_SELECTED, 442 buttonPressed); 443 }); 444 mSelectPhotosButton.setOnClickListener((v) -> { 445 ButtonState selectState = states.get(ButtonType.SELECT_PHOTOS); 446 if (selectState != null && selectState.isChecked() && !mPhotoPickerTriggered) { 447 mPhotoPickerTriggered = true; 448 mViewModel.openPhotoPicker(this); 449 } 450 }); 451 mDenyButton.setOnClickListener((v) -> { 452 if (mViewModel.getFullStorageStateLiveData().getValue() != null 453 && !mViewModel.getFullStorageStateLiveData().getValue().isLegacy()) { 454 mViewModel.setAllFilesAccess(false); 455 } 456 mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_BOTH, 457 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY); 458 setResult(DENIED_DO_NOT_ASK_AGAIN); 459 }); 460 mDenyForegroundButton.setOnClickListener((v) -> { 461 mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_FOREGROUND, 462 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY_FOREGROUND); 463 setResult(DENIED_DO_NOT_ASK_AGAIN); 464 }); 465 // Set long variable names to new variables to bypass linter errors. 466 int grantFineLocation = 467 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__GRANT_FINE_LOCATION; 468 int revokeFineLocation = 469 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__REVOKE_FINE_LOCATION; 470 mLocationAccuracy.setOnClickListener((v) -> { 471 mLocationAccuracySwitch.performClick(); 472 if (mLocationAccuracySwitch.isChecked()) { 473 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FINE_LOCATION, 474 grantFineLocation); 475 } else { 476 mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_FINE_LOCATION, 477 revokeFineLocation); 478 } 479 }); 480 481 setButtonState(mAllowButton, states.get(ButtonType.ALLOW)); 482 setButtonState(mAllowAlwaysButton, states.get(ButtonType.ALLOW_ALWAYS)); 483 setButtonState(mAllowForegroundButton, states.get(ButtonType.ALLOW_FOREGROUND)); 484 setButtonState(mAskOneTimeButton, states.get(ButtonType.ASK_ONCE)); 485 setButtonState(mAskButton, states.get(ButtonType.ASK)); 486 setButtonState(mDenyButton, states.get(ButtonType.DENY)); 487 setButtonState(mDenyForegroundButton, states.get(ButtonType.DENY_FOREGROUND)); 488 setButtonState(mAllowLimitedButton, states.get(ButtonType.SELECT_PHOTOS)); 489 if (mAllowLimitedButton.getVisibility() == View.VISIBLE) { 490 mAllowButton.setText(R.string.app_permission_button_always_allow_all); 491 } else { 492 mAllowButton.setText(R.string.app_permission_button_allow); 493 } 494 495 ButtonState locationAccuracyState = states.get(ButtonType.LOCATION_ACCURACY); 496 if (!locationAccuracyState.isShown()) { 497 mLocationAccuracy.setVisibility(View.GONE); 498 } else { 499 mLocationAccuracy.setVisibility(View.VISIBLE); 500 } 501 mLocationAccuracySwitch.setChecked(locationAccuracyState.isChecked()); 502 if (!locationAccuracyState.isEnabled()) { 503 mLocationAccuracy.setEnabled(false); 504 mLocationAccuracySwitch.setEnabled(false); 505 } 506 507 mIsInitialLoad = false; 508 509 if (mViewModel.getFullStorageStateLiveData().isInitialized()) { 510 setSpecialStorageState(mViewModel.getFullStorageStateLiveData().getValue()); 511 } 512 } 513 allowButtonFrameClickListener()514 private void allowButtonFrameClickListener() { 515 if (!mAllowButton.isEnabled()) { 516 mViewModel.handleDisabledAllowButton(this); 517 } else { 518 mAllowButton.setChecked(true); 519 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FOREGROUND, 520 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW); 521 setResult(GRANTED_ALWAYS); 522 } 523 } 524 setButtonState(CompoundButton button, AppPermissionViewModel.ButtonState state)525 private void setButtonState(CompoundButton button, AppPermissionViewModel.ButtonState state) { 526 int visible = state.isShown() ? View.VISIBLE : View.GONE; 527 button.setVisibility(visible); 528 if (state.isShown()) { 529 button.setChecked(state.isChecked()); 530 button.setEnabled(state.isEnabled()); 531 } 532 if (mIsInitialLoad) { 533 button.jumpDrawablesToCurrentState(); 534 } 535 536 if (button == mAllowLimitedButton) { 537 mAllowLimitedPhotosLayout.setVisibility(visible); 538 float endOpacity = state.isChecked() ? 1f : 0f; 539 // On initial load, do not show the fade in/out animation 540 if (mIsInitialLoad) { 541 mSelectPhotosDivider.setAlpha(endOpacity); 542 mSelectPhotosButton.setAlpha(endOpacity); 543 return; 544 } 545 mSelectPhotosButton.animate().alpha(endOpacity) 546 .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS); 547 mSelectPhotosDivider.animate().alpha(endOpacity) 548 .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS); 549 } 550 } 551 setSpecialStorageState(FullStoragePackageState storageState, View v)552 private void setSpecialStorageState(FullStoragePackageState storageState, View v) { 553 if (v == null) { 554 return; 555 } 556 557 TextView textView = v.requireViewById(R.id.footer_storage_special_app_access); 558 if (mAllowButton == null || !mIsStorageGroup) { 559 textView.setVisibility(View.GONE); 560 return; 561 } 562 563 mAllowAlwaysButton.setText(R.string.app_permission_button_allow_all_files); 564 mAllowForegroundButton.setText(R.string.app_permission_button_allow_media_only); 565 566 if (storageState == null) { 567 textView.setVisibility(View.GONE); 568 return; 569 } 570 571 if (storageState.isLegacy()) { 572 mAllowButton.setText(R.string.app_permission_button_allow_all_files); 573 textView.setVisibility(View.GONE); 574 return; 575 } 576 577 textView.setText(R.string.app_permission_footer_special_file_access); 578 textView.setVisibility(View.VISIBLE); 579 } 580 setResult(@rantPermissionsViewHandler.Result int result)581 private void setResult(@GrantPermissionsViewHandler.Result int result) { 582 if (!mPackageName.equals( 583 getActivity().getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME))) { 584 return; 585 } 586 Intent intent = new Intent() 587 .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED, mPermGroupName) 588 .putExtra(EXTRA_RESULT_PERMISSION_RESULT, result); 589 getActivity().setResult(RESULT_OK, intent); 590 } 591 setDetail(Pair<Integer, Integer> detailResIds)592 private void setDetail(Pair<Integer, Integer> detailResIds) { 593 if (detailResIds == null) { 594 mWidgetFrame.setVisibility(View.GONE); 595 mDivider.setVisibility(View.GONE); 596 return; 597 } 598 mWidgetFrame.setVisibility(View.VISIBLE); 599 if (detailResIds.getSecond() != null) { 600 // If the permissions are individually controlled, also show a link to the page that 601 // lets you control them. 602 mDivider.setVisibility(View.VISIBLE); 603 showRightIcon(R.drawable.ic_settings); 604 Bundle args = AllAppPermissionsFragment.createArgs(mPackageName, mPermGroupName, mUser); 605 mWidgetFrame.setOnClickListener(v -> mViewModel.showAllPermissions(this, args)); 606 mPermissionDetails.setText(getPreferenceManager().getContext().getString( 607 detailResIds.getFirst(), detailResIds.getSecond())); 608 } else { 609 mPermissionDetails.setText(getPreferenceManager().getContext().getString( 610 detailResIds.getFirst())); 611 } 612 mPermissionDetails.setVisibility(View.VISIBLE); 613 614 } 615 setAdminSupportDetail(EnforcedAdmin admin)616 private void setAdminSupportDetail(EnforcedAdmin admin) { 617 if (admin != null) { 618 showRightIcon(R.drawable.ic_info); 619 mWidgetFrame.setOnClickListener(v -> 620 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin) 621 ); 622 } else { 623 mWidgetFrame.removeAllViews(); 624 } 625 } 626 627 /** 628 * Show the given icon on the right of the first radio button. 629 * 630 * @param iconId the resourceId of the drawable to use. 631 */ showRightIcon(int iconId)632 private void showRightIcon(int iconId) { 633 mWidgetFrame.removeAllViews(); 634 ImageView imageView = new ImageView(getPreferenceManager().getContext()); 635 imageView.setImageResource(iconId); 636 mWidgetFrame.addView(imageView); 637 mWidgetFrame.setVisibility(View.VISIBLE); 638 } 639 640 /** 641 * Show a dialog that warns the users that they are about to revoke permissions that were 642 * granted by default, or that they are about to grant full file access to an app. 643 * 644 * 645 * The order of operation to revoke a permission granted by default is: 646 * 1. `showConfirmDialog` 647 * 1. [ConfirmDialog.onCreateDialog] 648 * 1. [AppPermissionViewModel.onDenyAnyWay] or [AppPermissionViewModel.onConfirmFileAccess] 649 * TODO: Remove once data can be passed between dialogs and fragments with nav component 650 * 651 * @param changeRequest Whether background or foreground should be changed 652 * @param messageId The Id of the string message to show 653 * @param buttonPressed Button which was pressed to initiate the dialog, one of 654 * AppPermissionFragmentActionReported.button_pressed constants 655 * @param oneTime Whether the one-time (ask) button was clicked rather than the deny 656 * button 657 */ 658 @Override showConfirmDialog(ChangeRequest changeRequest, @StringRes int messageId, int buttonPressed, boolean oneTime)659 public void showConfirmDialog(ChangeRequest changeRequest, @StringRes int messageId, 660 int buttonPressed, boolean oneTime) { 661 Bundle args = getArguments().deepCopy(); 662 args.putInt(ConfirmDialog.MSG, messageId); 663 args.putSerializable(ConfirmDialog.CHANGE_REQUEST, changeRequest); 664 args.putInt(ConfirmDialog.BUTTON, buttonPressed); 665 args.putBoolean(ConfirmDialog.ONE_TIME, oneTime); 666 ConfirmDialog defaultDenyDialog = new ConfirmDialog(); 667 defaultDenyDialog.setCancelable(true); 668 defaultDenyDialog.setArguments(args); 669 defaultDenyDialog.show(getChildFragmentManager().beginTransaction(), 670 ConfirmDialog.class.getName()); 671 } 672 673 /** 674 * A dialog warning the user that they are about to deny a permission that was granted by 675 * default, or that they are denying a permission on a Pre-M app 676 * 677 * @see AppPermissionViewModel.ConfirmDialogShowingFragment#showConfirmDialog(ChangeRequest, 678 * int, int, boolean) 679 * @see #showConfirmDialog(ChangeRequest, int, int) 680 */ 681 public static class ConfirmDialog extends DialogFragment { 682 static final String MSG = ConfirmDialog.class.getName() + ".arg.msg"; 683 static final String CHANGE_REQUEST = ConfirmDialog.class.getName() 684 + ".arg.changeRequest"; 685 private static final String KEY = ConfirmDialog.class.getName() + ".arg.key"; 686 private static final String BUTTON = ConfirmDialog.class.getName() + ".arg.button"; 687 private static final String ONE_TIME = ConfirmDialog.class.getName() + ".arg.onetime"; 688 private static int sCode = APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW; 689 @Override onCreateDialog(Bundle savedInstanceState)690 public Dialog onCreateDialog(Bundle savedInstanceState) { 691 // TODO(b/229024576): This code is duplicated, refactor ConfirmDialog for easier 692 // NFF sharing 693 AppPermissionFragment fragment = (AppPermissionFragment) getParentFragment(); 694 boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST) 695 == ChangeRequest.GRANT_ALL_FILE_ACCESS; 696 int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway; 697 if (isGrantFileAccess) { 698 positiveButtonStringResId = R.string.grant_dialog_button_allow; 699 } 700 AlertDialog.Builder b = new AlertDialog.Builder(getContext()) 701 .setMessage(getArguments().getInt(MSG)) 702 .setNegativeButton(R.string.cancel, 703 (DialogInterface dialog, int which) -> dialog.cancel()) 704 .setPositiveButton(positiveButtonStringResId, 705 (DialogInterface dialog, int which) -> { 706 if (isGrantFileAccess) { 707 fragment.mViewModel.setAllFilesAccess(true); 708 fragment.mViewModel.requestChange(false, fragment, 709 fragment, ChangeRequest.GRANT_BOTH, sCode); 710 } else { 711 fragment.mViewModel.onDenyAnyWay((ChangeRequest) 712 getArguments().getSerializable(CHANGE_REQUEST), 713 getArguments().getInt(BUTTON), 714 getArguments().getBoolean(ONE_TIME)); 715 } 716 }); 717 Dialog d = b.create(); 718 d.setCanceledOnTouchOutside(true); 719 return d; 720 } 721 722 @Override onCancel(DialogInterface dialog)723 public void onCancel(DialogInterface dialog) { 724 AppPermissionFragment fragment = (AppPermissionFragment) getParentFragment(); 725 fragment.setRadioButtonsState(fragment.mViewModel.getButtonStateLiveData().getValue()); 726 } 727 } 728 729 @Override showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args)730 public void showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args) { 731 AlertDialog.Builder b = new AlertDialog.Builder(getContext()) 732 .setIcon(args.getIconId()) 733 .setMessage(args.getMessageId()) 734 .setOnCancelListener((DialogInterface dialog) -> { 735 setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue()); 736 }) 737 .setNegativeButton(args.getNegativeButtonTextId(), 738 (DialogInterface dialog, int which) -> { 739 setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue()); 740 }) 741 .setPositiveButton(args.getPositiveButtonTextId(), 742 (DialogInterface dialog, int which) -> { 743 mViewModel.requestChange(args.getSetOneTime(), 744 AppPermissionFragment.this, AppPermissionFragment.this, 745 args.getChangeRequest(), args.getButtonClicked()); 746 }); 747 if (args.getTitleId() != 0) { 748 b.setTitle(args.getTitleId()); 749 } 750 b.show(); 751 } 752 } 753