1 /* 2 * Copyright (C) 2015 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.packageinstaller.permission.ui.handheld; 18 19 import android.app.ActionBar; 20 import android.app.AlertDialog; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageItemInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.pm.PermissionGroupInfo; 29 import android.content.pm.PermissionInfo; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.Build; 33 import android.os.Bundle; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.view.MenuItem; 39 import android.widget.Switch; 40 41 import androidx.annotation.NonNull; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceCategory; 44 import androidx.preference.PreferenceGroup; 45 46 import com.android.packageinstaller.permission.model.AppPermissionGroup; 47 import com.android.packageinstaller.permission.model.Permission; 48 import com.android.packageinstaller.permission.utils.ArrayUtils; 49 import com.android.packageinstaller.permission.utils.Utils; 50 import com.android.permissioncontroller.R; 51 52 import java.util.ArrayList; 53 import java.util.Collections; 54 import java.util.Comparator; 55 import java.util.List; 56 57 /** 58 * Show and manage individual permissions for an app. 59 * 60 * <p>Shows the list of individual runtime and non-runtime permissions the app has requested. 61 */ 62 public final class AllAppPermissionsFragment extends SettingsWithLargeHeader { 63 64 private static final String LOG_TAG = "AllAppPermissionsFragment"; 65 66 private static final String KEY_OTHER = "other_perms"; 67 68 private List<AppPermissionGroup> mGroups; 69 newInstance(@onNull String packageName, @NonNull UserHandle userHandle)70 public static AllAppPermissionsFragment newInstance(@NonNull String packageName, 71 @NonNull UserHandle userHandle) { 72 return newInstance(packageName, null, userHandle); 73 } 74 newInstance(@onNull String packageName, @NonNull String filterGroup, @NonNull UserHandle userHandle)75 public static AllAppPermissionsFragment newInstance(@NonNull String packageName, 76 @NonNull String filterGroup, @NonNull UserHandle userHandle) { 77 AllAppPermissionsFragment instance = new AllAppPermissionsFragment(); 78 Bundle arguments = new Bundle(); 79 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 80 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, filterGroup); 81 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 82 instance.setArguments(arguments); 83 return instance; 84 } 85 86 @Override onStart()87 public void onStart() { 88 super.onStart(); 89 90 final ActionBar ab = getActivity().getActionBar(); 91 if (ab != null) { 92 // If we target a group make this look like app permissions. 93 if (getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) { 94 ab.setTitle(R.string.all_permissions); 95 } else { 96 ab.setTitle(R.string.app_permissions); 97 } 98 ab.setDisplayHomeAsUpEnabled(true); 99 } 100 101 setHasOptionsMenu(true); 102 103 updateUi(); 104 } 105 106 @Override onOptionsItemSelected(MenuItem item)107 public boolean onOptionsItemSelected(MenuItem item) { 108 switch (item.getItemId()) { 109 case android.R.id.home: { 110 getFragmentManager().popBackStack(); 111 return true; 112 } 113 } 114 return super.onOptionsItemSelected(item); 115 } 116 updateUi()117 private void updateUi() { 118 if (getPreferenceScreen() != null) { 119 getPreferenceScreen().removeAll(); 120 } 121 addPreferencesFromResource(R.xml.all_permissions); 122 PreferenceGroup otherGroup = (PreferenceGroup) findPreference(KEY_OTHER); 123 ArrayList<Preference> prefs = new ArrayList<>(); // Used for sorting. 124 prefs.add(otherGroup); 125 String pkg = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 126 String filterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 127 UserHandle userHandle = getArguments().getParcelable(Intent.EXTRA_USER); 128 otherGroup.removeAll(); 129 PackageManager pm = getContext().getPackageManager(); 130 131 try { 132 PackageInfo info = getActivity().createPackageContextAsUser(pkg, 0, userHandle) 133 .getPackageManager().getPackageInfo(pkg, PackageManager.GET_PERMISSIONS); 134 135 ApplicationInfo appInfo = info.applicationInfo; 136 final Drawable icon = Utils.getBadgedIcon(getContext(), appInfo); 137 final CharSequence label = appInfo.loadLabel(pm); 138 Intent infoIntent = null; 139 if (!getActivity().getIntent().getBooleanExtra( 140 AppPermissionsFragment.EXTRA_HIDE_INFO_BUTTON, false)) { 141 infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 142 .setData(Uri.fromParts("package", pkg, null)); 143 } 144 setHeader(icon, label, infoIntent, userHandle, false); 145 146 if (info.requestedPermissions != null) { 147 for (int i = 0; i < info.requestedPermissions.length; i++) { 148 PermissionInfo perm; 149 try { 150 perm = pm.getPermissionInfo(info.requestedPermissions[i], 0); 151 } catch (NameNotFoundException e) { 152 Log.e(LOG_TAG, 153 "Can't get permission info for " + info.requestedPermissions[i], e); 154 continue; 155 } 156 157 if ((perm.flags & PermissionInfo.FLAG_INSTALLED) == 0 158 || (perm.flags & PermissionInfo.FLAG_REMOVED) != 0) { 159 continue; 160 } 161 162 if (appInfo.isInstantApp() 163 && (perm.protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTANT) 164 == 0) { 165 continue; 166 } 167 if (appInfo.targetSdkVersion < Build.VERSION_CODES.M 168 && (perm.protectionLevel & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) 169 != 0) { 170 continue; 171 } 172 173 if ((perm.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) 174 == PermissionInfo.PROTECTION_DANGEROUS) { 175 PackageItemInfo group = getGroup(Utils.getGroupOfPermission(perm), pm); 176 if (group == null) { 177 group = perm; 178 } 179 // If we show a targeted group, then ignore everything else. 180 if (filterGroup != null && !group.name.equals(filterGroup)) { 181 continue; 182 } 183 PreferenceGroup pref = findOrCreate(group, pm, prefs); 184 pref.addPreference(getPreference(info, perm, group, pm)); 185 } else if (filterGroup == null) { 186 if ((perm.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) 187 == PermissionInfo.PROTECTION_NORMAL) { 188 PermissionGroupInfo group = getGroup(perm.group, pm); 189 otherGroup.addPreference(getPreference(info, 190 perm, group, pm)); 191 } 192 } 193 194 // If we show a targeted group, then don't show 'other' permissions. 195 if (filterGroup != null) { 196 getPreferenceScreen().removePreference(otherGroup); 197 } 198 } 199 } 200 } catch (NameNotFoundException e) { 201 Log.e(LOG_TAG, "Problem getting package info for " + pkg, e); 202 } 203 // Sort an ArrayList of the groups and then set the order from the sorting. 204 Collections.sort(prefs, new Comparator<Preference>() { 205 @Override 206 public int compare(Preference lhs, Preference rhs) { 207 String lKey = lhs.getKey(); 208 String rKey = rhs.getKey(); 209 if (lKey.equals(KEY_OTHER)) { 210 return 1; 211 } else if (rKey.equals(KEY_OTHER)) { 212 return -1; 213 } else if (Utils.isModernPermissionGroup(lKey) 214 != Utils.isModernPermissionGroup(rKey)) { 215 return Utils.isModernPermissionGroup(lKey) ? -1 : 1; 216 } 217 return lhs.getTitle().toString().compareTo(rhs.getTitle().toString()); 218 } 219 }); 220 for (int i = 0; i < prefs.size(); i++) { 221 prefs.get(i).setOrder(i); 222 } 223 } 224 getGroup(String group, PackageManager pm)225 private PermissionGroupInfo getGroup(String group, PackageManager pm) { 226 try { 227 return pm.getPermissionGroupInfo(group, 0); 228 } catch (NameNotFoundException e) { 229 return null; 230 } 231 } 232 findOrCreate(PackageItemInfo group, PackageManager pm, ArrayList<Preference> prefs)233 private PreferenceGroup findOrCreate(PackageItemInfo group, PackageManager pm, 234 ArrayList<Preference> prefs) { 235 PreferenceGroup pref = (PreferenceGroup) findPreference(group.name); 236 if (pref == null) { 237 pref = new PreferenceCategory(getPreferenceManager().getContext()); 238 pref.setKey(group.name); 239 pref.setTitle(group.loadLabel(pm)); 240 prefs.add(pref); 241 getPreferenceScreen().addPreference(pref); 242 } 243 return pref; 244 } 245 getPreference(PackageInfo packageInfo, PermissionInfo perm, PackageItemInfo group, PackageManager pm)246 private Preference getPreference(PackageInfo packageInfo, PermissionInfo perm, 247 PackageItemInfo group, PackageManager pm) { 248 final Preference pref; 249 Context context = getPreferenceManager().getContext(); 250 251 // We allow individual permission control for some permissions if review enabled 252 final boolean mutable = Utils.isPermissionIndividuallyControlled(getContext(), perm.name); 253 if (mutable) { 254 pref = new MyMultiTargetSwitchPreference(context, perm.name, 255 getPermissionForegroundGroup(packageInfo, perm.name)); 256 } else { 257 pref = new Preference(context); 258 } 259 260 Drawable icon = null; 261 if (perm.icon != 0) { 262 icon = perm.loadUnbadgedIcon(pm); 263 } else if (group != null && group.icon != 0) { 264 icon = group.loadUnbadgedIcon(pm); 265 } else { 266 icon = context.getDrawable(R.drawable.ic_perm_device_info); 267 } 268 pref.setIcon(Utils.applyTint(context, icon, android.R.attr.colorControlNormal)); 269 pref.setTitle(perm.loadSafeLabel(pm, 20000, TextUtils.SAFE_STRING_FLAG_TRIM)); 270 pref.setSingleLineTitle(false); 271 final CharSequence desc = perm.loadDescription(pm); 272 273 pref.setOnPreferenceClickListener((Preference preference) -> { 274 new AlertDialog.Builder(getContext()) 275 .setMessage(desc) 276 .setPositiveButton(android.R.string.ok, null) 277 .show(); 278 return mutable; 279 }); 280 281 return pref; 282 } 283 284 /** 285 * Return the (foreground-) {@link AppPermissionGroup group} a permission belongs to. 286 * 287 * <p>For foreground or non background-foreground permissions this returns the group 288 * {@link AppPermissionGroup} the permission is in. For background permisisons this returns 289 * the group the matching foreground 290 * 291 * @param packageInfo Package information about the app 292 * @param permission The permission that belongs to a group 293 * 294 * @return the group the permissions belongs to 295 */ getPermissionForegroundGroup(PackageInfo packageInfo, String permission)296 private AppPermissionGroup getPermissionForegroundGroup(PackageInfo packageInfo, 297 String permission) { 298 AppPermissionGroup appPermissionGroup = null; 299 if (mGroups != null) { 300 final int groupCount = mGroups.size(); 301 for (int i = 0; i < groupCount; i++) { 302 AppPermissionGroup currentPermissionGroup = mGroups.get(i); 303 if (currentPermissionGroup.hasPermission(permission)) { 304 appPermissionGroup = currentPermissionGroup; 305 break; 306 } 307 if (currentPermissionGroup.getBackgroundPermissions() != null 308 && currentPermissionGroup.getBackgroundPermissions().hasPermission( 309 permission)) { 310 appPermissionGroup = currentPermissionGroup.getBackgroundPermissions(); 311 break; 312 } 313 } 314 } 315 if (appPermissionGroup == null) { 316 appPermissionGroup = AppPermissionGroup.create( 317 getContext(), packageInfo, permission, false); 318 if (mGroups == null) { 319 mGroups = new ArrayList<>(); 320 } 321 mGroups.add(appPermissionGroup); 322 } 323 return appPermissionGroup; 324 } 325 326 private static final class MyMultiTargetSwitchPreference extends MultiTargetSwitchPreference { MyMultiTargetSwitchPreference(Context context, String permission, AppPermissionGroup appPermissionGroup)327 MyMultiTargetSwitchPreference(Context context, String permission, 328 AppPermissionGroup appPermissionGroup) { 329 super(context); 330 331 setChecked(appPermissionGroup.areRuntimePermissionsGranted( 332 new String[] {permission})); 333 334 setSwitchOnClickListener(v -> { 335 Switch switchView = (Switch) v; 336 if (switchView.isChecked()) { 337 appPermissionGroup.grantRuntimePermissions(false, 338 new String[]{permission}); 339 // We are granting a permission from a group but since this is an 340 // individual permission control other permissions in the group may 341 // be revoked, hence we need to mark them user fixed to prevent the 342 // app from requesting a non-granted permission and it being granted 343 // because another permission in the group is granted. This applies 344 // only to apps that support runtime permissions. 345 if (appPermissionGroup.doesSupportRuntimePermissions()) { 346 int grantedCount = 0; 347 String[] revokedPermissionsToFix = null; 348 final int permissionCount = appPermissionGroup.getPermissions().size(); 349 for (int i = 0; i < permissionCount; i++) { 350 Permission current = appPermissionGroup.getPermissions().get(i); 351 if (!current.isGrantedIncludingAppOp()) { 352 if (!current.isUserFixed()) { 353 revokedPermissionsToFix = ArrayUtils.appendString( 354 revokedPermissionsToFix, current.getName()); 355 } 356 } else { 357 grantedCount++; 358 } 359 } 360 if (revokedPermissionsToFix != null) { 361 // If some permissions were not granted then they should be fixed. 362 appPermissionGroup.revokeRuntimePermissions(true, 363 revokedPermissionsToFix); 364 } else if (appPermissionGroup.getPermissions().size() == grantedCount) { 365 // If all permissions are granted then they should not be fixed. 366 appPermissionGroup.grantRuntimePermissions(false); 367 } 368 } 369 } else { 370 appPermissionGroup.revokeRuntimePermissions(true, 371 new String[]{permission}); 372 // If we just revoked the last permission we need to clear 373 // the user fixed state as now the app should be able to 374 // request them at runtime if supported. 375 if (appPermissionGroup.doesSupportRuntimePermissions() 376 && !appPermissionGroup.areRuntimePermissionsGranted()) { 377 appPermissionGroup.revokeRuntimePermissions(false); 378 } 379 } 380 }); 381 } 382 } 383 } 384