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.AlertDialog;
20import android.app.Dialog;
21import android.app.Fragment;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.Intent;
25import android.content.pm.IPackageDataObserver;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.content.pm.UserInfo;
29import android.os.Build;
30import android.os.Bundle;
31import android.os.Environment;
32import android.os.UserHandle;
33import android.os.UserManager;
34import android.os.storage.StorageEventListener;
35import android.os.storage.StorageManager;
36import android.os.storage.VolumeInfo;
37import android.os.storage.VolumeRecord;
38import android.provider.DocumentsContract;
39import android.support.v7.preference.Preference;
40import android.support.v7.preference.PreferenceCategory;
41import android.support.v7.preference.PreferenceGroup;
42import android.support.v7.preference.PreferenceScreen;
43import android.text.TextUtils;
44import android.text.format.Formatter;
45import android.text.format.Formatter.BytesResult;
46import android.util.Log;
47import android.view.LayoutInflater;
48import android.view.Menu;
49import android.view.MenuInflater;
50import android.view.MenuItem;
51import android.view.View;
52import android.widget.EditText;
53
54import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
55import com.android.settings.R;
56import com.android.settings.Settings.StorageUseActivity;
57import com.android.settings.SettingsPreferenceFragment;
58import com.android.settings.Utils;
59import com.android.settings.applications.manageapplications.ManageApplications;
60import com.android.settings.core.SubSettingLauncher;
61import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
62import com.android.settings.deviceinfo.StorageSettings.MountTask;
63import com.android.settingslib.deviceinfo.StorageMeasurement;
64import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementDetails;
65import com.android.settingslib.deviceinfo.StorageMeasurement.MeasurementReceiver;
66
67import com.google.android.collect.Lists;
68
69import java.util.HashMap;
70import java.util.List;
71import java.util.Objects;
72
73/**
74 * Panel showing summary and actions for a {@link VolumeInfo#TYPE_PRIVATE}
75 * storage volume.
76 */
77public class PrivateVolumeSettings extends SettingsPreferenceFragment {
78    // TODO: disable unmount when providing over MTP/PTP
79    // TODO: warn when mounted read-only
80
81    private static final String TAG = "PrivateVolumeSettings";
82    private static final boolean LOGV = false;
83
84    private static final String TAG_RENAME = "rename";
85    private static final String TAG_OTHER_INFO = "otherInfo";
86    private static final String TAG_SYSTEM_INFO = "systemInfo";
87    private static final String TAG_USER_INFO = "userInfo";
88    private static final String TAG_CONFIRM_CLEAR_CACHE = "confirmClearCache";
89
90    private static final String EXTRA_VOLUME_SIZE = "volume_size";
91
92    private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
93
94    private static final int[] ITEMS_NO_SHOW_SHARED = new int[] {
95            R.string.storage_detail_apps,
96            R.string.storage_detail_system,
97    };
98
99    private static final int[] ITEMS_SHOW_SHARED = new int[] {
100            R.string.storage_detail_apps,
101            R.string.storage_detail_images,
102            R.string.storage_detail_videos,
103            R.string.storage_detail_audio,
104            R.string.storage_detail_system,
105            R.string.storage_detail_other,
106    };
107
108    private StorageManager mStorageManager;
109    private UserManager mUserManager;
110
111    private String mVolumeId;
112    private VolumeInfo mVolume;
113    private VolumeInfo mSharedVolume;
114    private long mTotalSize;
115    private long mSystemSize;
116
117    private StorageMeasurement mMeasure;
118
119    private UserInfo mCurrentUser;
120
121    private StorageSummaryPreference mSummary;
122    private List<StorageItemPreference> mItemPreferencePool = Lists.newArrayList();
123    private List<PreferenceCategory> mHeaderPreferencePool = Lists.newArrayList();
124    private int mHeaderPoolIndex;
125    private int mItemPoolIndex;
126
127    private Preference mExplore;
128
129    private boolean mNeedsUpdate;
130
131    private boolean isVolumeValid() {
132        return (mVolume != null) && (mVolume.getType() == VolumeInfo.TYPE_PRIVATE)
133                && mVolume.isMountedReadable();
134    }
135
136    public PrivateVolumeSettings() {
137        setRetainInstance(true);
138    }
139
140    @Override
141    public int getMetricsCategory() {
142        return MetricsEvent.DEVICEINFO_STORAGE;
143    }
144
145    @Override
146    public void onCreate(Bundle icicle) {
147        super.onCreate(icicle);
148
149        final Context context = getActivity();
150
151        mUserManager = context.getSystemService(UserManager.class);
152        mStorageManager = context.getSystemService(StorageManager.class);
153
154        mVolumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID);
155        mVolume = mStorageManager.findVolumeById(mVolumeId);
156
157        final long sharedDataSize = mVolume.getPath().getTotalSpace();
158        mTotalSize = getArguments().getLong(EXTRA_VOLUME_SIZE, 0);
159        mSystemSize = mTotalSize - sharedDataSize;
160        if (LOGV) Log.v(TAG,
161                "onCreate() mTotalSize: " + mTotalSize + " sharedDataSize: " + sharedDataSize);
162
163        if (mTotalSize <= 0) {
164            mTotalSize = sharedDataSize;
165            mSystemSize = 0;
166        }
167
168        // Find the emulated shared storage layered above this private volume
169        mSharedVolume = mStorageManager.findEmulatedForPrivate(mVolume);
170
171        mMeasure = new StorageMeasurement(context, mVolume, mSharedVolume);
172        mMeasure.setReceiver(mReceiver);
173
174        if (!isVolumeValid()) {
175            getActivity().finish();
176            return;
177        }
178
179        addPreferencesFromResource(R.xml.device_info_storage_volume);
180        getPreferenceScreen().setOrderingAsAdded(true);
181
182        mSummary = new StorageSummaryPreference(getPrefContext());
183        mCurrentUser = mUserManager.getUserInfo(UserHandle.myUserId());
184
185        mExplore = buildAction(R.string.storage_menu_explore);
186
187        mNeedsUpdate = true;
188
189        setHasOptionsMenu(true);
190    }
191
192    private void setTitle() {
193        getActivity().setTitle(mStorageManager.getBestVolumeDescription(mVolume));
194    }
195
196    private void update() {
197        if (!isVolumeValid()) {
198            getActivity().finish();
199            return;
200        }
201
202        setTitle();
203
204        // Valid options may have changed
205        getFragmentManager().invalidateOptionsMenu();
206
207        final Context context = getActivity();
208        final PreferenceScreen screen = getPreferenceScreen();
209
210        screen.removeAll();
211
212        addPreference(screen, mSummary);
213
214        List<UserInfo> allUsers = mUserManager.getUsers();
215        final int userCount = allUsers.size();
216        final boolean showHeaders = userCount > 1;
217        final boolean showShared = (mSharedVolume != null) && mSharedVolume.isMountedReadable();
218
219        mItemPoolIndex = 0;
220        mHeaderPoolIndex = 0;
221
222        int addedUserCount = 0;
223        // Add current user and its profiles first
224        for (int userIndex = 0; userIndex < userCount; ++userIndex) {
225            final UserInfo userInfo = allUsers.get(userIndex);
226            if (Utils.isProfileOf(mCurrentUser, userInfo)) {
227                final PreferenceGroup details = showHeaders ?
228                        addCategory(screen, userInfo.name) : screen;
229                addDetailItems(details, showShared, userInfo.id);
230                ++addedUserCount;
231            }
232        }
233
234        // Add rest of users
235        if (userCount - addedUserCount > 0) {
236            PreferenceGroup otherUsers = addCategory(screen,
237                    getText(R.string.storage_other_users));
238            for (int userIndex = 0; userIndex < userCount; ++userIndex) {
239                final UserInfo userInfo = allUsers.get(userIndex);
240                if (!Utils.isProfileOf(mCurrentUser, userInfo)) {
241                    addItem(otherUsers, /* titleRes */ 0, userInfo.name, userInfo.id);
242                }
243            }
244        }
245
246        addItem(screen, R.string.storage_detail_cached, null, UserHandle.USER_NULL);
247
248        if (showShared) {
249            addPreference(screen, mExplore);
250        }
251
252        final long freeBytes = mVolume.getPath().getFreeSpace();
253        final long usedBytes = mTotalSize - freeBytes;
254
255        if (LOGV) Log.v(TAG, "update() freeBytes: " + freeBytes + " usedBytes: " + usedBytes);
256
257        final BytesResult result = Formatter.formatBytes(getResources(), usedBytes, 0);
258        mSummary.setTitle(TextUtils.expandTemplate(getText(R.string.storage_size_large),
259                result.value, result.units));
260        mSummary.setSummary(getString(R.string.storage_volume_used,
261                Formatter.formatFileSize(context, mTotalSize)));
262        mSummary.setPercent(usedBytes, mTotalSize);
263
264        mMeasure.forceMeasure();
265        mNeedsUpdate = false;
266    }
267
268    private void addPreference(PreferenceGroup group, Preference pref) {
269        pref.setOrder(Preference.DEFAULT_ORDER);
270        group.addPreference(pref);
271    }
272
273    private PreferenceCategory addCategory(PreferenceGroup group, CharSequence title) {
274        PreferenceCategory category;
275        if (mHeaderPoolIndex < mHeaderPreferencePool.size()) {
276            category = mHeaderPreferencePool.get(mHeaderPoolIndex);
277        } else {
278            category = new PreferenceCategory(getPrefContext());
279            mHeaderPreferencePool.add(category);
280        }
281        category.setTitle(title);
282        category.removeAll();
283        addPreference(group, category);
284        ++mHeaderPoolIndex;
285        return category;
286    }
287
288    private void addDetailItems(PreferenceGroup category, boolean showShared, int userId) {
289        final int[] itemsToAdd = (showShared ? ITEMS_SHOW_SHARED : ITEMS_NO_SHOW_SHARED);
290        for (int i = 0; i < itemsToAdd.length; ++i) {
291            addItem(category, itemsToAdd[i], null, userId);
292        }
293    }
294
295    private void addItem(PreferenceGroup group, int titleRes, CharSequence title, int userId) {
296        if (titleRes == R.string.storage_detail_system) {
297            if (mSystemSize <= 0) {
298                Log.w(TAG, "Skipping System storage because its size is " + mSystemSize);
299                return;
300            }
301            if (userId != UserHandle.myUserId()) {
302                // Only display system on current user.
303                return;
304            }
305        }
306        StorageItemPreference item;
307        if (mItemPoolIndex < mItemPreferencePool.size()) {
308            item = mItemPreferencePool.get(mItemPoolIndex);
309        } else {
310            item = buildItem();
311            mItemPreferencePool.add(item);
312        }
313        if (title != null) {
314            item.setTitle(title);
315            item.setKey(title.toString());
316        } else {
317            item.setTitle(titleRes);
318            item.setKey(Integer.toString(titleRes));
319        }
320        item.setSummary(R.string.memory_calculating_size);
321        item.userHandle = userId;
322        addPreference(group, item);
323        ++mItemPoolIndex;
324    }
325
326    private StorageItemPreference buildItem() {
327        final StorageItemPreference item = new StorageItemPreference(getPrefContext());
328        item.setIcon(R.drawable.empty_icon);
329        return item;
330    }
331
332    private Preference buildAction(int titleRes) {
333        final Preference pref = new Preference(getPrefContext());
334        pref.setTitle(titleRes);
335        pref.setKey(Integer.toString(titleRes));
336        return pref;
337    }
338
339    static void setVolumeSize(Bundle args, long size) {
340        args.putLong(EXTRA_VOLUME_SIZE, size);
341    }
342
343    @Override
344    public void onResume() {
345        super.onResume();
346
347        // Refresh to verify that we haven't been formatted away
348        mVolume = mStorageManager.findVolumeById(mVolumeId);
349        if (!isVolumeValid()) {
350            getActivity().finish();
351            return;
352        }
353
354        mStorageManager.registerListener(mStorageListener);
355
356        if (mNeedsUpdate) {
357            update();
358        } else {
359            setTitle();
360        }
361    }
362
363    @Override
364    public void onPause() {
365        super.onPause();
366        mStorageManager.unregisterListener(mStorageListener);
367    }
368
369    @Override
370    public void onDestroy() {
371        super.onDestroy();
372        if (mMeasure != null) {
373            mMeasure.onDestroy();
374        }
375    }
376
377    @Override
378    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
379        super.onCreateOptionsMenu(menu, inflater);
380        inflater.inflate(R.menu.storage_volume, menu);
381    }
382
383    @Override
384    public void onPrepareOptionsMenu(Menu menu) {
385        if (!isVolumeValid()) return;
386
387        final MenuItem rename = menu.findItem(R.id.storage_rename);
388        final MenuItem mount = menu.findItem(R.id.storage_mount);
389        final MenuItem unmount = menu.findItem(R.id.storage_unmount);
390        final MenuItem format = menu.findItem(R.id.storage_format);
391        final MenuItem migrate = menu.findItem(R.id.storage_migrate);
392        final MenuItem manage = menu.findItem(R.id.storage_free);
393
394        // Actions live in menu for non-internal private volumes; they're shown
395        // as preference items for public volumes.
396        if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(mVolume.getId())) {
397            rename.setVisible(false);
398            mount.setVisible(false);
399            unmount.setVisible(false);
400            format.setVisible(false);
401            manage.setVisible(getResources().getBoolean(
402                    R.bool.config_storage_manager_settings_enabled));
403        } else {
404            rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
405            mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
406            unmount.setVisible(mVolume.isMountedReadable());
407            format.setVisible(true);
408            manage.setVisible(false);
409        }
410
411        format.setTitle(R.string.storage_menu_format_public);
412
413        // Only offer to migrate when not current storage
414        final VolumeInfo privateVol = getActivity().getPackageManager()
415                .getPrimaryStorageCurrentVolume();
416        migrate.setVisible((privateVol != null)
417                && (privateVol.getType() == VolumeInfo.TYPE_PRIVATE)
418                && !Objects.equals(mVolume, privateVol));
419    }
420
421    @Override
422    public boolean onOptionsItemSelected(MenuItem item) {
423        final Context context = getActivity();
424        final Bundle args = new Bundle();
425        switch (item.getItemId()) {
426            case R.id.storage_rename:
427                RenameFragment.show(this, mVolume);
428                return true;
429            case R.id.storage_mount:
430                new MountTask(context, mVolume).execute();
431                return true;
432            case R.id.storage_unmount:
433                args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
434                new SubSettingLauncher(context)
435                        .setDestination(PrivateVolumeUnmount.class.getCanonicalName())
436                        .setTitle(R.string.storage_menu_unmount)
437                        .setSourceMetricsCategory(getMetricsCategory())
438                        .setArguments(args)
439                        .launch();
440                return true;
441            case R.id.storage_format:
442                args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
443                new SubSettingLauncher(context)
444                        .setDestination(PrivateVolumeFormat.class.getCanonicalName())
445                        .setTitle(R.string.storage_menu_format)
446                        .setSourceMetricsCategory(getMetricsCategory())
447                        .setArguments(args)
448                        .launch();
449                return true;
450            case R.id.storage_migrate:
451                final Intent intent = new Intent(context, StorageWizardMigrateConfirm.class);
452                intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
453                startActivity(intent);
454                return true;
455            case R.id.storage_free:
456                final Intent deletion_helper_intent =
457                        new Intent(StorageManager.ACTION_MANAGE_STORAGE);
458                startActivity(deletion_helper_intent);
459                return true;
460        }
461        return super.onOptionsItemSelected(item);
462    }
463
464    @Override
465    public boolean onPreferenceTreeClick(Preference pref) {
466        // TODO: launch better intents for specific volume
467
468        final int userId = (pref instanceof StorageItemPreference ?
469                ((StorageItemPreference) pref).userHandle : -1);
470        int itemTitleId;
471        try {
472            itemTitleId = Integer.parseInt(pref.getKey());
473        } catch (NumberFormatException e) {
474            itemTitleId = 0;
475        }
476        Intent intent = null;
477        switch (itemTitleId) {
478            case R.string.storage_detail_apps: {
479                Bundle args = new Bundle();
480                args.putString(ManageApplications.EXTRA_CLASSNAME,
481                        StorageUseActivity.class.getName());
482                args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
483                args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
484                args.putInt(
485                        ManageApplications.EXTRA_STORAGE_TYPE,
486                        ManageApplications.STORAGE_TYPE_LEGACY);
487                intent = new SubSettingLauncher(getActivity())
488                        .setDestination(ManageApplications.class.getName())
489                        .setArguments(args)
490                        .setTitle(R.string.apps_storage)
491                        .setSourceMetricsCategory(getMetricsCategory())
492                        .toIntent();
493
494            } break;
495            case R.string.storage_detail_images: {
496                intent = getIntentForStorage(AUTHORITY_MEDIA, "images_root");
497            } break;
498            case R.string.storage_detail_videos: {
499                intent = getIntentForStorage(AUTHORITY_MEDIA, "videos_root");
500            } break;
501            case R.string.storage_detail_audio: {
502                intent = getIntentForStorage(AUTHORITY_MEDIA, "audio_root");
503            } break;
504            case R.string.storage_detail_system: {
505                SystemInfoFragment.show(this);
506                return true;
507
508            }
509            case R.string.storage_detail_other: {
510                OtherInfoFragment.show(this, mStorageManager.getBestVolumeDescription(mVolume),
511                        mSharedVolume, userId);
512                return true;
513
514            }
515            case R.string.storage_detail_cached: {
516                ConfirmClearCacheFragment.show(this);
517                return true;
518
519            }
520            case R.string.storage_menu_explore: {
521                intent = mSharedVolume.buildBrowseIntent();
522            } break;
523            case 0: {
524                UserInfoFragment.show(this, pref.getTitle(), pref.getSummary());
525                return true;
526            }
527        }
528
529        if (intent != null) {
530            intent.putExtra(Intent.EXTRA_USER_ID, userId);
531
532            Utils.launchIntent(this, intent);
533            return true;
534        }
535        return super.onPreferenceTreeClick(pref);
536    }
537
538    private Intent getIntentForStorage(String authority, String root) {
539        Intent intent = new Intent(Intent.ACTION_VIEW);
540        intent.setDataAndType(
541                DocumentsContract.buildRootUri(authority, root),
542                DocumentsContract.Root.MIME_TYPE_ITEM);
543        intent.addCategory(Intent.CATEGORY_DEFAULT);
544
545        return intent;
546    }
547
548    private final MeasurementReceiver mReceiver = new MeasurementReceiver() {
549        @Override
550        public void onDetailsChanged(MeasurementDetails details) {
551            updateDetails(details);
552        }
553    };
554
555    private void updateDetails(MeasurementDetails details) {
556        StorageItemPreference otherItem = null;
557        long accountedSize = 0;
558        long totalMiscSize = 0;
559        long totalDownloadsSize = 0;
560
561        for (int i = 0; i < mItemPoolIndex; ++i) {
562            StorageItemPreference item = mItemPreferencePool.get(i);
563            final int userId = item.userHandle;
564            int itemTitleId;
565            try {
566                itemTitleId = Integer.parseInt(item.getKey());
567            } catch (NumberFormatException e) {
568                itemTitleId = 0;
569            }
570            switch (itemTitleId) {
571                case R.string.storage_detail_system: {
572                    updatePreference(item, mSystemSize);
573                    accountedSize += mSystemSize;
574                    if (LOGV) Log.v(TAG, "mSystemSize: " + mSystemSize
575                            + " accountedSize: " + accountedSize);
576                } break;
577                case R.string.storage_detail_apps: {
578                    updatePreference(item, details.appsSize.get(userId));
579                    accountedSize += details.appsSize.get(userId);
580                    if (LOGV) Log.v(TAG, "appsSize: " + details.appsSize.get(userId)
581                            + " accountedSize: " + accountedSize);
582                } break;
583                case R.string.storage_detail_images: {
584                    final long imagesSize = totalValues(details, userId,
585                            Environment.DIRECTORY_DCIM, Environment.DIRECTORY_PICTURES);
586                    updatePreference(item, imagesSize);
587                    accountedSize += imagesSize;
588                    if (LOGV) Log.v(TAG, "imagesSize: " + imagesSize
589                            + " accountedSize: " + accountedSize);
590                } break;
591                case R.string.storage_detail_videos: {
592                    final long videosSize = totalValues(details, userId,
593                            Environment.DIRECTORY_MOVIES);
594                    updatePreference(item, videosSize);
595                    accountedSize += videosSize;
596                    if (LOGV) Log.v(TAG, "videosSize: " + videosSize
597                            + " accountedSize: " + accountedSize);
598                } break;
599                case R.string.storage_detail_audio: {
600                    final long audioSize = totalValues(details, userId,
601                            Environment.DIRECTORY_MUSIC,
602                            Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
603                            Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS);
604                    updatePreference(item, audioSize);
605                    accountedSize += audioSize;
606                    if (LOGV) Log.v(TAG, "audioSize: " + audioSize
607                            + " accountedSize: " + accountedSize);
608                } break;
609                case R.string.storage_detail_other: {
610                    final long downloadsSize = totalValues(details, userId,
611                            Environment.DIRECTORY_DOWNLOADS);
612                    final long miscSize = details.miscSize.get(userId);
613                    totalDownloadsSize += downloadsSize;
614                    totalMiscSize += miscSize;
615                    accountedSize += miscSize + downloadsSize;
616
617                    if (LOGV)
618                        Log.v(TAG, "miscSize for " + userId + ": " + miscSize + "(total: "
619                                + totalMiscSize + ") \ndownloadsSize: " + downloadsSize + "(total: "
620                                + totalDownloadsSize + ") accountedSize: " + accountedSize);
621
622                    // Cannot display 'Other' until all known items are accounted for.
623                    otherItem = item;
624                } break;
625                case R.string.storage_detail_cached: {
626                    updatePreference(item, details.cacheSize);
627                    accountedSize += details.cacheSize;
628                    if (LOGV)
629                        Log.v(TAG, "cacheSize: " + details.cacheSize + " accountedSize: "
630                                + accountedSize);
631                } break;
632                case 0: {
633                    final long userSize = details.usersSize.get(userId);
634                    updatePreference(item, userSize);
635                    accountedSize += userSize;
636                    if (LOGV) Log.v(TAG, "userSize: " + userSize
637                            + " accountedSize: " + accountedSize);
638                } break;
639            }
640        }
641        if (otherItem != null) {
642            final long usedSize = mTotalSize - details.availSize;
643            final long unaccountedSize = usedSize - accountedSize;
644            final long otherSize = totalMiscSize + totalDownloadsSize + unaccountedSize;
645            Log.v(TAG, "Other items: \n\tmTotalSize: " + mTotalSize + " availSize: "
646                    + details.availSize + " usedSize: " + usedSize + "\n\taccountedSize: "
647                    + accountedSize + " unaccountedSize size: " + unaccountedSize
648                    + "\n\ttotalMiscSize: " + totalMiscSize + " totalDownloadsSize: "
649                    + totalDownloadsSize + "\n\tdetails: " + details);
650            updatePreference(otherItem, otherSize);
651        }
652    }
653
654    private void updatePreference(StorageItemPreference pref, long size) {
655        pref.setStorageSize(size, mTotalSize);
656    }
657
658    private static long totalValues(MeasurementDetails details, int userId, String... keys) {
659        long total = 0;
660        HashMap<String, Long> map = details.mediaSize.get(userId);
661        if (map != null) {
662            for (String key : keys) {
663                if (map.containsKey(key)) {
664                    total += map.get(key);
665                }
666            }
667        } else {
668            Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
669        }
670        return total;
671    }
672
673    private final StorageEventListener mStorageListener = new StorageEventListener() {
674        @Override
675        public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
676            if (Objects.equals(mVolume.getId(), vol.getId())) {
677                mVolume = vol;
678                update();
679            }
680        }
681
682        @Override
683        public void onVolumeRecordChanged(VolumeRecord rec) {
684            if (Objects.equals(mVolume.getFsUuid(), rec.getFsUuid())) {
685                mVolume = mStorageManager.findVolumeById(mVolumeId);
686                update();
687            }
688        }
689    };
690
691    /**
692     * Dialog that allows editing of volume nickname.
693     */
694    public static class RenameFragment extends InstrumentedDialogFragment {
695        public static void show(PrivateVolumeSettings parent, VolumeInfo vol) {
696            if (!parent.isAdded()) return;
697
698            final RenameFragment dialog = new RenameFragment();
699            dialog.setTargetFragment(parent, 0);
700            final Bundle args = new Bundle();
701            args.putString(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
702            dialog.setArguments(args);
703            dialog.show(parent.getFragmentManager(), TAG_RENAME);
704        }
705
706        @Override
707        public int getMetricsCategory() {
708            return MetricsEvent.DIALOG_VOLUME_RENAME;
709        }
710
711        @Override
712        public Dialog onCreateDialog(Bundle savedInstanceState) {
713            final Context context = getActivity();
714            final StorageManager storageManager = context.getSystemService(StorageManager.class);
715
716            final String fsUuid = getArguments().getString(VolumeRecord.EXTRA_FS_UUID);
717            final VolumeInfo vol = storageManager.findVolumeByUuid(fsUuid);
718            final VolumeRecord rec = storageManager.findRecordByUuid(fsUuid);
719
720            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
721            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
722
723            final View view = dialogInflater.inflate(R.layout.dialog_edittext, null, false);
724            final EditText nickname = (EditText) view.findViewById(R.id.edittext);
725            nickname.setText(rec.getNickname());
726
727            builder.setTitle(R.string.storage_rename_title);
728            builder.setView(view);
729
730            builder.setPositiveButton(R.string.save,
731                    new DialogInterface.OnClickListener() {
732                        @Override
733                        public void onClick(DialogInterface dialog, int which) {
734                            // TODO: move to background thread
735                            storageManager.setVolumeNickname(fsUuid,
736                                    nickname.getText().toString());
737                        }
738                    });
739            builder.setNegativeButton(R.string.cancel, null);
740
741            return builder.create();
742        }
743    }
744
745    public static class SystemInfoFragment extends InstrumentedDialogFragment {
746        public static void show(Fragment parent) {
747            if (!parent.isAdded()) return;
748
749            final SystemInfoFragment dialog = new SystemInfoFragment();
750            dialog.setTargetFragment(parent, 0);
751            dialog.show(parent.getFragmentManager(), TAG_SYSTEM_INFO);
752        }
753
754        @Override
755        public int getMetricsCategory() {
756            return MetricsEvent.DIALOG_STORAGE_SYSTEM_INFO;
757        }
758
759        @Override
760        public Dialog onCreateDialog(Bundle savedInstanceState) {
761            return new AlertDialog.Builder(getActivity())
762                    .setMessage(getContext().getString(R.string.storage_detail_dialog_system,
763                            Build.VERSION.RELEASE))
764                    .setPositiveButton(android.R.string.ok, null)
765                    .create();
766        }
767    }
768
769    public static class OtherInfoFragment extends InstrumentedDialogFragment {
770        public static void show(Fragment parent, String title, VolumeInfo sharedVol, int userId) {
771            if (!parent.isAdded()) return;
772
773            final OtherInfoFragment dialog = new OtherInfoFragment();
774            dialog.setTargetFragment(parent, 0);
775            final Bundle args = new Bundle();
776            args.putString(Intent.EXTRA_TITLE, title);
777
778            final Intent intent = sharedVol.buildBrowseIntent();
779            intent.putExtra(Intent.EXTRA_USER_ID, userId);
780            args.putParcelable(Intent.EXTRA_INTENT, intent);
781            dialog.setArguments(args);
782            dialog.show(parent.getFragmentManager(), TAG_OTHER_INFO);
783        }
784
785        @Override
786        public int getMetricsCategory() {
787            return MetricsEvent.DIALOG_STORAGE_OTHER_INFO;
788        }
789
790        @Override
791        public Dialog onCreateDialog(Bundle savedInstanceState) {
792            final Context context = getActivity();
793
794            final String title = getArguments().getString(Intent.EXTRA_TITLE);
795            final Intent intent = getArguments().getParcelable(Intent.EXTRA_INTENT);
796
797            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
798            builder.setMessage(
799                    TextUtils.expandTemplate(getText(R.string.storage_detail_dialog_other), title));
800
801            builder.setPositiveButton(R.string.storage_menu_explore,
802                    new DialogInterface.OnClickListener() {
803                        @Override
804                        public void onClick(DialogInterface dialog, int which) {
805                            Utils.launchIntent(OtherInfoFragment.this, intent);
806                        }
807                    });
808            builder.setNegativeButton(android.R.string.cancel, null);
809
810            return builder.create();
811        }
812    }
813
814    public static class UserInfoFragment extends InstrumentedDialogFragment {
815        public static void show(Fragment parent, CharSequence userLabel, CharSequence userSize) {
816            if (!parent.isAdded()) return;
817
818            final UserInfoFragment dialog = new UserInfoFragment();
819            dialog.setTargetFragment(parent, 0);
820            final Bundle args = new Bundle();
821            args.putCharSequence(Intent.EXTRA_TITLE, userLabel);
822            args.putCharSequence(Intent.EXTRA_SUBJECT, userSize);
823            dialog.setArguments(args);
824            dialog.show(parent.getFragmentManager(), TAG_USER_INFO);
825        }
826
827        @Override
828        public int getMetricsCategory() {
829            return MetricsEvent.DIALOG_STORAGE_USER_INFO;
830        }
831
832        @Override
833        public Dialog onCreateDialog(Bundle savedInstanceState) {
834            final Context context = getActivity();
835
836            final CharSequence userLabel = getArguments().getCharSequence(Intent.EXTRA_TITLE);
837            final CharSequence userSize = getArguments().getCharSequence(Intent.EXTRA_SUBJECT);
838
839            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
840            builder.setMessage(TextUtils.expandTemplate(
841                    getText(R.string.storage_detail_dialog_user), userLabel, userSize));
842
843            builder.setPositiveButton(android.R.string.ok, null);
844
845            return builder.create();
846        }
847    }
848
849    /**
850     * Dialog to request user confirmation before clearing all cache data.
851     */
852    public static class ConfirmClearCacheFragment extends InstrumentedDialogFragment {
853        public static void show(Fragment parent) {
854            if (!parent.isAdded()) return;
855
856            final ConfirmClearCacheFragment dialog = new ConfirmClearCacheFragment();
857            dialog.setTargetFragment(parent, 0);
858            dialog.show(parent.getFragmentManager(), TAG_CONFIRM_CLEAR_CACHE);
859        }
860
861        @Override
862        public int getMetricsCategory() {
863            return MetricsEvent.DIALOG_STORAGE_CLEAR_CACHE;
864        }
865
866        @Override
867        public Dialog onCreateDialog(Bundle savedInstanceState) {
868            final Context context = getActivity();
869
870            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
871            builder.setTitle(R.string.memory_clear_cache_title);
872            builder.setMessage(getString(R.string.memory_clear_cache_message));
873
874            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
875                @Override
876                public void onClick(DialogInterface dialog, int which) {
877                    final PrivateVolumeSettings target = (PrivateVolumeSettings) getTargetFragment();
878                    final PackageManager pm = context.getPackageManager();
879                    final UserManager um = context.getSystemService(UserManager.class);
880
881                    for (int userId : um.getProfileIdsWithDisabled(context.getUserId())) {
882                        final List<PackageInfo> infos = pm.getInstalledPackagesAsUser(0, userId);
883                        final ClearCacheObserver observer = new ClearCacheObserver(
884                                target, infos.size());
885                        for (PackageInfo info : infos) {
886                            pm.deleteApplicationCacheFilesAsUser(info.packageName, userId,
887                                    observer);
888                        }
889                    }
890                }
891            });
892            builder.setNegativeButton(android.R.string.cancel, null);
893
894            return builder.create();
895        }
896    }
897
898    private static class ClearCacheObserver extends IPackageDataObserver.Stub {
899        private final PrivateVolumeSettings mTarget;
900        private int mRemaining;
901
902        public ClearCacheObserver(PrivateVolumeSettings target, int remaining) {
903            mTarget = target;
904            mRemaining = remaining;
905        }
906
907        @Override
908        public void onRemoveCompleted(final String packageName, final boolean succeeded) {
909            synchronized (this) {
910                if (--mRemaining == 0) {
911                    mTarget.getActivity().runOnUiThread(new Runnable() {
912                        @Override
913                        public void run() {
914                            mTarget.update();
915                        }
916                    });
917                }
918            }
919        }
920    }
921}
922