1 /*
2  * Copyright (C) 2015 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.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.Fragment;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.graphics.Color;
28 import android.graphics.drawable.Drawable;
29 import android.os.AsyncTask;
30 import android.os.Bundle;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.os.storage.DiskInfo;
34 import android.os.storage.StorageEventListener;
35 import android.os.storage.StorageManager;
36 import android.os.storage.VolumeInfo;
37 import android.os.storage.VolumeRecord;
38 import android.support.v7.preference.Preference;
39 import android.support.v7.preference.PreferenceCategory;
40 import android.text.TextUtils;
41 import android.text.format.Formatter;
42 import android.text.format.Formatter.BytesResult;
43 import android.util.Log;
44 import android.widget.Toast;
45 import com.android.internal.logging.MetricsProto.MetricsEvent;
46 import com.android.settings.R;
47 import com.android.settings.SettingsPreferenceFragment;
48 import com.android.settings.Utils;
49 import com.android.settings.dashboard.SummaryLoader;
50 import com.android.settings.search.BaseSearchIndexProvider;
51 import com.android.settings.search.Indexable;
52 import com.android.settings.search.SearchIndexableRaw;
53 
54 import com.android.settingslib.RestrictedLockUtils;
55 import com.android.settingslib.drawer.SettingsDrawerActivity;
56 
57 import java.io.File;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.List;
61 
62 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
63 
64 /**
65  * Panel showing both internal storage (both built-in storage and private
66  * volumes) and removable storage (public volumes).
67  */
68 public class StorageSettings extends SettingsPreferenceFragment implements Indexable {
69     static final String TAG = "StorageSettings";
70 
71     private static final String TAG_VOLUME_UNMOUNTED = "volume_unmounted";
72     private static final String TAG_DISK_INIT = "disk_init";
73 
74     static final int COLOR_PUBLIC = Color.parseColor("#ff9e9e9e");
75     static final int COLOR_WARNING = Color.parseColor("#fff4511e");
76 
77     static final int[] COLOR_PRIVATE = new int[] {
78             Color.parseColor("#ff26a69a"),
79             Color.parseColor("#ffab47bc"),
80             Color.parseColor("#fff2a600"),
81             Color.parseColor("#ffec407a"),
82             Color.parseColor("#ffc0ca33"),
83     };
84 
85     private StorageManager mStorageManager;
86 
87     private PreferenceCategory mInternalCategory;
88     private PreferenceCategory mExternalCategory;
89 
90     private StorageSummaryPreference mInternalSummary;
91 
92     @Override
getMetricsCategory()93     protected int getMetricsCategory() {
94         return MetricsEvent.DEVICEINFO_STORAGE;
95     }
96 
97     @Override
getHelpResource()98     protected int getHelpResource() {
99         return R.string.help_uri_storage;
100     }
101 
102     @Override
onCreate(Bundle icicle)103     public void onCreate(Bundle icicle) {
104         super.onCreate(icicle);
105 
106         final Context context = getActivity();
107 
108         mStorageManager = context.getSystemService(StorageManager.class);
109         mStorageManager.registerListener(mStorageListener);
110 
111         addPreferencesFromResource(R.xml.device_info_storage);
112 
113         mInternalCategory = (PreferenceCategory) findPreference("storage_internal");
114         mExternalCategory = (PreferenceCategory) findPreference("storage_external");
115 
116         mInternalSummary = new StorageSummaryPreference(getPrefContext());
117 
118         setHasOptionsMenu(true);
119     }
120 
121     private final StorageEventListener mStorageListener = new StorageEventListener() {
122         @Override
123         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
124             if (isInteresting(vol)) {
125                 refresh();
126             }
127         }
128 
129         @Override
130         public void onDiskDestroyed(DiskInfo disk) {
131             refresh();
132         }
133     };
134 
isInteresting(VolumeInfo vol)135     private static boolean isInteresting(VolumeInfo vol) {
136         switch(vol.getType()) {
137             case VolumeInfo.TYPE_PRIVATE:
138             case VolumeInfo.TYPE_PUBLIC:
139                 return true;
140             default:
141                 return false;
142         }
143     }
144 
refresh()145     private void refresh() {
146         final Context context = getPrefContext();
147 
148         getPreferenceScreen().removeAll();
149         mInternalCategory.removeAll();
150         mExternalCategory.removeAll();
151 
152         mInternalCategory.addPreference(mInternalSummary);
153 
154         int privateCount = 0;
155         long privateUsedBytes = 0;
156         long privateTotalBytes = 0;
157 
158         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
159         Collections.sort(volumes, VolumeInfo.getDescriptionComparator());
160 
161         for (VolumeInfo vol : volumes) {
162             if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
163                 final int color = COLOR_PRIVATE[privateCount++ % COLOR_PRIVATE.length];
164                 mInternalCategory.addPreference(
165                         new StorageVolumePreference(context, vol, color));
166                 if (vol.isMountedReadable()) {
167                     final File path = vol.getPath();
168                     privateUsedBytes += path.getTotalSpace() - path.getFreeSpace();
169                     privateTotalBytes += path.getTotalSpace();
170                 }
171             } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
172                 mExternalCategory.addPreference(
173                         new StorageVolumePreference(context, vol, COLOR_PUBLIC));
174             }
175         }
176 
177         // Show missing private volumes
178         final List<VolumeRecord> recs = mStorageManager.getVolumeRecords();
179         for (VolumeRecord rec : recs) {
180             if (rec.getType() == VolumeInfo.TYPE_PRIVATE
181                     && mStorageManager.findVolumeByUuid(rec.getFsUuid()) == null) {
182                 // TODO: add actual storage type to record
183                 final Drawable icon = context.getDrawable(R.drawable.ic_sim_sd);
184                 icon.mutate();
185                 icon.setTint(COLOR_PUBLIC);
186 
187                 final Preference pref = new Preference(context);
188                 pref.setKey(rec.getFsUuid());
189                 pref.setTitle(rec.getNickname());
190                 pref.setSummary(com.android.internal.R.string.ext_media_status_missing);
191                 pref.setIcon(icon);
192                 mInternalCategory.addPreference(pref);
193             }
194         }
195 
196         // Show unsupported disks to give a chance to init
197         final List<DiskInfo> disks = mStorageManager.getDisks();
198         for (DiskInfo disk : disks) {
199             if (disk.volumeCount == 0 && disk.size > 0) {
200                 final Preference pref = new Preference(context);
201                 pref.setKey(disk.getId());
202                 pref.setTitle(disk.getDescription());
203                 pref.setSummary(com.android.internal.R.string.ext_media_status_unsupported);
204                 pref.setIcon(R.drawable.ic_sim_sd);
205                 mExternalCategory.addPreference(pref);
206             }
207         }
208 
209         final BytesResult result = Formatter.formatBytes(getResources(), privateUsedBytes, 0);
210         mInternalSummary.setTitle(TextUtils.expandTemplate(getText(R.string.storage_size_large),
211                 result.value, result.units));
212         mInternalSummary.setSummary(getString(R.string.storage_volume_used_total,
213                 Formatter.formatFileSize(context, privateTotalBytes)));
214 
215         if (mInternalCategory.getPreferenceCount() > 0) {
216             getPreferenceScreen().addPreference(mInternalCategory);
217         }
218         if (mExternalCategory.getPreferenceCount() > 0) {
219             getPreferenceScreen().addPreference(mExternalCategory);
220         }
221 
222         if (mInternalCategory.getPreferenceCount() == 2
223                 && mExternalCategory.getPreferenceCount() == 0) {
224             // Only showing primary internal storage, so just shortcut
225             final Bundle args = new Bundle();
226             args.putString(VolumeInfo.EXTRA_VOLUME_ID, VolumeInfo.ID_PRIVATE_INTERNAL);
227             Intent intent = Utils.onBuildStartFragmentIntent(getActivity(),
228                     PrivateVolumeSettings.class.getName(), args, null, R.string.apps_storage, null,
229                     false);
230             intent.putExtra(SettingsDrawerActivity.EXTRA_SHOW_MENU, true);
231             getActivity().startActivity(intent);
232             finish();
233         }
234     }
235 
236     @Override
onResume()237     public void onResume() {
238         super.onResume();
239         mStorageManager.registerListener(mStorageListener);
240         refresh();
241     }
242 
243     @Override
onPause()244     public void onPause() {
245         super.onPause();
246         mStorageManager.unregisterListener(mStorageListener);
247     }
248 
249     @Override
onPreferenceTreeClick(Preference pref)250     public boolean onPreferenceTreeClick(Preference pref) {
251         final String key = pref.getKey();
252         if (pref instanceof StorageVolumePreference) {
253             // Picked a normal volume
254             final VolumeInfo vol = mStorageManager.findVolumeById(key);
255 
256             if (vol == null) {
257                 return false;
258             }
259 
260             if (vol.getState() == VolumeInfo.STATE_UNMOUNTED) {
261                 VolumeUnmountedFragment.show(this, vol.getId());
262                 return true;
263             } else if (vol.getState() == VolumeInfo.STATE_UNMOUNTABLE) {
264                 DiskInitFragment.show(this, R.string.storage_dialog_unmountable, vol.getDiskId());
265                 return true;
266             }
267 
268             if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
269                 final Bundle args = new Bundle();
270                 args.putString(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
271                 startFragment(this, PrivateVolumeSettings.class.getCanonicalName(),
272                         -1, 0, args);
273                 return true;
274 
275             } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
276                 if (vol.isMountedReadable()) {
277                     startActivity(vol.buildBrowseIntent());
278                     return true;
279                 } else {
280                     final Bundle args = new Bundle();
281                     args.putString(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
282                     startFragment(this, PublicVolumeSettings.class.getCanonicalName(),
283                             -1, 0, args);
284                     return true;
285                 }
286             }
287 
288         } else if (key.startsWith("disk:")) {
289             // Picked an unsupported disk
290             DiskInitFragment.show(this, R.string.storage_dialog_unsupported, key);
291             return true;
292 
293         } else {
294             // Picked a missing private volume
295             final Bundle args = new Bundle();
296             args.putString(VolumeRecord.EXTRA_FS_UUID, key);
297             startFragment(this, PrivateVolumeForget.class.getCanonicalName(),
298                     R.string.storage_menu_forget, 0, args);
299             return true;
300         }
301 
302         return false;
303     }
304 
305     public static class MountTask extends AsyncTask<Void, Void, Exception> {
306         private final Context mContext;
307         private final StorageManager mStorageManager;
308         private final String mVolumeId;
309         private final String mDescription;
310 
MountTask(Context context, VolumeInfo volume)311         public MountTask(Context context, VolumeInfo volume) {
312             mContext = context.getApplicationContext();
313             mStorageManager = mContext.getSystemService(StorageManager.class);
314             mVolumeId = volume.getId();
315             mDescription = mStorageManager.getBestVolumeDescription(volume);
316         }
317 
318         @Override
doInBackground(Void... params)319         protected Exception doInBackground(Void... params) {
320             try {
321                 mStorageManager.mount(mVolumeId);
322                 return null;
323             } catch (Exception e) {
324                 return e;
325             }
326         }
327 
328         @Override
onPostExecute(Exception e)329         protected void onPostExecute(Exception e) {
330             if (e == null) {
331                 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_success,
332                         mDescription), Toast.LENGTH_SHORT).show();
333             } else {
334                 Log.e(TAG, "Failed to mount " + mVolumeId, e);
335                 Toast.makeText(mContext, mContext.getString(R.string.storage_mount_failure,
336                         mDescription), Toast.LENGTH_SHORT).show();
337             }
338         }
339     }
340 
341     public static class UnmountTask extends AsyncTask<Void, Void, Exception> {
342         private final Context mContext;
343         private final StorageManager mStorageManager;
344         private final String mVolumeId;
345         private final String mDescription;
346 
UnmountTask(Context context, VolumeInfo volume)347         public UnmountTask(Context context, VolumeInfo volume) {
348             mContext = context.getApplicationContext();
349             mStorageManager = mContext.getSystemService(StorageManager.class);
350             mVolumeId = volume.getId();
351             mDescription = mStorageManager.getBestVolumeDescription(volume);
352         }
353 
354         @Override
doInBackground(Void... params)355         protected Exception doInBackground(Void... params) {
356             try {
357                 mStorageManager.unmount(mVolumeId);
358                 return null;
359             } catch (Exception e) {
360                 return e;
361             }
362         }
363 
364         @Override
onPostExecute(Exception e)365         protected void onPostExecute(Exception e) {
366             if (e == null) {
367                 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_success,
368                         mDescription), Toast.LENGTH_SHORT).show();
369             } else {
370                 Log.e(TAG, "Failed to unmount " + mVolumeId, e);
371                 Toast.makeText(mContext, mContext.getString(R.string.storage_unmount_failure,
372                         mDescription), Toast.LENGTH_SHORT).show();
373             }
374         }
375     }
376 
377     public static class VolumeUnmountedFragment extends DialogFragment {
show(Fragment parent, String volumeId)378         public static void show(Fragment parent, String volumeId) {
379             final Bundle args = new Bundle();
380             args.putString(VolumeInfo.EXTRA_VOLUME_ID, volumeId);
381 
382             final VolumeUnmountedFragment dialog = new VolumeUnmountedFragment();
383             dialog.setArguments(args);
384             dialog.setTargetFragment(parent, 0);
385             dialog.show(parent.getFragmentManager(), TAG_VOLUME_UNMOUNTED);
386         }
387 
388         @Override
onCreateDialog(Bundle savedInstanceState)389         public Dialog onCreateDialog(Bundle savedInstanceState) {
390             final Context context = getActivity();
391             final StorageManager sm = context.getSystemService(StorageManager.class);
392 
393             final String volumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID);
394             final VolumeInfo vol = sm.findVolumeById(volumeId);
395 
396             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
397             builder.setMessage(TextUtils.expandTemplate(
398                     getText(R.string.storage_dialog_unmounted), vol.getDisk().getDescription()));
399 
400             builder.setPositiveButton(R.string.storage_menu_mount,
401                     new DialogInterface.OnClickListener() {
402                 @Override
403                 public void onClick(DialogInterface dialog, int which) {
404                     EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(
405                             getActivity(), UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
406                             UserHandle.myUserId());
407                     boolean hasBaseUserRestriction = RestrictedLockUtils.hasBaseUserRestriction(
408                             getActivity(), UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
409                             UserHandle.myUserId());
410                     if (admin != null && !hasBaseUserRestriction) {
411                         RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(), admin);
412                         return;
413                     }
414                     new MountTask(context, vol).execute();
415                 }
416             });
417             builder.setNegativeButton(R.string.cancel, null);
418 
419             return builder.create();
420         }
421     }
422 
423     public static class DiskInitFragment extends DialogFragment {
show(Fragment parent, int resId, String diskId)424         public static void show(Fragment parent, int resId, String diskId) {
425             final Bundle args = new Bundle();
426             args.putInt(Intent.EXTRA_TEXT, resId);
427             args.putString(DiskInfo.EXTRA_DISK_ID, diskId);
428 
429             final DiskInitFragment dialog = new DiskInitFragment();
430             dialog.setArguments(args);
431             dialog.setTargetFragment(parent, 0);
432             dialog.show(parent.getFragmentManager(), TAG_DISK_INIT);
433         }
434 
435         @Override
onCreateDialog(Bundle savedInstanceState)436         public Dialog onCreateDialog(Bundle savedInstanceState) {
437             final Context context = getActivity();
438             final StorageManager sm = context.getSystemService(StorageManager.class);
439 
440             final int resId = getArguments().getInt(Intent.EXTRA_TEXT);
441             final String diskId = getArguments().getString(DiskInfo.EXTRA_DISK_ID);
442             final DiskInfo disk = sm.findDiskById(diskId);
443 
444             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
445             builder.setMessage(TextUtils.expandTemplate(getText(resId), disk.getDescription()));
446 
447             builder.setPositiveButton(R.string.storage_menu_set_up,
448                     new DialogInterface.OnClickListener() {
449                 @Override
450                 public void onClick(DialogInterface dialog, int which) {
451                     final Intent intent = new Intent(context, StorageWizardInit.class);
452                     intent.putExtra(DiskInfo.EXTRA_DISK_ID, diskId);
453                     startActivity(intent);
454                 }
455             });
456             builder.setNegativeButton(R.string.cancel, null);
457 
458             return builder.create();
459         }
460     }
461 
462     private static class SummaryProvider implements SummaryLoader.SummaryProvider {
463         private final Context mContext;
464         private final SummaryLoader mLoader;
465 
SummaryProvider(Context context, SummaryLoader loader)466         private SummaryProvider(Context context, SummaryLoader loader) {
467             mContext = context;
468             mLoader = loader;
469         }
470 
471         @Override
setListening(boolean listening)472         public void setListening(boolean listening) {
473             if (listening) {
474                 updateSummary();
475             }
476         }
477 
updateSummary()478         private void updateSummary() {
479             // TODO: Register listener.
480             StorageManager storageManager = mContext.getSystemService(StorageManager.class);
481             final List<VolumeInfo> volumes = storageManager.getVolumes();
482             long privateUsedBytes = 0;
483             long privateTotalBytes = 0;
484             for (VolumeInfo info : volumes) {
485                 if (info.getType() != VolumeInfo.TYPE_PUBLIC
486                         && info.getType() != VolumeInfo.TYPE_PRIVATE) {
487                     continue;
488                 }
489                 final File path = info.getPath();
490                 if (path == null) {
491                     continue;
492                 }
493                 privateUsedBytes += path.getTotalSpace() - path.getFreeSpace();
494                 privateTotalBytes += path.getTotalSpace();
495             }
496             mLoader.setSummary(this, mContext.getString(R.string.storage_summary,
497                     Formatter.formatFileSize(mContext, privateUsedBytes),
498                     Formatter.formatFileSize(mContext, privateTotalBytes)));
499         }
500     }
501 
502     public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
503             = new SummaryLoader.SummaryProviderFactory() {
504         @Override
505         public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
506                                                                    SummaryLoader summaryLoader) {
507             return new SummaryProvider(activity, summaryLoader);
508         }
509     };
510 
511     /**
512      * Enable indexing of searchable data
513      */
514     public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
515         new BaseSearchIndexProvider() {
516             @Override
517             public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
518                 final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>();
519 
520                 SearchIndexableRaw data = new SearchIndexableRaw(context);
521                 data.title = context.getString(R.string.storage_settings);
522                 data.screenTitle = context.getString(R.string.storage_settings);
523                 result.add(data);
524 
525                 data = new SearchIndexableRaw(context);
526                 data.title = context.getString(R.string.internal_storage);
527                 data.screenTitle = context.getString(R.string.storage_settings);
528                 result.add(data);
529 
530                 data = new SearchIndexableRaw(context);
531                 final StorageManager storage = context.getSystemService(StorageManager.class);
532                 final List<VolumeInfo> vols = storage.getVolumes();
533                 for (VolumeInfo vol : vols) {
534                     if (isInteresting(vol)) {
535                         data.title = storage.getBestVolumeDescription(vol);
536                         data.screenTitle = context.getString(R.string.storage_settings);
537                         result.add(data);
538                     }
539                 }
540 
541                 data = new SearchIndexableRaw(context);
542                 data.title = context.getString(R.string.memory_size);
543                 data.screenTitle = context.getString(R.string.storage_settings);
544                 result.add(data);
545 
546                 data = new SearchIndexableRaw(context);
547                 data.title = context.getString(R.string.memory_available);
548                 data.screenTitle = context.getString(R.string.storage_settings);
549                 result.add(data);
550 
551                 data = new SearchIndexableRaw(context);
552                 data.title = context.getString(R.string.memory_apps_usage);
553                 data.screenTitle = context.getString(R.string.storage_settings);
554                 result.add(data);
555 
556                 data = new SearchIndexableRaw(context);
557                 data.title = context.getString(R.string.memory_dcim_usage);
558                 data.screenTitle = context.getString(R.string.storage_settings);
559                 result.add(data);
560 
561                 data = new SearchIndexableRaw(context);
562                 data.title = context.getString(R.string.memory_music_usage);
563                 data.screenTitle = context.getString(R.string.storage_settings);
564                 result.add(data);
565 
566                 data = new SearchIndexableRaw(context);
567                 data.title = context.getString(R.string.memory_downloads_usage);
568                 data.screenTitle = context.getString(R.string.storage_settings);
569                 result.add(data);
570 
571                 data = new SearchIndexableRaw(context);
572                 data.title = context.getString(R.string.memory_media_cache_usage);
573                 data.screenTitle = context.getString(R.string.storage_settings);
574                 result.add(data);
575 
576                 data = new SearchIndexableRaw(context);
577                 data.title = context.getString(R.string.memory_media_misc_usage);
578                 data.screenTitle = context.getString(R.string.storage_settings);
579                 result.add(data);
580 
581                 return result;
582             }
583         };
584 }
585