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