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 android.app.Activity; 20 import android.content.Intent; 21 import android.content.IntentSender; 22 import android.content.pm.PackageInfo; 23 import android.graphics.drawable.Drawable; 24 import android.os.Bundle; 25 import android.os.RemoteCallback; 26 import android.text.Html; 27 import android.text.SpannableString; 28 import android.text.style.ForegroundColorSpan; 29 import android.util.Log; 30 import android.util.TypedValue; 31 32 import androidx.preference.Preference; 33 import androidx.preference.PreferenceCategory; 34 import androidx.preference.PreferenceFragmentCompat; 35 import androidx.preference.PreferenceGroup; 36 import androidx.preference.PreferenceScreen; 37 import androidx.preference.SwitchPreference; 38 import androidx.preference.TwoStatePreference; 39 import androidx.wear.ble.view.WearableDialogHelper; 40 41 import com.android.permissioncontroller.R; 42 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 43 import com.android.permissioncontroller.permission.model.AppPermissions; 44 import com.android.permissioncontroller.permission.utils.Utils; 45 46 import java.util.List; 47 48 public class ReviewPermissionsWearFragment extends PreferenceFragmentCompat 49 implements Preference.OnPreferenceChangeListener { 50 private static final String TAG = "ReviewPermWear"; 51 52 private static final int ORDER_NEW_PERMS = 1; 53 private static final int ORDER_CURRENT_PERMS = 2; 54 // Category for showing actions should be displayed last. 55 private static final int ORDER_ACTION = 100000; 56 private static final int ORDER_PERM_OFFSET_START = 100; 57 58 private static final String EXTRA_PACKAGE_INFO = 59 "com.android.permissioncontroller.permission.ui.extra.PACKAGE_INFO"; 60 newInstance(PackageInfo packageInfo)61 public static ReviewPermissionsWearFragment newInstance(PackageInfo packageInfo) { 62 Bundle arguments = new Bundle(); 63 arguments.putParcelable(EXTRA_PACKAGE_INFO, packageInfo); 64 ReviewPermissionsWearFragment instance = new ReviewPermissionsWearFragment(); 65 instance.setArguments(arguments); 66 instance.setRetainInstance(true); 67 return instance; 68 } 69 70 private AppPermissions mAppPermissions; 71 72 private PreferenceCategory mNewPermissionsCategory; 73 74 private boolean mHasConfirmedRevoke; 75 76 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)77 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 78 Activity activity = getActivity(); 79 if (activity == null) { 80 return; 81 } 82 83 PackageInfo packageInfo = getArguments().getParcelable(EXTRA_PACKAGE_INFO); 84 if (packageInfo == null) { 85 activity.finish(); 86 return; 87 } 88 89 mAppPermissions = new AppPermissions(activity, packageInfo, false, 90 () -> getActivity().finish()); 91 92 if (mAppPermissions.getPermissionGroups().isEmpty()) { 93 activity.finish(); 94 return; 95 } 96 97 boolean reviewRequired = false; 98 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 99 if (group.isReviewRequired()) { 100 reviewRequired = true; 101 break; 102 } 103 } 104 105 if (!reviewRequired) { 106 activity.finish(); 107 } 108 } 109 110 @Override onResume()111 public void onResume() { 112 super.onResume(); 113 mAppPermissions.refresh(); 114 loadPreferences(); 115 } 116 loadPreferences()117 private void loadPreferences() { 118 Activity activity = getActivity(); 119 if (activity == null) { 120 return; 121 } 122 123 PreferenceScreen screen = getPreferenceScreen(); 124 if (screen == null) { 125 screen = getPreferenceManager().createPreferenceScreen(getActivity()); 126 setPreferenceScreen(screen); 127 } else { 128 screen.removeAll(); 129 } 130 131 PreferenceGroup currentPermissionsCategory = null; 132 PreferenceGroup oldNewPermissionsCategory = mNewPermissionsCategory; 133 mNewPermissionsCategory = null; 134 135 final boolean isPackageUpdated = isPackageUpdated(); 136 int permOrder = ORDER_PERM_OFFSET_START; 137 138 for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) { 139 if (!Utils.shouldShowPermission(getContext(), group) 140 || !Utils.OS_PKG.equals(group.getDeclaringPackage())) { 141 continue; 142 } 143 144 final SwitchPreference preference; 145 Preference cachedPreference = oldNewPermissionsCategory != null 146 ? oldNewPermissionsCategory.findPreference(group.getName()) : null; 147 if (cachedPreference instanceof SwitchPreference) { 148 preference = (SwitchPreference) cachedPreference; 149 } else { 150 preference = new SwitchPreference(getActivity()); 151 152 preference.setKey(group.getName()); 153 preference.setTitle(group.getLabel()); 154 preference.setPersistent(false); 155 preference.setOrder(permOrder++); 156 157 preference.setOnPreferenceChangeListener(this); 158 } 159 160 preference.setChecked(group.areRuntimePermissionsGranted()); 161 162 // Mutable state 163 if (group.isSystemFixed() || group.isPolicyFixed()) { 164 preference.setEnabled(false); 165 } else { 166 preference.setEnabled(true); 167 } 168 169 if (group.isReviewRequired()) { 170 if (!isPackageUpdated) { 171 // An app just being installed, which means all groups requiring reviews. 172 screen.addPreference(preference); 173 } else { 174 if (mNewPermissionsCategory == null) { 175 mNewPermissionsCategory = new PreferenceCategory(activity); 176 mNewPermissionsCategory.setTitle(R.string.new_permissions_category); 177 mNewPermissionsCategory.setOrder(ORDER_NEW_PERMS); 178 screen.addPreference(mNewPermissionsCategory); 179 } 180 mNewPermissionsCategory.addPreference(preference); 181 } 182 } else { 183 if (currentPermissionsCategory == null) { 184 currentPermissionsCategory = new PreferenceCategory(activity); 185 currentPermissionsCategory.setTitle(R.string.current_permissions_category); 186 currentPermissionsCategory.setOrder(ORDER_CURRENT_PERMS); 187 screen.addPreference(currentPermissionsCategory); 188 } 189 currentPermissionsCategory.addPreference(preference); 190 } 191 } 192 193 addTitlePreferenceToScreen(screen); 194 addActionPreferencesToScreen(screen); 195 } 196 isPackageUpdated()197 private boolean isPackageUpdated() { 198 List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups(); 199 final int groupCount = groups.size(); 200 for (int i = 0; i < groupCount; i++) { 201 AppPermissionGroup group = groups.get(i); 202 if (!group.isReviewRequired()) { 203 return true; 204 } 205 } 206 return false; 207 } 208 209 @Override onPreferenceChange(Preference preference, Object newValue)210 public boolean onPreferenceChange(Preference preference, Object newValue) { 211 Log.d(TAG, "onPreferenceChange " + preference.getTitle()); 212 if (mHasConfirmedRevoke) { 213 return true; 214 } 215 if (preference instanceof SwitchPreference) { 216 SwitchPreference switchPreference = (SwitchPreference) preference; 217 if (switchPreference.isChecked()) { 218 showWarnRevokeDialog(switchPreference); 219 } else { 220 return true; 221 } 222 } 223 return false; 224 } 225 showWarnRevokeDialog(final SwitchPreference preference)226 private void showWarnRevokeDialog(final SwitchPreference preference) { 227 // When revoking, we set "confirm" as the negative icon to be shown at the bottom. 228 new WearableDialogHelper.DialogBuilder(getContext()) 229 .setPositiveIcon(R.drawable.cancel_button) 230 .setNegativeIcon(R.drawable.confirm_button) 231 .setPositiveButton(R.string.cancel, null) 232 .setNegativeButton(R.string.grant_dialog_button_deny_anyway, 233 (dialog, which) -> { 234 preference.setChecked(false); 235 mHasConfirmedRevoke = true; 236 }) 237 .setMessage(R.string.old_sdk_deny_warning) 238 .show(); 239 } 240 confirmPermissionsReview()241 private void confirmPermissionsReview() { 242 PreferenceGroup preferenceGroup = mNewPermissionsCategory != null 243 ? mNewPermissionsCategory : getPreferenceScreen(); 244 245 final int preferenceCount = preferenceGroup.getPreferenceCount(); 246 for (int i = 0; i < preferenceCount; i++) { 247 Preference preference = preferenceGroup.getPreference(i); 248 if (preference instanceof TwoStatePreference) { 249 TwoStatePreference twoStatePreference = (TwoStatePreference) preference; 250 String groupName = preference.getKey(); 251 AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName); 252 if (twoStatePreference.isChecked()) { 253 group.grantRuntimePermissions(true, false); 254 } else { 255 group.revokeRuntimePermissions(false); 256 } 257 group.unsetReviewRequired(); 258 } 259 } 260 } 261 addTitlePreferenceToScreen(PreferenceScreen screen)262 private void addTitlePreferenceToScreen(PreferenceScreen screen) { 263 Activity activity = getActivity(); 264 Preference titlePref = new Preference(activity); 265 screen.addPreference(titlePref); 266 267 // Set icon 268 Drawable icon = mAppPermissions.getPackageInfo().applicationInfo.loadIcon( 269 activity.getPackageManager()); 270 titlePref.setIcon(icon); 271 272 // Set message 273 String appLabel = mAppPermissions.getAppLabel().toString(); 274 final int labelTemplateResId = isPackageUpdated() 275 ? R.string.permission_review_title_template_update 276 : R.string.permission_review_title_template_install; 277 SpannableString message = 278 new SpannableString(Html.fromHtml(getString(labelTemplateResId, appLabel))); 279 280 // Color the app name. 281 final int appLabelStart = message.toString().indexOf(appLabel, 0); 282 final int appLabelLength = appLabel.length(); 283 284 TypedValue typedValue = new TypedValue(); 285 activity.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true); 286 final int color = activity.getColor(typedValue.resourceId); 287 288 if (appLabelStart >= 0) { 289 message.setSpan(new ForegroundColorSpan(color), appLabelStart, 290 appLabelStart + appLabelLength, 0); 291 } 292 293 titlePref.setTitle(message); 294 295 titlePref.setSelectable(false); 296 titlePref.setLayoutResource(R.layout.wear_review_permission_title_pref); 297 } 298 addActionPreferencesToScreen(PreferenceScreen screen)299 private void addActionPreferencesToScreen(PreferenceScreen screen) { 300 final Activity activity = getActivity(); 301 302 Preference cancelPref = new Preference(activity); 303 cancelPref.setTitle(R.string.review_button_cancel); 304 cancelPref.setOrder(ORDER_ACTION); 305 cancelPref.setEnabled(true); 306 cancelPref.setLayoutResource(R.layout.wear_review_permission_action_pref); 307 cancelPref.setOnPreferenceClickListener(p -> { 308 executeCallback(false); 309 activity.setResult(Activity.RESULT_CANCELED); 310 activity.finish(); 311 return true; 312 }); 313 screen.addPreference(cancelPref); 314 315 Preference continuePref = new Preference(activity); 316 continuePref.setTitle(R.string.review_button_continue); 317 continuePref.setOrder(ORDER_ACTION + 1); 318 continuePref.setEnabled(true); 319 continuePref.setLayoutResource(R.layout.wear_review_permission_action_pref); 320 continuePref.setOnPreferenceClickListener(p -> { 321 confirmPermissionsReview(); 322 executeCallback(true); 323 getActivity().finish(); 324 return true; 325 }); 326 screen.addPreference(continuePref); 327 } 328 executeCallback(boolean success)329 private void executeCallback(boolean success) { 330 Activity activity = getActivity(); 331 if (activity == null) { 332 return; 333 } 334 if (success) { 335 IntentSender intent = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT); 336 if (intent != null) { 337 try { 338 int flagMask = 0; 339 int flagValues = 0; 340 if (activity.getIntent().getBooleanExtra( 341 Intent.EXTRA_RESULT_NEEDED, false)) { 342 flagMask = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 343 flagValues = Intent.FLAG_ACTIVITY_FORWARD_RESULT; 344 } 345 activity.startIntentSenderForResult(intent, -1, null, 346 flagMask, flagValues, 0); 347 } catch (IntentSender.SendIntentException e) { 348 /* ignore */ 349 } 350 return; 351 } 352 } 353 RemoteCallback callback = activity.getIntent().getParcelableExtra( 354 Intent.EXTRA_REMOTE_CALLBACK); 355 if (callback != null) { 356 Bundle result = new Bundle(); 357 result.putBoolean(Intent.EXTRA_RETURN_RESULT, success); 358 callback.sendResult(result); 359 } 360 } 361 } 362