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.deviceinfo; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.Fragment; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.graphics.Color; 28 import android.graphics.drawable.Drawable; 29 import android.os.AsyncTask; 30 import android.os.Bundle; 31 import android.os.UserHandle; 32 import android.os.UserManager; 33 import android.os.storage.DiskInfo; 34 import android.os.storage.StorageEventListener; 35 import android.os.storage.StorageManager; 36 import android.os.storage.VolumeInfo; 37 import android.os.storage.VolumeRecord; 38 import android.support.v7.preference.Preference; 39 import android.support.v7.preference.PreferenceCategory; 40 import android.text.TextUtils; 41 import android.text.format.Formatter; 42 import android.text.format.Formatter.BytesResult; 43 import android.util.Log; 44 import android.widget.Toast; 45 46 import com.android.internal.logging.MetricsProto.MetricsEvent; 47 import com.android.settings.R; 48 import com.android.settings.SettingsPreferenceFragment; 49 import com.android.settings.Utils; 50 import com.android.settings.dashboard.SummaryLoader; 51 import com.android.settings.search.BaseSearchIndexProvider; 52 import com.android.settings.search.Indexable; 53 import com.android.settings.search.SearchIndexableRaw; 54 import com.android.settingslib.RestrictedLockUtils; 55 import com.android.settingslib.drawer.SettingsDrawerActivity; 56 57 import java.io.File; 58 import java.util.ArrayList; 59 import java.util.Collections; 60 import java.util.List; 61 62 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 63 64 /** 65 * Panel showing both internal storage (both built-in storage and private 66 * volumes) and removable storage (public volumes). 67 */ 68 public class StorageSettings extends SettingsPreferenceFragment implements Indexable { 69 static final String TAG = "StorageSettings"; 70 71 private static final String TAG_VOLUME_UNMOUNTED = "volume_unmounted"; 72 private static final String TAG_DISK_INIT = "disk_init"; 73 74 static final int COLOR_PUBLIC = Color.parseColor("#ff9e9e9e"); 75 static final int COLOR_WARNING = Color.parseColor("#fff4511e"); 76 77 static final int[] COLOR_PRIVATE = new int[] { 78 Color.parseColor("#ff26a69a"), 79 Color.parseColor("#ffab47bc"), 80 Color.parseColor("#fff2a600"), 81 Color.parseColor("#ffec407a"), 82 Color.parseColor("#ffc0ca33"), 83 }; 84 85 private StorageManager mStorageManager; 86 87 private PreferenceCategory mInternalCategory; 88 private PreferenceCategory mExternalCategory; 89 90 private StorageSummaryPreference mInternalSummary; 91 private static long sTotalInternalStorage; 92 93 @Override 94 protected int getMetricsCategory() { 95 return MetricsEvent.DEVICEINFO_STORAGE; 96 } 97 98 @Override 99 protected int getHelpResource() { 100 return R.string.help_uri_storage; 101 } 102 103 @Override 104 public void onCreate(Bundle icicle) { 105 super.onCreate(icicle); 106 107 final Context context = getActivity(); 108 109 mStorageManager = context.getSystemService(StorageManager.class); 110 mStorageManager.registerListener(mStorageListener); 111 112 if (sTotalInternalStorage <= 0) { 113 sTotalInternalStorage = mStorageManager.getPrimaryStorageSize(); 114 } 115 116 addPreferencesFromResource(R.xml.device_info_storage); 117 118 mInternalCategory = (PreferenceCategory) findPreference("storage_internal"); 119 mExternalCategory = (PreferenceCategory) findPreference("storage_external"); 120 121 mInternalSummary = new StorageSummaryPreference(getPrefContext()); 122 123 setHasOptionsMenu(true); 124 } 125 126 private final StorageEventListener mStorageListener = new StorageEventListener() { 127 @Override 128 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 129 if (isInteresting(vol)) { 130 refresh(); 131 } 132 } 133 134 @Override 135 public void onDiskDestroyed(DiskInfo disk) { 136 refresh(); 137 } 138 }; 139 140 private static boolean isInteresting(VolumeInfo vol) { 141 switch(vol.getType()) { 142 case VolumeInfo.TYPE_PRIVATE: 143 case VolumeInfo.TYPE_PUBLIC: 144 return true; 145 default: 146 return false; 147 } 148 } 149 150 private void refresh() { 151 final Context context = getPrefContext(); 152 153 getPreferenceScreen().removeAll(); 154 mInternalCategory.removeAll(); 155 mExternalCategory.removeAll(); 156 157 mInternalCategory.addPreference(mInternalSummary); 158 159 int privateCount = 0; 160 long privateUsedBytes = 0; 161 long privateTotalBytes = 0; 162 163 final List<VolumeInfo> volumes = mStorageManager.getVolumes(); 164 Collections.sort(volumes, VolumeInfo.getDescriptionComparator()); 165 166 for (VolumeInfo vol : volumes) { 167 if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { 168 final int color = COLOR_PRIVATE[privateCount++ % COLOR_PRIVATE.length]; 169 mInternalCategory.addPreference( 170 new StorageVolumePreference(context, vol, color, sTotalInternalStorage)); 171 if (vol.isMountedReadable()) { 172 final File path = vol.getPath(); 173 privateUsedBytes += path.getTotalSpace() - path.getFreeSpace(); 174 if (sTotalInternalStorage > 0) { 175 privateTotalBytes = sTotalInternalStorage; 176 } else { 177 privateTotalBytes += path.getTotalSpace(); 178 } 179 } 180 } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) { 181 mExternalCategory.addPreference( 182 new StorageVolumePreference(context, vol, COLOR_PUBLIC, 0)); 183 } 184 } 185 186 // Show missing private volumes 187 final List<VolumeRecord> recs = mStorageManager.getVolumeRecords(); 188 for (VolumeRecord rec : recs) { 189 if (rec.getType() == VolumeInfo.TYPE_PRIVATE 190 && mStorageManager.findVolumeByUuid(rec.getFsUuid()) == null) { 191 // TODO: add actual storage type to record 192 final Drawable icon = context.getDrawable(R.drawable.ic_sim_sd); 193 icon.mutate(); 194 icon.setTint(COLOR_PUBLIC); 195 196 final Preference pref = new Preference(context); 197 pref.setKey(rec.getFsUuid()); 198 pref.setTitle(rec.getNickname()); 199 pref.setSummary(com.android.internal.R.string.ext_media_status_missing); 200 pref.setIcon(icon); 201 mInternalCategory.addPreference(pref); 202 } 203 } 204 205 // Show unsupported disks to give a chance to init 206 final List<DiskInfo> disks = mStorageManager.getDisks(); 207 for (DiskInfo disk : disks) { 208 if (disk.volumeCount == 0 && disk.size > 0) { 209 final Preference pref = new Preference(context); 210 pref.setKey(disk.getId()); 211 pref.setTitle(disk.getDescription()); 212 pref.setSummary(com.android.internal.R.string.ext_media_status_unsupported); 213 pref.setIcon(R.drawable.ic_sim_sd); 214 mExternalCategory.addPreference(pref); 215 } 216 } 217 218 final BytesResult result = Formatter.formatBytes(getResources(), privateUsedBytes, 0); 219 mInternalSummary.setTitle(TextUtils.expandTemplate(getText(R.string.storage_size_large), 220 result.value, result.units)); 221 mInternalSummary.setSummary(getString(R.string.storage_volume_used_total, 222 Formatter.formatFileSize(context, privateTotalBytes))); 223 if (mInternalCategory.getPreferenceCount() > 0) { 224 getPreferenceScreen().addPreference(mInternalCategory); 225 } 226 if (mExternalCategory.getPreferenceCount() > 0) { 227 getPreferenceScreen().addPreference(mExternalCategory); 228 } 229 230 if (mInternalCategory.getPreferenceCount() == 2 231 && mExternalCategory.getPreferenceCount() == 0) { 232 // Only showing primary internal storage, so just shortcut 233 final Bundle args = new Bundle(); 234 args.putString(VolumeInfo.EXTRA_VOLUME_ID, VolumeInfo.ID_PRIVATE_INTERNAL); 235 PrivateVolumeSettings.setVolumeSize(args, sTotalInternalStorage); 236 Intent intent = Utils.onBuildStartFragmentIntent(getActivity(), 237 PrivateVolumeSettings.class.getName(), args, null, R.string.apps_storage, null, 238 false); 239 intent.putExtra(SettingsDrawerActivity.EXTRA_SHOW_MENU, true); 240 getActivity().startActivity(intent); 241 finish(); 242 } 243 } 244 245 @Override 246 public void onResume() { 247 super.onResume(); 248 mStorageManager.registerListener(mStorageListener); 249 refresh(); 250 } 251 252 @Override 253 public void onPause() { 254 super.onPause(); 255 mStorageManager.unregisterListener(mStorageListener); 256 } 257 258 @Override 259 public boolean onPreferenceTreeClick(Preference pref) { 260 final String key = pref.getKey(); 261 if (pref instanceof StorageVolumePreference) { 262 // Picked a normal volume 263 final VolumeInfo vol = mStorageManager.findVolumeById(key); 264 265 if (vol == null) { 266 return false; 267 } 268 269 if (vol.getState() == VolumeInfo.STATE_UNMOUNTED) { 270 VolumeUnmountedFragment.show(this, vol.getId()); 271 return true; 272 } else if (vol.getState() == VolumeInfo.STATE_UNMOUNTABLE) { 273 DiskInitFragment.show(this, R.string.storage_dialog_unmountable, vol.getDiskId()); 274 return true; 275 } 276 277 if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { 278 final Bundle args = new Bundle(); 279 args.putString(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 280 PrivateVolumeSettings.setVolumeSize(args, sTotalInternalStorage); 281 startFragment(this, PrivateVolumeSettings.class.getCanonicalName(), 282 -1, 0, args); 283 return true; 284 285 } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) { 286 if (vol.isMountedReadable()) { 287 startActivity(vol.buildBrowseIntent()); 288 return true; 289 } else { 290 final Bundle args = new Bundle(); 291 args.putString(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); 292 startFragment(this, PublicVolumeSettings.class.getCanonicalName(), 293 -1, 0, args); 294 return true; 295 } 296 } 297 298 } else if (key.startsWith("disk:")) { 299 // Picked an unsupported disk 300 DiskInitFragment.show(this, R.string.storage_dialog_unsupported, key); 301 return true; 302 303 } else { 304 // Picked a missing private volume 305 final Bundle args = new Bundle(); 306 args.putString(VolumeRecord.EXTRA_FS_UUID, key); 307 startFragment(this, PrivateVolumeForget.class.getCanonicalName(), 308 R.string.storage_menu_forget, 0, args); 309 return true; 310 } 311 312 return false; 313 } 314 315 public static class MountTask extends AsyncTask<Void, Void, Exception> { 316 private final Context mContext; 317 private final StorageManager mStorageManager; 318 private final String mVolumeId; 319 private final String mDescription; 320 321 public MountTask(Context context, VolumeInfo volume) { 322 mContext = context.getApplicationContext(); 323 mStorageManager = mContext.getSystemService(StorageManager.class); 324 mVolumeId = volume.getId(); 325 mDescription = mStorageManager.getBestVolumeDescription(volume); 326 } 327 328 @Override 329 protected Exception doInBackground(Void... params) { 330 try { 331 mStorageManager.mount(mVolumeId); 332 return null; 333 } catch (Exception e) { 334 return e; 335 } 336 } 337 338 @Override 339 protected void onPostExecute(Exception e) { 340 if (e == null) { 341 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_success, 342 mDescription), Toast.LENGTH_SHORT).show(); 343 } else { 344 Log.e(TAG, "Failed to mount " + mVolumeId, e); 345 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_failure, 346 mDescription), Toast.LENGTH_SHORT).show(); 347 } 348 } 349 } 350 351 public static class UnmountTask extends AsyncTask<Void, Void, Exception> { 352 private final Context mContext; 353 private final StorageManager mStorageManager; 354 private final String mVolumeId; 355 private final String mDescription; 356 357 public UnmountTask(Context context, VolumeInfo volume) { 358 mContext = context.getApplicationContext(); 359 mStorageManager = mContext.getSystemService(StorageManager.class); 360 mVolumeId = volume.getId(); 361 mDescription = mStorageManager.getBestVolumeDescription(volume); 362 } 363 364 @Override 365 protected Exception doInBackground(Void... params) { 366 try { 367 mStorageManager.unmount(mVolumeId); 368 return null; 369 } catch (Exception e) { 370 return e; 371 } 372 } 373 374 @Override 375 protected void onPostExecute(Exception e) { 376 if (e == null) { 377 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_success, 378 mDescription), Toast.LENGTH_SHORT).show(); 379 } else { 380 Log.e(TAG, "Failed to unmount " + mVolumeId, e); 381 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_failure, 382 mDescription), Toast.LENGTH_SHORT).show(); 383 } 384 } 385 } 386 387 public static class VolumeUnmountedFragment extends DialogFragment { 388 public static void show(Fragment parent, String volumeId) { 389 final Bundle args = new Bundle(); 390 args.putString(VolumeInfo.EXTRA_VOLUME_ID, volumeId); 391 392 final VolumeUnmountedFragment dialog = new VolumeUnmountedFragment(); 393 dialog.setArguments(args); 394 dialog.setTargetFragment(parent, 0); 395 dialog.show(parent.getFragmentManager(), TAG_VOLUME_UNMOUNTED); 396 } 397 398 @Override 399 public Dialog onCreateDialog(Bundle savedInstanceState) { 400 final Context context = getActivity(); 401 final StorageManager sm = context.getSystemService(StorageManager.class); 402 403 final String volumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID); 404 final VolumeInfo vol = sm.findVolumeById(volumeId); 405 406 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 407 builder.setMessage(TextUtils.expandTemplate( 408 getText(R.string.storage_dialog_unmounted), vol.getDisk().getDescription())); 409 410 builder.setPositiveButton(R.string.storage_menu_mount, 411 new DialogInterface.OnClickListener() { 412 @Override 413 public void onClick(DialogInterface dialog, int which) { 414 EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( 415 getActivity(), UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA, 416 UserHandle.myUserId()); 417 boolean hasBaseUserRestriction = RestrictedLockUtils.hasBaseUserRestriction( 418 getActivity(), UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA, 419 UserHandle.myUserId()); 420 if (admin != null && !hasBaseUserRestriction) { 421 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(), admin); 422 return; 423 } 424 new MountTask(context, vol).execute(); 425 } 426 }); 427 builder.setNegativeButton(R.string.cancel, null); 428 429 return builder.create(); 430 } 431 } 432 433 public static class DiskInitFragment extends DialogFragment { 434 public static void show(Fragment parent, int resId, String diskId) { 435 final Bundle args = new Bundle(); 436 args.putInt(Intent.EXTRA_TEXT, resId); 437 args.putString(DiskInfo.EXTRA_DISK_ID, diskId); 438 439 final DiskInitFragment dialog = new DiskInitFragment(); 440 dialog.setArguments(args); 441 dialog.setTargetFragment(parent, 0); 442 dialog.show(parent.getFragmentManager(), TAG_DISK_INIT); 443 } 444 445 @Override 446 public Dialog onCreateDialog(Bundle savedInstanceState) { 447 final Context context = getActivity(); 448 final StorageManager sm = context.getSystemService(StorageManager.class); 449 450 final int resId = getArguments().getInt(Intent.EXTRA_TEXT); 451 final String diskId = getArguments().getString(DiskInfo.EXTRA_DISK_ID); 452 final DiskInfo disk = sm.findDiskById(diskId); 453 454 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 455 builder.setMessage(TextUtils.expandTemplate(getText(resId), disk.getDescription())); 456 457 builder.setPositiveButton(R.string.storage_menu_set_up, 458 new DialogInterface.OnClickListener() { 459 @Override 460 public void onClick(DialogInterface dialog, int which) { 461 final Intent intent = new Intent(context, StorageWizardInit.class); 462 intent.putExtra(DiskInfo.EXTRA_DISK_ID, diskId); 463 startActivity(intent); 464 } 465 }); 466 builder.setNegativeButton(R.string.cancel, null); 467 468 return builder.create(); 469 } 470 } 471 472 private static class SummaryProvider implements SummaryLoader.SummaryProvider { 473 private final Context mContext; 474 private final SummaryLoader mLoader; 475 476 private SummaryProvider(Context context, SummaryLoader loader) { 477 mContext = context; 478 mLoader = loader; 479 } 480 481 @Override 482 public void setListening(boolean listening) { 483 if (listening) { 484 updateSummary(); 485 } 486 } 487 488 private void updateSummary() { 489 // TODO: Register listener. 490 final StorageManager storageManager = mContext.getSystemService(StorageManager.class); 491 if (sTotalInternalStorage <= 0) { 492 sTotalInternalStorage = storageManager.getPrimaryStorageSize(); 493 } 494 final List<VolumeInfo> volumes = storageManager.getVolumes(); 495 long privateFreeBytes = 0; 496 long privateTotalBytes = 0; 497 for (VolumeInfo info : volumes) { 498 if (info.getType() != VolumeInfo.TYPE_PUBLIC 499 && info.getType() != VolumeInfo.TYPE_PRIVATE) { 500 continue; 501 } 502 final File path = info.getPath(); 503 if (path == null) { 504 continue; 505 } 506 if (info.getType() == VolumeInfo.TYPE_PRIVATE && sTotalInternalStorage > 0) { 507 privateTotalBytes = sTotalInternalStorage; 508 } else { 509 privateTotalBytes += path.getTotalSpace(); 510 } 511 privateFreeBytes += path.getFreeSpace(); 512 } 513 long privateUsedBytes = privateTotalBytes - privateFreeBytes; 514 mLoader.setSummary(this, mContext.getString(R.string.storage_summary, 515 Formatter.formatFileSize(mContext, privateUsedBytes), 516 Formatter.formatFileSize(mContext, privateTotalBytes))); 517 } 518 } 519 520 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 521 = new SummaryLoader.SummaryProviderFactory() { 522 @Override 523 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 524 SummaryLoader summaryLoader) { 525 return new SummaryProvider(activity, summaryLoader); 526 } 527 }; 528 529 /** 530 * Enable indexing of searchable data 531 */ 532 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 533 new BaseSearchIndexProvider() { 534 @Override 535 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 536 final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); 537 538 SearchIndexableRaw data = new SearchIndexableRaw(context); 539 data.title = context.getString(R.string.storage_settings); 540 data.screenTitle = context.getString(R.string.storage_settings); 541 result.add(data); 542 543 data = new SearchIndexableRaw(context); 544 data.title = context.getString(R.string.internal_storage); 545 data.screenTitle = context.getString(R.string.storage_settings); 546 result.add(data); 547 548 data = new SearchIndexableRaw(context); 549 final StorageManager storage = context.getSystemService(StorageManager.class); 550 final List<VolumeInfo> vols = storage.getVolumes(); 551 for (VolumeInfo vol : vols) { 552 if (isInteresting(vol)) { 553 data.title = storage.getBestVolumeDescription(vol); 554 data.screenTitle = context.getString(R.string.storage_settings); 555 result.add(data); 556 } 557 } 558 559 data = new SearchIndexableRaw(context); 560 data.title = context.getString(R.string.memory_size); 561 data.screenTitle = context.getString(R.string.storage_settings); 562 result.add(data); 563 564 data = new SearchIndexableRaw(context); 565 data.title = context.getString(R.string.memory_available); 566 data.screenTitle = context.getString(R.string.storage_settings); 567 result.add(data); 568 569 data = new SearchIndexableRaw(context); 570 data.title = context.getString(R.string.memory_apps_usage); 571 data.screenTitle = context.getString(R.string.storage_settings); 572 result.add(data); 573 574 data = new SearchIndexableRaw(context); 575 data.title = context.getString(R.string.memory_dcim_usage); 576 data.screenTitle = context.getString(R.string.storage_settings); 577 result.add(data); 578 579 data = new SearchIndexableRaw(context); 580 data.title = context.getString(R.string.memory_music_usage); 581 data.screenTitle = context.getString(R.string.storage_settings); 582 result.add(data); 583 584 data = new SearchIndexableRaw(context); 585 data.title = context.getString(R.string.memory_downloads_usage); 586 data.screenTitle = context.getString(R.string.storage_settings); 587 result.add(data); 588 589 data = new SearchIndexableRaw(context); 590 data.title = context.getString(R.string.memory_media_cache_usage); 591 data.screenTitle = context.getString(R.string.storage_settings); 592 result.add(data); 593 594 data = new SearchIndexableRaw(context); 595 data.title = context.getString(R.string.memory_media_misc_usage); 596 data.screenTitle = context.getString(R.string.storage_settings); 597 result.add(data); 598 599 return result; 600 } 601 }; 602 } 603