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.wear; 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 android.app.Activity; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentSender; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.graphics.drawable.Drawable; 30 import android.os.Build; 31 import android.os.Bundle; 32 import android.os.RemoteCallback; 33 import android.os.UserHandle; 34 import android.text.Html; 35 import android.text.SpannableString; 36 import android.text.style.ForegroundColorSpan; 37 import android.util.Log; 38 import android.util.TypedValue; 39 40 import androidx.preference.Preference; 41 import androidx.preference.PreferenceCategory; 42 import androidx.preference.PreferenceFragmentCompat; 43 import androidx.preference.PreferenceGroup; 44 import androidx.preference.PreferenceScreen; 45 import androidx.preference.SwitchPreference; 46 import androidx.preference.TwoStatePreference; 47 import androidx.wear.ble.view.WearableDialogHelper; 48 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.utils.Utils; 53 54 import java.util.ArrayList; 55 import java.util.List; 56 57 public class ReviewPermissionsWearFragment extends PreferenceFragmentCompat 58 implements Preference.OnPreferenceChangeListener { 59 private static final String TAG = "ReviewPermWear"; 60 61 private static final int ORDER_NEW_PERMS = 1; 62 private static final int ORDER_CURRENT_PERMS = 2; 63 // Category for showing actions should be displayed last. 64 private static final int ORDER_ACTION = 100000; 65 private static final int ORDER_PERM_OFFSET_START = 100; 66 67 private static final String EXTRA_PACKAGE_INFO = 68 "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO"; 69 newInstance(PackageInfo packageInfo)70 public static ReviewPermissionsWearFragment newInstance(PackageInfo packageInfo) { 71 Bundle arguments = new Bundle(); 72 arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo); 73 ReviewPermissionsWearFragment instance = new ReviewPermissionsWearFragment(); 74 instance.setArguments(arguments); 75 instance.setRetainInstance(true); 76 return instance; 77 } 78 79 private AppPermissions mAppPermissions; 80 81 private PreferenceCategory mNewPermissionsCategory; 82 private PreferenceCategory mCurrentPermissionsCategory; 83 84 private boolean mHasConfirmedRevoke; 85 86 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)87 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 88 Activity activity = getActivity(); 89 if (activity == null) { 90 return; 91 } 92 93 PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO); 94 if (packageInfo == null) { 95 activity.finish(); 96 return; 97 } 98 99 mAppPermissions = new AppPermissions(activity, packageInfo, false, 100 () -> getActivity().finish()); 101 102 boolean reviewRequired = false; 103 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 104 if (group.isReviewRequired()) { 105 reviewRequired = true; 106 break; 107 } 108 } 109 110 if (!reviewRequired) { 111 confirmPermissionsReview(); 112 activity.finish(); 113 } 114 } 115 116 @Override onResume()117 public void onResume() { 118 super.onResume(); 119 mAppPermissions.refresh(); 120 loadPreferences(); 121 } 122 loadPreferences()123 private void loadPreferences() { 124 Activity activity = getActivity(); 125 if (activity == null) { 126 return; 127 } 128 129 PreferenceScreen screen = getPreferenceScreen(); 130 if (screen == null) { 131 screen = getPreferenceManager().createPreferenceScreen(getActivity()); 132 setPreferenceScreen(screen); 133 } else { 134 screen.removeAll(); 135 } 136 137 mCurrentPermissionsCategory = null; 138 PreferenceGroup oldNewPermissionsCategory = mNewPermissionsCategory; 139 mNewPermissionsCategory = null; 140 141 final boolean isPackageUpdated = isPackageUpdated(); 142 int permOrder = ORDER_PERM_OFFSET_START; 143 144 PackageInfo pkg = mAppPermissions.getPackageInfo(); 145 ApplicationInfo appInfo = pkg.applicationInfo; 146 147 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 148 if (!Utils.shouldShowPermission(getContext(), group) 149 || !Utils.OS_PKG.equals(group.getDeclaringPackage())) { 150 continue; 151 } 152 153 final PermissionSwitchPreference preference; 154 Preference cachedPreference = oldNewPermissionsCategory != null 155 ? oldNewPermissionsCategory.findPreference(group.getName()) : null; 156 if (cachedPreference instanceof PermissionSwitchPreference) { 157 preference = (PermissionSwitchPreference) cachedPreference; 158 } else { 159 preference = new PermissionSwitchPreference(getActivity()); 160 161 preference.setKey(group.getName()); 162 preference.setTitle(group.getLabel()); 163 preference.setPersistent(false); 164 preference.setOrder(permOrder++); 165 166 preference.setOnPreferenceChangeListener(this); 167 } 168 169 if (appInfo.targetSdkVersion < Build.VERSION_CODES.M && 170 group.isReviewRequired() ) { 171 preference.setChecked(true); 172 } else { 173 preference.setChecked(group.areRuntimePermissionsGranted()); 174 } 175 176 // Mutable state 177 if (group.isSystemFixed() || group.isPolicyFixed()) { 178 preference.setEnabled(false); 179 } else { 180 preference.setEnabled(true); 181 } 182 if (preference.getParent() != null) { 183 // Remove first if already added. 184 preference.getParent().removePreference(preference); 185 } 186 if (group.isReviewRequired()) { 187 if (!isPackageUpdated) { 188 // An app just being installed, which means all groups requiring reviews. 189 screen.addPreference(preference); 190 } else { 191 if (mNewPermissionsCategory == null) { 192 mNewPermissionsCategory = new PreferenceCategory(activity); 193 mNewPermissionsCategory.setTitle(R.string.new_permissions_category); 194 mNewPermissionsCategory.setOrder(ORDER_NEW_PERMS); 195 screen.addPreference(mNewPermissionsCategory); 196 } 197 mNewPermissionsCategory.addPreference(preference); 198 } 199 } else { 200 if (mCurrentPermissionsCategory == null) { 201 mCurrentPermissionsCategory = new PreferenceCategory(activity); 202 mCurrentPermissionsCategory.setTitle(R.string.current_permissions_category); 203 mCurrentPermissionsCategory.setOrder(ORDER_CURRENT_PERMS); 204 screen.addPreference(mCurrentPermissionsCategory); 205 } 206 mCurrentPermissionsCategory.addPreference(preference); 207 } 208 } 209 addTitlePreferenceToScreen(screen); 210 addActionPreferencesToScreen(screen); 211 } 212 isPackageUpdated()213 private boolean isPackageUpdated() { 214 List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups(); 215 final int groupCount = groups.size(); 216 for (int i = 0; i < groupCount; i++) { 217 AppPermissionGroup group = groups.get(i); 218 if (!group.isReviewRequired()) { 219 return true; 220 } 221 } 222 return false; 223 } 224 225 @Override onPreferenceChange(Preference preference, Object newValue)226 public boolean onPreferenceChange(Preference preference, Object newValue) { 227 Log.d(TAG, "onPreferenceChange " + preference.getTitle()); 228 if (mHasConfirmedRevoke) { 229 return true; 230 } 231 if (preference instanceof PermissionSwitchPreference) { 232 PermissionSwitchPreference permPreference = (PermissionSwitchPreference) 233 preference; 234 permPreference.setChanged(); 235 if (permPreference.isChecked()) { 236 showWarnRevokeDialog(permPreference); 237 } else { 238 return true; 239 } 240 } 241 return false; 242 } 243 showWarnRevokeDialog(final SwitchPreference preference)244 private void showWarnRevokeDialog(final SwitchPreference preference) { 245 // When revoking, we set "confirm" as the negative icon to be shown at the bottom. 246 new WearableDialogHelper.DialogBuilder(getContext()) 247 .setPositiveIcon(R.drawable.cancel_button) 248 .setNegativeIcon(R.drawable.confirm_button) 249 .setPositiveButton(R.string.grant_dialog_button_deny_anyway, (dialog, which) -> { 250 preference.setChecked(false); 251 mHasConfirmedRevoke = true; 252 }) 253 .setNegativeButton(R.string.cancel, null) 254 .setMessage(R.string.old_sdk_deny_warning) 255 .show(); 256 } 257 confirmPermissionsReview()258 private void confirmPermissionsReview() { 259 final List<PreferenceGroup> preferenceGroups = new ArrayList<>(); 260 if (mNewPermissionsCategory != null) { 261 preferenceGroups.add(mNewPermissionsCategory); 262 } 263 if (mCurrentPermissionsCategory != null) { 264 preferenceGroups.add(mCurrentPermissionsCategory); 265 } 266 if (preferenceGroups.isEmpty()){ 267 PreferenceScreen preferenceScreen = getPreferenceScreen(); 268 if (preferenceScreen != null) { 269 preferenceGroups.add(preferenceScreen); 270 } 271 } 272 273 for (PreferenceGroup preferenceGroup: preferenceGroups) { 274 final int preferenceCount = preferenceGroup.getPreferenceCount(); 275 for (int i = 0; i < preferenceCount; i++) { 276 Preference preference = preferenceGroup.getPreference(i); 277 if (preference instanceof PermissionSwitchPreference) { 278 PermissionSwitchPreference permPreference = (PermissionSwitchPreference) 279 preference; 280 String groupName = preference.getKey(); 281 AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName); 282 if (group.isReviewRequired() || permPreference.wasChanged()) { 283 if (permPreference.isChecked()) { 284 Log.i(TAG, groupName + " permPreference.isChecked()"); 285 group.grantRuntimePermissions(true, false); 286 } else { 287 Log.i(TAG, groupName + " !permPreference.isChecked()"); 288 group.revokeRuntimePermissions(false); 289 } 290 } 291 292 AppPermissionGroup backgroundGroup = group.getBackgroundPermissions(); 293 if (backgroundGroup != null) { 294 // If the preference wasn't toggled we show it as "fully granted" 295 if (backgroundGroup.isReviewRequired() && !permPreference.wasChanged()) { 296 backgroundGroup.grantRuntimePermissions(true, false); 297 } 298 backgroundGroup.unsetReviewRequired(); 299 } 300 } 301 } 302 } 303 304 // Some permission might be restricted and hence there is no AppPermissionGroup for it. 305 // Manually unset all review-required flags, regardless of restriction. 306 PackageManager pm = getContext().getPackageManager(); 307 PackageInfo pkg = mAppPermissions.getPackageInfo(); 308 UserHandle user = UserHandle.getUserHandleForUid(pkg.applicationInfo.uid); 309 310 if (pkg.requestedPermissions == null) { 311 // No flag updating to do 312 return; 313 } 314 315 for (String perm : pkg.requestedPermissions) { 316 try { 317 pm.updatePermissionFlags(perm, pkg.packageName, 318 FLAG_PERMISSION_REVIEW_REQUIRED | FLAG_PERMISSION_USER_SET, 319 FLAG_PERMISSION_USER_SET, user); 320 } catch (IllegalArgumentException e) { 321 Log.e(TAG, "Cannot unmark " + perm + " requested by " + pkg.packageName 322 + " as review required", e); 323 } 324 } 325 } 326 addTitlePreferenceToScreen(PreferenceScreen screen)327 private void addTitlePreferenceToScreen(PreferenceScreen screen) { 328 Activity activity = getActivity(); 329 Preference titlePref = new Preference(activity); 330 screen.addPreference(titlePref); 331 332 // Set icon 333 Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon( 334 activity.getPackageManager()); 335 titlePref.setIcon(icon); 336 337 // Set message 338 String appLabel = Html.escapeHtml(mAppPermissions.getAppLabel().toString()); 339 final int labelTemplateResId = isPackageUpdated() 340 ? R.string.permission_review_title_template_update 341 : R.string.permission_review_title_template_install; 342 SpannableString message = 343 new SpannableString(Html.fromHtml(getString(labelTemplateResId, appLabel))); 344 345 // Color the app name. 346 final int appLabelStart = message.toString().indexOf(appLabel, 0); 347 final int appLabelLength = appLabel.length(); 348 349 TypedValue typedValue = new TypedValue(); 350 activity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true); 351 final int color = activity.getColor(typedValue.resourceId); 352 353 if (appLabelStart >= 0) { 354 message.setSpan(new ForegroundColorSpan(color), appLabelStart, 355 appLabelStart + appLabelLength, 0); 356 } 357 358 titlePref.setTitle(message); 359 360 titlePref.setSelectable(false); 361 titlePref.setLayoutResource(R.layout.wear_review_permission_title_pref); 362 } 363 addActionPreferencesToScreen(PreferenceScreen screen)364 private void addActionPreferencesToScreen(PreferenceScreen screen) { 365 final Activity activity = getActivity(); 366 367 Preference cancelPref = new Preference(activity); 368 cancelPref.setTitle(R.string.review_button_cancel); 369 cancelPref.setOrder(ORDER_ACTION); 370 cancelPref.setEnabled(true); 371 cancelPref.setLayoutResource(R.layout.wear_review_permission_action_pref); 372 cancelPref.setOnPreferenceClickListener(p -> { 373 executeCallback(false); 374 activity.setResult(Activity.RESULT_CANCELED); 375 activity.finish(); 376 return true; 377 }); 378 screen.addPreference(cancelPref); 379 380 Preference continuePref = new Preference(activity); 381 continuePref.setTitle(R.string.review_button_continue); 382 continuePref.setOrder(ORDER_ACTION + 1); 383 continuePref.setEnabled(true); 384 continuePref.setLayoutResource(R.layout.wear_review_permission_action_pref); 385 continuePref.setOnPreferenceClickListener(p -> { 386 confirmPermissionsReview(); 387 executeCallback(true); 388 activity.setResult(Activity.RESULT_OK); 389 getActivity().finish(); 390 return true; 391 }); 392 screen.addPreference(continuePref); 393 } 394 executeCallback(boolean success)395 private void executeCallback(boolean success) { 396 Activity activity = getActivity(); 397 if (activity == null) { 398 return; 399 } 400 if (success) { 401 IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT); 402 if (intent != null) { 403 try { 404 int flagMask = 0; 405 int flagValues = 0; 406 if (activity.getIntent().getBooleanExtra( 407 Intent.EXTRA_RESULT_NEEDED, false)) { 408 flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 409 flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 410 } 411 activity.startIntentSenderForResult(intent, -1, null, 412 flagMask, flagValues, 0); 413 } catch (IntentSender.SendIntentException e) { 414 /* ignore */ 415 } 416 return; 417 } 418 } 419 RemoteCallback callback = activity.getIntent().getParcelableExtra( 420 Intent.EXTRA_REMOTE_CALLBACK); 421 if (callback != null) { 422 Bundle result = new Bundle(); 423 result.putBoolean(Intent.EXTRA_RETURN_RESULT, success); 424 callback.sendResult(result); 425 } 426 } 427 428 /** 429 * Extend the {@link SwitchPreference}: 430 * <ul> 431 * <li>Monitor the changed state</li> 432 * </ul> 433 */ 434 private static class PermissionSwitchPreference extends SwitchPreference { 435 private boolean mWasChanged = false; 436 PermissionSwitchPreference(Context context)437 PermissionSwitchPreference(Context context) { 438 super(context); 439 } 440 441 /** 442 * Mark the permission as changed by the user 443 */ setChanged()444 void setChanged() { 445 mWasChanged = true; 446 } 447 448 /** 449 * @return {@code true} iff the permission was changed by the user 450 */ wasChanged()451 boolean wasChanged() { 452 return mWasChanged; 453 } 454 } 455 } 456