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