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