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