2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.deviceinfo;
19 import static com.android.settings.deviceinfo.StorageSettings.TAG;
21 import android.app.AlertDialog;
22 import android.app.Dialog;
23 import android.app.DialogFragment;
24 import android.app.Fragment;
25 import android.content.ActivityNotFoundException;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.pm.IPackageDataObserver;
30 import android.content.pm.PackageInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.UserInfo;
33 import android.os.Bundle;
34 import android.os.Environment;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.os.storage.StorageEventListener;
38 import android.os.storage.StorageManager;
39 import android.os.storage.VolumeInfo;
40 import android.os.storage.VolumeRecord;
41 import android.preference.Preference;
42 import android.preference.PreferenceCategory;
43 import android.preference.PreferenceGroup;
44 import android.preference.PreferenceScreen;
45 import android.provider.DocumentsContract;
46 import android.text.TextUtils;
47 import android.text.format.Formatter;
48 import android.text.format.Formatter.BytesResult;
49 import android.util.Log;
50 import android.view.LayoutInflater;
51 import android.view.Menu;
52 import android.view.MenuInflater;
53 import android.view.MenuItem;
54 import android.view.View;
55 import android.widget.EditText;
57 import com.android.internal.logging.MetricsLogger;
58 import com.android.settings.R;
59 import com.android.settings.Settings.StorageUseActivity;
60 import com.android.settings.SettingsPreferenceFragment;
61 import com.android.settings.Utils;
62 import com.android.settings.applications.ManageApplications;
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 import com.google.android.collect.Lists;
70 import java.util.HashMap;
71 import java.util.List;
72 import java.util.Objects;
75 * Panel showing summary and actions for a {@link VolumeInfo#TYPE_PRIVATE}
78 public class PrivateVolumeSettings extends SettingsPreferenceFragment {
79 // TODO: disable unmount when providing over MTP/PTP
80 // TODO: warn when mounted read-only
82 private static final String TAG_RENAME = "rename";
83 private static final String TAG_OTHER_INFO = "otherInfo";
84 private static final String TAG_USER_INFO = "userInfo";
85 private static final String TAG_CONFIRM_CLEAR_CACHE = "confirmClearCache";
87 private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
89 private static final int[] ITEMS_NO_SHOW_SHARED = new int[] {
90 R.string.storage_detail_apps,
93 private static final int[] ITEMS_SHOW_SHARED = new int[] {
94 R.string.storage_detail_apps,
95 R.string.storage_detail_images,
96 R.string.storage_detail_videos,
97 R.string.storage_detail_audio,
98 R.string.storage_detail_other
101 private StorageManager mStorageManager;
102 private UserManager mUserManager;
104 private String mVolumeId;
105 private VolumeInfo mVolume;
106 private VolumeInfo mSharedVolume;
108 private StorageMeasurement mMeasure;
110 private UserInfo mCurrentUser;
112 private StorageSummaryPreference mSummary;
113 private List<StorageItemPreference> mItemPreferencePool = Lists.newArrayList();
114 private List<PreferenceCategory> mHeaderPreferencePool = Lists.newArrayList();
115 private int mHeaderPoolIndex;
116 private int mItemPoolIndex;
118 private Preference mExplore;
120 private boolean isVolumeValid() {
121 return (mVolume != null) && (mVolume.getType() == VolumeInfo.TYPE_PRIVATE)
122 && mVolume.isMountedReadable();
126 protected int getMetricsCategory() {
127 return MetricsLogger.DEVICEINFO_STORAGE;
131 public void onCreate(Bundle icicle) {
132 super.onCreate(icicle);
134 final Context context = getActivity();
136 mUserManager = context.getSystemService(UserManager.class);
137 mStorageManager = context.getSystemService(StorageManager.class);
139 mVolumeId = getArguments().getString(VolumeInfo.EXTRA_VOLUME_ID);
140 mVolume = mStorageManager.findVolumeById(mVolumeId);
142 // Find the emulated shared storage layered above this private volume
143 mSharedVolume = mStorageManager.findEmulatedForPrivate(mVolume);
145 mMeasure = new StorageMeasurement(context, mVolume, mSharedVolume);
146 mMeasure.setReceiver(mReceiver);
148 if (!isVolumeValid()) {
149 getActivity().finish();
153 addPreferencesFromResource(R.xml.device_info_storage_volume);
154 getPreferenceScreen().setOrderingAsAdded(true);
156 mSummary = new StorageSummaryPreference(context);
157 mCurrentUser = mUserManager.getUserInfo(UserHandle.myUserId());
159 mExplore = buildAction(R.string.storage_menu_explore);
161 setHasOptionsMenu(true);
164 public void update() {
165 if (!isVolumeValid()) {
166 getActivity().finish();
170 getActivity().setTitle(mStorageManager.getBestVolumeDescription(mVolume));
172 // Valid options may have changed
173 getFragmentManager().invalidateOptionsMenu();
175 final Context context = getActivity();
176 final PreferenceScreen screen = getPreferenceScreen();
180 addPreference(screen, mSummary);
182 List<UserInfo> allUsers = mUserManager.getUsers();
183 final int userCount = allUsers.size();
184 final boolean showHeaders = userCount > 1;
185 final boolean showShared = (mSharedVolume != null) && mSharedVolume.isMountedReadable();
188 mHeaderPoolIndex = 0;
190 int addedUserCount = 0;
191 // Add current user and its profiles first
192 for (int userIndex = 0; userIndex < userCount; ++userIndex) {
193 final UserInfo userInfo = allUsers.get(userIndex);
194 if (isProfileOf(mCurrentUser, userInfo)) {
195 PreferenceCategory details = addCategory(screen,
196 showHeaders ? userInfo.name : null);
197 addDetailItems(details, showShared, userInfo.id);
203 if (userCount - addedUserCount > 0) {
204 PreferenceCategory otherUsers = addCategory(screen,
205 getText(R.string.storage_other_users));
206 for (int userIndex = 0; userIndex < userCount; ++userIndex) {
207 final UserInfo userInfo = allUsers.get(userIndex);
208 if (!isProfileOf(mCurrentUser, userInfo)) {
209 addItem(otherUsers, /* titleRes */ 0, userInfo.name, userInfo.id);
214 addItem(screen, R.string.storage_detail_cached, null, UserHandle.USER_NULL);
217 addPreference(screen, mExplore);
220 final File file = mVolume.getPath();
221 final long totalBytes = file.getTotalSpace();
222 final long freeBytes = file.getFreeSpace();
223 final long usedBytes = totalBytes - freeBytes;
225 final BytesResult result = Formatter.formatBytes(getResources(), usedBytes, 0);
226 mSummary.setTitle(TextUtils.expandTemplate(getText(R.string.storage_size_large),
227 result.value, result.units));
228 mSummary.setSummary(getString(R.string.storage_volume_used,
229 Formatter.formatFileSize(context, totalBytes)));
230 mSummary.setPercent((int) ((usedBytes * 100) / totalBytes));
232 mMeasure.forceMeasure();
235 private void addPreference(PreferenceGroup group, Preference pref) {
236 pref.setOrder(Preference.DEFAULT_ORDER);
237 group.addPreference(pref);
240 private PreferenceCategory addCategory(PreferenceGroup group, CharSequence title) {
241 PreferenceCategory category;
242 if (mHeaderPoolIndex < mHeaderPreferencePool.size()) {
243 category = mHeaderPreferencePool.get(mHeaderPoolIndex);
245 category = new PreferenceCategory(getActivity(), null,
246 com.android.internal.R.attr.preferenceCategoryStyle);
247 mHeaderPreferencePool.add(category);
249 category.setTitle(title);
250 category.removeAll();
251 addPreference(group, category);
256 private void addDetailItems(PreferenceCategory category, boolean showShared, int userId) {
257 final int[] itemsToAdd = (showShared ? ITEMS_SHOW_SHARED : ITEMS_NO_SHOW_SHARED);
258 for (int i = 0; i < itemsToAdd.length; ++i) {
259 addItem(category, itemsToAdd[i], null, userId);
263 private void addItem(PreferenceGroup group, int titleRes, CharSequence title, int userId) {
264 StorageItemPreference item;
265 if (mItemPoolIndex < mItemPreferencePool.size()) {
266 item = mItemPreferencePool.get(mItemPoolIndex);
269 mItemPreferencePool.add(item);
272 item.setTitle(title);
274 item.setTitle(titleRes);
276 item.setSummary(R.string.memory_calculating_size);
277 item.userHandle = userId;
278 addPreference(group, item);
282 private StorageItemPreference buildItem() {
283 final StorageItemPreference item = new StorageItemPreference(getActivity());
287 private Preference buildAction(int titleRes) {
288 final Preference pref = new Preference(getActivity());
289 pref.setTitle(titleRes);
294 public void onResume() {
297 // Refresh to verify that we haven't been formatted away
298 mVolume = mStorageManager.findVolumeById(mVolumeId);
299 if (!isVolumeValid()) {
300 getActivity().finish();
304 mStorageManager.registerListener(mStorageListener);
309 public void onPause() {
311 mStorageManager.unregisterListener(mStorageListener);
315 public void onDestroy() {
317 if (mMeasure != null) {
318 mMeasure.onDestroy();
323 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
324 inflater.inflate(R.menu.storage_volume, menu);
328 public void onPrepareOptionsMenu(Menu menu) {
329 if (!isVolumeValid()) return;
331 final MenuItem rename = menu.findItem(R.id.storage_rename);
332 final MenuItem mount = menu.findItem(R.id.storage_mount);
333 final MenuItem unmount = menu.findItem(R.id.storage_unmount);
334 final MenuItem format = menu.findItem(R.id.storage_format);
335 final MenuItem migrate = menu.findItem(R.id.storage_migrate);
337 // Actions live in menu for non-internal private volumes; they're shown
338 // as preference items for public volumes.
339 if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(mVolume.getId())) {
340 rename.setVisible(false);
341 mount.setVisible(false);
342 unmount.setVisible(false);
343 format.setVisible(false);
345 rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
346 mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
347 unmount.setVisible(mVolume.isMountedReadable());
348 format.setVisible(true);
351 format.setTitle(R.string.storage_menu_format_public);
353 // Only offer to migrate when not current storage
354 final VolumeInfo privateVol = getActivity().getPackageManager()
355 .getPrimaryStorageCurrentVolume();
356 migrate.setVisible(!Objects.equals(mVolume, privateVol));
360 public boolean onOptionsItemSelected(MenuItem item) {
361 final Context context = getActivity();
362 final Bundle args = new Bundle();
363 switch (item.getItemId()) {
364 case R.id.storage_rename:
365 RenameFragment.show(this, mVolume);
367 case R.id.storage_mount:
368 new MountTask(context, mVolume).execute();
370 case R.id.storage_unmount:
371 args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
372 startFragment(this, PrivateVolumeUnmount.class.getCanonicalName(),
373 R.string.storage_menu_unmount, 0, args);
375 case R.id.storage_format:
376 args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
377 startFragment(this, PrivateVolumeFormat.class.getCanonicalName(),
378 R.string.storage_menu_format, 0, args);
380 case R.id.storage_migrate:
381 final Intent intent = new Intent(context, StorageWizardMigrateConfirm.class);
382 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
383 startActivity(intent);
386 return super.onOptionsItemSelected(item);
390 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference pref) {
391 // TODO: launch better intents for specific volume
393 final int userId = (pref instanceof StorageItemPreference ?
394 ((StorageItemPreference)pref).userHandle : -1);
395 final int itemTitleId = pref.getTitleRes();
396 Intent intent = null;
397 switch (itemTitleId) {
398 case R.string.storage_detail_apps: {
399 Bundle args = new Bundle();
400 args.putString(ManageApplications.EXTRA_CLASSNAME,
401 StorageUseActivity.class.getName());
402 args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
403 args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
404 intent = Utils.onBuildStartFragmentIntent(getActivity(),
405 ManageApplications.class.getName(), args, null, R.string.apps_storage, null,
409 case R.string.storage_detail_images: {
410 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
411 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "images_root"));
412 intent.addCategory(Intent.CATEGORY_DEFAULT);
415 case R.string.storage_detail_videos: {
416 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
417 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "videos_root"));
418 intent.addCategory(Intent.CATEGORY_DEFAULT);
421 case R.string.storage_detail_audio: {
422 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
423 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "audio_root"));
424 intent.addCategory(Intent.CATEGORY_DEFAULT);
427 case R.string.storage_detail_other: {
428 OtherInfoFragment.show(this, mStorageManager.getBestVolumeDescription(mVolume),
433 case R.string.storage_detail_cached: {
434 ConfirmClearCacheFragment.show(this);
438 case R.string.storage_menu_explore: {
439 intent = mSharedVolume.buildBrowseIntent();
442 UserInfoFragment.show(this, pref.getTitle(), pref.getSummary());
447 if (intent != null) {
450 startActivity(intent);
452 getActivity().startActivityAsUser(intent, new UserHandle(userId));
454 } catch (ActivityNotFoundException e) {
455 Log.w(TAG, "No activity found for " + intent);
459 return super.onPreferenceTreeClick(preferenceScreen, pref);
462 private final MeasurementReceiver mReceiver = new MeasurementReceiver() {
464 public void onDetailsChanged(MeasurementDetails details) {
465 updateDetails(details);
469 private void updateDetails(MeasurementDetails details) {
470 for (int i = 0; i < mItemPoolIndex; ++i) {
471 StorageItemPreference item = mItemPreferencePool.get(i);
472 final int userId = item.userHandle;
473 final int itemTitleId = item.getTitleRes();
474 switch (itemTitleId) {
475 case R.string.storage_detail_apps: {
476 updatePreference(item, details.appsSize.get(userId));
478 case R.string.storage_detail_images: {
479 final long imagesSize = totalValues(details, userId,
480 Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
481 Environment.DIRECTORY_PICTURES);
482 updatePreference(item, imagesSize);
484 case R.string.storage_detail_videos: {
485 final long videosSize = totalValues(details, userId,
486 Environment.DIRECTORY_MOVIES);
487 updatePreference(item, videosSize);
489 case R.string.storage_detail_audio: {
490 final long audioSize = totalValues(details, userId,
491 Environment.DIRECTORY_MUSIC,
492 Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
493 Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS);
494 updatePreference(item, audioSize);
496 case R.string.storage_detail_other: {
497 updatePreference(item, details.miscSize.get(userId));
499 case R.string.storage_detail_cached: {
500 updatePreference(item, details.cacheSize);
503 final long userSize = details.usersSize.get(userId);
504 updatePreference(item, userSize);
510 private void updatePreference(StorageItemPreference pref, long size) {
511 pref.setSummary(Formatter.formatFileSize(getActivity(), size));
514 private boolean isProfileOf(UserInfo user, UserInfo profile) {
515 return user.id == profile.id ||
516 (user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
517 && user.profileGroupId == profile.profileGroupId);
520 private static long totalValues(MeasurementDetails details, int userId, String... keys) {
522 HashMap<String, Long> map = details.mediaSize.get(userId);
524 for (String key : keys) {
525 if (map.containsKey(key)) {
526 total += map.get(key);
530 Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
535 private final StorageEventListener mStorageListener = new StorageEventListener() {
537 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
538 if (Objects.equals(mVolume.getId(), vol.getId())) {
545 public void onVolumeRecordChanged(VolumeRecord rec) {
546 if (Objects.equals(mVolume.getFsUuid(), rec.getFsUuid())) {
547 mVolume = mStorageManager.findVolumeById(mVolumeId);
554 * Dialog that allows editing of volume nickname.
556 public static class RenameFragment extends DialogFragment {
557 public static void show(PrivateVolumeSettings parent, VolumeInfo vol) {
558 if (!parent.isAdded()) return;
560 final RenameFragment dialog = new RenameFragment();
561 dialog.setTargetFragment(parent, 0);
562 final Bundle args = new Bundle();
563 args.putString(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
564 dialog.setArguments(args);
565 dialog.show(parent.getFragmentManager(), TAG_RENAME);
569 public Dialog onCreateDialog(Bundle savedInstanceState) {
570 final Context context = getActivity();
571 final StorageManager storageManager = context.getSystemService(StorageManager.class);
573 final String fsUuid = getArguments().getString(VolumeRecord.EXTRA_FS_UUID);
574 final VolumeInfo vol = storageManager.findVolumeByUuid(fsUuid);
575 final VolumeRecord rec = storageManager.findRecordByUuid(fsUuid);
577 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
578 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
580 final View view = dialogInflater.inflate(R.layout.dialog_edittext, null, false);
581 final EditText nickname = (EditText) view.findViewById(R.id.edittext);
582 nickname.setText(rec.getNickname());
584 builder.setTitle(R.string.storage_rename_title);
585 builder.setView(view);
587 builder.setPositiveButton(R.string.save,
588 new DialogInterface.OnClickListener() {
590 public void onClick(DialogInterface dialog, int which) {
591 // TODO: move to background thread
592 storageManager.setVolumeNickname(fsUuid,
593 nickname.getText().toString());
596 builder.setNegativeButton(R.string.cancel, null);
598 return builder.create();
602 public static class OtherInfoFragment extends DialogFragment {
603 public static void show(Fragment parent, String title, VolumeInfo sharedVol) {
604 if (!parent.isAdded()) return;
606 final OtherInfoFragment dialog = new OtherInfoFragment();
607 dialog.setTargetFragment(parent, 0);
608 final Bundle args = new Bundle();
609 args.putString(Intent.EXTRA_TITLE, title);
610 args.putParcelable(Intent.EXTRA_INTENT, sharedVol.buildBrowseIntent());
611 dialog.setArguments(args);
612 dialog.show(parent.getFragmentManager(), TAG_OTHER_INFO);
616 public Dialog onCreateDialog(Bundle savedInstanceState) {
617 final Context context = getActivity();
619 final String title = getArguments().getString(Intent.EXTRA_TITLE);
620 final Intent intent = getArguments().getParcelable(Intent.EXTRA_INTENT);
622 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
624 TextUtils.expandTemplate(getText(R.string.storage_detail_dialog_other), title));
626 builder.setPositiveButton(R.string.storage_menu_explore,
627 new DialogInterface.OnClickListener() {
629 public void onClick(DialogInterface dialog, int which) {
630 startActivity(intent);
633 builder.setNegativeButton(android.R.string.cancel, null);
635 return builder.create();
639 public static class UserInfoFragment extends DialogFragment {
640 public static void show(Fragment parent, CharSequence userLabel, CharSequence userSize) {
641 if (!parent.isAdded()) return;
643 final UserInfoFragment dialog = new UserInfoFragment();
644 dialog.setTargetFragment(parent, 0);
645 final Bundle args = new Bundle();
646 args.putCharSequence(Intent.EXTRA_TITLE, userLabel);
647 args.putCharSequence(Intent.EXTRA_SUBJECT, userSize);
648 dialog.setArguments(args);
649 dialog.show(parent.getFragmentManager(), TAG_USER_INFO);
653 public Dialog onCreateDialog(Bundle savedInstanceState) {
654 final Context context = getActivity();
656 final CharSequence userLabel = getArguments().getCharSequence(Intent.EXTRA_TITLE);
657 final CharSequence userSize = getArguments().getCharSequence(Intent.EXTRA_SUBJECT);
659 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
660 builder.setMessage(TextUtils.expandTemplate(
661 getText(R.string.storage_detail_dialog_user), userLabel, userSize));
663 builder.setPositiveButton(android.R.string.ok, null);
665 return builder.create();
670 * Dialog to request user confirmation before clearing all cache data.
672 public static class ConfirmClearCacheFragment extends DialogFragment {
673 public static void show(Fragment parent) {
674 if (!parent.isAdded()) return;
676 final ConfirmClearCacheFragment dialog = new ConfirmClearCacheFragment();
677 dialog.setTargetFragment(parent, 0);
678 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_CLEAR_CACHE);
682 public Dialog onCreateDialog(Bundle savedInstanceState) {
683 final Context context = getActivity();
685 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
686 builder.setTitle(R.string.memory_clear_cache_title);
687 builder.setMessage(getString(R.string.memory_clear_cache_message));
689 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
691 public void onClick(DialogInterface dialog, int which) {
692 final PrivateVolumeSettings target = (PrivateVolumeSettings) getTargetFragment();
693 final PackageManager pm = context.getPackageManager();
694 final List<PackageInfo> infos = pm.getInstalledPackages(0);
695 final ClearCacheObserver observer = new ClearCacheObserver(
696 target, infos.size());
697 for (PackageInfo info : infos) {
698 pm.deleteApplicationCacheFiles(info.packageName, observer);
702 builder.setNegativeButton(android.R.string.cancel, null);
704 return builder.create();
708 private static class ClearCacheObserver extends IPackageDataObserver.Stub {
709 private final PrivateVolumeSettings mTarget;
710 private int mRemaining;
712 public ClearCacheObserver(PrivateVolumeSettings target, int remaining) {
714 mRemaining = remaining;
718 public void onRemoveCompleted(final String packageName, final boolean succeeded) {
719 synchronized (this) {
720 if (--mRemaining == 0) {