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