1 /* 2 * Copyright (C) 2011 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.ActivityManagerNative; 20 import android.app.ActivityThread; 21 import android.app.DownloadManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.IPackageManager; 25 import android.content.pm.UserInfo; 26 import android.content.res.Resources; 27 import android.hardware.usb.UsbManager; 28 import android.os.Environment; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.os.RemoteException; 32 import android.os.UserManager; 33 import android.os.storage.StorageManager; 34 import android.os.storage.StorageVolume; 35 import android.preference.Preference; 36 import android.preference.PreferenceCategory; 37 import android.text.format.Formatter; 38 39 import com.android.settings.R; 40 import com.android.settings.deviceinfo.StorageMeasurement.MeasurementDetails; 41 import com.android.settings.deviceinfo.StorageMeasurement.MeasurementReceiver; 42 import com.google.android.collect.Lists; 43 44 import java.util.HashMap; 45 import java.util.Iterator; 46 import java.util.List; 47 48 public class StorageVolumePreferenceCategory extends PreferenceCategory { 49 public static final String KEY_CACHE = "cache"; 50 51 private static final int ORDER_USAGE_BAR = -2; 52 private static final int ORDER_STORAGE_LOW = -1; 53 54 /** Physical volume being measured, or {@code null} for internal. */ 55 private final StorageVolume mVolume; 56 private final StorageMeasurement mMeasure; 57 58 private final Resources mResources; 59 private final StorageManager mStorageManager; 60 private final UserManager mUserManager; 61 62 private UsageBarPreference mUsageBarPreference; 63 private Preference mMountTogglePreference; 64 private Preference mFormatPreference; 65 private Preference mStorageLow; 66 67 private StorageItemPreference mItemTotal; 68 private StorageItemPreference mItemAvailable; 69 private StorageItemPreference mItemApps; 70 private StorageItemPreference mItemDcim; 71 private StorageItemPreference mItemMusic; 72 private StorageItemPreference mItemDownloads; 73 private StorageItemPreference mItemCache; 74 private StorageItemPreference mItemMisc; 75 private List<StorageItemPreference> mItemUsers = Lists.newArrayList(); 76 77 private boolean mUsbConnected; 78 private String mUsbFunction; 79 80 private long mTotalSize; 81 82 private static final int MSG_UI_UPDATE_APPROXIMATE = 1; 83 private static final int MSG_UI_UPDATE_DETAILS = 2; 84 85 private Handler mUpdateHandler = new Handler() { 86 @Override 87 public void handleMessage(Message msg) { 88 switch (msg.what) { 89 case MSG_UI_UPDATE_APPROXIMATE: { 90 final long[] size = (long[]) msg.obj; 91 updateApproximate(size[0], size[1]); 92 break; 93 } 94 case MSG_UI_UPDATE_DETAILS: { 95 final MeasurementDetails details = (MeasurementDetails) msg.obj; 96 updateDetails(details); 97 break; 98 } 99 } 100 } 101 }; 102 103 /** 104 * Build category to summarize internal storage, including any emulated 105 * {@link StorageVolume}. 106 */ 107 public static StorageVolumePreferenceCategory buildForInternal(Context context) { 108 return new StorageVolumePreferenceCategory(context, null); 109 } 110 111 /** 112 * Build category to summarize specific physical {@link StorageVolume}. 113 */ 114 public static StorageVolumePreferenceCategory buildForPhysical( 115 Context context, StorageVolume volume) { 116 return new StorageVolumePreferenceCategory(context, volume); 117 } 118 119 private StorageVolumePreferenceCategory(Context context, StorageVolume volume) { 120 super(context); 121 122 mVolume = volume; 123 mMeasure = StorageMeasurement.getInstance(context, volume); 124 125 mResources = context.getResources(); 126 mStorageManager = StorageManager.from(context); 127 mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 128 129 setTitle(volume != null ? volume.getDescription(context) 130 : context.getText(R.string.internal_storage)); 131 } 132 133 private StorageItemPreference buildItem(int titleRes, int colorRes) { 134 return new StorageItemPreference(getContext(), titleRes, colorRes); 135 } 136 137 public void init() { 138 final Context context = getContext(); 139 140 final UserInfo currentUser; 141 try { 142 currentUser = ActivityManagerNative.getDefault().getCurrentUser(); 143 } catch (RemoteException e) { 144 throw new RuntimeException("Failed to get current user"); 145 } 146 147 final List<UserInfo> otherUsers = getUsersExcluding(currentUser); 148 final boolean showUsers = mVolume == null && otherUsers.size() > 0; 149 150 mUsageBarPreference = new UsageBarPreference(context); 151 mUsageBarPreference.setOrder(ORDER_USAGE_BAR); 152 addPreference(mUsageBarPreference); 153 154 mItemTotal = buildItem(R.string.memory_size, 0); 155 mItemAvailable = buildItem(R.string.memory_available, R.color.memory_avail); 156 addPreference(mItemTotal); 157 addPreference(mItemAvailable); 158 159 mItemApps = buildItem(R.string.memory_apps_usage, R.color.memory_apps_usage); 160 mItemDcim = buildItem(R.string.memory_dcim_usage, R.color.memory_dcim); 161 mItemMusic = buildItem(R.string.memory_music_usage, R.color.memory_music); 162 mItemDownloads = buildItem(R.string.memory_downloads_usage, R.color.memory_downloads); 163 mItemCache = buildItem(R.string.memory_media_cache_usage, R.color.memory_cache); 164 mItemMisc = buildItem(R.string.memory_media_misc_usage, R.color.memory_misc); 165 166 mItemCache.setKey(KEY_CACHE); 167 168 final boolean showDetails = mVolume == null || mVolume.isPrimary(); 169 if (showDetails) { 170 if (showUsers) { 171 addPreference(new PreferenceHeader(context, currentUser.name)); 172 } 173 174 addPreference(mItemApps); 175 addPreference(mItemDcim); 176 addPreference(mItemMusic); 177 addPreference(mItemDownloads); 178 addPreference(mItemCache); 179 addPreference(mItemMisc); 180 181 if (showUsers) { 182 addPreference(new PreferenceHeader(context, R.string.storage_other_users)); 183 184 int count = 0; 185 for (UserInfo info : otherUsers) { 186 final int colorRes = count++ % 2 == 0 ? R.color.memory_user_light 187 : R.color.memory_user_dark; 188 final StorageItemPreference userPref = new StorageItemPreference( 189 getContext(), info.name, colorRes, info.id); 190 mItemUsers.add(userPref); 191 addPreference(userPref); 192 } 193 } 194 } 195 196 final boolean isRemovable = mVolume != null ? mVolume.isRemovable() : false; 197 if (isRemovable) { 198 mMountTogglePreference = new Preference(context); 199 mMountTogglePreference.setTitle(R.string.sd_eject); 200 mMountTogglePreference.setSummary(R.string.sd_eject_summary); 201 addPreference(mMountTogglePreference); 202 } 203 204 // Only allow formatting of primary physical storage 205 // TODO: enable for non-primary volumes once MTP is fixed 206 final boolean allowFormat = mVolume != null ? mVolume.isPrimary() : false; 207 if (allowFormat) { 208 mFormatPreference = new Preference(context); 209 mFormatPreference.setTitle(R.string.sd_format); 210 mFormatPreference.setSummary(R.string.sd_format_summary); 211 addPreference(mFormatPreference); 212 } 213 214 final IPackageManager pm = ActivityThread.getPackageManager(); 215 try { 216 if (pm.isStorageLow()) { 217 mStorageLow = new Preference(context); 218 mStorageLow.setOrder(ORDER_STORAGE_LOW); 219 mStorageLow.setTitle(R.string.storage_low_title); 220 mStorageLow.setSummary(R.string.storage_low_summary); 221 addPreference(mStorageLow); 222 } else if (mStorageLow != null) { 223 removePreference(mStorageLow); 224 mStorageLow = null; 225 } 226 } catch (RemoteException e) { 227 } 228 } 229 230 public StorageVolume getStorageVolume() { 231 return mVolume; 232 } 233 234 private void updatePreferencesFromState() { 235 // Only update for physical volumes 236 if (mVolume == null) return; 237 238 mMountTogglePreference.setEnabled(true); 239 240 final String state = mStorageManager.getVolumeState(mVolume.getPath()); 241 242 if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 243 mItemAvailable.setSummary(R.string.memory_available_read_only); 244 if (mFormatPreference != null) { 245 removePreference(mFormatPreference); 246 } 247 } else { 248 mItemAvailable.setSummary(R.string.memory_available); 249 } 250 251 if (Environment.MEDIA_MOUNTED.equals(state) 252 || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 253 mMountTogglePreference.setEnabled(true); 254 mMountTogglePreference.setTitle(mResources.getString(R.string.sd_eject)); 255 mMountTogglePreference.setSummary(mResources.getString(R.string.sd_eject_summary)); 256 } else { 257 if (Environment.MEDIA_UNMOUNTED.equals(state) || Environment.MEDIA_NOFS.equals(state) 258 || Environment.MEDIA_UNMOUNTABLE.equals(state)) { 259 mMountTogglePreference.setEnabled(true); 260 mMountTogglePreference.setTitle(mResources.getString(R.string.sd_mount)); 261 mMountTogglePreference.setSummary(mResources.getString(R.string.sd_mount_summary)); 262 } else { 263 mMountTogglePreference.setEnabled(false); 264 mMountTogglePreference.setTitle(mResources.getString(R.string.sd_mount)); 265 mMountTogglePreference.setSummary(mResources.getString(R.string.sd_insert_summary)); 266 } 267 268 removePreference(mUsageBarPreference); 269 removePreference(mItemTotal); 270 removePreference(mItemAvailable); 271 if (mFormatPreference != null) { 272 removePreference(mFormatPreference); 273 } 274 } 275 276 if (mUsbConnected && (UsbManager.USB_FUNCTION_MTP.equals(mUsbFunction) || 277 UsbManager.USB_FUNCTION_PTP.equals(mUsbFunction))) { 278 mMountTogglePreference.setEnabled(false); 279 if (Environment.MEDIA_MOUNTED.equals(state) 280 || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 281 mMountTogglePreference.setSummary( 282 mResources.getString(R.string.mtp_ptp_mode_summary)); 283 } 284 285 if (mFormatPreference != null) { 286 mFormatPreference.setEnabled(false); 287 mFormatPreference.setSummary(mResources.getString(R.string.mtp_ptp_mode_summary)); 288 } 289 } else if (mFormatPreference != null) { 290 mFormatPreference.setEnabled(true); 291 mFormatPreference.setSummary(mResources.getString(R.string.sd_format_summary)); 292 } 293 } 294 295 public void updateApproximate(long totalSize, long availSize) { 296 mItemTotal.setSummary(formatSize(totalSize)); 297 mItemAvailable.setSummary(formatSize(availSize)); 298 299 mTotalSize = totalSize; 300 301 final long usedSize = totalSize - availSize; 302 303 mUsageBarPreference.clear(); 304 mUsageBarPreference.addEntry(0, usedSize / (float) totalSize, android.graphics.Color.GRAY); 305 mUsageBarPreference.commit(); 306 307 updatePreferencesFromState(); 308 } 309 310 private static long totalValues(HashMap<String, Long> map, String... keys) { 311 long total = 0; 312 for (String key : keys) { 313 total += map.get(key); 314 } 315 return total; 316 } 317 318 public void updateDetails(MeasurementDetails details) { 319 final boolean showDetails = mVolume == null || mVolume.isPrimary(); 320 if (!showDetails) return; 321 322 // Count caches as available space, since system manages them 323 mItemTotal.setSummary(formatSize(details.totalSize)); 324 mItemAvailable.setSummary(formatSize(details.availSize)); 325 326 mUsageBarPreference.clear(); 327 328 updatePreference(mItemApps, details.appsSize); 329 330 final long dcimSize = totalValues(details.mediaSize, Environment.DIRECTORY_DCIM, 331 Environment.DIRECTORY_MOVIES, Environment.DIRECTORY_PICTURES); 332 updatePreference(mItemDcim, dcimSize); 333 334 final long musicSize = totalValues(details.mediaSize, Environment.DIRECTORY_MUSIC, 335 Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, 336 Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS); 337 updatePreference(mItemMusic, musicSize); 338 339 final long downloadsSize = totalValues(details.mediaSize, Environment.DIRECTORY_DOWNLOADS); 340 updatePreference(mItemDownloads, downloadsSize); 341 342 updatePreference(mItemCache, details.cacheSize); 343 updatePreference(mItemMisc, details.miscSize); 344 345 for (StorageItemPreference userPref : mItemUsers) { 346 final long userSize = details.usersSize.get(userPref.userHandle); 347 updatePreference(userPref, userSize); 348 } 349 350 mUsageBarPreference.commit(); 351 } 352 353 private void updatePreference(StorageItemPreference pref, long size) { 354 if (size > 0) { 355 pref.setSummary(formatSize(size)); 356 final int order = pref.getOrder(); 357 mUsageBarPreference.addEntry(order, size / (float) mTotalSize, pref.color); 358 } else { 359 removePreference(pref); 360 } 361 } 362 363 private void measure() { 364 mMeasure.invalidate(); 365 mMeasure.measure(); 366 } 367 368 public void onResume() { 369 mMeasure.setReceiver(mReceiver); 370 measure(); 371 } 372 373 public void onStorageStateChanged() { 374 measure(); 375 } 376 377 public void onUsbStateChanged(boolean isUsbConnected, String usbFunction) { 378 mUsbConnected = isUsbConnected; 379 mUsbFunction = usbFunction; 380 measure(); 381 } 382 383 public void onMediaScannerFinished() { 384 measure(); 385 } 386 387 public void onCacheCleared() { 388 measure(); 389 } 390 391 public void onPause() { 392 mMeasure.cleanUp(); 393 } 394 395 private String formatSize(long size) { 396 return Formatter.formatFileSize(getContext(), size); 397 } 398 399 private MeasurementReceiver mReceiver = new MeasurementReceiver() { 400 @Override 401 public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize) { 402 mUpdateHandler.obtainMessage(MSG_UI_UPDATE_APPROXIMATE, new long[] { 403 totalSize, availSize }).sendToTarget(); 404 } 405 406 @Override 407 public void updateDetails(StorageMeasurement meas, MeasurementDetails details) { 408 mUpdateHandler.obtainMessage(MSG_UI_UPDATE_DETAILS, details).sendToTarget(); 409 } 410 }; 411 412 public boolean mountToggleClicked(Preference preference) { 413 return preference == mMountTogglePreference; 414 } 415 416 public Intent intentForClick(Preference pref) { 417 Intent intent = null; 418 419 // TODO The current "delete" story is not fully handled by the respective applications. 420 // When it is done, make sure the intent types below are correct. 421 // If that cannot be done, remove these intents. 422 final String key = pref.getKey(); 423 if (pref == mFormatPreference) { 424 intent = new Intent(Intent.ACTION_VIEW); 425 intent.setClass(getContext(), com.android.settings.MediaFormat.class); 426 intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, mVolume); 427 } else if (pref == mItemApps) { 428 intent = new Intent(Intent.ACTION_MANAGE_PACKAGE_STORAGE); 429 intent.setClass(getContext(), 430 com.android.settings.Settings.ManageApplicationsActivity.class); 431 } else if (pref == mItemDownloads) { 432 intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).putExtra( 433 DownloadManager.INTENT_EXTRAS_SORT_BY_SIZE, true); 434 } else if (pref == mItemMusic) { 435 intent = new Intent(Intent.ACTION_GET_CONTENT); 436 intent.setType("audio/mp3"); 437 } else if (pref == mItemDcim) { 438 intent = new Intent(Intent.ACTION_VIEW); 439 intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 440 // TODO Create a Videos category, type = vnd.android.cursor.dir/video 441 intent.setType("vnd.android.cursor.dir/image"); 442 } else if (pref == mItemMisc) { 443 Context context = getContext().getApplicationContext(); 444 intent = new Intent(context, MiscFilesHandler.class); 445 intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, mVolume); 446 } 447 448 return intent; 449 } 450 451 public static class PreferenceHeader extends Preference { 452 public PreferenceHeader(Context context, int titleRes) { 453 super(context, null, com.android.internal.R.attr.preferenceCategoryStyle); 454 setTitle(titleRes); 455 } 456 457 public PreferenceHeader(Context context, CharSequence title) { 458 super(context, null, com.android.internal.R.attr.preferenceCategoryStyle); 459 setTitle(title); 460 } 461 462 @Override 463 public boolean isEnabled() { 464 return false; 465 } 466 } 467 468 /** 469 * Return list of other users, excluding the current user. 470 */ 471 private List<UserInfo> getUsersExcluding(UserInfo excluding) { 472 final List<UserInfo> users = mUserManager.getUsers(); 473 final Iterator<UserInfo> i = users.iterator(); 474 while (i.hasNext()) { 475 if (i.next().id == excluding.id) { 476 i.remove(); 477 } 478 } 479 return users; 480 } 481 } 482