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
17package com.android.settings.deviceinfo;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.DialogFragment;
23import android.app.Fragment;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.content.Intent;
27import android.graphics.Color;
28import android.graphics.drawable.Drawable;
29import android.os.AsyncTask;
30import android.os.Bundle;
31import android.os.UserHandle;
32import android.os.UserManager;
33import android.os.storage.DiskInfo;
34import android.os.storage.StorageEventListener;
35import android.os.storage.StorageManager;
36import android.os.storage.VolumeInfo;
37import android.os.storage.VolumeRecord;
38import android.support.v7.preference.Preference;
39import android.support.v7.preference.PreferenceCategory;
40import android.text.TextUtils;
41import android.text.format.Formatter;
42import android.text.format.Formatter.BytesResult;
43import android.util.Log;
44import android.widget.Toast;
45import com.android.internal.logging.MetricsProto.MetricsEvent;
46import com.android.settings.R;
47import com.android.settings.SettingsPreferenceFragment;
48import com.android.settings.Utils;
49import com.android.settings.dashboard.SummaryLoader;
50import com.android.settings.search.BaseSearchIndexProvider;
51import com.android.settings.search.Indexable;
52import com.android.settings.search.SearchIndexableRaw;
53
54import com.android.settingslib.RestrictedLockUtils;
55import com.android.settingslib.drawer.SettingsDrawerActivity;
56
57import java.io.File;
58import java.util.ArrayList;
59import java.util.Collections;
60import java.util.List;
61
62import 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 */
68public 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
93    protected int getMetricsCategory() {
94        return MetricsEvent.DEVICEINFO_STORAGE;
95    }
96
97    @Override
98    protected int getHelpResource() {
99        return R.string.help_uri_storage;
100    }
101
102    @Override
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
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
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
237    public void onResume() {
238        super.onResume();
239        mStorageManager.registerListener(mStorageListener);
240        refresh();
241    }
242
243    @Override
244    public void onPause() {
245        super.onPause();
246        mStorageManager.unregisterListener(mStorageListener);
247    }
248
249    @Override
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
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
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
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
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
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
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 {
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
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 {
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
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
466        private SummaryProvider(Context context, SummaryLoader loader) {
467            mContext = context;
468            mLoader = loader;
469        }
470
471        @Override
472        public void setListening(boolean listening) {
473            if (listening) {
474                updateSummary();
475            }
476        }
477
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