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