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