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