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