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 package com.android.permissioncontroller.permission.ui.handheld; 17 18 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 19 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; 20 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED; 21 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 22 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED; 25 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED; 26 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND; 27 import static com.android.permissioncontroller.permission.ui.Category.ASK; 28 import static com.android.permissioncontroller.permission.ui.Category.DENIED; 29 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; 30 31 import android.Manifest; 32 import android.app.ActionBar; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.UserHandle; 40 import android.util.ArrayMap; 41 import android.util.Log; 42 import android.view.Menu; 43 import android.view.MenuInflater; 44 import android.view.MenuItem; 45 import android.view.View; 46 47 import androidx.annotation.NonNull; 48 import androidx.lifecycle.ViewModelProvider; 49 import androidx.preference.Preference; 50 import androidx.preference.PreferenceCategory; 51 52 import com.android.permissioncontroller.PermissionControllerStatsLog; 53 import com.android.permissioncontroller.R; 54 import com.android.permissioncontroller.permission.ui.Category; 55 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel; 56 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory; 57 import com.android.permissioncontroller.permission.utils.KotlinUtils; 58 import com.android.permissioncontroller.permission.utils.Utils; 59 import com.android.settingslib.HelpUtils; 60 import com.android.settingslib.utils.applications.AppUtils; 61 62 import java.text.Collator; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Random; 66 67 import kotlin.Pair; 68 69 /** 70 * Show and manage apps which request a single permission group. 71 * 72 * <p>Shows a list of apps which request at least on permission of this group. 73 */ 74 public final class PermissionAppsFragment extends SettingsWithLargeHeader { 75 76 private static final String KEY_SHOW_SYSTEM_PREFS = "_showSystem"; 77 private static final String CREATION_LOGGED_SYSTEM_PREFS = "_creationLogged"; 78 private static final String KEY_FOOTER = "_footer"; 79 private static final String KEY_EMPTY = "_empty"; 80 private static final String LOG_TAG = "PermissionAppsFragment"; 81 private static final String STORAGE_ALLOWED_FULL = "allowed_storage_full"; 82 private static final String STORAGE_ALLOWED_SCOPED = "allowed_storage_scoped"; 83 private static final int SHOW_LOAD_DELAY_MS = 200; 84 85 /** 86 * Create a bundle with the arguments needed by this fragment 87 * 88 * @param permGroupName The name of the permission group 89 * @param sessionId The current session ID 90 * @return A bundle with all of the args placed 91 */ createArgs(String permGroupName, long sessionId)92 public static Bundle createArgs(String permGroupName, long sessionId) { 93 Bundle arguments = new Bundle(); 94 arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName); 95 arguments.putLong(EXTRA_SESSION_ID, sessionId); 96 return arguments; 97 } 98 99 private MenuItem mShowSystemMenu; 100 private MenuItem mHideSystemMenu; 101 private String mPermGroupName; 102 private Collator mCollator; 103 private PermissionAppsViewModel mViewModel; 104 105 @Override onCreate(Bundle savedInstanceState)106 public void onCreate(Bundle savedInstanceState) { 107 super.onCreate(savedInstanceState); 108 109 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 110 if (mPermGroupName == null) { 111 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); 112 } 113 114 mCollator = Collator.getInstance( 115 getContext().getResources().getConfiguration().getLocales().get(0)); 116 117 PermissionAppsViewModelFactory factory = 118 new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName, 119 this, new Bundle()); 120 mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class); 121 122 mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded); 123 mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu); 124 mViewModel.getHasSystemAppsLiveData().observe(this, (Boolean hasSystem) -> 125 getActivity().invalidateOptionsMenu()); 126 127 if (!mViewModel.arePackagesLoaded()) { 128 Handler handler = new Handler(Looper.getMainLooper()); 129 handler.postDelayed(() -> { 130 if (!mViewModel.arePackagesLoaded()) { 131 setLoading(true /* loading */, false /* animate */); 132 } 133 }, SHOW_LOAD_DELAY_MS); 134 } 135 136 setHasOptionsMenu(true); 137 final ActionBar ab = getActivity().getActionBar(); 138 if (ab != null) { 139 ab.setDisplayHomeAsUpEnabled(true); 140 } 141 } 142 143 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)144 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 145 super.onCreateOptionsMenu(menu, inflater); 146 147 if (mViewModel.getHasSystemAppsLiveData().getValue()) { 148 mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, 149 R.string.menu_show_system); 150 mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, 151 R.string.menu_hide_system); 152 updateMenu(mViewModel.getShouldShowSystemLiveData().getValue()); 153 } 154 155 HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, 156 getClass().getName()); 157 } 158 159 @Override onOptionsItemSelected(MenuItem item)160 public boolean onOptionsItemSelected(MenuItem item) { 161 switch (item.getItemId()) { 162 case android.R.id.home: 163 mViewModel.updateShowSystem(false); 164 pressBack(this); 165 return true; 166 case MENU_SHOW_SYSTEM: 167 case MENU_HIDE_SYSTEM: 168 mViewModel.updateShowSystem(item.getItemId() == MENU_SHOW_SYSTEM); 169 break; 170 } 171 return super.onOptionsItemSelected(item); 172 } 173 updateMenu(Boolean showSystem)174 private void updateMenu(Boolean showSystem) { 175 if (showSystem == null) { 176 showSystem = false; 177 } 178 if (mShowSystemMenu != null && mHideSystemMenu != null) { 179 mShowSystemMenu.setVisible(!showSystem); 180 mHideSystemMenu.setVisible(showSystem); 181 } 182 } 183 184 @Override onViewCreated(View view, Bundle savedInstanceState)185 public void onViewCreated(View view, Bundle savedInstanceState) { 186 super.onViewCreated(view, savedInstanceState); 187 bindUi(this, mPermGroupName); 188 } 189 bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName)190 private static void bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName) { 191 Context context = fragment.getContext(); 192 if (context == null || fragment.getActivity() == null) { 193 return; 194 } 195 Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName); 196 197 CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName); 198 CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(context, groupName); 199 200 fragment.setHeader(icon, label, null, null, true); 201 fragment.setSummary(Utils.getPermissionGroupDescriptionString(fragment.getActivity(), 202 groupName, description), null); 203 fragment.getActivity().setTitle(label); 204 } 205 onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories)206 private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) { 207 boolean isStorage = mPermGroupName.equals(Manifest.permission_group.STORAGE); 208 if (getPreferenceScreen() == null) { 209 if (isStorage) { 210 addPreferencesFromResource(R.xml.allowed_denied_storage); 211 } else { 212 addPreferencesFromResource(R.xml.allowed_denied); 213 } 214 // Hide allowed foreground label by default, to avoid briefly showing it before updating 215 findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false); 216 } 217 Context context = getPreferenceManager().getContext(); 218 219 if (context == null || getActivity() == null || categories == null) { 220 return; 221 } 222 223 Map<String, Preference> existingPrefs = new ArrayMap<>(); 224 225 for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { 226 PreferenceCategory category = (PreferenceCategory) 227 getPreferenceScreen().getPreference(i); 228 category.setOrderingAsAdded(true); 229 int numPreferences = category.getPreferenceCount(); 230 for (int j = 0; j < numPreferences; j++) { 231 Preference preference = category.getPreference(j); 232 existingPrefs.put(preference.getKey(), preference); 233 } 234 category.removeAll(); 235 } 236 237 long viewIdForLogging = new Random().nextLong(); 238 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 239 240 Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue(); 241 if (!isStorage) { 242 if (showAlways != null && showAlways) { 243 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header); 244 } else { 245 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header); 246 } 247 } 248 249 for (Category grantCategory : categories.keySet()) { 250 List<Pair<String, UserHandle>> packages = categories.get(grantCategory); 251 PreferenceCategory category = findPreference(grantCategory.getCategoryName()); 252 253 254 // If this category is empty, and this isn't the "allowed" category of the storage 255 // permission, set up the empty preference. 256 if (packages.size() == 0 && (!isStorage || !grantCategory.equals(ALLOWED))) { 257 Preference empty = new Preference(context); 258 empty.setSelectable(false); 259 empty.setKey(category.getKey() + KEY_EMPTY); 260 if (grantCategory.equals(ALLOWED)) { 261 empty.setTitle(getString(R.string.no_apps_allowed)); 262 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 263 category.setVisible(false); 264 } else if (grantCategory.equals(ASK)) { 265 category.setVisible(false); 266 } else { 267 empty.setTitle(getString(R.string.no_apps_denied)); 268 } 269 category.addPreference(empty); 270 continue; 271 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 272 category.setVisible(true); 273 } else if (grantCategory.equals(ASK)) { 274 category.setVisible(true); 275 } 276 277 for (Pair<String, UserHandle> packageUserLabel : packages) { 278 String packageName = packageUserLabel.getFirst(); 279 UserHandle user = packageUserLabel.getSecond(); 280 281 String key = user + packageName; 282 283 if (isStorage && grantCategory.equals(ALLOWED)) { 284 category = mViewModel.packageHasFullStorage(packageName, user) 285 ? findPreference(STORAGE_ALLOWED_FULL) 286 : findPreference(STORAGE_ALLOWED_SCOPED); 287 } 288 289 Preference existingPref = existingPrefs.get(key); 290 if (existingPref != null) { 291 category.addPreference(existingPref); 292 continue; 293 } 294 295 SmartIconLoadPackagePermissionPreference pref = 296 new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(), 297 packageName, user, context); 298 pref.setKey(key); 299 pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), 300 packageName, user)); 301 pref.setOnPreferenceClickListener((Preference p) -> { 302 mViewModel.navigateToAppPermission(this, packageName, user, 303 AppPermissionFragment.createArgs(packageName, null, mPermGroupName, 304 user, getClass().getName(), sessionId, 305 grantCategory.getCategoryName())); 306 return true; 307 }); 308 pref.setTitleContentDescription(AppUtils.getAppContentDescription(context, 309 packageName, user.getIdentifier())); 310 311 category.addPreference(pref); 312 if (!mViewModel.getCreationLogged()) { 313 logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging, 314 grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND), 315 grantCategory.equals(DENIED)); 316 } 317 } 318 319 if (isStorage && grantCategory.equals(ALLOWED)) { 320 PreferenceCategory full = findPreference(STORAGE_ALLOWED_FULL); 321 PreferenceCategory scoped = findPreference(STORAGE_ALLOWED_SCOPED); 322 if (full.getPreferenceCount() == 0) { 323 Preference empty = new Preference(context); 324 empty.setSelectable(false); 325 empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY); 326 empty.setTitle(getString(R.string.no_apps_allowed_full)); 327 full.addPreference(empty); 328 } 329 330 if (scoped.getPreferenceCount() == 0) { 331 Preference empty = new Preference(context); 332 empty.setSelectable(false); 333 empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY); 334 empty.setTitle(getString(R.string.no_apps_allowed_scoped)); 335 scoped.addPreference(empty); 336 } 337 KotlinUtils.INSTANCE.sortPreferenceGroup(full, this::comparePreference, false); 338 KotlinUtils.INSTANCE.sortPreferenceGroup(scoped, this::comparePreference, false); 339 } else { 340 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false); 341 } 342 } 343 344 mViewModel.setCreationLogged(true); 345 346 setLoading(false /* loading */, true /* animate */); 347 } 348 comparePreference(Preference lhs, Preference rhs)349 private int comparePreference(Preference lhs, Preference rhs) { 350 int result = mCollator.compare(lhs.getTitle().toString(), 351 rhs.getTitle().toString()); 352 if (result == 0) { 353 result = lhs.getKey().compareTo(rhs.getKey()); 354 } 355 return result; 356 } 357 logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, boolean isAllowed, boolean isAllowedForeground, boolean isDenied)358 private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, 359 boolean isAllowed, boolean isAllowedForeground, boolean isDenied) { 360 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0); 361 362 int category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__UNDEFINED; 363 if (isAllowed) { 364 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED; 365 } else if (isAllowedForeground) { 366 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND; 367 } else if (isDenied) { 368 category = PERMISSION_APPS_FRAGMENT_VIEWED__CATEGORY__DENIED; 369 } 370 371 Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(), 372 packageName, user); 373 if (uid == null) { 374 return; 375 } 376 377 PermissionControllerStatsLog.write(PERMISSION_APPS_FRAGMENT_VIEWED, sessionId, viewId, 378 mPermGroupName, uid, packageName, category); 379 Log.v(LOG_TAG, "PermissionAppsFragment created with sessionId=" + sessionId 380 + " permissionGroupName=" + mPermGroupName + " appUid=" 381 + uid + " packageName=" + packageName 382 + " category=" + category); 383 } 384 } 385