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