AppStorageSettings.java revision 025883da78cdb0fbf70bcd3c2c2d32169c25c7f7
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 17package com.android.settings.applications; 18 19import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; 20import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; 21 22import android.app.ActivityManager; 23import android.app.AlertDialog; 24import android.app.AppGlobals; 25import android.app.GrantedUriPermission; 26import android.app.LoaderManager; 27import android.content.Context; 28import android.content.DialogInterface; 29import android.content.Intent; 30import android.content.Loader; 31import android.content.UriPermission; 32import android.content.pm.ApplicationInfo; 33import android.content.pm.IPackageDataObserver; 34import android.content.pm.PackageManager; 35import android.content.pm.ProviderInfo; 36import android.os.Bundle; 37import android.os.Handler; 38import android.os.Message; 39import android.os.RemoteException; 40import android.os.UserHandle; 41import android.os.storage.StorageManager; 42import android.os.storage.VolumeInfo; 43import android.support.annotation.VisibleForTesting; 44import android.support.v7.preference.Preference; 45import android.support.v7.preference.PreferenceCategory; 46import android.util.Log; 47import android.util.MutableInt; 48import android.view.View; 49import android.view.View.OnClickListener; 50import android.widget.Button; 51 52import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 53import com.android.settings.R; 54import com.android.settings.Utils; 55import com.android.settings.deviceinfo.StorageWizardMoveConfirm; 56import com.android.settings.widget.ActionButtonPreference; 57import com.android.settingslib.RestrictedLockUtils; 58import com.android.settingslib.applications.ApplicationsState.Callbacks; 59import com.android.settingslib.applications.StorageStatsSource; 60import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; 61 62import java.util.Collections; 63import java.util.List; 64import java.util.Map; 65import java.util.Objects; 66import java.util.TreeMap; 67 68public class AppStorageSettings extends AppInfoWithHeader 69 implements OnClickListener, Callbacks, DialogInterface.OnClickListener, 70 LoaderManager.LoaderCallbacks<AppStorageStats> { 71 private static final String TAG = AppStorageSettings.class.getSimpleName(); 72 73 //internal constants used in Handler 74 private static final int OP_SUCCESSFUL = 1; 75 private static final int OP_FAILED = 2; 76 private static final int MSG_CLEAR_USER_DATA = 1; 77 private static final int MSG_CLEAR_CACHE = 3; 78 79 // invalid size value used initially and also when size retrieval through PackageManager 80 // fails for whatever reason 81 private static final int SIZE_INVALID = -1; 82 83 // Result code identifiers 84 public static final int REQUEST_MANAGE_SPACE = 2; 85 86 private static final int DLG_CLEAR_DATA = DLG_BASE + 1; 87 private static final int DLG_CANNOT_CLEAR_DATA = DLG_BASE + 2; 88 89 private static final String KEY_STORAGE_USED = "storage_used"; 90 private static final String KEY_CHANGE_STORAGE = "change_storage_button"; 91 private static final String KEY_STORAGE_SPACE = "storage_space"; 92 private static final String KEY_STORAGE_CATEGORY = "storage_category"; 93 94 private static final String KEY_TOTAL_SIZE = "total_size"; 95 private static final String KEY_APP_SIZE = "app_size"; 96 private static final String KEY_DATA_SIZE = "data_size"; 97 private static final String KEY_CACHE_SIZE = "cache_size"; 98 99 private static final String KEY_HEADER_BUTTONS = "header_view"; 100 101 private static final String KEY_URI_CATEGORY = "uri_category"; 102 private static final String KEY_CLEAR_URI = "clear_uri_button"; 103 104 private static final String KEY_CACHE_CLEARED = "cache_cleared"; 105 private static final String KEY_DATA_CLEARED = "data_cleared"; 106 107 // Views related to cache info 108 @VisibleForTesting 109 ActionButtonPreference mButtonsPref; 110 111 private Preference mStorageUsed; 112 private Button mChangeStorageButton; 113 114 // Views related to URI permissions 115 private Button mClearUriButton; 116 private LayoutPreference mClearUri; 117 private PreferenceCategory mUri; 118 119 private boolean mCanClearData = true; 120 private boolean mCacheCleared; 121 private boolean mDataCleared; 122 123 @VisibleForTesting 124 AppStorageSizesController mSizeController; 125 126 private ClearCacheObserver mClearCacheObserver; 127 private ClearUserDataObserver mClearDataObserver; 128 129 private VolumeInfo[] mCandidates; 130 private AlertDialog.Builder mDialogBuilder; 131 private ApplicationInfo mInfo; 132 133 @Override 134 public void onCreate(Bundle savedInstanceState) { 135 super.onCreate(savedInstanceState); 136 if (savedInstanceState != null) { 137 mCacheCleared = savedInstanceState.getBoolean(KEY_CACHE_CLEARED, false); 138 mDataCleared = savedInstanceState.getBoolean(KEY_DATA_CLEARED, false); 139 mCacheCleared = mCacheCleared || mDataCleared; 140 } 141 142 addPreferencesFromResource(R.xml.app_storage_settings); 143 setupViews(); 144 initMoveDialog(); 145 } 146 147 @Override 148 public void onResume() { 149 super.onResume(); 150 updateSize(); 151 } 152 153 @Override 154 public void onSaveInstanceState(Bundle outState) { 155 super.onSaveInstanceState(outState); 156 outState.putBoolean(KEY_CACHE_CLEARED, mCacheCleared); 157 outState.putBoolean(KEY_DATA_CLEARED, mDataCleared); 158 } 159 160 private void setupViews() { 161 // Set default values on sizes 162 mSizeController = new AppStorageSizesController.Builder() 163 .setTotalSizePreference(findPreference(KEY_TOTAL_SIZE)) 164 .setAppSizePreference(findPreference(KEY_APP_SIZE)) 165 .setDataSizePreference(findPreference(KEY_DATA_SIZE)) 166 .setCacheSizePreference(findPreference(KEY_CACHE_SIZE)) 167 .setComputingString(R.string.computing_size) 168 .setErrorString(R.string.invalid_size_value) 169 .build(); 170 mButtonsPref = ((ActionButtonPreference) findPreference(KEY_HEADER_BUTTONS)) 171 .setButton1Positive(false) 172 .setButton2Positive(false); 173 174 mStorageUsed = findPreference(KEY_STORAGE_USED); 175 mChangeStorageButton = (Button) ((LayoutPreference) findPreference(KEY_CHANGE_STORAGE)) 176 .findViewById(R.id.button); 177 mChangeStorageButton.setText(R.string.change); 178 mChangeStorageButton.setOnClickListener(this); 179 180 // Cache section 181 mButtonsPref.setButton2Text(R.string.clear_cache_btn_text); 182 183 // URI permissions section 184 mUri = (PreferenceCategory) findPreference(KEY_URI_CATEGORY); 185 mClearUri = (LayoutPreference) mUri.findPreference(KEY_CLEAR_URI); 186 mClearUriButton = (Button) mClearUri.findViewById(R.id.button); 187 mClearUriButton.setText(R.string.clear_uri_btn_text); 188 mClearUriButton.setOnClickListener(this); 189 } 190 191 @VisibleForTesting 192 void handleClearCacheClick() { 193 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 194 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 195 getActivity(), mAppsControlDisallowedAdmin); 196 return; 197 } else if (mClearCacheObserver == null) { // Lazy initialization of observer 198 mClearCacheObserver = new ClearCacheObserver(); 199 } 200 mMetricsFeatureProvider.action(getContext(), 201 MetricsEvent.ACTION_SETTINGS_CLEAR_APP_CACHE); 202 mPm.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver); 203 } 204 205 @VisibleForTesting 206 void handleClearDataClick() { 207 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 208 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 209 getActivity(), mAppsControlDisallowedAdmin); 210 } else if (mAppEntry.info.manageSpaceActivityName != null) { 211 if (!Utils.isMonkeyRunning()) { 212 Intent intent = new Intent(Intent.ACTION_DEFAULT); 213 intent.setClassName(mAppEntry.info.packageName, 214 mAppEntry.info.manageSpaceActivityName); 215 startActivityForResult(intent, REQUEST_MANAGE_SPACE); 216 } 217 } else { 218 showDialogInner(DLG_CLEAR_DATA, 0); 219 } 220 } 221 222 @Override 223 public void onClick(View v) { 224 if (v == mChangeStorageButton && mDialogBuilder != null && !isMoveInProgress()) { 225 mDialogBuilder.show(); 226 } else if (v == mClearUriButton) { 227 if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { 228 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 229 getActivity(), mAppsControlDisallowedAdmin); 230 } else { 231 clearUriPermissions(); 232 } 233 } 234 } 235 236 private boolean isMoveInProgress() { 237 try { 238 // TODO: define a cleaner API for this 239 AppGlobals.getPackageManager().checkPackageStartable(mPackageName, 240 UserHandle.myUserId()); 241 return false; 242 } catch (RemoteException | SecurityException e) { 243 return true; 244 } 245 } 246 247 @Override 248 public void onClick(DialogInterface dialog, int which) { 249 final Context context = getActivity(); 250 251 // If not current volume, kick off move wizard 252 final VolumeInfo targetVol = mCandidates[which]; 253 final VolumeInfo currentVol = context.getPackageManager().getPackageCurrentVolume( 254 mAppEntry.info); 255 if (!Objects.equals(targetVol, currentVol)) { 256 final Intent intent = new Intent(context, StorageWizardMoveConfirm.class); 257 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, targetVol.getId()); 258 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mAppEntry.info.packageName); 259 startActivity(intent); 260 } 261 dialog.dismiss(); 262 } 263 264 @Override 265 protected boolean refreshUi() { 266 retrieveAppEntry(); 267 if (mAppEntry == null) { 268 return false; 269 } 270 updateUiWithSize(mSizeController.getLastResult()); 271 refreshGrantedUriPermissions(); 272 273 final VolumeInfo currentVol = getActivity().getPackageManager() 274 .getPackageCurrentVolume(mAppEntry.info); 275 final StorageManager storage = getContext().getSystemService(StorageManager.class); 276 mStorageUsed.setSummary(storage.getBestVolumeDescription(currentVol)); 277 278 refreshButtons(); 279 280 return true; 281 } 282 283 private void refreshButtons() { 284 initMoveDialog(); 285 initDataButtons(); 286 } 287 288 private void initDataButtons() { 289 final boolean appHasSpaceManagementUI = mAppEntry.info.manageSpaceActivityName != null; 290 final boolean appHasActiveAdmins = mDpm.packageHasActiveAdmins(mPackageName); 291 // Check that SYSTEM_APP flag is set, and ALLOW_CLEAR_USER_DATA is not set. 292 final boolean isNonClearableSystemApp = 293 (mAppEntry.info.flags & (FLAG_SYSTEM | FLAG_ALLOW_CLEAR_USER_DATA)) == FLAG_SYSTEM; 294 final boolean appRestrictsClearingData = isNonClearableSystemApp || appHasActiveAdmins; 295 296 final Intent intent = new Intent(Intent.ACTION_DEFAULT); 297 if (appHasSpaceManagementUI) { 298 intent.setClassName(mAppEntry.info.packageName, mAppEntry.info.manageSpaceActivityName); 299 } 300 final boolean isManageSpaceActivityAvailable = 301 getPackageManager().resolveActivity(intent, 0) != null; 302 303 if ((!appHasSpaceManagementUI && appRestrictsClearingData) 304 || !isManageSpaceActivityAvailable) { 305 mButtonsPref 306 .setButton1Text(R.string.clear_user_data_text) 307 .setButton1Enabled(false); 308 mCanClearData = false; 309 } else { 310 if (appHasSpaceManagementUI) { 311 mButtonsPref.setButton1Text(R.string.manage_space_text); 312 } else { 313 mButtonsPref.setButton1Text(R.string.clear_user_data_text); 314 } 315 mButtonsPref 316 .setButton1Text(R.string.clear_user_data_text) 317 .setButton1OnClickListener(v -> handleClearDataClick()); 318 } 319 320 if (mAppsControlDisallowedBySystem) { 321 mButtonsPref.setButton1Enabled(false); 322 } 323 } 324 325 private void initMoveDialog() { 326 final Context context = getActivity(); 327 final StorageManager storage = context.getSystemService(StorageManager.class); 328 329 final List<VolumeInfo> candidates = context.getPackageManager() 330 .getPackageCandidateVolumes(mAppEntry.info); 331 if (candidates.size() > 1) { 332 Collections.sort(candidates, VolumeInfo.getDescriptionComparator()); 333 334 CharSequence[] labels = new CharSequence[candidates.size()]; 335 int current = -1; 336 for (int i = 0; i < candidates.size(); i++) { 337 final String volDescrip = storage.getBestVolumeDescription(candidates.get(i)); 338 if (Objects.equals(volDescrip, mStorageUsed.getSummary())) { 339 current = i; 340 } 341 labels[i] = volDescrip; 342 } 343 mCandidates = candidates.toArray(new VolumeInfo[candidates.size()]); 344 mDialogBuilder = new AlertDialog.Builder(getContext()) 345 .setTitle(R.string.change_storage) 346 .setSingleChoiceItems(labels, current, this) 347 .setNegativeButton(R.string.cancel, null); 348 } else { 349 removePreference(KEY_STORAGE_USED); 350 removePreference(KEY_CHANGE_STORAGE); 351 removePreference(KEY_STORAGE_SPACE); 352 } 353 } 354 355 /* 356 * Private method to initiate clearing user data when the user clicks the clear data 357 * button for a system package 358 */ 359 private void initiateClearUserData() { 360 mMetricsFeatureProvider.action(getContext(), MetricsEvent.ACTION_SETTINGS_CLEAR_APP_DATA); 361 mButtonsPref.setButton1Enabled(false); 362 // Invoke uninstall or clear user data based on sysPackage 363 String packageName = mAppEntry.info.packageName; 364 Log.i(TAG, "Clearing user data for package : " + packageName); 365 if (mClearDataObserver == null) { 366 mClearDataObserver = new ClearUserDataObserver(); 367 } 368 ActivityManager am = (ActivityManager) 369 getActivity().getSystemService(Context.ACTIVITY_SERVICE); 370 boolean res = am.clearApplicationUserData(packageName, mClearDataObserver); 371 if (!res) { 372 // Clearing data failed for some obscure reason. Just log error for now 373 Log.i(TAG, "Couldnt clear application user data for package:" + packageName); 374 showDialogInner(DLG_CANNOT_CLEAR_DATA, 0); 375 } else { 376 mButtonsPref.setButton1Text(R.string.recompute_size); 377 } 378 } 379 380 /* 381 * Private method to handle clear message notification from observer when 382 * the async operation from PackageManager is complete 383 */ 384 private void processClearMsg(Message msg) { 385 int result = msg.arg1; 386 String packageName = mAppEntry.info.packageName; 387 mButtonsPref.setButton1Text(R.string.clear_user_data_text); 388 if (result == OP_SUCCESSFUL) { 389 Log.i(TAG, "Cleared user data for package : " + packageName); 390 updateSize(); 391 } else { 392 mButtonsPref.setButton1Enabled(true); 393 } 394 } 395 396 private void refreshGrantedUriPermissions() { 397 // Clear UI first (in case the activity has been resumed) 398 removeUriPermissionsFromUi(); 399 400 // Gets all URI permissions from am. 401 ActivityManager am = (ActivityManager) getActivity().getSystemService( 402 Context.ACTIVITY_SERVICE); 403 List<GrantedUriPermission> perms = 404 am.getGrantedUriPermissions(mAppEntry.info.packageName).getList(); 405 406 if (perms.isEmpty()) { 407 mClearUriButton.setVisibility(View.GONE); 408 return; 409 } 410 411 PackageManager pm = getActivity().getPackageManager(); 412 413 // Group number of URIs by app. 414 Map<CharSequence, MutableInt> uriCounters = new TreeMap<>(); 415 for (GrantedUriPermission perm : perms) { 416 String authority = perm.uri.getAuthority(); 417 ProviderInfo provider = pm.resolveContentProvider(authority, 0); 418 CharSequence app = provider.applicationInfo.loadLabel(pm); 419 MutableInt count = uriCounters.get(app); 420 if (count == null) { 421 uriCounters.put(app, new MutableInt(1)); 422 } else { 423 count.value++; 424 } 425 } 426 427 // Dynamically add the preferences, one per app. 428 int order = 0; 429 for (Map.Entry<CharSequence, MutableInt> entry : uriCounters.entrySet()) { 430 int numberResources = entry.getValue().value; 431 Preference pref = new Preference(getPrefContext()); 432 pref.setTitle(entry.getKey()); 433 pref.setSummary(getPrefContext().getResources() 434 .getQuantityString(R.plurals.uri_permissions_text, numberResources, 435 numberResources)); 436 pref.setSelectable(false); 437 pref.setLayoutResource(R.layout.horizontal_preference); 438 pref.setOrder(order); 439 Log.v(TAG, "Adding preference '" + pref + "' at order " + order); 440 mUri.addPreference(pref); 441 } 442 443 if (mAppsControlDisallowedBySystem) { 444 mClearUriButton.setEnabled(false); 445 } 446 447 mClearUri.setOrder(order); 448 mClearUriButton.setVisibility(View.VISIBLE); 449 450 } 451 452 private void clearUriPermissions() { 453 // Synchronously revoke the permissions. 454 final ActivityManager am = (ActivityManager) getActivity().getSystemService( 455 Context.ACTIVITY_SERVICE); 456 am.clearGrantedUriPermissions(mAppEntry.info.packageName); 457 458 // Update UI 459 refreshGrantedUriPermissions(); 460 } 461 462 private void removeUriPermissionsFromUi() { 463 // Remove all preferences but the clear button. 464 int count = mUri.getPreferenceCount(); 465 for (int i = count - 1; i >= 0; i--) { 466 Preference pref = mUri.getPreference(i); 467 if (pref != mClearUri) { 468 mUri.removePreference(pref); 469 } 470 } 471 } 472 473 @Override 474 protected AlertDialog createDialog(int id, int errorCode) { 475 switch (id) { 476 case DLG_CLEAR_DATA: 477 return new AlertDialog.Builder(getActivity()) 478 .setTitle(getActivity().getText(R.string.clear_data_dlg_title)) 479 .setMessage(getActivity().getText(R.string.clear_data_dlg_text)) 480 .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { 481 public void onClick(DialogInterface dialog, int which) { 482 // Clear user data here 483 initiateClearUserData(); 484 } 485 }) 486 .setNegativeButton(R.string.dlg_cancel, null) 487 .create(); 488 case DLG_CANNOT_CLEAR_DATA: 489 return new AlertDialog.Builder(getActivity()) 490 .setTitle(getActivity().getText(R.string.clear_failed_dlg_title)) 491 .setMessage(getActivity().getText(R.string.clear_failed_dlg_text)) 492 .setNeutralButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { 493 public void onClick(DialogInterface dialog, int which) { 494 mButtonsPref.setButton1Enabled(false); 495 //force to recompute changed value 496 setIntentAndFinish(false, false); 497 } 498 }) 499 .create(); 500 } 501 return null; 502 } 503 504 @Override 505 public void onPackageSizeChanged(String packageName) { 506 } 507 508 @Override 509 public Loader<AppStorageStats> onCreateLoader(int id, Bundle args) { 510 Context context = getContext(); 511 return new FetchPackageStorageAsyncLoader( 512 context, new StorageStatsSource(context), mInfo, UserHandle.of(mUserId)); 513 } 514 515 @Override 516 public void onLoadFinished(Loader<AppStorageStats> loader, AppStorageStats result) { 517 mSizeController.setResult(result); 518 updateUiWithSize(result); 519 } 520 521 @Override 522 public void onLoaderReset(Loader<AppStorageStats> loader) { 523 } 524 525 private void updateSize() { 526 PackageManager packageManager = getPackageManager(); 527 try { 528 mInfo = packageManager.getApplicationInfo(mPackageName, 0); 529 } catch (PackageManager.NameNotFoundException e) { 530 Log.e(TAG, "Could not find package", e); 531 } 532 533 if (mInfo == null) { 534 return; 535 } 536 537 getLoaderManager().restartLoader(1, Bundle.EMPTY, this); 538 } 539 540 @VisibleForTesting 541 void updateUiWithSize(AppStorageStats result) { 542 if (mCacheCleared) { 543 mSizeController.setCacheCleared(true); 544 } 545 if (mDataCleared) { 546 mSizeController.setDataCleared(true); 547 } 548 549 mSizeController.updateUi(getContext()); 550 551 if (result == null) { 552 mButtonsPref.setButton1Enabled(false).setButton2Enabled(false); 553 } else { 554 long cacheSize = result.getCacheBytes(); 555 long dataSize = result.getDataBytes() - cacheSize; 556 557 if (dataSize <= 0 || !mCanClearData || mDataCleared) { 558 mButtonsPref.setButton1Enabled(false); 559 } else { 560 mButtonsPref.setButton1Enabled(true) 561 .setButton1OnClickListener(v -> handleClearDataClick()); 562 } 563 if (cacheSize <= 0 || mCacheCleared) { 564 mButtonsPref.setButton2Enabled(false); 565 } else { 566 mButtonsPref.setButton2Enabled(true) 567 .setButton2OnClickListener(v -> handleClearCacheClick()); 568 } 569 } 570 if (mAppsControlDisallowedBySystem) { 571 mButtonsPref.setButton1Enabled(false).setButton2Enabled(false); 572 } 573 } 574 575 private final Handler mHandler = new Handler() { 576 public void handleMessage(Message msg) { 577 if (getView() == null) { 578 return; 579 } 580 switch (msg.what) { 581 case MSG_CLEAR_USER_DATA: 582 mDataCleared = true; 583 mCacheCleared = true; 584 processClearMsg(msg); 585 break; 586 case MSG_CLEAR_CACHE: 587 mCacheCleared = true; 588 // Refresh size info 589 updateSize(); 590 break; 591 } 592 } 593 }; 594 595 @Override 596 public int getMetricsCategory() { 597 return MetricsEvent.APPLICATIONS_APP_STORAGE; 598 } 599 600 class ClearCacheObserver extends IPackageDataObserver.Stub { 601 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 602 final Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE); 603 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 604 mHandler.sendMessage(msg); 605 } 606 } 607 608 class ClearUserDataObserver extends IPackageDataObserver.Stub { 609 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 610 final Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA); 611 msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED; 612 mHandler.sendMessage(msg); 613 } 614 } 615} 616