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