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