1/*
2 * Copyright (C) 2011 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.ActivityManagerNative;
20import android.app.ActivityThread;
21import android.app.DownloadManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.IPackageManager;
25import android.content.pm.UserInfo;
26import android.content.res.Resources;
27import android.hardware.usb.UsbManager;
28import android.os.Environment;
29import android.os.Handler;
30import android.os.Message;
31import android.os.RemoteException;
32import android.os.UserManager;
33import android.os.storage.StorageManager;
34import android.os.storage.StorageVolume;
35import android.preference.Preference;
36import android.preference.PreferenceCategory;
37import android.provider.MediaStore;
38import android.text.format.Formatter;
39
40import com.android.settings.R;
41import com.android.settings.deviceinfo.StorageMeasurement.MeasurementDetails;
42import com.android.settings.deviceinfo.StorageMeasurement.MeasurementReceiver;
43import com.google.android.collect.Lists;
44
45import java.util.HashMap;
46import java.util.Iterator;
47import java.util.List;
48
49public class StorageVolumePreferenceCategory extends PreferenceCategory {
50    public static final String KEY_CACHE = "cache";
51
52    private static final int ORDER_USAGE_BAR = -2;
53    private static final int ORDER_STORAGE_LOW = -1;
54
55    /** Physical volume being measured, or {@code null} for internal. */
56    private final StorageVolume mVolume;
57    private final StorageMeasurement mMeasure;
58
59    private final Resources mResources;
60    private final StorageManager mStorageManager;
61    private final UserManager mUserManager;
62
63    private UsageBarPreference mUsageBarPreference;
64    private Preference mMountTogglePreference;
65    private Preference mFormatPreference;
66    private Preference mStorageLow;
67
68    private StorageItemPreference mItemTotal;
69    private StorageItemPreference mItemAvailable;
70    private StorageItemPreference mItemApps;
71    private StorageItemPreference mItemDcim;
72    private StorageItemPreference mItemMusic;
73    private StorageItemPreference mItemDownloads;
74    private StorageItemPreference mItemCache;
75    private StorageItemPreference mItemMisc;
76    private List<StorageItemPreference> mItemUsers = Lists.newArrayList();
77
78    private boolean mUsbConnected;
79    private String mUsbFunction;
80
81    private long mTotalSize;
82
83    private static final int MSG_UI_UPDATE_APPROXIMATE = 1;
84    private static final int MSG_UI_UPDATE_DETAILS = 2;
85
86    private Handler mUpdateHandler = new Handler() {
87        @Override
88        public void handleMessage(Message msg) {
89            switch (msg.what) {
90                case MSG_UI_UPDATE_APPROXIMATE: {
91                    final long[] size = (long[]) msg.obj;
92                    updateApproximate(size[0], size[1]);
93                    break;
94                }
95                case MSG_UI_UPDATE_DETAILS: {
96                    final MeasurementDetails details = (MeasurementDetails) msg.obj;
97                    updateDetails(details);
98                    break;
99                }
100            }
101        }
102    };
103
104    /**
105     * Build category to summarize internal storage, including any emulated
106     * {@link StorageVolume}.
107     */
108    public static StorageVolumePreferenceCategory buildForInternal(Context context) {
109        return new StorageVolumePreferenceCategory(context, null);
110    }
111
112    /**
113     * Build category to summarize specific physical {@link StorageVolume}.
114     */
115    public static StorageVolumePreferenceCategory buildForPhysical(
116            Context context, StorageVolume volume) {
117        return new StorageVolumePreferenceCategory(context, volume);
118    }
119
120    private StorageVolumePreferenceCategory(Context context, StorageVolume volume) {
121        super(context);
122
123        mVolume = volume;
124        mMeasure = StorageMeasurement.getInstance(context, volume);
125
126        mResources = context.getResources();
127        mStorageManager = StorageManager.from(context);
128        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
129
130        setTitle(volume != null ? volume.getDescription(context)
131                : context.getText(R.string.internal_storage));
132    }
133
134    private StorageItemPreference buildItem(int titleRes, int colorRes) {
135        return new StorageItemPreference(getContext(), titleRes, colorRes);
136    }
137
138    public void init() {
139        final Context context = getContext();
140
141        removeAll();
142
143        final UserInfo currentUser;
144        try {
145            currentUser = ActivityManagerNative.getDefault().getCurrentUser();
146        } catch (RemoteException e) {
147            throw new RuntimeException("Failed to get current user");
148        }
149
150        final List<UserInfo> otherUsers = getUsersExcluding(currentUser);
151        final boolean showUsers = mVolume == null && otherUsers.size() > 0;
152
153        mUsageBarPreference = new UsageBarPreference(context);
154        mUsageBarPreference.setOrder(ORDER_USAGE_BAR);
155        addPreference(mUsageBarPreference);
156
157        mItemTotal = buildItem(R.string.memory_size, 0);
158        mItemAvailable = buildItem(R.string.memory_available, R.color.memory_avail);
159        addPreference(mItemTotal);
160        addPreference(mItemAvailable);
161
162        mItemApps = buildItem(R.string.memory_apps_usage, R.color.memory_apps_usage);
163        mItemDcim = buildItem(R.string.memory_dcim_usage, R.color.memory_dcim);
164        mItemMusic = buildItem(R.string.memory_music_usage, R.color.memory_music);
165        mItemDownloads = buildItem(R.string.memory_downloads_usage, R.color.memory_downloads);
166        mItemCache = buildItem(R.string.memory_media_cache_usage, R.color.memory_cache);
167        mItemMisc = buildItem(R.string.memory_media_misc_usage, R.color.memory_misc);
168
169        mItemCache.setKey(KEY_CACHE);
170
171        final boolean showDetails = mVolume == null || mVolume.isPrimary();
172        if (showDetails) {
173            if (showUsers) {
174                addPreference(new PreferenceHeader(context, currentUser.name));
175            }
176
177            addPreference(mItemApps);
178            addPreference(mItemDcim);
179            addPreference(mItemMusic);
180            addPreference(mItemDownloads);
181            addPreference(mItemCache);
182            addPreference(mItemMisc);
183
184            if (showUsers) {
185                addPreference(new PreferenceHeader(context, R.string.storage_other_users));
186
187                int count = 0;
188                for (UserInfo info : otherUsers) {
189                    final int colorRes = count++ % 2 == 0 ? R.color.memory_user_light
190                            : R.color.memory_user_dark;
191                    final StorageItemPreference userPref = new StorageItemPreference(
192                            getContext(), info.name, colorRes, info.id);
193                    mItemUsers.add(userPref);
194                    addPreference(userPref);
195                }
196            }
197        }
198
199        final boolean isRemovable = mVolume != null ? mVolume.isRemovable() : false;
200        // Always create the preference since many code rely on it existing
201        mMountTogglePreference = new Preference(context);
202        if (isRemovable) {
203            mMountTogglePreference.setTitle(R.string.sd_eject);
204            mMountTogglePreference.setSummary(R.string.sd_eject_summary);
205            addPreference(mMountTogglePreference);
206        }
207
208        final boolean allowFormat = mVolume != null;
209        if (allowFormat) {
210            mFormatPreference = new Preference(context);
211            mFormatPreference.setTitle(R.string.sd_format);
212            mFormatPreference.setSummary(R.string.sd_format_summary);
213            addPreference(mFormatPreference);
214        }
215
216        final IPackageManager pm = ActivityThread.getPackageManager();
217        try {
218            if (pm.isStorageLow()) {
219                mStorageLow = new Preference(context);
220                mStorageLow.setOrder(ORDER_STORAGE_LOW);
221                mStorageLow.setTitle(R.string.storage_low_title);
222                mStorageLow.setSummary(R.string.storage_low_summary);
223                addPreference(mStorageLow);
224            } else if (mStorageLow != null) {
225                removePreference(mStorageLow);
226                mStorageLow = null;
227            }
228        } catch (RemoteException e) {
229        }
230    }
231
232    public StorageVolume getStorageVolume() {
233        return mVolume;
234    }
235
236    private void updatePreferencesFromState() {
237        // Only update for physical volumes
238        if (mVolume == null) return;
239
240        mMountTogglePreference.setEnabled(true);
241
242        final String state = mStorageManager.getVolumeState(mVolume.getPath());
243
244        if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
245            mItemAvailable.setTitle(R.string.memory_available_read_only);
246        } else {
247            mItemAvailable.setTitle(R.string.memory_available);
248        }
249
250        if (Environment.MEDIA_MOUNTED.equals(state)
251                || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
252            mMountTogglePreference.setEnabled(true);
253            mMountTogglePreference.setTitle(mResources.getString(R.string.sd_eject));
254            mMountTogglePreference.setSummary(mResources.getString(R.string.sd_eject_summary));
255        } else {
256            if (Environment.MEDIA_UNMOUNTED.equals(state) || Environment.MEDIA_NOFS.equals(state)
257                    || Environment.MEDIA_UNMOUNTABLE.equals(state)) {
258                mMountTogglePreference.setEnabled(true);
259                mMountTogglePreference.setTitle(mResources.getString(R.string.sd_mount));
260                mMountTogglePreference.setSummary(mResources.getString(R.string.sd_mount_summary));
261            } else {
262                mMountTogglePreference.setEnabled(false);
263                mMountTogglePreference.setTitle(mResources.getString(R.string.sd_mount));
264                mMountTogglePreference.setSummary(mResources.getString(R.string.sd_insert_summary));
265            }
266
267            removePreference(mUsageBarPreference);
268            removePreference(mItemTotal);
269            removePreference(mItemAvailable);
270        }
271
272        if (mUsbConnected && (UsbManager.USB_FUNCTION_MTP.equals(mUsbFunction) ||
273                UsbManager.USB_FUNCTION_PTP.equals(mUsbFunction))) {
274            mMountTogglePreference.setEnabled(false);
275            if (Environment.MEDIA_MOUNTED.equals(state)
276                    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
277                mMountTogglePreference.setSummary(
278                        mResources.getString(R.string.mtp_ptp_mode_summary));
279            }
280
281            if (mFormatPreference != null) {
282                mFormatPreference.setEnabled(false);
283                mFormatPreference.setSummary(mResources.getString(R.string.mtp_ptp_mode_summary));
284            }
285        } else if (mFormatPreference != null) {
286            mFormatPreference.setEnabled(true);
287            mFormatPreference.setSummary(mResources.getString(R.string.sd_format_summary));
288        }
289    }
290
291    public void updateApproximate(long totalSize, long availSize) {
292        mItemTotal.setSummary(formatSize(totalSize));
293        mItemAvailable.setSummary(formatSize(availSize));
294
295        mTotalSize = totalSize;
296
297        final long usedSize = totalSize - availSize;
298
299        mUsageBarPreference.clear();
300        mUsageBarPreference.addEntry(0, usedSize / (float) totalSize, android.graphics.Color.GRAY);
301        mUsageBarPreference.commit();
302
303        updatePreferencesFromState();
304    }
305
306    private static long totalValues(HashMap<String, Long> map, String... keys) {
307        long total = 0;
308        for (String key : keys) {
309            if (map.containsKey(key)) {
310                total += map.get(key);
311            }
312        }
313        return total;
314    }
315
316    public void updateDetails(MeasurementDetails details) {
317        final boolean showDetails = mVolume == null || mVolume.isPrimary();
318        if (!showDetails) return;
319
320        // Count caches as available space, since system manages them
321        mItemTotal.setSummary(formatSize(details.totalSize));
322        mItemAvailable.setSummary(formatSize(details.availSize));
323
324        mUsageBarPreference.clear();
325
326        updatePreference(mItemApps, details.appsSize);
327
328        final long dcimSize = totalValues(details.mediaSize, Environment.DIRECTORY_DCIM,
329                Environment.DIRECTORY_MOVIES, Environment.DIRECTORY_PICTURES);
330        updatePreference(mItemDcim, dcimSize);
331
332        final long musicSize = totalValues(details.mediaSize, Environment.DIRECTORY_MUSIC,
333                Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
334                Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS);
335        updatePreference(mItemMusic, musicSize);
336
337        final long downloadsSize = totalValues(details.mediaSize, Environment.DIRECTORY_DOWNLOADS);
338        updatePreference(mItemDownloads, downloadsSize);
339
340        updatePreference(mItemCache, details.cacheSize);
341        updatePreference(mItemMisc, details.miscSize);
342
343        for (StorageItemPreference userPref : mItemUsers) {
344            final long userSize = details.usersSize.get(userPref.userHandle);
345            updatePreference(userPref, userSize);
346        }
347
348        mUsageBarPreference.commit();
349    }
350
351    private void updatePreference(StorageItemPreference pref, long size) {
352        if (size > 0) {
353            pref.setSummary(formatSize(size));
354            final int order = pref.getOrder();
355            mUsageBarPreference.addEntry(order, size / (float) mTotalSize, pref.color);
356        } else {
357            removePreference(pref);
358        }
359    }
360
361    private void measure() {
362        mMeasure.invalidate();
363        mMeasure.measure();
364    }
365
366    public void onResume() {
367        mMeasure.setReceiver(mReceiver);
368        measure();
369    }
370
371    public void onStorageStateChanged() {
372        init();
373        measure();
374    }
375
376    public void onUsbStateChanged(boolean isUsbConnected, String usbFunction) {
377        mUsbConnected = isUsbConnected;
378        mUsbFunction = usbFunction;
379        measure();
380    }
381
382    public void onMediaScannerFinished() {
383        measure();
384    }
385
386    public void onCacheCleared() {
387        measure();
388    }
389
390    public void onPause() {
391        mMeasure.cleanUp();
392    }
393
394    private String formatSize(long size) {
395        return Formatter.formatFileSize(getContext(), size);
396    }
397
398    private MeasurementReceiver mReceiver = new MeasurementReceiver() {
399        @Override
400        public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize) {
401            mUpdateHandler.obtainMessage(MSG_UI_UPDATE_APPROXIMATE, new long[] {
402                    totalSize, availSize }).sendToTarget();
403        }
404
405        @Override
406        public void updateDetails(StorageMeasurement meas, MeasurementDetails details) {
407            mUpdateHandler.obtainMessage(MSG_UI_UPDATE_DETAILS, details).sendToTarget();
408        }
409    };
410
411    public boolean mountToggleClicked(Preference preference) {
412        return preference == mMountTogglePreference;
413    }
414
415    public Intent intentForClick(Preference pref) {
416        Intent intent = null;
417
418        // TODO The current "delete" story is not fully handled by the respective applications.
419        // When it is done, make sure the intent types below are correct.
420        // If that cannot be done, remove these intents.
421        final String key = pref.getKey();
422        if (pref == mFormatPreference) {
423            intent = new Intent(Intent.ACTION_VIEW);
424            intent.setClass(getContext(), com.android.settings.MediaFormat.class);
425            intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, mVolume);
426        } else if (pref == mItemApps) {
427            intent = new Intent(Intent.ACTION_MANAGE_PACKAGE_STORAGE);
428            intent.setClass(getContext(),
429                    com.android.settings.Settings.ManageApplicationsActivity.class);
430        } else if (pref == mItemDownloads) {
431            intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).putExtra(
432                    DownloadManager.INTENT_EXTRAS_SORT_BY_SIZE, true);
433        } else if (pref == mItemMusic) {
434            intent = new Intent(Intent.ACTION_GET_CONTENT);
435            intent.setType("audio/mp3");
436        } else if (pref == mItemDcim) {
437            intent = new Intent(Intent.ACTION_VIEW);
438            intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
439            // TODO Create a Videos category, MediaStore.Video.Media.EXTERNAL_CONTENT_URI
440            intent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
441        } else if (pref == mItemMisc) {
442            Context context = getContext().getApplicationContext();
443            intent = new Intent(context, MiscFilesHandler.class);
444            intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, mVolume);
445        }
446
447        return intent;
448    }
449
450    public static class PreferenceHeader extends Preference {
451        public PreferenceHeader(Context context, int titleRes) {
452            super(context, null, com.android.internal.R.attr.preferenceCategoryStyle);
453            setTitle(titleRes);
454        }
455
456        public PreferenceHeader(Context context, CharSequence title) {
457            super(context, null, com.android.internal.R.attr.preferenceCategoryStyle);
458            setTitle(title);
459        }
460
461        @Override
462        public boolean isEnabled() {
463            return false;
464        }
465    }
466
467    /**
468     * Return list of other users, excluding the current user.
469     */
470    private List<UserInfo> getUsersExcluding(UserInfo excluding) {
471        final List<UserInfo> users = mUserManager.getUsers();
472        final Iterator<UserInfo> i = users.iterator();
473        while (i.hasNext()) {
474            if (i.next().id == excluding.id) {
475                i.remove();
476            }
477        }
478        return users;
479    }
480}
481