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