1 /* 2 * Copyright (C) 2008 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.content.ActivityNotFoundException; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.IPackageDataObserver; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.hardware.usb.UsbManager; 32 import android.os.Bundle; 33 import android.os.Environment; 34 import android.os.IBinder; 35 import android.os.RemoteException; 36 import android.os.ServiceManager; 37 import android.os.UserManager; 38 import android.os.storage.IMountService; 39 import android.os.storage.StorageEventListener; 40 import android.os.storage.StorageManager; 41 import android.os.storage.StorageVolume; 42 import android.preference.Preference; 43 import android.preference.PreferenceScreen; 44 import android.util.Log; 45 import android.view.Menu; 46 import android.view.MenuInflater; 47 import android.view.MenuItem; 48 import android.widget.Toast; 49 50 import com.android.settings.R; 51 import com.android.settings.SettingsActivity; 52 import com.android.settings.SettingsPreferenceFragment; 53 import com.android.settings.Utils; 54 import com.android.settings.search.BaseSearchIndexProvider; 55 import com.android.settings.search.Indexable; 56 import com.android.settings.search.SearchIndexableRaw; 57 import com.google.android.collect.Lists; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 /** 63 * Panel showing storage usage on disk for known {@link StorageVolume} returned 64 * by {@link StorageManager}. Calculates and displays usage of data types. 65 */ 66 public class Memory extends SettingsPreferenceFragment implements Indexable { 67 private static final String TAG = "MemorySettings"; 68 69 private static final String TAG_CONFIRM_CLEAR_CACHE = "confirmClearCache"; 70 71 private static final int DLG_CONFIRM_UNMOUNT = 1; 72 private static final int DLG_ERROR_UNMOUNT = 2; 73 74 // The mountToggle Preference that has last been clicked. 75 // Assumes no two successive unmount event on 2 different volumes are performed before the first 76 // one's preference is disabled 77 private static Preference sLastClickedMountToggle; 78 private static String sClickedMountPoint; 79 80 // Access using getMountService() 81 private IMountService mMountService; 82 private StorageManager mStorageManager; 83 private UsbManager mUsbManager; 84 85 private ArrayList<StorageVolumePreferenceCategory> mCategories = Lists.newArrayList(); 86 87 @Override onCreate(Bundle icicle)88 public void onCreate(Bundle icicle) { 89 super.onCreate(icicle); 90 91 final Context context = getActivity(); 92 93 mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); 94 95 mStorageManager = StorageManager.from(context); 96 mStorageManager.registerListener(mStorageListener); 97 98 addPreferencesFromResource(R.xml.device_info_memory); 99 100 addCategory(StorageVolumePreferenceCategory.buildForInternal(context)); 101 102 final StorageVolume[] storageVolumes = mStorageManager.getVolumeList(); 103 for (StorageVolume volume : storageVolumes) { 104 if (!volume.isEmulated()) { 105 addCategory(StorageVolumePreferenceCategory.buildForPhysical(context, volume)); 106 } 107 } 108 109 setHasOptionsMenu(true); 110 } 111 addCategory(StorageVolumePreferenceCategory category)112 private void addCategory(StorageVolumePreferenceCategory category) { 113 mCategories.add(category); 114 getPreferenceScreen().addPreference(category); 115 category.init(); 116 } 117 isMassStorageEnabled()118 private boolean isMassStorageEnabled() { 119 // Mass storage is enabled if primary volume supports it 120 final StorageVolume[] volumes = mStorageManager.getVolumeList(); 121 final StorageVolume primary = StorageManager.getPrimaryVolume(volumes); 122 return primary != null && primary.allowMassStorage(); 123 } 124 125 @Override onResume()126 public void onResume() { 127 super.onResume(); 128 IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED); 129 intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 130 intentFilter.addDataScheme("file"); 131 getActivity().registerReceiver(mMediaScannerReceiver, intentFilter); 132 133 intentFilter = new IntentFilter(); 134 intentFilter.addAction(UsbManager.ACTION_USB_STATE); 135 getActivity().registerReceiver(mMediaScannerReceiver, intentFilter); 136 137 for (StorageVolumePreferenceCategory category : mCategories) { 138 category.onResume(); 139 } 140 } 141 142 StorageEventListener mStorageListener = new StorageEventListener() { 143 @Override 144 public void onStorageStateChanged(String path, String oldState, String newState) { 145 Log.i(TAG, "Received storage state changed notification that " + path + 146 " changed state from " + oldState + " to " + newState); 147 for (StorageVolumePreferenceCategory category : mCategories) { 148 final StorageVolume volume = category.getStorageVolume(); 149 if (volume != null && path.equals(volume.getPath())) { 150 category.onStorageStateChanged(); 151 break; 152 } 153 } 154 } 155 }; 156 157 @Override onPause()158 public void onPause() { 159 super.onPause(); 160 getActivity().unregisterReceiver(mMediaScannerReceiver); 161 for (StorageVolumePreferenceCategory category : mCategories) { 162 category.onPause(); 163 } 164 } 165 166 @Override onDestroy()167 public void onDestroy() { 168 if (mStorageManager != null && mStorageListener != null) { 169 mStorageManager.unregisterListener(mStorageListener); 170 } 171 super.onDestroy(); 172 } 173 174 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)175 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 176 inflater.inflate(R.menu.storage, menu); 177 } 178 179 @Override onPrepareOptionsMenu(Menu menu)180 public void onPrepareOptionsMenu(Menu menu) { 181 final MenuItem usb = menu.findItem(R.id.storage_usb); 182 UserManager um = (UserManager)getActivity().getSystemService(Context.USER_SERVICE); 183 boolean usbItemVisible = !isMassStorageEnabled() 184 && !um.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER); 185 usb.setVisible(usbItemVisible); 186 } 187 188 @Override onOptionsItemSelected(MenuItem item)189 public boolean onOptionsItemSelected(MenuItem item) { 190 switch (item.getItemId()) { 191 case R.id.storage_usb: 192 if (getActivity() instanceof SettingsActivity) { 193 ((SettingsActivity) getActivity()).startPreferencePanel( 194 UsbSettings.class.getCanonicalName(), 195 null, R.string.storage_title_usb, null, this, 0); 196 } else { 197 startFragment(this, UsbSettings.class.getCanonicalName(), 198 R.string.storage_title_usb, -1, null); 199 } 200 return true; 201 } 202 return super.onOptionsItemSelected(item); 203 } 204 getMountService()205 private synchronized IMountService getMountService() { 206 if (mMountService == null) { 207 IBinder service = ServiceManager.getService("mount"); 208 if (service != null) { 209 mMountService = IMountService.Stub.asInterface(service); 210 } else { 211 Log.e(TAG, "Can't get mount service"); 212 } 213 } 214 return mMountService; 215 } 216 217 @Override onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference)218 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { 219 if (StorageVolumePreferenceCategory.KEY_CACHE.equals(preference.getKey())) { 220 ConfirmClearCacheFragment.show(this); 221 return true; 222 } 223 224 for (StorageVolumePreferenceCategory category : mCategories) { 225 Intent intent = category.intentForClick(preference); 226 if (intent != null) { 227 // Don't go across app boundary if monkey is running 228 if (!Utils.isMonkeyRunning()) { 229 try { 230 startActivity(intent); 231 } catch (ActivityNotFoundException anfe) { 232 Log.w(TAG, "No activity found for intent " + intent); 233 } 234 } 235 return true; 236 } 237 238 final StorageVolume volume = category.getStorageVolume(); 239 if (volume != null && category.mountToggleClicked(preference)) { 240 sLastClickedMountToggle = preference; 241 sClickedMountPoint = volume.getPath(); 242 String state = mStorageManager.getVolumeState(volume.getPath()); 243 if (Environment.MEDIA_MOUNTED.equals(state) || 244 Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { 245 unmount(); 246 } else { 247 mount(); 248 } 249 return true; 250 } 251 } 252 253 return false; 254 } 255 256 private final BroadcastReceiver mMediaScannerReceiver = new BroadcastReceiver() { 257 @Override 258 public void onReceive(Context context, Intent intent) { 259 String action = intent.getAction(); 260 if (action.equals(UsbManager.ACTION_USB_STATE)) { 261 boolean isUsbConnected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false); 262 String usbFunction = mUsbManager.getDefaultFunction(); 263 for (StorageVolumePreferenceCategory category : mCategories) { 264 category.onUsbStateChanged(isUsbConnected, usbFunction); 265 } 266 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) { 267 for (StorageVolumePreferenceCategory category : mCategories) { 268 category.onMediaScannerFinished(); 269 } 270 } 271 } 272 }; 273 274 @Override onCreateDialog(int id)275 public Dialog onCreateDialog(int id) { 276 switch (id) { 277 case DLG_CONFIRM_UNMOUNT: 278 return new AlertDialog.Builder(getActivity()) 279 .setTitle(R.string.dlg_confirm_unmount_title) 280 .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { 281 public void onClick(DialogInterface dialog, int which) { 282 doUnmount(); 283 }}) 284 .setNegativeButton(R.string.cancel, null) 285 .setMessage(R.string.dlg_confirm_unmount_text) 286 .create(); 287 case DLG_ERROR_UNMOUNT: 288 return new AlertDialog.Builder(getActivity()) 289 .setTitle(R.string.dlg_error_unmount_title) 290 .setNeutralButton(R.string.dlg_ok, null) 291 .setMessage(R.string.dlg_error_unmount_text) 292 .create(); 293 } 294 return null; 295 } 296 297 private void doUnmount() { 298 // Present a toast here 299 Toast.makeText(getActivity(), R.string.unmount_inform_text, Toast.LENGTH_SHORT).show(); 300 IMountService mountService = getMountService(); 301 try { 302 sLastClickedMountToggle.setEnabled(false); 303 sLastClickedMountToggle.setTitle(getString(R.string.sd_ejecting_title)); 304 sLastClickedMountToggle.setSummary(getString(R.string.sd_ejecting_summary)); 305 mountService.unmountVolume(sClickedMountPoint, true, false); 306 } catch (RemoteException e) { 307 // Informative dialog to user that unmount failed. 308 showDialogInner(DLG_ERROR_UNMOUNT); 309 } 310 } 311 312 private void showDialogInner(int id) { 313 removeDialog(id); 314 showDialog(id); 315 } 316 317 private boolean hasAppsAccessingStorage() throws RemoteException { 318 IMountService mountService = getMountService(); 319 int stUsers[] = mountService.getStorageUsers(sClickedMountPoint); 320 if (stUsers != null && stUsers.length > 0) { 321 return true; 322 } 323 // TODO FIXME Parameterize with mountPoint and uncomment. 324 // On HC-MR2, no apps can be installed on sd and the emulated internal storage is not 325 // removable: application cannot interfere with unmount 326 /* 327 ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); 328 List<ApplicationInfo> list = am.getRunningExternalApplications(); 329 if (list != null && list.size() > 0) { 330 return true; 331 } 332 */ 333 // Better safe than sorry. Assume the storage is used to ask for confirmation. 334 return true; 335 } 336 337 private void unmount() { 338 // Check if external media is in use. 339 try { 340 if (hasAppsAccessingStorage()) { 341 // Present dialog to user 342 showDialogInner(DLG_CONFIRM_UNMOUNT); 343 } else { 344 doUnmount(); 345 } 346 } catch (RemoteException e) { 347 // Very unlikely. But present an error dialog anyway 348 Log.e(TAG, "Is MountService running?"); 349 showDialogInner(DLG_ERROR_UNMOUNT); 350 } 351 } 352 353 private void mount() { 354 IMountService mountService = getMountService(); 355 try { 356 if (mountService != null) { 357 mountService.mountVolume(sClickedMountPoint); 358 } else { 359 Log.e(TAG, "Mount service is null, can't mount"); 360 } 361 } catch (RemoteException ex) { 362 // Not much can be done 363 } 364 } 365 366 private void onCacheCleared() { 367 for (StorageVolumePreferenceCategory category : mCategories) { 368 category.onCacheCleared(); 369 } 370 } 371 372 private static class ClearCacheObserver extends IPackageDataObserver.Stub { 373 private final Memory mTarget; 374 private int mRemaining; 375 376 public ClearCacheObserver(Memory target, int remaining) { 377 mTarget = target; 378 mRemaining = remaining; 379 } 380 381 @Override 382 public void onRemoveCompleted(final String packageName, final boolean succeeded) { 383 synchronized (this) { 384 if (--mRemaining == 0) { 385 mTarget.onCacheCleared(); 386 } 387 } 388 } 389 } 390 391 /** 392 * Dialog to request user confirmation before clearing all cache data. 393 */ 394 public static class ConfirmClearCacheFragment extends DialogFragment { 395 public static void show(Memory parent) { 396 if (!parent.isAdded()) return; 397 398 final ConfirmClearCacheFragment dialog = new ConfirmClearCacheFragment(); 399 dialog.setTargetFragment(parent, 0); 400 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_CLEAR_CACHE); 401 } 402 403 @Override 404 public Dialog onCreateDialog(Bundle savedInstanceState) { 405 final Context context = getActivity(); 406 407 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 408 builder.setTitle(R.string.memory_clear_cache_title); 409 builder.setMessage(getString(R.string.memory_clear_cache_message)); 410 411 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 412 @Override 413 public void onClick(DialogInterface dialog, int which) { 414 final Memory target = (Memory) getTargetFragment(); 415 final PackageManager pm = context.getPackageManager(); 416 final List<PackageInfo> infos = pm.getInstalledPackages(0); 417 final ClearCacheObserver observer = new ClearCacheObserver( 418 target, infos.size()); 419 for (PackageInfo info : infos) { 420 pm.deleteApplicationCacheFiles(info.packageName, observer); 421 } 422 } 423 }); 424 builder.setNegativeButton(android.R.string.cancel, null); 425 426 return builder.create(); 427 } 428 } 429 430 /** 431 * Enable indexing of searchable data 432 */ 433 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 434 new BaseSearchIndexProvider() { 435 @Override 436 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 437 final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); 438 439 SearchIndexableRaw data = new SearchIndexableRaw(context); 440 data.title = context.getString(R.string.storage_settings); 441 data.screenTitle = context.getString(R.string.storage_settings); 442 result.add(data); 443 444 data = new SearchIndexableRaw(context); 445 data.title = context.getString(R.string.internal_storage); 446 data.screenTitle = context.getString(R.string.storage_settings); 447 result.add(data); 448 449 data = new SearchIndexableRaw(context); 450 final StorageVolume[] storageVolumes = StorageManager.from(context).getVolumeList(); 451 for (StorageVolume volume : storageVolumes) { 452 if (!volume.isEmulated()) { 453 data.title = volume.getDescription(context); 454 data.screenTitle = context.getString(R.string.storage_settings); 455 result.add(data); 456 } 457 } 458 459 data = new SearchIndexableRaw(context); 460 data.title = context.getString(R.string.memory_size); 461 data.screenTitle = context.getString(R.string.storage_settings); 462 result.add(data); 463 464 data = new SearchIndexableRaw(context); 465 data.title = context.getString(R.string.memory_available); 466 data.screenTitle = context.getString(R.string.storage_settings); 467 result.add(data); 468 469 data = new SearchIndexableRaw(context); 470 data.title = context.getString(R.string.memory_apps_usage); 471 data.screenTitle = context.getString(R.string.storage_settings); 472 result.add(data); 473 474 data = new SearchIndexableRaw(context); 475 data.title = context.getString(R.string.memory_dcim_usage); 476 data.screenTitle = context.getString(R.string.storage_settings); 477 result.add(data); 478 479 data = new SearchIndexableRaw(context); 480 data.title = context.getString(R.string.memory_music_usage); 481 data.screenTitle = context.getString(R.string.storage_settings); 482 result.add(data); 483 484 data = new SearchIndexableRaw(context); 485 data.title = context.getString(R.string.memory_downloads_usage); 486 data.screenTitle = context.getString(R.string.storage_settings); 487 result.add(data); 488 489 data = new SearchIndexableRaw(context); 490 data.title = context.getString(R.string.memory_media_cache_usage); 491 data.screenTitle = context.getString(R.string.storage_settings); 492 result.add(data); 493 494 data = new SearchIndexableRaw(context); 495 data.title = context.getString(R.string.memory_media_misc_usage); 496 data.screenTitle = context.getString(R.string.storage_settings); 497 result.add(data); 498 499 return result; 500 } 501 }; 502 503 } 504