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