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