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