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