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