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