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