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 super.onCreateOptionsMenu(menu, inflater);
325 inflater.inflate(R.menu.storage_volume, menu);
329 public void onPrepareOptionsMenu(Menu menu) {
330 if (!isVolumeValid()) return;
332 final MenuItem rename = menu.findItem(R.id.storage_rename);
333 final MenuItem mount = menu.findItem(R.id.storage_mount);
334 final MenuItem unmount = menu.findItem(R.id.storage_unmount);
335 final MenuItem format = menu.findItem(R.id.storage_format);
336 final MenuItem migrate = menu.findItem(R.id.storage_migrate);
338 // Actions live in menu for non-internal private volumes; they're shown
339 // as preference items for public volumes.
340 if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(mVolume.getId())) {
341 rename.setVisible(false);
342 mount.setVisible(false);
343 unmount.setVisible(false);
344 format.setVisible(false);
346 rename.setVisible(mVolume.getType() == VolumeInfo.TYPE_PRIVATE);
347 mount.setVisible(mVolume.getState() == VolumeInfo.STATE_UNMOUNTED);
348 unmount.setVisible(mVolume.isMountedReadable());
349 format.setVisible(true);
352 format.setTitle(R.string.storage_menu_format_public);
354 // Only offer to migrate when not current storage
355 final VolumeInfo privateVol = getActivity().getPackageManager()
356 .getPrimaryStorageCurrentVolume();
357 migrate.setVisible(!Objects.equals(mVolume, privateVol));
361 public boolean onOptionsItemSelected(MenuItem item) {
362 final Context context = getActivity();
363 final Bundle args = new Bundle();
364 switch (item.getItemId()) {
365 case R.id.storage_rename:
366 RenameFragment.show(this, mVolume);
368 case R.id.storage_mount:
369 new MountTask(context, mVolume).execute();
371 case R.id.storage_unmount:
372 args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
373 startFragment(this, PrivateVolumeUnmount.class.getCanonicalName(),
374 R.string.storage_menu_unmount, 0, args);
376 case R.id.storage_format:
377 args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
378 startFragment(this, PrivateVolumeFormat.class.getCanonicalName(),
379 R.string.storage_menu_format, 0, args);
381 case R.id.storage_migrate:
382 final Intent intent = new Intent(context, StorageWizardMigrateConfirm.class);
383 intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId());
384 startActivity(intent);
387 return super.onOptionsItemSelected(item);
391 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference pref) {
392 // TODO: launch better intents for specific volume
394 final int userId = (pref instanceof StorageItemPreference ?
395 ((StorageItemPreference)pref).userHandle : -1);
396 final int itemTitleId = pref.getTitleRes();
397 Intent intent = null;
398 switch (itemTitleId) {
399 case R.string.storage_detail_apps: {
400 Bundle args = new Bundle();
401 args.putString(ManageApplications.EXTRA_CLASSNAME,
402 StorageUseActivity.class.getName());
403 args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
404 args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
405 intent = Utils.onBuildStartFragmentIntent(getActivity(),
406 ManageApplications.class.getName(), args, null, R.string.apps_storage, null,
410 case R.string.storage_detail_images: {
411 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
412 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "images_root"));
413 intent.addCategory(Intent.CATEGORY_DEFAULT);
416 case R.string.storage_detail_videos: {
417 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
418 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "videos_root"));
419 intent.addCategory(Intent.CATEGORY_DEFAULT);
422 case R.string.storage_detail_audio: {
423 intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
424 intent.setData(DocumentsContract.buildRootUri(AUTHORITY_MEDIA, "audio_root"));
425 intent.addCategory(Intent.CATEGORY_DEFAULT);
428 case R.string.storage_detail_other: {
429 OtherInfoFragment.show(this, mStorageManager.getBestVolumeDescription(mVolume),
434 case R.string.storage_detail_cached: {
435 ConfirmClearCacheFragment.show(this);
439 case R.string.storage_menu_explore: {
440 intent = mSharedVolume.buildBrowseIntent();
443 UserInfoFragment.show(this, pref.getTitle(), pref.getSummary());
448 if (intent != null) {
451 startActivity(intent);
453 getActivity().startActivityAsUser(intent, new UserHandle(userId));
455 } catch (ActivityNotFoundException e) {
456 Log.w(TAG, "No activity found for " + intent);
460 return super.onPreferenceTreeClick(preferenceScreen, pref);
463 private final MeasurementReceiver mReceiver = new MeasurementReceiver() {
465 public void onDetailsChanged(MeasurementDetails details) {
466 updateDetails(details);
470 private void updateDetails(MeasurementDetails details) {
471 for (int i = 0; i < mItemPoolIndex; ++i) {
472 StorageItemPreference item = mItemPreferencePool.get(i);
473 final int userId = item.userHandle;
474 final int itemTitleId = item.getTitleRes();
475 switch (itemTitleId) {
476 case R.string.storage_detail_apps: {
477 updatePreference(item, details.appsSize.get(userId));
479 case R.string.storage_detail_images: {
480 final long imagesSize = totalValues(details, userId,
481 Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES,
482 Environment.DIRECTORY_PICTURES);
483 updatePreference(item, imagesSize);
485 case R.string.storage_detail_videos: {
486 final long videosSize = totalValues(details, userId,
487 Environment.DIRECTORY_MOVIES);
488 updatePreference(item, videosSize);
490 case R.string.storage_detail_audio: {
491 final long audioSize = totalValues(details, userId,
492 Environment.DIRECTORY_MUSIC,
493 Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS,
494 Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS);
495 updatePreference(item, audioSize);
497 case R.string.storage_detail_other: {
498 updatePreference(item, details.miscSize.get(userId));
500 case R.string.storage_detail_cached: {
501 updatePreference(item, details.cacheSize);
504 final long userSize = details.usersSize.get(userId);
505 updatePreference(item, userSize);
511 private void updatePreference(StorageItemPreference pref, long size) {
512 pref.setSummary(Formatter.formatFileSize(getActivity(), size));
515 private boolean isProfileOf(UserInfo user, UserInfo profile) {
516 return user.id == profile.id ||
517 (user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
518 && user.profileGroupId == profile.profileGroupId);
521 private static long totalValues(MeasurementDetails details, int userId, String... keys) {
523 HashMap<String, Long> map = details.mediaSize.get(userId);
525 for (String key : keys) {
526 if (map.containsKey(key)) {
527 total += map.get(key);
531 Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
536 private final StorageEventListener mStorageListener = new StorageEventListener() {
538 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
539 if (Objects.equals(mVolume.getId(), vol.getId())) {
546 public void onVolumeRecordChanged(VolumeRecord rec) {
547 if (Objects.equals(mVolume.getFsUuid(), rec.getFsUuid())) {
548 mVolume = mStorageManager.findVolumeById(mVolumeId);
555 * Dialog that allows editing of volume nickname.
557 public static class RenameFragment extends DialogFragment {
558 public static void show(PrivateVolumeSettings parent, VolumeInfo vol) {
559 if (!parent.isAdded()) return;
561 final RenameFragment dialog = new RenameFragment();
562 dialog.setTargetFragment(parent, 0);
563 final Bundle args = new Bundle();
564 args.putString(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
565 dialog.setArguments(args);
566 dialog.show(parent.getFragmentManager(), TAG_RENAME);
570 public Dialog onCreateDialog(Bundle savedInstanceState) {
571 final Context context = getActivity();
572 final StorageManager storageManager = context.getSystemService(StorageManager.class);
574 final String fsUuid = getArguments().getString(VolumeRecord.EXTRA_FS_UUID);
575 final VolumeInfo vol = storageManager.findVolumeByUuid(fsUuid);
576 final VolumeRecord rec = storageManager.findRecordByUuid(fsUuid);
578 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
579 final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
581 final View view = dialogInflater.inflate(R.layout.dialog_edittext, null, false);
582 final EditText nickname = (EditText) view.findViewById(R.id.edittext);
583 nickname.setText(rec.getNickname());
585 builder.setTitle(R.string.storage_rename_title);
586 builder.setView(view);
588 builder.setPositiveButton(R.string.save,
589 new DialogInterface.OnClickListener() {
591 public void onClick(DialogInterface dialog, int which) {
592 // TODO: move to background thread
593 storageManager.setVolumeNickname(fsUuid,
594 nickname.getText().toString());
597 builder.setNegativeButton(R.string.cancel, null);
599 return builder.create();
603 public static class OtherInfoFragment extends DialogFragment {
604 public static void show(Fragment parent, String title, VolumeInfo sharedVol) {
605 if (!parent.isAdded()) return;
607 final OtherInfoFragment dialog = new OtherInfoFragment();
608 dialog.setTargetFragment(parent, 0);
609 final Bundle args = new Bundle();
610 args.putString(Intent.EXTRA_TITLE, title);
611 args.putParcelable(Intent.EXTRA_INTENT, sharedVol.buildBrowseIntent());
612 dialog.setArguments(args);
613 dialog.show(parent.getFragmentManager(), TAG_OTHER_INFO);
617 public Dialog onCreateDialog(Bundle savedInstanceState) {
618 final Context context = getActivity();
620 final String title = getArguments().getString(Intent.EXTRA_TITLE);
621 final Intent intent = getArguments().getParcelable(Intent.EXTRA_INTENT);
623 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
625 TextUtils.expandTemplate(getText(R.string.storage_detail_dialog_other), title));
627 builder.setPositiveButton(R.string.storage_menu_explore,
628 new DialogInterface.OnClickListener() {
630 public void onClick(DialogInterface dialog, int which) {
631 startActivity(intent);
634 builder.setNegativeButton(android.R.string.cancel, null);
636 return builder.create();
640 public static class UserInfoFragment extends DialogFragment {
641 public static void show(Fragment parent, CharSequence userLabel, CharSequence userSize) {
642 if (!parent.isAdded()) return;
644 final UserInfoFragment dialog = new UserInfoFragment();
645 dialog.setTargetFragment(parent, 0);
646 final Bundle args = new Bundle();
647 args.putCharSequence(Intent.EXTRA_TITLE, userLabel);
648 args.putCharSequence(Intent.EXTRA_SUBJECT, userSize);
649 dialog.setArguments(args);
650 dialog.show(parent.getFragmentManager(), TAG_USER_INFO);
654 public Dialog onCreateDialog(Bundle savedInstanceState) {
655 final Context context = getActivity();
657 final CharSequence userLabel = getArguments().getCharSequence(Intent.EXTRA_TITLE);
658 final CharSequence userSize = getArguments().getCharSequence(Intent.EXTRA_SUBJECT);
660 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
661 builder.setMessage(TextUtils.expandTemplate(
662 getText(R.string.storage_detail_dialog_user), userLabel, userSize));
664 builder.setPositiveButton(android.R.string.ok, null);
666 return builder.create();
671 * Dialog to request user confirmation before clearing all cache data.
673 public static class ConfirmClearCacheFragment extends DialogFragment {
674 public static void show(Fragment parent) {
675 if (!parent.isAdded()) return;
677 final ConfirmClearCacheFragment dialog = new ConfirmClearCacheFragment();
678 dialog.setTargetFragment(parent, 0);
679 dialog.show(parent.getFragmentManager(), TAG_CONFIRM_CLEAR_CACHE);
683 public Dialog onCreateDialog(Bundle savedInstanceState) {
684 final Context context = getActivity();
686 final AlertDialog.Builder builder = new AlertDialog.Builder(context);
687 builder.setTitle(R.string.memory_clear_cache_title);
688 builder.setMessage(getString(R.string.memory_clear_cache_message));
690 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
692 public void onClick(DialogInterface dialog, int which) {
693 final PrivateVolumeSettings target = (PrivateVolumeSettings) getTargetFragment();
694 final PackageManager pm = context.getPackageManager();
695 final List<PackageInfo> infos = pm.getInstalledPackages(0);
696 final ClearCacheObserver observer = new ClearCacheObserver(
697 target, infos.size());
698 for (PackageInfo info : infos) {
699 pm.deleteApplicationCacheFiles(info.packageName, observer);
703 builder.setNegativeButton(android.R.string.cancel, null);
705 return builder.create();
709 private static class ClearCacheObserver extends IPackageDataObserver.Stub {
710 private final PrivateVolumeSettings mTarget;
711 private int mRemaining;
713 public ClearCacheObserver(PrivateVolumeSettings target, int remaining) {
715 mRemaining = remaining;
719 public void onRemoveCompleted(final String packageName, final boolean succeeded) {
720 synchronized (this) {
721 if (--mRemaining == 0) {