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.permission.ui.Category.ALLOWED; 21 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND; 22 import static com.android.permissioncontroller.permission.ui.Category.ASK; 23 import static com.android.permissioncontroller.permission.ui.Category.DENIED; 24 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack; 25 import static com.android.permissioncontroller.permission.ui.handheld.dashboard.UtilsKt.shouldShowPermissionsDashboard; 26 27 import android.Manifest; 28 import android.app.ActionBar; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.graphics.drawable.Drawable; 32 import android.os.Build; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.UserHandle; 37 import android.util.ArrayMap; 38 import android.view.Menu; 39 import android.view.MenuInflater; 40 import android.view.MenuItem; 41 import android.view.View; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.RequiresApi; 45 import androidx.lifecycle.ViewModelProvider; 46 import androidx.preference.Preference; 47 import androidx.preference.PreferenceCategory; 48 49 import com.android.modules.utils.build.SdkLevel; 50 import com.android.permissioncontroller.R; 51 import com.android.permissioncontroller.permission.model.AppPermissionUsage; 52 import com.android.permissioncontroller.permission.model.PermissionUsages; 53 import com.android.permissioncontroller.permission.ui.Category; 54 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity; 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.ArrayList; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Random; 67 68 import kotlin.Pair; 69 70 /** 71 * Show and manage apps which request a single permission group. 72 * 73 * <p>Shows a list of apps which request at least on permission of this group. 74 */ 75 public final class PermissionAppsFragment extends SettingsWithLargeHeader implements 76 PermissionUsages.PermissionsUsagesChangeCallback { 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 private PermissionUsages mPermissionUsages; 109 private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>(); 110 111 @Override onCreate(Bundle savedInstanceState)112 public void onCreate(Bundle savedInstanceState) { 113 super.onCreate(savedInstanceState); 114 115 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME); 116 if (mPermGroupName == null) { 117 mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME); 118 } 119 120 mCollator = Collator.getInstance( 121 getContext().getResources().getConfiguration().getLocales().get(0)); 122 123 PermissionAppsViewModelFactory factory = 124 new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName, 125 this, new Bundle()); 126 mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class); 127 128 mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded); 129 mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu); 130 mViewModel.getHasSystemAppsLiveData().observe(this, (Boolean hasSystem) -> 131 getActivity().invalidateOptionsMenu()); 132 133 if (!mViewModel.arePackagesLoaded()) { 134 Handler handler = new Handler(Looper.getMainLooper()); 135 handler.postDelayed(() -> { 136 if (!mViewModel.arePackagesLoaded()) { 137 setLoading(true /* loading */, false /* animate */); 138 } 139 }, SHOW_LOAD_DELAY_MS); 140 } 141 142 setHasOptionsMenu(true); 143 final ActionBar ab = getActivity().getActionBar(); 144 if (ab != null) { 145 ab.setDisplayHomeAsUpEnabled(true); 146 } 147 148 // If the build type is below S, the app ops for permission usage can't be found. Thus, we 149 // shouldn't load permission usages, for them. 150 if (SdkLevel.isAtLeastS()) { 151 Context context = getPreferenceManager().getContext(); 152 mPermissionUsages = new PermissionUsages(context); 153 154 long filterTimeBeginMillis = mViewModel.getFilterTimeBeginMillis(); 155 mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE, 156 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), 157 false, false, this, false); 158 } 159 } 160 161 @Override 162 @RequiresApi(Build.VERSION_CODES.S) onPermissionUsagesChanged()163 public void onPermissionUsagesChanged() { 164 if (mPermissionUsages.getUsages().isEmpty()) { 165 return; 166 } 167 if (getContext() == null) { 168 // Async result has come in after our context is gone. 169 return; 170 } 171 172 mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages()); 173 onPackagesLoaded(mViewModel.getCategorizedAppsLiveData().getValue()); 174 } 175 176 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)177 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 178 super.onCreateOptionsMenu(menu, inflater); 179 180 if (mViewModel.getHasSystemAppsLiveData().getValue()) { 181 mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, 182 R.string.menu_show_system); 183 mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, 184 R.string.menu_hide_system); 185 updateMenu(mViewModel.getShouldShowSystemLiveData().getValue()); 186 } 187 188 if (shouldShowPermissionsDashboard()) { 189 menu.add(Menu.NONE, MENU_PERMISSION_USAGE, Menu.NONE, R.string.permission_usage_title); 190 } 191 192 if (!SdkLevel.isAtLeastS()) { 193 HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions, 194 getClass().getName()); 195 } 196 } 197 198 @Override onOptionsItemSelected(MenuItem item)199 public boolean onOptionsItemSelected(MenuItem item) { 200 switch (item.getItemId()) { 201 case android.R.id.home: 202 mViewModel.updateShowSystem(false); 203 pressBack(this); 204 return true; 205 case MENU_SHOW_SYSTEM: 206 case MENU_HIDE_SYSTEM: 207 mViewModel.updateShowSystem(item.getItemId() == MENU_SHOW_SYSTEM); 208 break; 209 case MENU_PERMISSION_USAGE: 210 getActivity().startActivity(new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE) 211 .setClass(getContext(), ManagePermissionsActivity.class) 212 .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName)); 213 return true; 214 } 215 return super.onOptionsItemSelected(item); 216 } 217 updateMenu(Boolean showSystem)218 private void updateMenu(Boolean showSystem) { 219 if (showSystem == null) { 220 showSystem = false; 221 } 222 if (mShowSystemMenu != null && mHideSystemMenu != null) { 223 mShowSystemMenu.setVisible(!showSystem); 224 mHideSystemMenu.setVisible(showSystem); 225 } 226 } 227 228 @Override onViewCreated(View view, Bundle savedInstanceState)229 public void onViewCreated(View view, Bundle savedInstanceState) { 230 super.onViewCreated(view, savedInstanceState); 231 bindUi(this, mPermGroupName); 232 } 233 bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName)234 private static void bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName) { 235 Context context = fragment.getContext(); 236 if (context == null || fragment.getActivity() == null) { 237 return; 238 } 239 Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName); 240 241 CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName); 242 CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(context, groupName); 243 244 fragment.setHeader(icon, label, null, null, true); 245 fragment.setSummary(Utils.getPermissionGroupDescriptionString(fragment.getActivity(), 246 groupName, description), null); 247 fragment.getActivity().setTitle(label); 248 } 249 onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories)250 private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) { 251 boolean isStorage = mPermGroupName.equals(Manifest.permission_group.STORAGE); 252 if (getPreferenceScreen() == null) { 253 if (isStorage) { 254 addPreferencesFromResource(R.xml.allowed_denied_storage); 255 } else { 256 addPreferencesFromResource(R.xml.allowed_denied); 257 } 258 // Hide allowed foreground label by default, to avoid briefly showing it before updating 259 findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false); 260 } 261 Context context = getPreferenceManager().getContext(); 262 263 if (context == null || getActivity() == null || categories == null) { 264 return; 265 } 266 267 Map<String, Preference> existingPrefs = new ArrayMap<>(); 268 269 for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { 270 PreferenceCategory category = (PreferenceCategory) 271 getPreferenceScreen().getPreference(i); 272 category.setOrderingAsAdded(true); 273 int numPreferences = category.getPreferenceCount(); 274 for (int j = 0; j < numPreferences; j++) { 275 Preference preference = category.getPreference(j); 276 existingPrefs.put(preference.getKey(), preference); 277 } 278 category.removeAll(); 279 } 280 281 long viewIdForLogging = new Random().nextLong(); 282 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID); 283 284 Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue(); 285 if (!isStorage) { 286 if (showAlways != null && showAlways) { 287 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header); 288 } else { 289 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header); 290 } 291 } 292 293 // A mapping of user + packageName to their last access timestamps for the permission group. 294 Map<String, Long> groupUsageLastAccessTime = 295 mViewModel.extractGroupUsageLastAccessTime(mAppPermissionUsages); 296 297 for (Category grantCategory : categories.keySet()) { 298 List<Pair<String, UserHandle>> packages = categories.get(grantCategory); 299 PreferenceCategory category = findPreference(grantCategory.getCategoryName()); 300 301 302 // If this category is empty, and this isn't the "allowed" category of the storage 303 // permission, set up the empty preference. 304 if (packages.size() == 0 && (!isStorage || !grantCategory.equals(ALLOWED))) { 305 Preference empty = new Preference(context); 306 empty.setSelectable(false); 307 empty.setKey(category.getKey() + KEY_EMPTY); 308 if (grantCategory.equals(ALLOWED)) { 309 empty.setTitle(getString(R.string.no_apps_allowed)); 310 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 311 category.setVisible(false); 312 } else if (grantCategory.equals(ASK)) { 313 category.setVisible(false); 314 } else { 315 empty.setTitle(getString(R.string.no_apps_denied)); 316 } 317 category.addPreference(empty); 318 continue; 319 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) { 320 category.setVisible(true); 321 } else if (grantCategory.equals(ASK)) { 322 category.setVisible(true); 323 } 324 325 for (Pair<String, UserHandle> packageUserLabel : packages) { 326 String packageName = packageUserLabel.getFirst(); 327 UserHandle user = packageUserLabel.getSecond(); 328 329 String key = user + packageName; 330 331 Long lastAccessTime = groupUsageLastAccessTime.get(key); 332 Pair<String, Integer> summaryTimestamp = Utils 333 .getPermissionLastAccessSummaryTimestamp( 334 lastAccessTime, context, mPermGroupName); 335 336 if (isStorage && grantCategory.equals(ALLOWED)) { 337 category = mViewModel.packageHasFullStorage(packageName, user) 338 ? findPreference(STORAGE_ALLOWED_FULL) 339 : findPreference(STORAGE_ALLOWED_SCOPED); 340 } 341 342 Preference existingPref = existingPrefs.get(key); 343 if (existingPref != null) { 344 updatePreferenceSummary(existingPref, summaryTimestamp); 345 category.addPreference(existingPref); 346 continue; 347 } 348 349 SmartIconLoadPackagePermissionPreference pref = 350 new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(), 351 packageName, user, context); 352 pref.setKey(key); 353 pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), 354 packageName, user)); 355 pref.setOnPreferenceClickListener((Preference p) -> { 356 mViewModel.navigateToAppPermission(this, packageName, user, 357 AppPermissionFragment.createArgs(packageName, null, mPermGroupName, 358 user, getClass().getName(), sessionId, 359 grantCategory.getCategoryName())); 360 return true; 361 }); 362 pref.setTitleContentDescription(AppUtils.getAppContentDescription(context, 363 packageName, user.getIdentifier())); 364 365 updatePreferenceSummary(pref, summaryTimestamp); 366 367 category.addPreference(pref); 368 if (!mViewModel.getCreationLogged()) { 369 logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging, 370 grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND), 371 grantCategory.equals(DENIED)); 372 } 373 } 374 375 if (isStorage && grantCategory.equals(ALLOWED)) { 376 PreferenceCategory full = findPreference(STORAGE_ALLOWED_FULL); 377 PreferenceCategory scoped = findPreference(STORAGE_ALLOWED_SCOPED); 378 if (full.getPreferenceCount() == 0) { 379 Preference empty = new Preference(context); 380 empty.setSelectable(false); 381 empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY); 382 empty.setTitle(getString(R.string.no_apps_allowed_full)); 383 full.addPreference(empty); 384 } 385 386 if (scoped.getPreferenceCount() == 0) { 387 Preference empty = new Preference(context); 388 empty.setSelectable(false); 389 empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY); 390 empty.setTitle(getString(R.string.no_apps_allowed_scoped)); 391 scoped.addPreference(empty); 392 } 393 KotlinUtils.INSTANCE.sortPreferenceGroup(full, this::comparePreference, false); 394 KotlinUtils.INSTANCE.sortPreferenceGroup(scoped, this::comparePreference, false); 395 } else { 396 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false); 397 } 398 } 399 400 mViewModel.setCreationLogged(true); 401 402 setLoading(false /* loading */, true /* animate */); 403 } 404 updatePreferenceSummary(Preference preference, Pair<String, Integer> summaryTimestamp)405 private void updatePreferenceSummary(Preference preference, 406 Pair<String, Integer> summaryTimestamp) { 407 String summary = mViewModel.getPreferenceSummary(getResources(), summaryTimestamp); 408 if (!summary.isEmpty()) { 409 preference.setSummary(summary); 410 } 411 } 412 413 comparePreference(Preference lhs, Preference rhs)414 private int comparePreference(Preference lhs, Preference rhs) { 415 int result = mCollator.compare(lhs.getTitle().toString(), 416 rhs.getTitle().toString()); 417 if (result == 0) { 418 result = lhs.getKey().compareTo(rhs.getKey()); 419 } 420 return result; 421 } 422 logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, boolean isAllowed, boolean isAllowedForeground, boolean isDenied)423 private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, 424 boolean isAllowed, boolean isAllowedForeground, boolean isDenied) { 425 long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0); 426 mViewModel.logPermissionAppsFragmentCreated(packageName, user, viewId, isAllowed, 427 isAllowedForeground, isDenied, sessionId, getActivity().getApplication(), 428 mPermGroupName, LOG_TAG); 429 } 430 } 431