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