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