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.permissioncontroller.permission.ui.handheld; 18 19 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; 20 21 import android.app.ActionBar; 22 import android.app.AlertDialog; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.graphics.drawable.Drawable; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.UserHandle; 29 import android.provider.Settings; 30 import android.util.Log; 31 import android.view.MenuItem; 32 import android.widget.Switch; 33 import android.widget.Toast; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.lifecycle.ViewModelProvider; 38 import androidx.preference.Preference; 39 import androidx.preference.PreferenceCategory; 40 import androidx.preference.PreferenceGroup; 41 42 import com.android.permissioncontroller.R; 43 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData; 44 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 45 import com.android.permissioncontroller.permission.model.Permission; 46 import com.android.permissioncontroller.permission.ui.model.AllAppPermissionsViewModel; 47 import com.android.permissioncontroller.permission.ui.model.AllAppPermissionsViewModelFactory; 48 import com.android.permissioncontroller.permission.utils.ArrayUtils; 49 import com.android.permissioncontroller.permission.utils.KotlinUtils; 50 import com.android.permissioncontroller.permission.utils.PermissionMapping; 51 import com.android.permissioncontroller.permission.utils.Utils; 52 53 import java.text.Collator; 54 import java.util.List; 55 import java.util.Map; 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 AllAppPermissionsViewModel mViewModel; 69 private Collator mCollator; 70 private String mPackageName; 71 private String mFilterGroup; 72 private UserHandle mUser; 73 74 /** 75 * Create a bundle with the arguments needed by this fragment 76 * 77 * @param packageName The name of the package 78 * @param filterGroup An optional group to filter out permissions not in the group 79 * @param userHandle The user of this package 80 * @return A bundle with all of the args placed 81 */ createArgs(@onNull String packageName, @Nullable String filterGroup, @NonNull UserHandle userHandle)82 public static Bundle createArgs(@NonNull String packageName, @Nullable String filterGroup, 83 @NonNull UserHandle userHandle) { 84 Bundle arguments = new Bundle(); 85 arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName); 86 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, filterGroup); 87 arguments.putParcelable(Intent.EXTRA_USER, userHandle); 88 return arguments; 89 } 90 91 /** 92 * Create a bundle with the arguments needed by this fragment 93 * 94 * @param packageName The name of the package 95 * @param userHandle The user of this package 96 * @return A bundle with all of the args placed 97 */ createArgs(@onNull String packageName, @NonNull UserHandle userHandle)98 public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle) { 99 return createArgs(packageName, null, userHandle); 100 } 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 super.onCreate(savedInstanceState); 105 mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME); 106 mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 107 mUser = getArguments().getParcelable(Intent.EXTRA_USER); 108 if (mPackageName == null || mUser == null) { 109 Log.e(LOG_TAG, "Missing required argument EXTRA_PACKAGE_NAME or " 110 + "EXTRA_USER"); 111 pressBack(this); 112 } 113 114 AllAppPermissionsViewModelFactory factory = new AllAppPermissionsViewModelFactory( 115 mPackageName, mUser, mFilterGroup); 116 117 mViewModel = new ViewModelProvider(this, factory).get(AllAppPermissionsViewModel.class); 118 mViewModel.getAllPackagePermissionsLiveData().observe(this, this::updateUi); 119 120 mCollator = Collator.getInstance( 121 getContext().getResources().getConfiguration().getLocales().get(0)); 122 } 123 124 @Override onStart()125 public void onStart() { 126 super.onStart(); 127 128 final ActionBar ab = getActivity().getActionBar(); 129 if (ab != null) { 130 ab.setDisplayHomeAsUpEnabled(true); 131 } 132 133 // If we target a group make this look like app permissions. 134 if (getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) { 135 getActivity().setTitle(R.string.all_permissions); 136 } else { 137 getActivity().setTitle(R.string.app_permissions); 138 } 139 140 setHasOptionsMenu(true); 141 } 142 143 @Override onOptionsItemSelected(MenuItem item)144 public boolean onOptionsItemSelected(MenuItem item) { 145 switch (item.getItemId()) { 146 case android.R.id.home: { 147 pressBack(this); 148 return true; 149 } 150 } 151 return super.onOptionsItemSelected(item); 152 } 153 updateUi(Map<String, List<String>> groupMap)154 private void updateUi(Map<String, List<String>> groupMap) { 155 if (groupMap == null && mViewModel.getAllPackagePermissionsLiveData().isInitialized()) { 156 Toast.makeText( 157 getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show(); 158 Log.w(LOG_TAG, "invalid package " + mPackageName); 159 pressBack(this); 160 return; 161 } 162 163 if (getPreferenceScreen() == null) { 164 addPreferencesFromResource(R.xml.all_permissions); 165 } 166 167 PreferenceGroup otherGroup = findPreference(KEY_OTHER); 168 otherGroup.removeAll(); 169 Preference header = findPreference(HEADER_KEY); 170 171 getPreferenceScreen().removeAll(); 172 getPreferenceScreen().addPreference(otherGroup); 173 getPreferenceScreen().addPreference(header); 174 175 Drawable icon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(), 176 mPackageName, mUser); 177 CharSequence label = KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), 178 mPackageName, mUser); 179 Intent infoIntent = null; 180 if (!getActivity().getIntent().getBooleanExtra( 181 AppPermissionGroupsFragment.EXTRA_HIDE_INFO_BUTTON, false)) { 182 infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 183 .setData(Uri.fromParts("package", mPackageName, null)); 184 } 185 setHeader(icon, label, infoIntent, mUser, false); 186 if (groupMap != null) { 187 for (String groupName : groupMap.keySet()) { 188 List<String> permissions = groupMap.get(groupName); 189 if (permissions == null || permissions.isEmpty()) { 190 continue; 191 } 192 193 PreferenceGroup pref = findOrCreatePrefGroup(groupName); 194 for (String permName : permissions) { 195 pref.addPreference(getPreference(permName, groupName)); 196 } 197 } 198 } 199 if (otherGroup.getPreferenceCount() == 0) { 200 otherGroup.setVisible(false); 201 } else { 202 otherGroup.setVisible(true); 203 } 204 KotlinUtils.INSTANCE.sortPreferenceGroup(getPreferenceScreen(), this::comparePreferences, 205 true 206 ); 207 208 setLoading(false, true); 209 } 210 comparePreferences(Preference lhs, Preference rhs)211 private int comparePreferences(Preference lhs, Preference rhs) { 212 String lKey = lhs.getKey(); 213 String rKey = rhs.getKey(); 214 if (lKey.equals(KEY_OTHER)) { 215 return 1; 216 } else if (rKey.equals(KEY_OTHER)) { 217 return -1; 218 } 219 if (PermissionMapping.isPlatformPermissionGroup(lKey) 220 != PermissionMapping.isPlatformPermissionGroup(rKey)) { 221 return PermissionMapping.isPlatformPermissionGroup(lKey) ? -1 : 1; 222 } 223 return mCollator.compare(lhs.getTitle().toString(), rhs.getTitle().toString()); 224 } 225 findOrCreatePrefGroup(String groupName)226 private PreferenceGroup findOrCreatePrefGroup(String groupName) { 227 if (groupName.equals(PackagePermissionsLiveData.NON_RUNTIME_NORMAL_PERMS)) { 228 return findPreference(KEY_OTHER); 229 } 230 PreferenceGroup pref = findPreference(groupName); 231 if (pref == null) { 232 pref = new PreferenceCategory(getPreferenceManager().getContext()); 233 pref.setKey(groupName); 234 pref.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), groupName)); 235 getPreferenceScreen().addPreference(pref); 236 } else { 237 pref.removeAll(); 238 } 239 return pref; 240 } 241 getPreference(String permName, String groupName)242 private Preference getPreference(String permName, String groupName) { 243 final Preference pref; 244 Context context = getPreferenceManager().getContext(); 245 246 // We allow individual permission control for some permissions if review enabled 247 final boolean mutable = Utils.isPermissionIndividuallyControlled(getContext(), 248 permName); 249 if (mutable) { 250 AppPermissionGroup appPermGroup = AppPermissionGroup.create( 251 getActivity().getApplication(), mPackageName, groupName, mUser, false); 252 pref = new MyMultiTargetSwitchPreference(context, permName, appPermGroup); 253 } else { 254 pref = new Preference(context); 255 } 256 pref.setIcon(KotlinUtils.INSTANCE.getPermInfoIcon(context, permName)); 257 pref.setTitle(KotlinUtils.INSTANCE.getPermInfoLabel(context, permName)); 258 pref.setSingleLineTitle(false); 259 final CharSequence desc = KotlinUtils.INSTANCE.getPermInfoDescription(context, 260 permName); 261 262 pref.setOnPreferenceClickListener((Preference preference) -> { 263 new AlertDialog.Builder(getContext()) 264 .setMessage(desc) 265 .setPositiveButton(android.R.string.ok, null) 266 .show(); 267 return mutable; 268 }); 269 270 return pref; 271 } 272 273 private static final class MyMultiTargetSwitchPreference extends MultiTargetSwitchPreference { MyMultiTargetSwitchPreference(Context context, String permission, AppPermissionGroup appPermissionGroup)274 MyMultiTargetSwitchPreference(Context context, String permission, 275 AppPermissionGroup appPermissionGroup) { 276 super(context); 277 278 setChecked(appPermissionGroup.areRuntimePermissionsGranted( 279 new String[]{permission})); 280 281 setSwitchOnClickListener(v -> { 282 Switch switchView = (Switch) v; 283 if (switchView.isChecked()) { 284 appPermissionGroup.grantRuntimePermissions(true, false, 285 new String[]{permission}); 286 // We are granting a permission from a group but since this is an 287 // individual permission control other permissions in the group may 288 // be revoked, hence we need to mark them user fixed to prevent the 289 // app from requesting a non-granted permission and it being granted 290 // because another permission in the group is granted. This applies 291 // only to apps that support runtime permissions. 292 if (appPermissionGroup.doesSupportRuntimePermissions()) { 293 int grantedCount = 0; 294 String[] revokedPermissionsToFix = null; 295 final int permissionCount = appPermissionGroup.getPermissions().size(); 296 for (int i = 0; i < permissionCount; i++) { 297 Permission current = appPermissionGroup.getPermissions().get(i); 298 if (!current.isGrantedIncludingAppOp()) { 299 if (!current.isUserFixed()) { 300 revokedPermissionsToFix = ArrayUtils.appendString( 301 revokedPermissionsToFix, current.getName()); 302 } 303 } else { 304 grantedCount++; 305 } 306 } 307 if (revokedPermissionsToFix != null) { 308 // If some permissions were not granted then they should be fixed. 309 appPermissionGroup.revokeRuntimePermissions(true, 310 revokedPermissionsToFix); 311 } else if (appPermissionGroup.getPermissions().size() == grantedCount) { 312 // If all permissions are granted then they should not be fixed. 313 appPermissionGroup.grantRuntimePermissions(true, false); 314 } 315 } 316 } else { 317 appPermissionGroup.revokeRuntimePermissions(true, 318 new String[]{permission}); 319 // If we just revoked the last permission we need to clear 320 // the user fixed state as now the app should be able to 321 // request them at runtime if supported. 322 if (appPermissionGroup.doesSupportRuntimePermissions() 323 && !appPermissionGroup.areRuntimePermissionsGranted()) { 324 appPermissionGroup.revokeRuntimePermissions(false); 325 } 326 } 327 }); 328 } 329 } 330 } 331