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