1 /* 2 * Copyright (C) 2019 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.auto; 18 19 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 20 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 21 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW; 22 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS; 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY; 25 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED; 26 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT; 27 28 import android.app.Activity; 29 import android.app.AlertDialog; 30 import android.app.Dialog; 31 import android.content.Context; 32 import android.content.DialogInterface; 33 import android.content.Intent; 34 import android.content.pm.PackageManager; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.os.UserHandle; 38 import android.text.BidiFormatter; 39 import android.util.Log; 40 import android.view.View; 41 import android.widget.RadioButton; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.core.content.res.TypedArrayUtils; 46 import androidx.fragment.app.DialogFragment; 47 import androidx.fragment.app.Fragment; 48 import androidx.lifecycle.ViewModelProvider; 49 import androidx.preference.PreferenceCategory; 50 import androidx.preference.PreferenceGroup; 51 import androidx.preference.PreferenceScreen; 52 import androidx.preference.PreferenceViewHolder; 53 import androidx.preference.TwoStatePreference; 54 55 import com.android.car.ui.AlertDialogBuilder; 56 import com.android.permissioncontroller.R; 57 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment; 58 import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler; 59 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel; 60 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ChangeRequest; 61 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModelFactory; 62 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs; 63 import com.android.permissioncontroller.permission.utils.KotlinUtils; 64 import com.android.permissioncontroller.permission.utils.PackageRemovalMonitor; 65 import com.android.settingslib.RestrictedLockUtils; 66 67 import java.util.Map; 68 69 import kotlin.Pair; 70 71 /** Settings related to a particular permission for the given app. */ 72 public class AutoAppPermissionFragment extends AutoSettingsFrameFragment 73 implements AppPermissionViewModel.ConfirmDialogShowingFragment { 74 75 private static final String LOG_TAG = "AppPermissionFragment"; 76 private static final long POST_DELAY_MS = 20; 77 78 @NonNull 79 private TwoStatePreference mAllowPermissionPreference; 80 @NonNull 81 private TwoStatePreference mAlwaysPermissionPreference; 82 @NonNull 83 private TwoStatePreference mForegroundOnlyPermissionPreference; 84 @NonNull 85 private TwoStatePreference mDenyPermissionPreference; 86 @NonNull 87 private AutoTwoTargetPreference mDetailsPreference; 88 89 @NonNull 90 private AppPermissionViewModel mViewModel; 91 @NonNull 92 private String mPackageName; 93 @NonNull 94 private String mPermGroupName; 95 @NonNull 96 private UserHandle mUser; 97 @NonNull 98 private String mPackageLabel; 99 @NonNull 100 private String mPermGroupLabel; 101 private Drawable mPackageIcon; 102 103 /** 104 * Listens for changes to the app the permission is currently getting granted to. {@code null} 105 * when unregistered. 106 */ 107 @Nullable 108 private PackageRemovalMonitor mPackageRemovalMonitor; 109 110 /** 111 * Returns a new {@link AutoAppPermissionFragment}. 112 * 113 * @param packageName the package name for which the permission is being changed 114 * @param permName the name of the permission being changed 115 * @param groupName the name of the permission group being changed 116 * @param userHandle the user for which the permission is being changed 117 */ 118 @NonNull newInstance(@onNull String packageName, @NonNull String permName, @Nullable String groupName, @NonNull UserHandle userHandle, @NonNull long sessionId)119 public static AutoAppPermissionFragment newInstance(@NonNull String packageName, 120 @NonNull String permName, @Nullable String groupName, @NonNull UserHandle userHandle, 121 @NonNull long sessionId) { 122 AutoAppPermissionFragment fragment = new AutoAppPermissionFragment(); 123 Bundle arguments = new Bundle(); 124 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 125 if (groupName == null) { 126 arguments.putString(Intent.EXTRA_PERMISSION_NAME, permName); 127 } else { 128 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName); 129 } 130 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 131 arguments.putLong(EXTRA_SESSION_ID, sessionId); 132 fragment.setArguments(arguments); 133 return fragment; 134 } 135 136 @Override onCreatePreferences(Bundle bundle, String s)137 public void onCreatePreferences(Bundle bundle, String s) { 138 setPreferenceScreen(getPreferenceManager().createPreferenceScreen(requireContext())); 139 } 140 141 @Override onCreate(@ullable Bundle savedInstanceState)142 public void onCreate(@Nullable Bundle savedInstanceState) { 143 super.onCreate(savedInstanceState); 144 mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 145 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 146 if (mPermGroupName == null) { 147 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); 148 } 149 mUser = getArguments().getParcelable(Intent.EXTRA_USER); 150 mPackageLabel = BidiFormatter.getInstance().unicodeWrap( 151 KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), mPackageName, 152 mUser)); 153 mPermGroupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), 154 mPermGroupName).toString(); 155 mPackageIcon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(), 156 mPackageName, mUser); 157 setHeaderLabel( 158 requireContext().getString(R.string.app_permission_title, mPermGroupLabel)); 159 } 160 161 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)162 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 163 super.onViewCreated(view, savedInstanceState); 164 165 PreferenceScreen screen = getPreferenceScreen(); 166 screen.addPreference( 167 AutoPermissionsUtils.createHeaderPreference(requireContext(), 168 mPackageIcon, mPackageName, mPackageLabel)); 169 170 // Add permissions selector preferences. 171 PreferenceGroup permissionSelector = new PreferenceCategory(requireContext()); 172 permissionSelector.setTitle( 173 getString(R.string.app_permission_header, mPermGroupLabel)); 174 screen.addPreference(permissionSelector); 175 176 mAllowPermissionPreference = new SelectedPermissionPreference(requireContext()); 177 mAllowPermissionPreference.setTitle(R.string.app_permission_button_allow); 178 permissionSelector.addPreference(mAllowPermissionPreference); 179 180 mAlwaysPermissionPreference = new SelectedPermissionPreference(requireContext()); 181 mAlwaysPermissionPreference.setTitle(R.string.app_permission_button_allow_always); 182 permissionSelector.addPreference(mAlwaysPermissionPreference); 183 184 mForegroundOnlyPermissionPreference = new SelectedPermissionPreference(requireContext()); 185 mForegroundOnlyPermissionPreference.setTitle( 186 R.string.app_permission_button_allow_foreground); 187 permissionSelector.addPreference(mForegroundOnlyPermissionPreference); 188 189 mDenyPermissionPreference = new SelectedPermissionPreference(requireContext()); 190 mDenyPermissionPreference.setTitle(R.string.app_permission_button_deny); 191 permissionSelector.addPreference(mDenyPermissionPreference); 192 193 mAllowPermissionPreference.setOnPreferenceClickListener(v -> { 194 checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW); 195 setResult(GrantPermissionsViewHandler.GRANTED_ALWAYS); 196 requestChange(ChangeRequest.GRANT_FOREGROUND, 197 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW); 198 return true; 199 }); 200 mAlwaysPermissionPreference.setOnPreferenceClickListener(v -> { 201 checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW_ALWAYS); 202 setResult(GrantPermissionsViewHandler.GRANTED_ALWAYS); 203 requestChange(ChangeRequest.GRANT_BOTH, 204 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS); 205 return true; 206 }); 207 mForegroundOnlyPermissionPreference.setOnPreferenceClickListener(v -> { 208 checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND); 209 setResult(GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY); 210 requestChange(ChangeRequest.GRANT_FOREGROUND_ONLY, 211 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND); 212 return true; 213 }); 214 mDenyPermissionPreference.setOnPreferenceClickListener(v -> { 215 checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.DENY); 216 setResult(GrantPermissionsViewHandler.DENIED); 217 requestChange(ChangeRequest.REVOKE_BOTH, 218 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY); 219 return true; 220 }); 221 222 mDetailsPreference = new AutoTwoTargetPreference(requireContext()); 223 screen.addPreference(mDetailsPreference); 224 } 225 226 @Override onStart()227 public void onStart() { 228 super.onStart(); 229 Activity activity = requireActivity(); 230 231 // Get notified when the package is removed. 232 mPackageRemovalMonitor = new PackageRemovalMonitor(requireContext(), mPackageName) { 233 @Override 234 public void onPackageRemoved() { 235 Log.w(LOG_TAG, mPackageName + " was uninstalled"); 236 activity.setResult(Activity.RESULT_CANCELED); 237 activity.finish(); 238 } 239 }; 240 mPackageRemovalMonitor.register(); 241 242 // Check if the package was removed while this activity was not started. 243 try { 244 activity.createPackageContextAsUser(mPackageName, /* flags= */ 0, 245 mUser).getPackageManager().getPackageInfo(mPackageName, 246 /* flags= */ 0); 247 } catch (PackageManager.NameNotFoundException e) { 248 Log.w(LOG_TAG, mPackageName + " was uninstalled while this activity was stopped", e); 249 activity.setResult(Activity.RESULT_CANCELED); 250 activity.finish(); 251 } 252 253 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 254 AppPermissionViewModelFactory factory = new AppPermissionViewModelFactory( 255 getActivity().getApplication(), mPackageName, mPermGroupName, mUser, sessionId); 256 mViewModel = new ViewModelProvider(this, factory).get(AppPermissionViewModel.class); 257 mViewModel.getButtonStateLiveData().observe(this, this::setRadioButtonsState); 258 mViewModel.getDetailResIdLiveData().observe(this, this::setDetail); 259 mViewModel.getShowAdminSupportLiveData().observe(this, this::setAdminSupportDetail); 260 } 261 262 @Override onStop()263 public void onStop() { 264 super.onStop(); 265 266 if (mPackageRemovalMonitor != null) { 267 mPackageRemovalMonitor.unregister(); 268 mPackageRemovalMonitor = null; 269 } 270 } 271 272 @Override showConfirmDialog(ChangeRequest changeRequest, int messageId, int buttonPressed, boolean oneTime)273 public void showConfirmDialog(ChangeRequest changeRequest, int messageId, 274 int buttonPressed, boolean oneTime) { 275 Bundle args = new Bundle(); 276 277 args.putInt(ConfirmDialog.MSG, messageId); 278 args.putSerializable(ConfirmDialog.CHANGE_REQUEST, changeRequest); 279 args.putSerializable(ConfirmDialog.BUTTON, buttonPressed); 280 281 ConfirmDialog confirmDialog = new ConfirmDialog(); 282 confirmDialog.setArguments(args); 283 confirmDialog.setTargetFragment(this, 0); 284 confirmDialog.show(requireFragmentManager().beginTransaction(), 285 ConfirmDialog.class.getName()); 286 } 287 setResult(@rantPermissionsViewHandler.Result int result)288 private void setResult(@GrantPermissionsViewHandler.Result int result) { 289 Intent intent = new Intent() 290 .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED, 291 requireArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME)) 292 .putExtra(EXTRA_RESULT_PERMISSION_RESULT, result); 293 requireActivity().setResult(Activity.RESULT_OK, intent); 294 } 295 setRadioButtonsState( Map<AppPermissionViewModel.ButtonType, AppPermissionViewModel.ButtonState> states)296 private void setRadioButtonsState( 297 Map<AppPermissionViewModel.ButtonType, AppPermissionViewModel.ButtonState> states) { 298 setButtonState(mAllowPermissionPreference, 299 states.get(AppPermissionViewModel.ButtonType.ALLOW)); 300 setButtonState(mAlwaysPermissionPreference, 301 states.get(AppPermissionViewModel.ButtonType.ALLOW_ALWAYS)); 302 setButtonState(mForegroundOnlyPermissionPreference, 303 states.get(AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND)); 304 setButtonState(mDenyPermissionPreference, 305 states.get(AppPermissionViewModel.ButtonType.DENY)); 306 } 307 setButtonState(TwoStatePreference button, AppPermissionViewModel.ButtonState state)308 private void setButtonState(TwoStatePreference button, 309 AppPermissionViewModel.ButtonState state) { 310 button.setVisible(state.isShown()); 311 if (state.isShown()) { 312 button.setChecked(state.isChecked()); 313 button.setEnabled(state.isEnabled()); 314 } 315 } 316 317 /** 318 * Helper method to handle the UX edge case where the confirmation dialog is shown and two 319 * buttons are selected at once. This happens since the Auto UI doesn't use a proper radio 320 * group, so there is nothing that enforces that tapping on a button unchecks a previously 321 * checked button. Apart from this case, this UI is not necessary since the UI is entirely 322 * driven by the ViewModel. 323 */ checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType buttonType)324 private void checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType buttonType) { 325 mAllowPermissionPreference.setChecked( 326 buttonType == AppPermissionViewModel.ButtonType.ALLOW); 327 mAlwaysPermissionPreference.setChecked( 328 buttonType == AppPermissionViewModel.ButtonType.ALLOW_ALWAYS); 329 mForegroundOnlyPermissionPreference.setChecked( 330 buttonType == AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND); 331 mDenyPermissionPreference.setChecked(buttonType == AppPermissionViewModel.ButtonType.DENY); 332 } 333 setDetail(Pair<Integer, Integer> detailResIds)334 private void setDetail(Pair<Integer, Integer> detailResIds) { 335 if (detailResIds == null) { 336 mDetailsPreference.setVisible(false); 337 return; 338 } 339 if (detailResIds.getSecond() != null) { 340 mDetailsPreference.setWidgetLayoutResource(R.layout.settings_preference_widget); 341 mDetailsPreference.setOnSecondTargetClickListener( 342 v -> showAllPermissions(mPermGroupName)); 343 mDetailsPreference.setSummary( 344 getString(detailResIds.getFirst(), detailResIds.getSecond())); 345 } else { 346 mDetailsPreference.setSummary(detailResIds.getFirst()); 347 } 348 } 349 350 /** 351 * Show all individual permissions in this group in a new fragment. 352 */ showAllPermissions(@onNull String filterGroup)353 private void showAllPermissions(@NonNull String filterGroup) { 354 Fragment frag = AutoAllAppPermissionsFragment.newInstance(mPackageName, 355 filterGroup, mUser, 356 getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)); 357 requireFragmentManager().beginTransaction() 358 .replace(android.R.id.content, frag) 359 .addToBackStack("AllPerms") 360 .commit(); 361 } 362 setAdminSupportDetail(RestrictedLockUtils.EnforcedAdmin admin)363 private void setAdminSupportDetail(RestrictedLockUtils.EnforcedAdmin admin) { 364 if (admin != null) { 365 mDetailsPreference.setWidgetLayoutResource(R.layout.info_preference_widget); 366 mDetailsPreference.setOnSecondTargetClickListener(v -> 367 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin) 368 ); 369 } 370 } 371 372 /** 373 * Request to grant/revoke permissions group. 374 */ requestChange(ChangeRequest changeRequest, int buttonClicked)375 private void requestChange(ChangeRequest changeRequest, 376 int buttonClicked) { 377 mViewModel.requestChange(/* setOneTime= */false, /* fragment= */ this, 378 /* defaultDeny= */this, changeRequest, buttonClicked); 379 } 380 381 /** Preference used to represent apps that can be picked as a default app. */ 382 private static class SelectedPermissionPreference extends TwoStatePreference { 383 SelectedPermissionPreference(Context context)384 SelectedPermissionPreference(Context context) { 385 super(context, null, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle, 386 android.R.attr.preferenceStyle)); 387 setPersistent(false); 388 setLayoutResource(R.layout.car_radio_button_preference); 389 setWidgetLayoutResource(R.layout.radio_button_preference_widget); 390 } 391 392 @Override onBindViewHolder(PreferenceViewHolder holder)393 public void onBindViewHolder(PreferenceViewHolder holder) { 394 super.onBindViewHolder(holder); 395 396 RadioButton radioButton = (RadioButton) holder.findViewById(R.id.radio_button); 397 radioButton.setChecked(isChecked()); 398 } 399 } 400 401 /** 402 * A dialog warning the user that they are about to deny a permission that was granted by 403 * default. 404 * 405 * @see #showConfirmDialog(ChangeRequest, int, int, boolean) 406 */ 407 public static class ConfirmDialog extends DialogFragment { 408 private static final String MSG = ConfirmDialog.class.getName() + ".arg.msg"; 409 private static final String CHANGE_REQUEST = ConfirmDialog.class.getName() 410 + ".arg.changeRequest"; 411 private static final String BUTTON = ConfirmDialog.class.getName() 412 + ".arg.button"; 413 private static int sCode = APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW; 414 415 @NonNull 416 @Override onCreateDialog(Bundle savedInstanceState)417 public Dialog onCreateDialog(Bundle savedInstanceState) { 418 // TODO(b/229024576): This code is duplicated, refactor ConfirmDialog for easier 419 // NFF sharing 420 boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST) 421 == ChangeRequest.GRANT_All_FILE_ACCESS; 422 boolean isGrantStorageSupergroup = getArguments().getSerializable(CHANGE_REQUEST) 423 == ChangeRequest.GRANT_STORAGE_SUPERGROUP; 424 int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway; 425 if (isGrantFileAccess || isGrantStorageSupergroup) { 426 positiveButtonStringResId = R.string.grant_dialog_button_allow; 427 } 428 AutoAppPermissionFragment fragment = (AutoAppPermissionFragment) getTargetFragment(); 429 return new AlertDialogBuilder(getContext()) 430 .setMessage(requireArguments().getInt(MSG)) 431 .setNegativeButton(R.string.cancel, 432 (dialog, which) -> dialog.cancel()) 433 .setPositiveButton(positiveButtonStringResId, 434 (dialog, which) -> { 435 if (isGrantFileAccess) { 436 fragment.mViewModel.setAllFilesAccess(true); 437 fragment.mViewModel.requestChange(false, fragment, 438 fragment, ChangeRequest.GRANT_BOTH, sCode); 439 } else if (isGrantStorageSupergroup) { 440 fragment.mViewModel.requestChange(false, fragment, 441 fragment, ChangeRequest.GRANT_BOTH, sCode); 442 } else { 443 fragment.mViewModel.onDenyAnyWay((ChangeRequest) 444 getArguments().getSerializable(CHANGE_REQUEST), 445 getArguments().getInt(BUTTON), 446 /* oneTime= */ false); 447 } 448 }) 449 .create(); 450 } 451 452 @Override onCancel(DialogInterface dialog)453 public void onCancel(DialogInterface dialog) { 454 AutoAppPermissionFragment fragment = (AutoAppPermissionFragment) getTargetFragment(); 455 fragment.setRadioButtonsState(fragment.mViewModel.getButtonStateLiveData().getValue()); 456 } 457 } 458 459 @Override 460 public void showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args) { 461 AlertDialog.Builder b = new AlertDialog.Builder(getContext()) 462 .setIcon(args.getIconId()) 463 .setMessage(args.getMessageId()) 464 .setOnCancelListener((DialogInterface dialog) -> { 465 setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue()); 466 }) 467 .setNegativeButton(args.getNegativeButtonTextId(), 468 (DialogInterface dialog, int which) -> { 469 setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue()); 470 }) 471 .setPositiveButton(args.getPositiveButtonTextId(), 472 (DialogInterface dialog, int which) -> { 473 mViewModel.requestChange(args.getSetOneTime(), 474 AutoAppPermissionFragment.this, AutoAppPermissionFragment.this, 475 args.getChangeRequest(), args.getButtonClicked()); 476 }); 477 if (args.getTitleId() != 0) { 478 b.setTitle(args.getTitleId()); 479 } 480 b.show(); 481 } 482 } 483