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