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