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