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