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