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