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 
17 package com.android.settings.deviceinfo;
18 
19 import android.app.ActivityManagerNative;
20 import android.app.ActivityThread;
21 import android.app.DownloadManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.IPackageManager;
25 import android.content.pm.UserInfo;
26 import android.content.res.Resources;
27 import android.hardware.usb.UsbManager;
28 import android.os.Environment;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.os.RemoteException;
32 import android.os.UserManager;
33 import android.os.storage.StorageManager;
34 import android.os.storage.StorageVolume;
35 import android.preference.Preference;
36 import android.preference.PreferenceCategory;
37 import android.provider.MediaStore;
38 import android.text.format.Formatter;
39 
40 import com.android.settings.R;
41 import com.android.settings.Settings;
42 import com.android.settings.deviceinfo.StorageMeasurement.MeasurementDetails;
43 import com.android.settings.deviceinfo.StorageMeasurement.MeasurementReceiver;
44 import com.google.android.collect.Lists;
45 
46 import java.util.HashMap;
47 import java.util.Iterator;
48 import java.util.List;
49 
50 public 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      */
buildForInternal(Context context)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      */
buildForPhysical( Context context, StorageVolume volume)116     public static StorageVolumePreferenceCategory buildForPhysical(
117             Context context, StorageVolume volume) {
118         return new StorageVolumePreferenceCategory(context, volume);
119     }
120 
StorageVolumePreferenceCategory(Context context, StorageVolume volume)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 
buildItem(int titleRes, int colorRes)135     private StorageItemPreference buildItem(int titleRes, int colorRes) {
136         return new StorageItemPreference(getContext(), titleRes, colorRes);
137     }
138 
init()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 
getStorageVolume()233     public StorageVolume getStorageVolume() {
234         return mVolume;
235     }
236 
updatePreferencesFromState()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 
updateApproximate(long totalSize, long availSize)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 
totalValues(HashMap<String, Long> map, String... keys)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 
updateDetails(MeasurementDetails details)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 
updatePreference(StorageItemPreference pref, long size)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 
measure()365     private void measure() {
366         mMeasure.invalidate();
367         mMeasure.measure();
368     }
369 
onResume()370     public void onResume() {
371         mMeasure.setReceiver(mReceiver);
372         measure();
373     }
374 
onStorageStateChanged()375     public void onStorageStateChanged() {
376         init();
377         measure();
378     }
379 
onUsbStateChanged(boolean isUsbConnected, String usbFunction)380     public void onUsbStateChanged(boolean isUsbConnected, String usbFunction) {
381         mUsbConnected = isUsbConnected;
382         mUsbFunction = usbFunction;
383         measure();
384     }
385 
onMediaScannerFinished()386     public void onMediaScannerFinished() {
387         measure();
388     }
389 
onCacheCleared()390     public void onCacheCleared() {
391         measure();
392     }
393 
onPause()394     public void onPause() {
395         mMeasure.cleanUp();
396     }
397 
formatSize(long size)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 
mountToggleClicked(Preference preference)415     public boolean mountToggleClicked(Preference preference) {
416         return preference == mMountTogglePreference;
417     }
418 
intentForClick(Preference pref)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 {
PreferenceHeader(Context context, int titleRes)454         public PreferenceHeader(Context context, int titleRes) {
455             super(context, null, com.android.internal.R.attr.preferenceCategoryStyle);
456             setTitle(titleRes);
457         }
458 
PreferenceHeader(Context context, CharSequence title)459         public PreferenceHeader(Context context, CharSequence title) {
460             super(context, null, com.android.internal.R.attr.preferenceCategoryStyle);
461             setTitle(title);
462         }
463 
464         @Override
isEnabled()465         public boolean isEnabled() {
466             return false;
467         }
468     }
469 
470     /**
471      * Return list of other users, excluding the current user.
472      */
getUsersExcluding(UserInfo excluding)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