1 /* 2 * Copyright (C) 2017 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.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; 20 21 import static com.android.permissioncontroller.PermissionControllerStatsLog.REVIEW_PERMISSIONS_FRAGMENT_RESULT_REPORTED; 22 23 import android.app.Activity; 24 import android.content.Intent; 25 import android.content.IntentSender; 26 import android.content.pm.PackageInfo; 27 import android.content.pm.PackageManager; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.os.RemoteCallback; 31 import android.os.UserHandle; 32 import android.text.Html; 33 import android.text.Spanned; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.view.View; 37 import android.widget.Button; 38 import android.widget.ImageView; 39 import android.widget.TextView; 40 41 import androidx.annotation.NonNull; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceCategory; 44 import androidx.preference.PreferenceFragmentCompat; 45 import androidx.preference.PreferenceGroup; 46 import androidx.preference.PreferenceScreen; 47 48 import com.android.permissioncontroller.PermissionControllerStatsLog; 49 import com.android.permissioncontroller.R; 50 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 51 import com.android.permissioncontroller.permission.model.AppPermissions; 52 import com.android.permissioncontroller.permission.model.Permission; 53 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity; 54 import com.android.permissioncontroller.permission.utils.ArrayUtils; 55 import com.android.permissioncontroller.permission.utils.Utils; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 import java.util.Random; 60 61 /** 62 * If an app does not support runtime permissions the user is prompted via this fragment to select 63 * which permissions to grant to the app before first use and if an update changed the permissions. 64 */ 65 public final class ReviewPermissionsFragment extends PreferenceFragmentCompat 66 implements View.OnClickListener, PermissionPreference.PermissionPreferenceChangeListener, 67 PermissionPreference.PermissionPreferenceOwnerFragment { 68 69 private static final String EXTRA_PACKAGE_INFO = 70 "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO"; 71 private static final String LOG_TAG = ReviewPermissionsFragment.class.getSimpleName(); 72 73 private AppPermissions mAppPermissions; 74 75 private Button mContinueButton; 76 private Button mCancelButton; 77 private Button mMoreInfoButton; 78 79 private PreferenceCategory mNewPermissionsCategory; 80 private PreferenceCategory mCurrentPermissionsCategory; 81 82 private boolean mHasConfirmedRevoke; 83 84 /** 85 * @return a new fragment 86 */ newInstance(PackageInfo packageInfo)87 public static ReviewPermissionsFragment newInstance(PackageInfo packageInfo) { 88 Bundle arguments = new Bundle(); 89 arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo); 90 ReviewPermissionsFragment instance = new ReviewPermissionsFragment(); 91 instance.setArguments(arguments); 92 instance.setRetainInstance(true); 93 return instance; 94 } 95 96 @Override onCreate(Bundle savedInstanceState)97 public void onCreate(Bundle savedInstanceState) { 98 super.onCreate(savedInstanceState); 99 100 Activity activity = getActivity(); 101 if (activity == null) { 102 return; 103 } 104 105 PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO); 106 if (packageInfo == null) { 107 activity.finishAfterTransition(); 108 return; 109 } 110 111 mAppPermissions = new AppPermissions(activity, packageInfo, false, true, 112 () -> getActivity().finishAfterTransition()); 113 114 boolean reviewRequired = false; 115 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 116 if (group.isReviewRequired() || (group.getBackgroundPermissions() != null 117 && group.getBackgroundPermissions().isReviewRequired())) { 118 reviewRequired = true; 119 break; 120 } 121 } 122 123 if (!reviewRequired) { 124 // If the system called for a review but no groups are found, this means that all groups 125 // are restricted. Hence there is nothing to review and instantly continue. 126 confirmPermissionsReview(); 127 executeCallback(true); 128 activity.finishAfterTransition(); 129 } 130 } 131 132 @Override onCreatePreferences(Bundle bundle, String s)133 public void onCreatePreferences(Bundle bundle, String s) { 134 // empty 135 } 136 137 @Override onViewCreated(@onNull View view, Bundle savedInstanceState)138 public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { 139 super.onViewCreated(view, savedInstanceState); 140 bindUi(); 141 } 142 143 @Override onResume()144 public void onResume() { 145 super.onResume(); 146 mAppPermissions.refresh(); 147 loadPreferences(); 148 } 149 150 @Override onClick(View view)151 public void onClick(View view) { 152 Activity activity = getActivity(); 153 if (activity == null) { 154 return; 155 } 156 if (view == mContinueButton) { 157 confirmPermissionsReview(); 158 executeCallback(true); 159 } else if (view == mCancelButton) { 160 executeCallback(false); 161 activity.setResult(Activity.RESULT_CANCELED); 162 } else if (view == mMoreInfoButton) { 163 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 164 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, 165 mAppPermissions.getPackageInfo().packageName); 166 intent.putExtra(Intent.EXTRA_USER, UserHandle.getUserHandleForUid( 167 mAppPermissions.getPackageInfo().applicationInfo.uid)); 168 intent.putExtra(ManagePermissionsActivity.EXTRA_ALL_PERMISSIONS, true); 169 getActivity().startActivity(intent); 170 } 171 activity.finishAfterTransition(); 172 } 173 grantReviewedPermission(AppPermissionGroup group)174 private void grantReviewedPermission(AppPermissionGroup group) { 175 String[] permissionsToGrant = null; 176 final int permissionCount = group.getPermissions().size(); 177 for (int j = 0; j < permissionCount; j++) { 178 final Permission permission = group.getPermissions().get(j); 179 if (permission.isReviewRequired()) { 180 permissionsToGrant = ArrayUtils.appendString( 181 permissionsToGrant, permission.getName()); 182 } 183 } 184 if (permissionsToGrant != null) { 185 group.grantRuntimePermissions(true, false, permissionsToGrant); 186 } 187 } 188 confirmPermissionsReview()189 private void confirmPermissionsReview() { 190 final List<PreferenceGroup> preferenceGroups = new ArrayList<>(); 191 if (mNewPermissionsCategory != null) { 192 preferenceGroups.add(mNewPermissionsCategory); 193 preferenceGroups.add(mCurrentPermissionsCategory); 194 } else { 195 PreferenceScreen preferenceScreen = getPreferenceScreen(); 196 if (preferenceScreen != null) { 197 preferenceGroups.add(preferenceScreen); 198 } 199 } 200 201 final int preferenceGroupCount = preferenceGroups.size(); 202 long changeIdForLogging = new Random().nextLong(); 203 204 for (int groupNum = 0; groupNum < preferenceGroupCount; groupNum++) { 205 final PreferenceGroup preferenceGroup = preferenceGroups.get(groupNum); 206 207 final int preferenceCount = preferenceGroup.getPreferenceCount(); 208 for (int prefNum = 0; prefNum < preferenceCount; prefNum++) { 209 Preference preference = preferenceGroup.getPreference(prefNum); 210 if (preference instanceof PermissionReviewPreference) { 211 PermissionReviewPreference permPreference = 212 (PermissionReviewPreference) preference; 213 AppPermissionGroup group = permPreference.getGroup(); 214 215 // If the preference wasn't toggled we show it as "granted" 216 if (group.isReviewRequired() && !permPreference.wasChanged()) { 217 grantReviewedPermission(group); 218 } 219 logReviewPermissionsFragmentResult(changeIdForLogging, group); 220 221 AppPermissionGroup backgroundGroup = group.getBackgroundPermissions(); 222 if (backgroundGroup != null) { 223 // If the preference wasn't toggled we show it as "fully granted" 224 if (backgroundGroup.isReviewRequired() && !permPreference.wasChanged()) { 225 grantReviewedPermission(backgroundGroup); 226 } 227 logReviewPermissionsFragmentResult(changeIdForLogging, backgroundGroup); 228 } 229 } 230 } 231 } 232 mAppPermissions.persistChanges(true); 233 234 // Some permission might be restricted and hence there is no AppPermissionGroup for it. 235 // Manually unset all review-required flags, regardless of restriction. 236 PackageManager pm = getContext().getPackageManager(); 237 PackageInfo pkg = mAppPermissions.getPackageInfo(); 238 UserHandle user = UserHandle.getUserHandleForUid(pkg.applicationInfo.uid); 239 240 for (String perm : pkg.requestedPermissions) { 241 try { 242 pm.updatePermissionFlags(perm, pkg.packageName, FLAG_PERMISSION_REVIEW_REQUIRED, 243 0, user); 244 } catch (IllegalArgumentException e) { 245 Log.e(LOG_TAG, "Cannot unmark " + perm + " requested by " + pkg.packageName 246 + " as review required", e); 247 } 248 } 249 } 250 logReviewPermissionsFragmentResult(long changeId, AppPermissionGroup group)251 private void logReviewPermissionsFragmentResult(long changeId, AppPermissionGroup group) { 252 ArrayList<Permission> permissions = group.getPermissions(); 253 254 int numPermissions = permissions.size(); 255 for (int i = 0; i < numPermissions; i++) { 256 Permission permission = permissions.get(i); 257 258 PermissionControllerStatsLog.write(REVIEW_PERMISSIONS_FRAGMENT_RESULT_REPORTED, 259 changeId, group.getApp().applicationInfo.uid, group.getApp().packageName, 260 permission.getName(), permission.isGrantedIncludingAppOp()); 261 Log.v(LOG_TAG, "Permission grant via permission review changeId=" + changeId + " uid=" 262 + group.getApp().applicationInfo.uid + " packageName=" 263 + group.getApp().packageName + " permission=" 264 + permission.getName() + " granted=" + permission.isGrantedIncludingAppOp()); 265 } 266 } 267 bindUi()268 private void bindUi() { 269 Activity activity = getActivity(); 270 if (activity == null) { 271 return; 272 } 273 274 // Set icon 275 Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon( 276 activity.getPackageManager()); 277 ImageView iconView = activity.requireViewById(R.id.app_icon); 278 iconView.setImageDrawable(icon); 279 280 // Set message 281 final int labelTemplateResId = isPackageUpdated() 282 ? R.string.permission_review_title_template_update 283 : R.string.permission_review_title_template_install; 284 Spanned message = Html.fromHtml(getString(labelTemplateResId, 285 mAppPermissions.getAppLabel()), 0); 286 287 // Set the permission message as the title so it can be announced. 288 activity.setTitle(message.toString()); 289 290 // Color the app name. 291 TextView permissionsMessageView = activity.requireViewById( 292 R.id.permissions_message); 293 permissionsMessageView.setText(message); 294 295 mContinueButton = getActivity().requireViewById(R.id.continue_button); 296 mContinueButton.setOnClickListener(this); 297 298 mCancelButton = getActivity().requireViewById(R.id.cancel_button); 299 mCancelButton.setOnClickListener(this); 300 301 if (activity.getPackageManager().arePermissionsIndividuallyControlled()) { 302 mMoreInfoButton = getActivity().requireViewById( 303 R.id.permission_more_info_button); 304 mMoreInfoButton.setOnClickListener(this); 305 mMoreInfoButton.setVisibility(View.VISIBLE); 306 } 307 } 308 getPreference(String key)309 private PermissionReviewPreference getPreference(String key) { 310 if (mNewPermissionsCategory != null) { 311 PermissionReviewPreference pref = 312 (PermissionReviewPreference) mNewPermissionsCategory.findPreference(key); 313 314 if (pref == null && mCurrentPermissionsCategory != null) { 315 return (PermissionReviewPreference) mCurrentPermissionsCategory.findPreference(key); 316 } else { 317 return pref; 318 } 319 } else { 320 return (PermissionReviewPreference) getPreferenceScreen().findPreference(key); 321 } 322 } 323 loadPreferences()324 private void loadPreferences() { 325 Activity activity = getActivity(); 326 if (activity == null) { 327 return; 328 } 329 330 PreferenceScreen screen = getPreferenceScreen(); 331 if (screen == null) { 332 screen = getPreferenceManager().createPreferenceScreen(getContext()); 333 setPreferenceScreen(screen); 334 } else { 335 screen.removeAll(); 336 } 337 338 mCurrentPermissionsCategory = null; 339 mNewPermissionsCategory = null; 340 341 final boolean isPackageUpdated = isPackageUpdated(); 342 343 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 344 if (!Utils.shouldShowPermission(getContext(), group) 345 || !Utils.OS_PKG.equals(group.getDeclaringPackage())) { 346 continue; 347 } 348 349 PermissionReviewPreference preference = getPreference(group.getName()); 350 if (preference == null) { 351 preference = new PermissionReviewPreference(this, group, this); 352 353 preference.setKey(group.getName()); 354 Drawable icon = Utils.loadDrawable(activity.getPackageManager(), 355 group.getIconPkg(), group.getIconResId()); 356 preference.setIcon(Utils.applyTint(getContext(), icon, 357 android.R.attr.colorControlNormal)); 358 preference.setTitle(group.getLabel()); 359 } else { 360 preference.updateUi(); 361 } 362 363 if (group.isReviewRequired() || (group.getBackgroundPermissions() != null 364 && group.getBackgroundPermissions().isReviewRequired())) { 365 if (!isPackageUpdated) { 366 screen.addPreference(preference); 367 } else { 368 if (mNewPermissionsCategory == null) { 369 mNewPermissionsCategory = new PreferenceCategory(activity); 370 mNewPermissionsCategory.setTitle(R.string.new_permissions_category); 371 mNewPermissionsCategory.setOrder(1); 372 screen.addPreference(mNewPermissionsCategory); 373 } 374 mNewPermissionsCategory.addPreference(preference); 375 } 376 } else { 377 if (mCurrentPermissionsCategory == null) { 378 mCurrentPermissionsCategory = new PreferenceCategory(activity); 379 mCurrentPermissionsCategory.setTitle(R.string.current_permissions_category); 380 mCurrentPermissionsCategory.setOrder(2); 381 screen.addPreference(mCurrentPermissionsCategory); 382 } 383 mCurrentPermissionsCategory.addPreference(preference); 384 } 385 } 386 } 387 isPackageUpdated()388 private boolean isPackageUpdated() { 389 List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups(); 390 final int groupCount = groups.size(); 391 for (int i = 0; i < groupCount; i++) { 392 AppPermissionGroup group = groups.get(i); 393 if (!(group.isReviewRequired() || (group.getBackgroundPermissions() != null 394 && group.getBackgroundPermissions().isReviewRequired()))) { 395 return true; 396 } 397 } 398 return false; 399 } 400 executeCallback(boolean success)401 private void executeCallback(boolean success) { 402 Activity activity = getActivity(); 403 if (activity == null) { 404 return; 405 } 406 if (success) { 407 IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT); 408 if (intent != null) { 409 try { 410 int flagMask = 0; 411 int flagValues = 0; 412 if (activity.getIntent().getBooleanExtra( 413 Intent.EXTRA_RESULT_NEEDED, false)) { 414 flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 415 flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 416 } 417 activity.startIntentSenderForResult(intent, -1, null, 418 flagMask, flagValues, 0); 419 } catch (IntentSender.SendIntentException e) { 420 /* ignore */ 421 } 422 return; 423 } 424 } 425 RemoteCallback callback = activity.getIntent().getParcelableExtra( 426 Intent.EXTRA_REMOTE_CALLBACK); 427 if (callback != null) { 428 Bundle result = new Bundle(); 429 result.putBoolean(Intent.EXTRA_RETURN_RESULT, success); 430 callback.sendResult(result); 431 } 432 } 433 434 @Override shouldConfirmDefaultPermissionRevoke()435 public boolean shouldConfirmDefaultPermissionRevoke() { 436 return !mHasConfirmedRevoke; 437 } 438 439 @Override hasConfirmDefaultPermissionRevoke()440 public void hasConfirmDefaultPermissionRevoke() { 441 mHasConfirmedRevoke = true; 442 } 443 444 @Override onPreferenceChanged(String key)445 public void onPreferenceChanged(String key) { 446 getPreference(key).setChanged(); 447 } 448 449 @Override onDenyAnyWay(String key, int changeTarget)450 public void onDenyAnyWay(String key, int changeTarget) { 451 getPreference(key).onDenyAnyWay(changeTarget); 452 } 453 454 @Override onBackgroundAccessChosen(String key, int chosenItem)455 public void onBackgroundAccessChosen(String key, int chosenItem) { 456 getPreference(key).onBackgroundAccessChosen(chosenItem); 457 } 458 459 /** 460 * Extend the {@link PermissionPreference}: 461 * <ul> 462 * <li>Show the description of the permission group</li> 463 * <li>Show the permission group as granted if the user has not toggled it yet. This means 464 * that if the user does not touch the preference, we will later grant the permission 465 * in {@link #confirmPermissionsReview()}.</li> 466 * </ul> 467 */ 468 private static class PermissionReviewPreference extends PermissionPreference { 469 private final AppPermissionGroup mGroup; 470 private boolean mWasChanged; 471 PermissionReviewPreference(PreferenceFragmentCompat fragment, AppPermissionGroup group, PermissionPreferenceChangeListener callbacks)472 PermissionReviewPreference(PreferenceFragmentCompat fragment, AppPermissionGroup group, 473 PermissionPreferenceChangeListener callbacks) { 474 super(fragment, group, callbacks); 475 476 mGroup = group; 477 updateUi(); 478 } 479 getGroup()480 AppPermissionGroup getGroup() { 481 return mGroup; 482 } 483 484 /** 485 * Mark the permission as changed by the user 486 */ setChanged()487 void setChanged() { 488 mWasChanged = true; 489 updateUi(); 490 } 491 492 /** 493 * @return {@code true} iff the permission was changed by the user 494 */ wasChanged()495 boolean wasChanged() { 496 return mWasChanged; 497 } 498 499 @Override updateUi()500 void updateUi() { 501 // updateUi might be called in super-constructor before group is initialized 502 if (mGroup == null) { 503 return; 504 } 505 506 super.updateUi(); 507 508 if (isEnabled()) { 509 if (mGroup.isReviewRequired() && !mWasChanged) { 510 setSummary(mGroup.getDescription()); 511 setCheckedOverride(true); 512 } else if (TextUtils.isEmpty(getSummary())) { 513 // Sometimes the summary is already used, e.g. when this for a 514 // foreground/background group. In this case show leave the original summary. 515 setSummary(mGroup.getDescription()); 516 } 517 } 518 } 519 } 520 } 521