diff --git a/res/values/strings.xml b/res/values/strings.xml
index 75884fade5e..3de47504d3b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3333,6 +3333,10 @@
Manage storage
clean, storage
+
+ Free up space
+
+ Go to Files app to manage and free up space
USB computer connection
@@ -11455,6 +11459,10 @@
used
+
+ %1$s %2$s used
+
+ Total %1$s %2$s
Clear app
diff --git a/res/xml/storage_dashboard_fragment.xml b/res/xml/storage_dashboard_fragment.xml
index bc58d7ea5d0..b49228ea6f4 100644
--- a/res/xml/storage_dashboard_fragment.xml
+++ b/res/xml/storage_dashboard_fragment.xml
@@ -19,11 +19,22 @@
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/storage_settings"
android:orderingFromXml="false">
-
+ settings:controller="com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController"/>
+
+
-
\ No newline at end of file
+
diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java
index 00a79a03f18..549a02f18c4 100644
--- a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java
+++ b/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java
@@ -82,4 +82,8 @@ public class PrivateVolumeOptionMenuController implements LifecycleObserver, OnC
}
return false;
}
+
+ public void setVolumeInfo(VolumeInfo volumeInfo) {
+ mVolumeInfo = volumeInfo;
+ }
}
diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
index 10c3a43fe8e..c9d2c6d5083 100644
--- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
+++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
@@ -17,39 +17,55 @@
package com.android.settings.deviceinfo;
import android.app.Activity;
+import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.app.usage.StorageStatsManager;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageEventListener;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
+import android.os.storage.VolumeRecord;
import android.provider.SearchIndexableResource;
+import android.text.TextUtils;
import android.util.SparseArray;
import android.view.View;
import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.Utils;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.dashboard.profileselector.ProfileSelectFragment;
import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController;
import com.android.settings.deviceinfo.storage.CachedStorageValuesHelper;
import com.android.settings.deviceinfo.storage.SecondaryUserController;
import com.android.settings.deviceinfo.storage.StorageAsyncLoader;
+import com.android.settings.deviceinfo.storage.StorageEntry;
import com.android.settings.deviceinfo.storage.StorageItemPreferenceController;
+import com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController;
+import com.android.settings.deviceinfo.storage.StorageUsageProgressBarPreferenceController;
import com.android.settings.deviceinfo.storage.UserIconLoader;
import com.android.settings.deviceinfo.storage.VolumeSizesLoader;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.applications.StorageStatsSource;
import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.deviceinfo.PrivateStorageInfo;
import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
import com.android.settingslib.search.SearchIndexable;
@@ -57,48 +73,184 @@ import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
@SearchIndexable
public class StorageDashboardFragment extends DashboardFragment
implements
- LoaderManager.LoaderCallbacks> {
+ LoaderManager.LoaderCallbacks>,
+ Preference.OnPreferenceClickListener {
private static final String TAG = "StorageDashboardFrag";
private static final String SUMMARY_PREF_KEY = "storage_summary";
+ private static final String FREE_UP_SPACE_PREF_KEY = "free_up_space";
+ private static final String SELECTED_STORAGE_ENTRY_KEY = "selected_storage_entry_key";
private static final int STORAGE_JOB_ID = 0;
private static final int ICON_JOB_ID = 1;
private static final int VOLUME_SIZE_JOB_ID = 2;
- private VolumeInfo mVolume;
+ private StorageManager mStorageManager;
+ private final List mStorageEntries = new ArrayList<>();
+ private StorageEntry mSelectedStorageEntry;
private PrivateStorageInfo mStorageInfo;
private SparseArray mAppsResult;
private CachedStorageValuesHelper mCachedStorageValuesHelper;
private StorageItemPreferenceController mPreferenceController;
private PrivateVolumeOptionMenuController mOptionMenuController;
+ private StorageSelectionPreferenceController mStorageSelectionController;
+ private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController;
private List mSecondaryUsers;
private boolean mPersonalOnly;
+ private Preference mFreeUpSpacePreference;
+
+ private final StorageEventListener mStorageEventListener = new StorageEventListener() {
+ @Override
+ public void onVolumeStateChanged(VolumeInfo volumeInfo, int oldState, int newState) {
+ if (!isInteresting(volumeInfo)) {
+ return;
+ }
+
+ final StorageEntry storageEntry = new StorageEntry(getContext(), volumeInfo);
+ switch (volumeInfo.getState()) {
+ case VolumeInfo.STATE_MOUNTED:
+ case VolumeInfo.STATE_MOUNTED_READ_ONLY:
+ case VolumeInfo.STATE_UNMOUNTABLE:
+ if (!mStorageEntries.contains(storageEntry)) {
+ mStorageEntries.add(storageEntry);
+ refreshUi();
+ }
+ break;
+ case VolumeInfo.STATE_REMOVED:
+ case VolumeInfo.STATE_UNMOUNTED:
+ case VolumeInfo.STATE_BAD_REMOVAL:
+ case VolumeInfo.STATE_EJECTING:
+ if (mStorageEntries.remove(storageEntry)) {
+ if (mSelectedStorageEntry.equals(storageEntry)) {
+ mSelectedStorageEntry =
+ StorageEntry.getDefaultInternalStorageEntry(getContext());
+ }
+ refreshUi();
+ }
+ break;
+ default:
+ // Do nothing.
+ }
+ }
+
+ @Override
+ public void onVolumeRecordChanged(VolumeRecord volumeRecord) {
+ final StorageEntry storageEntry = new StorageEntry(volumeRecord);
+ if (!mStorageEntries.contains(storageEntry)) {
+ mStorageEntries.add(storageEntry);
+ refreshUi();
+ }
+ }
+
+ @Override
+ public void onVolumeForgotten(String fsUuid) {
+ final StorageEntry storageEntry = new StorageEntry(
+ new VolumeRecord(VolumeInfo.TYPE_PUBLIC, fsUuid));
+ if (mStorageEntries.remove(storageEntry)) {
+ if (mSelectedStorageEntry.equals(storageEntry)) {
+ mSelectedStorageEntry =
+ StorageEntry.getDefaultInternalStorageEntry(getContext());
+ }
+ refreshUi();
+ }
+ }
+
+ @Override
+ public void onDiskScanned(DiskInfo disk, int volumeCount) {
+ if (!isInteresting(disk)) {
+ return;
+ }
+ final StorageEntry storageEntry = new StorageEntry(disk);
+ if (!mStorageEntries.contains(storageEntry)) {
+ mStorageEntries.add(storageEntry);
+ refreshUi();
+ }
+ }
+
+ @Override
+ public void onDiskDestroyed(DiskInfo disk) {
+ final StorageEntry storageEntry = new StorageEntry(disk);
+ if (mStorageEntries.remove(storageEntry)) {
+ if (mSelectedStorageEntry.equals(storageEntry)) {
+ mSelectedStorageEntry =
+ StorageEntry.getDefaultInternalStorageEntry(getContext());
+ }
+ refreshUi();
+ }
+ }
+ };
+
+ private static boolean isInteresting(VolumeInfo volumeInfo) {
+ switch (volumeInfo.getType()) {
+ case VolumeInfo.TYPE_PRIVATE:
+ case VolumeInfo.TYPE_PUBLIC:
+ case VolumeInfo.TYPE_STUB:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Only interested in unsupported disk.
+ private static boolean isInteresting(DiskInfo disk) {
+ return disk.volumeCount == 0 && disk.size > 0;
+ }
+
+ private void refreshUi() {
+ mStorageSelectionController.setStorageEntries(mStorageEntries);
+ mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry);
+ mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry);
+
+ mOptionMenuController.setVolumeInfo(mSelectedStorageEntry.getVolumeInfo());
+
+ mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
+
+ if (mSelectedStorageEntry.isMounted()) {
+ getLoaderManager().restartLoader(STORAGE_JOB_ID, Bundle.EMPTY, this);
+ getLoaderManager()
+ .restartLoader(VOLUME_SIZE_JOB_ID, Bundle.EMPTY, new VolumeSizeCallbacks());
+ getLoaderManager().restartLoader(ICON_JOB_ID, Bundle.EMPTY, new IconLoaderCallbacks());
+ } else {
+ mPreferenceController.clearStorageSizeDisplay();
+ }
+ }
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
- // Initialize the storage sizes that we can quickly calc.
final Activity activity = getActivity();
- StorageManager sm = activity.getSystemService(StorageManager.class);
- mVolume = Utils.maybeInitializeVolume(sm, getArguments());
+ mStorageManager = activity.getSystemService(StorageManager.class);
mPersonalOnly = getArguments().getInt(ProfileSelectFragment.EXTRA_PROFILE)
== ProfileSelectFragment.ProfileType.PERSONAL;
- if (mVolume == null) {
- activity.finish();
- return;
+
+ if (icicle == null) {
+ final VolumeInfo specifiedVolumeInfo =
+ Utils.maybeInitializeVolume(mStorageManager, getArguments());
+ mSelectedStorageEntry = specifiedVolumeInfo == null
+ ? StorageEntry.getDefaultInternalStorageEntry(getContext())
+ : new StorageEntry(getContext(), specifiedVolumeInfo);
+ } else {
+ mSelectedStorageEntry = icicle.getParcelable(SELECTED_STORAGE_ENTRY_KEY);
}
+
+ initializePreference();
initializeOptionsMenu(activity);
+ }
+
+ private void initializePreference() {
if (mPersonalOnly) {
final Preference summary = getPreferenceScreen().findPreference(SUMMARY_PREF_KEY);
if (summary != null) {
summary.setVisible(false);
}
}
+ mFreeUpSpacePreference = getPreferenceScreen().findPreference(FREE_UP_SPACE_PREF_KEY);
+ mFreeUpSpacePreference.setOnPreferenceClickListener(this);
}
@Override
@@ -106,12 +258,33 @@ public class StorageDashboardFragment extends DashboardFragment
super.onAttach(context);
use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager(
getFragmentManager());
+ mStorageSelectionController = use(StorageSelectionPreferenceController.class);
+ mStorageSelectionController.setOnItemSelectedListener(storageEntry -> {
+ mSelectedStorageEntry = storageEntry;
+ refreshUi();
+
+ if (storageEntry.isUnsupportedDiskInfo() || storageEntry.isUnmountable()) {
+ DiskInitFragment.show(this, R.string.storage_dialog_unmountable,
+ storageEntry.getDiskId());
+ } else if (storageEntry.isMissingVolumeRecord()) {
+ final Bundle args = new Bundle();
+ args.putString(VolumeRecord.EXTRA_FS_UUID, storageEntry.getFsUuid());
+ new SubSettingLauncher(getContext())
+ .setDestination(PrivateVolumeForget.class.getCanonicalName())
+ .setTitleRes(R.string.storage_menu_forget)
+ .setSourceMetricsCategory(getMetricsCategory())
+ .setArguments(args)
+ .launch();
+ }
+ });
+ mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class);
}
@VisibleForTesting
void initializeOptionsMenu(Activity activity) {
- mOptionMenuController = new PrivateVolumeOptionMenuController(
- activity, mVolume, activity.getPackageManager());
+ mOptionMenuController = new PrivateVolumeOptionMenuController(activity,
+ mSelectedStorageEntry.getVolumeInfo(),
+ activity.getPackageManager());
getSettingsLifecycle().addObserver(mOptionMenuController);
setHasOptionsMenu(true);
activity.invalidateOptionsMenu();
@@ -133,10 +306,37 @@ public class StorageDashboardFragment extends DashboardFragment
@Override
public void onResume() {
super.onResume();
- getLoaderManager().restartLoader(STORAGE_JOB_ID, Bundle.EMPTY, this);
- getLoaderManager()
- .restartLoader(VOLUME_SIZE_JOB_ID, Bundle.EMPTY, new VolumeSizeCallbacks());
- getLoaderManager().restartLoader(ICON_JOB_ID, Bundle.EMPTY, new IconLoaderCallbacks());
+
+ mStorageEntries.clear();
+ mStorageEntries.addAll(mStorageManager.getVolumes().stream()
+ .filter(volumeInfo -> isInteresting(volumeInfo))
+ .map(volumeInfo -> new StorageEntry(getContext(), volumeInfo))
+ .collect(Collectors.toList()));
+ // Shows unsupported disks to give a chance to init.
+ mStorageEntries.addAll(mStorageManager.getDisks().stream()
+ .filter(disk -> isInteresting(disk))
+ .map(disk -> new StorageEntry(disk))
+ .collect(Collectors.toList()));
+ // Shows missing private volumes.
+ mStorageEntries.addAll(mStorageManager.getVolumeRecords().stream()
+ .filter(volumeRecord -> volumeRecord.getType() == VolumeInfo.TYPE_PRIVATE
+ && mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()) == null)
+ .map(volumeRecord -> new StorageEntry(volumeRecord))
+ .collect(Collectors.toList()));
+ refreshUi();
+ mStorageManager.registerListener(mStorageEventListener);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mStorageManager.unregisterListener(mStorageEventListener);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putParcelable(SELECTED_STORAGE_ENTRY_KEY, mSelectedStorageEntry);
+ super.onSaveInstanceState(outState);
}
@Override
@@ -148,7 +348,7 @@ public class StorageDashboardFragment extends DashboardFragment
boolean stopLoading = false;
if (mStorageInfo != null) {
long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes;
- mPreferenceController.setVolume(mVolume);
+ mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
mPreferenceController.setUsedSize(privateUsedBytes);
mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) {
@@ -197,7 +397,7 @@ public class StorageDashboardFragment extends DashboardFragment
StorageManager sm = context.getSystemService(StorageManager.class);
mPreferenceController = new StorageItemPreferenceController(context, this,
- mVolume, new StorageManagerVolumeProvider(sm));
+ null /* volume */, new StorageManagerVolumeProvider(sm));
controllers.add(mPreferenceController);
final UserManager userManager = context.getSystemService(UserManager.class);
@@ -209,7 +409,7 @@ public class StorageDashboardFragment extends DashboardFragment
@VisibleForTesting
protected void setVolume(VolumeInfo info) {
- mVolume = info;
+ mSelectedStorageEntry = new StorageEntry(getContext(), info);
}
/**
@@ -260,7 +460,7 @@ public class StorageDashboardFragment extends DashboardFragment
Bundle args) {
final Context context = getContext();
return new StorageAsyncLoader(context, context.getSystemService(UserManager.class),
- mVolume.fsUuid,
+ mSelectedStorageEntry.getFsUuid(),
new StorageStatsSource(context),
context.getPackageManager());
}
@@ -277,6 +477,21 @@ public class StorageDashboardFragment extends DashboardFragment
public void onLoaderReset(Loader> loader) {
}
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == mFreeUpSpacePreference) {
+ final Context context = getContext();
+ final MetricsFeatureProvider metricsFeatureProvider =
+ FeatureFactory.getFactory(context).getMetricsFeatureProvider();
+ metricsFeatureProvider.logClickedPreference(preference, getMetricsCategory());
+ metricsFeatureProvider.action(context, SettingsEnums.STORAGE_FREE_UP_SPACE_NOW);
+ final Intent intent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
+ context.startActivity(intent);
+ return true;
+ }
+ return false;
+ }
+
@VisibleForTesting
public void setCachedStorageValuesHelper(CachedStorageValuesHelper helper) {
mCachedStorageValuesHelper = helper;
@@ -340,8 +555,9 @@ public class StorageDashboardFragment extends DashboardFragment
}
private boolean isQuotaSupported() {
- final StorageStatsManager stats = getActivity().getSystemService(StorageStatsManager.class);
- return stats.isQuotaSupported(mVolume.fsUuid);
+ return mSelectedStorageEntry.isMounted()
+ && getActivity().getSystemService(StorageStatsManager.class)
+ .isQuotaSupported(mSelectedStorageEntry.getFsUuid());
}
/**
@@ -378,11 +594,12 @@ public class StorageDashboardFragment extends DashboardFragment
implements LoaderManager.LoaderCallbacks {
@Override
public Loader onCreateLoader(int id, Bundle args) {
- Context context = getContext();
- StorageManager sm = context.getSystemService(StorageManager.class);
- StorageManagerVolumeProvider smvp = new StorageManagerVolumeProvider(sm);
+ final Context context = getContext();
+ final StorageManagerVolumeProvider smvp =
+ new StorageManagerVolumeProvider(mStorageManager);
final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
- return new VolumeSizesLoader(context, smvp, stats, mVolume);
+ return new VolumeSizesLoader(context, smvp, stats,
+ mSelectedStorageEntry.getVolumeInfo());
}
@Override
@@ -402,4 +619,52 @@ public class StorageDashboardFragment extends DashboardFragment
onReceivedSizes();
}
}
+
+ /** A dialog which guides users to initialize a specified unsupported disk. */
+ public static class DiskInitFragment extends InstrumentedDialogFragment {
+
+ private static final String TAG_DISK_INIT = "disk_init";
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.DIALOG_VOLUME_INIT;
+ }
+
+ /** Shows the dialo for the specified diskId from DiskInfo. */
+ public static void show(Fragment parent, int resId, String diskId) {
+ final Bundle args = new Bundle();
+ args.putInt(Intent.EXTRA_TEXT, resId);
+ args.putString(DiskInfo.EXTRA_DISK_ID, diskId);
+
+ final DiskInitFragment dialog = new DiskInitFragment();
+ dialog.setArguments(args);
+ dialog.setTargetFragment(parent, 0);
+ dialog.show(parent.getFragmentManager(), TAG_DISK_INIT);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity();
+ final StorageManager storageManager = context.getSystemService(StorageManager.class);
+ final int resId = getArguments().getInt(Intent.EXTRA_TEXT);
+ final String diskId = getArguments().getString(DiskInfo.EXTRA_DISK_ID);
+ final DiskInfo disk = storageManager.findDiskById(diskId);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(TextUtils.expandTemplate(getText(resId), disk.getDescription()));
+
+ builder.setPositiveButton(R.string.storage_menu_set_up,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Intent intent = new Intent(context, StorageWizardInit.class);
+ intent.putExtra(DiskInfo.EXTRA_DISK_ID, diskId);
+ startActivity(intent);
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, null);
+
+ return builder.create();
+ }
+ }
}
diff --git a/src/com/android/settings/deviceinfo/storage/StorageEntry.java b/src/com/android/settings/deviceinfo/storage/StorageEntry.java
new file mode 100644
index 00000000000..f86fa794d69
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/storage/StorageEntry.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.os.storage.VolumeRecord;
+import android.text.TextUtils;
+
+import java.io.File;
+
+/**
+ * This object contains a {@link VolumeInfo} for a mountable storage or a {@link DiskInfo} for an
+ * unsupported disk which is not able to be mounted automatically.
+ */
+public class StorageEntry implements Comparable, Parcelable {
+
+ private final VolumeInfo mVolumeInfo;
+ private final DiskInfo mUnsupportedDiskInfo;
+ private final VolumeRecord mMissingVolumeRecord;
+
+ private final String mVolumeInfoDescription;
+
+ public StorageEntry(@NonNull Context context, @NonNull VolumeInfo volumeInfo) {
+ mVolumeInfo = volumeInfo;
+ mUnsupportedDiskInfo = null;
+ mMissingVolumeRecord = null;
+ mVolumeInfoDescription = context.getSystemService(StorageManager.class)
+ .getBestVolumeDescription(mVolumeInfo);
+ }
+
+ public StorageEntry(@NonNull DiskInfo diskInfo) {
+ mVolumeInfo = null;
+ mUnsupportedDiskInfo = diskInfo;
+ mMissingVolumeRecord = null;
+ mVolumeInfoDescription = null;
+ }
+
+ public StorageEntry(@NonNull VolumeRecord volumeRecord) {
+ mVolumeInfo = null;
+ mUnsupportedDiskInfo = null;
+ mMissingVolumeRecord = volumeRecord;
+ mVolumeInfoDescription = null;
+ }
+
+ private StorageEntry(Parcel in) {
+ mVolumeInfo = in.readParcelable(VolumeInfo.class.getClassLoader());
+ mUnsupportedDiskInfo = in.readParcelable(DiskInfo.class.getClassLoader());
+ mMissingVolumeRecord = in.readParcelable(VolumeRecord.class.getClassLoader());
+ mVolumeInfoDescription = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(mVolumeInfo, 0 /* parcelableFlags */);
+ out.writeParcelable(mUnsupportedDiskInfo, 0 /* parcelableFlags */);
+ out.writeParcelable(mMissingVolumeRecord , 0 /* parcelableFlags */);
+ out.writeString(mVolumeInfoDescription);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public StorageEntry createFromParcel(Parcel in) {
+ return new StorageEntry(in);
+ }
+
+ public StorageEntry[] newArray(int size) {
+ return new StorageEntry[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof StorageEntry)) {
+ return false;
+ }
+
+ final StorageEntry StorageEntry = (StorageEntry) o;
+ if (isVolumeInfo()) {
+ return mVolumeInfo.equals(StorageEntry.mVolumeInfo);
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.equals(StorageEntry.mUnsupportedDiskInfo);
+ }
+ return mMissingVolumeRecord.equals(StorageEntry.mMissingVolumeRecord);
+ }
+
+ @Override
+ public int hashCode() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.hashCode();
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.hashCode();
+ }
+ return mMissingVolumeRecord.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.toString();
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.toString();
+ }
+ return mMissingVolumeRecord.toString();
+ }
+
+ @Override
+ public int compareTo(StorageEntry other) {
+ if (isDefaultInternalStorage() && !other.isDefaultInternalStorage()) {
+ return -1;
+ }
+ if (!isDefaultInternalStorage() && other.isDefaultInternalStorage()) {
+ return 1;
+ }
+
+ if (isVolumeInfo() && !other.isVolumeInfo()) {
+ return -1;
+ }
+ if (!isVolumeInfo() && other.isVolumeInfo()) {
+ return 1;
+ }
+
+ if (isPrivate() && !other.isPrivate()) {
+ return -1;
+ }
+ if (!isPrivate() && other.isPrivate()) {
+ return 1;
+ }
+
+ if (isMounted() && !other.isMounted()) {
+ return -1;
+ }
+ if (!isMounted() && other.isMounted()) {
+ return 1;
+ }
+
+ if (!isMissingVolumeRecord() && other.isMissingVolumeRecord()) {
+ return -1;
+ }
+ if (isMissingVolumeRecord() && !other.isMissingVolumeRecord()) {
+ return 1;
+ }
+
+ if (getDescription() == null) {
+ return 1;
+ }
+ if (other.getDescription() == null) {
+ return -1;
+ }
+ return getDescription().compareTo(other.getDescription());
+ }
+
+ /** Returns default internal storage. */
+ public static StorageEntry getDefaultInternalStorageEntry(Context context) {
+ return new StorageEntry(context, context.getSystemService(StorageManager.class)
+ .findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL));
+ }
+
+ /** If it's a VolumeInfo. */
+ public boolean isVolumeInfo() {
+ return mVolumeInfo != null;
+ }
+
+ /** If it's an unsupported DiskInfo. */
+ public boolean isUnsupportedDiskInfo() {
+ return mUnsupportedDiskInfo != null;
+ }
+
+ /** If it's a missing VolumeRecord. */
+ public boolean isMissingVolumeRecord() {
+ return mMissingVolumeRecord != null;
+ }
+
+ /** If it's a default internal storage. */
+ public boolean isDefaultInternalStorage() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.getType() == VolumeInfo.TYPE_PRIVATE
+ && TextUtils.equals(mVolumeInfo.getId(), VolumeInfo.ID_PRIVATE_INTERNAL);
+ }
+ return false;
+ }
+
+ /** If it's a mounted storage. */
+ public boolean isMounted() {
+ return mVolumeInfo == null ? false : (mVolumeInfo.getState() == VolumeInfo.STATE_MOUNTED
+ || mVolumeInfo.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY);
+ }
+
+ /** If it's an unmountable storage. */
+ public boolean isUnmountable() {
+ return mVolumeInfo == null ? false : mVolumeInfo.getState() == VolumeInfo.STATE_UNMOUNTABLE;
+ }
+
+ /** If it's a private storage. */
+ public boolean isPrivate() {
+ return mVolumeInfo == null ? false : mVolumeInfo.getType() == VolumeInfo.TYPE_PRIVATE;
+ }
+
+ /** Returns description. */
+ public String getDescription() {
+ if (isVolumeInfo()) {
+ return mVolumeInfoDescription;
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.getDescription();
+ }
+ return mMissingVolumeRecord.getNickname();
+ }
+
+ /** Returns ID. */
+ public String getId() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.getId();
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.getId();
+ }
+ return mMissingVolumeRecord.getFsUuid();
+ }
+
+ /** Returns disk ID. */
+ public String getDiskId() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.getDiskId();
+ }
+ if (isUnsupportedDiskInfo()) {
+ return mUnsupportedDiskInfo.getId();
+ }
+ return null;
+ }
+
+ /** Returns fsUuid. */
+ public String getFsUuid() {
+ if (isVolumeInfo()) {
+ return mVolumeInfo.getFsUuid();
+ }
+ if (isUnsupportedDiskInfo()) {
+ return null;
+ }
+ return mMissingVolumeRecord.getFsUuid();
+ }
+
+ /** Returns root file if it's a VolumeInfo. */
+ public File getPath() {
+ return mVolumeInfo == null ? null : mVolumeInfo.getPath();
+ }
+
+ /** Returns VolumeInfo of the StorageEntry. */
+ public VolumeInfo getVolumeInfo() {
+ return mVolumeInfo;
+ }
+}
+
diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
index c2a0b626196..dba72ba5ac6 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java
@@ -158,6 +158,9 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
intent = getAppsIntent();
break;
case FILES_KEY:
+ if (mVolume == null) {
+ break;
+ }
intent = getFilesIntent();
FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(
mContext, SettingsEnums.STORAGE_FILES);
@@ -293,6 +296,17 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
mTotalSize = totalSizeBytes;
}
+ /** Set storage size to 0 for each preference. */
+ public void clearStorageSizeDisplay() {
+ mPhotoPreference.setStorageSize(0L, 0L);
+ mAudioPreference.setStorageSize(0L, 0L);
+ mGamePreference.setStorageSize(0L, 0L);
+ mMoviesPreference.setStorageSize(0L, 0L);
+ mAppPreference.setStorageSize(0L, 0L);
+ mFilePreference.setStorageSize(0L, 0L);
+ mSystemPreference.setStorageSize(0L, 0L);
+ }
+
/**
* Returns a list of keys used by this preference controller.
*/
diff --git a/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceController.java
new file mode 100644
index 00000000000..03fddecea56
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceController.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.widget.SettingsSpinnerPreference;
+import com.android.settingslib.widget.settingsspinner.SettingsSpinnerAdapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Shows a spinner for users to select a storage volume.
+ */
+public class StorageSelectionPreferenceController extends BasePreferenceController implements
+ AdapterView.OnItemSelectedListener {
+
+ @VisibleForTesting
+ SettingsSpinnerPreference mSpinnerPreference;
+ @VisibleForTesting
+ StorageAdapter mStorageAdapter;
+
+ private final List mStorageEntries = new ArrayList<>();
+
+ /** The interface for spinner selection callback. */
+ public interface OnItemSelectedListener {
+ /** Callbacked when the spinner selection is changed. */
+ void onItemSelected(StorageEntry storageEntry);
+ }
+ private OnItemSelectedListener mOnItemSelectedListener;
+
+ public StorageSelectionPreferenceController(Context context, String key) {
+ super(context, key);
+
+ mStorageAdapter = new StorageAdapter(context);
+ }
+
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ /** Set the storages in the spinner. */
+ public void setStorageEntries(List storageEntries) {
+ mStorageAdapter.clear();
+ mStorageEntries.clear();
+ if (storageEntries == null || storageEntries.isEmpty()) {
+ return;
+ }
+ Collections.sort(mStorageEntries);
+ mStorageEntries.addAll(storageEntries);
+ mStorageAdapter.addAll(storageEntries);
+ }
+
+ /** set selected storage in the spinner. */
+ public void setSelectedStorageEntry(StorageEntry selectedStorageEntry) {
+ if (mSpinnerPreference == null || !mStorageEntries.contains(selectedStorageEntry)) {
+ return;
+ }
+ mSpinnerPreference.setSelection(mStorageAdapter.getPosition(selectedStorageEntry));
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE_UNSEARCHABLE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ mSpinnerPreference = screen.findPreference(getPreferenceKey());
+ mSpinnerPreference.setAdapter(mStorageAdapter);
+ mSpinnerPreference.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView> arg0, View arg1, int position, long id) {
+ if (mOnItemSelectedListener == null) {
+ return;
+ }
+ mOnItemSelectedListener.onItemSelected(mStorageAdapter.getItem(position));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> arg0) {
+ // Do nothing.
+ }
+
+ @VisibleForTesting
+ class StorageAdapter extends SettingsSpinnerAdapter {
+
+ StorageAdapter(Context context) {
+ super(context);
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ view = getDefaultView(position, view, parent);
+ }
+
+ TextView textView = null;
+ try {
+ textView = (TextView) view;
+ } catch (ClassCastException e) {
+ throw new IllegalStateException("Default view should be a TextView, ", e);
+ }
+ textView.setText(getItem(position).getDescription());
+ return textView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ view = getDefaultDropDownView(position, view, parent);
+ }
+
+ TextView textView = null;
+ try {
+ textView = (TextView) view;
+ } catch (ClassCastException e) {
+ throw new IllegalStateException("Default drop down view should be a TextView, ", e);
+ }
+ textView.setText(getItem(position).getDescription());
+ return textView;
+ }
+ }
+}
+
diff --git a/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java
new file mode 100644
index 00000000000..a00b25a3cd5
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import android.app.usage.StorageStatsManager;
+import android.content.Context;
+import android.text.format.Formatter;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.utils.ThreadUtils;
+import com.android.settingslib.widget.UsageProgressBarPreference;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Shows storage summary and progress.
+ */
+public class StorageUsageProgressBarPreferenceController extends BasePreferenceController {
+
+ private static final String TAG = "StorageProgressCtrl";
+
+ private final StorageStatsManager mStorageStatsManager;
+ @VisibleForTesting
+ long mUsedBytes;
+ @VisibleForTesting
+ long mTotalBytes;
+ private UsageProgressBarPreference mUsageProgressBarPreference;
+ private StorageEntry mStorageEntry;
+
+ public StorageUsageProgressBarPreferenceController(Context context, String key) {
+ super(context, key);
+
+ mStorageStatsManager = context.getSystemService(StorageStatsManager.class);
+ }
+
+ /** Set StorageEntry to display. */
+ public void setSelectedStorageEntry(StorageEntry storageEntry) {
+ mStorageEntry = storageEntry;
+ getStorageStatsAndUpdateUi();
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE_UNSEARCHABLE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ mUsageProgressBarPreference = screen.findPreference(getPreferenceKey());
+ getStorageStatsAndUpdateUi();
+ }
+
+ private void getStorageStatsAndUpdateUi() {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ try {
+ if (mStorageEntry == null || !mStorageEntry.isMounted()) {
+ throw new IOException();
+ }
+
+ if (mStorageEntry.isPrivate()) {
+ // StorageStatsManager can only query private storages.
+ mTotalBytes = mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());
+ mUsedBytes = mTotalBytes
+ - mStorageStatsManager.getFreeBytes(mStorageEntry.getFsUuid());
+ } else {
+ final File rootFile = mStorageEntry.getPath();
+ if (rootFile == null) {
+ Log.d(TAG, "Mounted public storage has null root path: " + mStorageEntry);
+ throw new IOException();
+ }
+ mTotalBytes = rootFile.getTotalSpace();
+ mUsedBytes = mTotalBytes - rootFile.getFreeSpace();
+ }
+ } catch (IOException e) {
+ // The storage device isn't present.
+ mTotalBytes = 0;
+ mUsedBytes = 0;
+ }
+
+ if (mUsageProgressBarPreference == null) {
+ return;
+ }
+ ThreadUtils.postOnMainThread(() ->
+ updateState(mUsageProgressBarPreference)
+ );
+ });
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ mUsageProgressBarPreference.setUsageSummary(
+ getStorageSummary(R.string.storage_usage_summary, mUsedBytes));
+ mUsageProgressBarPreference.setTotalSummary(
+ getStorageSummary(R.string.storage_total_summary, mTotalBytes));
+ mUsageProgressBarPreference.setPercent(mUsedBytes, mTotalBytes);
+ }
+
+ private String getStorageSummary(int resId, long bytes) {
+ final Formatter.BytesResult result = Formatter.formatBytes(mContext.getResources(),
+ bytes, 0);
+ return mContext.getString(resId, result.value, result.units);
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java b/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java
index d95befaa3ae..64510c6f602 100644
--- a/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java
+++ b/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java
@@ -26,6 +26,7 @@ import com.android.settingslib.deviceinfo.PrivateStorageInfo;
import com.android.settingslib.deviceinfo.StorageVolumeProvider;
import com.android.settingslib.utils.AsyncLoaderCompat;
+import java.io.File;
import java.io.IOException;
public class VolumeSizesLoader extends AsyncLoaderCompat {
@@ -49,6 +50,11 @@ public class VolumeSizesLoader extends AsyncLoaderCompat {
@Override
public PrivateStorageInfo loadInBackground() {
+ if (mVolume == null || (mVolume.getState() != VolumeInfo.STATE_MOUNTED
+ && mVolume.getState() != VolumeInfo.STATE_MOUNTED_READ_ONLY)) {
+ return new PrivateStorageInfo(0L /* freeBytes */, 0L /* totalBytes */);
+ }
+
PrivateStorageInfo volumeSizes;
try {
volumeSizes = getVolumeSize(mVolumeProvider, mStats, mVolume);
@@ -62,8 +68,14 @@ public class VolumeSizesLoader extends AsyncLoaderCompat {
static PrivateStorageInfo getVolumeSize(
StorageVolumeProvider storageVolumeProvider, StorageStatsManager stats, VolumeInfo info)
throws IOException {
- long privateTotalBytes = storageVolumeProvider.getTotalBytes(stats, info);
- long privateFreeBytes = storageVolumeProvider.getFreeBytes(stats, info);
- return new PrivateStorageInfo(privateFreeBytes, privateTotalBytes);
+ if (info.getType() == VolumeInfo.TYPE_PRIVATE) {
+ return new PrivateStorageInfo(storageVolumeProvider.getFreeBytes(stats, info),
+ storageVolumeProvider.getTotalBytes(stats, info));
+ }
+ // TODO(b/174964885): It's confusing to use PrivateStorageInfo for a public storage,
+ // replace it with a new naming or a different object.
+ final File rootFile = info.getPath();
+ return rootFile == null ? new PrivateStorageInfo(0L /* freeBytes */, 0L /* totalBytes */)
+ : new PrivateStorageInfo(rootFile.getFreeSpace(), rootFile.getTotalSpace());
}
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index b55a788988f..b7ac4b1bb0b 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -32,6 +32,8 @@ android_test {
"platform-test-annotations",
"truth-prebuilt",
"ub-uiautomator",
+ "SettingsLibSettingsSpinner",
+ "SettingsLibUsageProgressBarPreference",
],
// Include all test java files.
diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java
new file mode 100644
index 00000000000..603d51e236f
--- /dev/null
+++ b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.os.storage.VolumeRecord;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+public class StorageEntryTest {
+
+ private static final String VOLUME_INFO_ID = "volume_info_id";
+ private static final String DISK_INFO_ID = "disk_info_id";
+ private static final String VOLUME_RECORD_UUID = "volume_record_id";
+
+ @Mock
+ private VolumeInfo mVolumeInfo;
+ @Mock
+ private DiskInfo mDiskInfo;
+ @Mock
+ private VolumeRecord mVolumeRecord;
+
+ private Context mContext;
+ @Mock
+ private StorageManager mStorageManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ when(mContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager);
+ }
+
+ @Test
+ public void equals_volumesOfSameId_shouldBeTheSame() {
+ final StorageEntry volumeStorage1 = new StorageEntry(mContext,
+ new VolumeInfo(VOLUME_INFO_ID, 0 /* type */, null /* disk */, null /* partGuid */));
+ final StorageEntry volumeStorage2 = new StorageEntry(mContext,
+ new VolumeInfo(VOLUME_INFO_ID, 0 /* type */, null /* disk */, null /* partGuid */));
+ final StorageEntry diskStorage1 =
+ new StorageEntry(new DiskInfo(DISK_INFO_ID, 0 /* flags */));
+ final StorageEntry diskStorage2 =
+ new StorageEntry(new DiskInfo(DISK_INFO_ID, 0 /* flags */));
+ final StorageEntry volumeRecordStorage1 = new StorageEntry(new VolumeRecord(0 /* flags */,
+ VOLUME_RECORD_UUID));
+ final StorageEntry volumeRecordStorage2 = new StorageEntry(new VolumeRecord(0 /* flags */,
+ VOLUME_RECORD_UUID));
+
+ assertThat(Objects.equals(volumeStorage1, volumeStorage2)).isTrue();
+ assertThat(Objects.equals(diskStorage1, diskStorage2)).isTrue();
+ assertThat(Objects.equals(volumeRecordStorage1, volumeRecordStorage2)).isTrue();
+ }
+
+ @Test
+ public void equals_volumesOfDifferentId_shouldBeDifferent() {
+ final StorageEntry volumeStorage1 = new StorageEntry(mContext,
+ new VolumeInfo(VOLUME_INFO_ID, 0 /* type */, null /* disk */, null /* partGuid */));
+ final StorageEntry volumeStorage2 = new StorageEntry(mContext,
+ new VolumeInfo("id2", 0 /* type */, null /* disk */, null /* partGuid */));
+ final StorageEntry diskStorage1 =
+ new StorageEntry(new DiskInfo(DISK_INFO_ID, 0 /* flags */));
+ final StorageEntry diskStorage2 =
+ new StorageEntry(new DiskInfo("id2", 0 /* flags */));
+ final StorageEntry volumeRecordStorage1 = new StorageEntry(new VolumeRecord(0 /* flags */,
+ VOLUME_RECORD_UUID));
+ final StorageEntry volumeRecordStorage2 = new StorageEntry(new VolumeRecord(0 /* flags */,
+ "id2"));
+
+ assertThat(Objects.equals(volumeStorage1, volumeStorage2)).isFalse();
+ assertThat(Objects.equals(diskStorage1, diskStorage2)).isFalse();
+ assertThat(Objects.equals(volumeRecordStorage1, volumeRecordStorage2)).isFalse();
+ }
+
+ @Test
+ public void compareTo_defaultInternalStorage_shouldBeAtTopMost() {
+ final StorageEntry storage1 = mock(StorageEntry.class);
+ when(storage1.isDefaultInternalStorage()).thenReturn(true);
+ final StorageEntry storage2 = mock(StorageEntry.class);
+ when(storage2.isDefaultInternalStorage()).thenReturn(false);
+
+ assertThat(storage1.compareTo(storage2) > 0).isTrue();
+ }
+
+ @Test
+ public void getDefaultInternalStorageEntry_shouldReturnVolumeInfoStorageOfIdPrivateInternal() {
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(mStorageManager.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL)).thenReturn(volumeInfo);
+
+ assertThat(StorageEntry.getDefaultInternalStorageEntry(mContext))
+ .isEqualTo(new StorageEntry(mContext, volumeInfo));
+ }
+
+ @Test
+ public void isVolumeInfo_shouldReturnTrueForVolumeInfo() {
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ final StorageEntry storage = new StorageEntry(mContext, volumeInfo);
+
+ assertThat(storage.isVolumeInfo()).isTrue();
+ assertThat(storage.isUnsupportedDiskInfo()).isFalse();
+ assertThat(storage.isMissingVolumeRecord()).isFalse();
+ }
+
+ @Test
+ public void isUnsupportedDiskInfo_shouldReturnTrueForDiskInfo() {
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry storage = new StorageEntry(diskInfo);
+
+ assertThat(storage.isVolumeInfo()).isFalse();
+ assertThat(storage.isUnsupportedDiskInfo()).isTrue();
+ assertThat(storage.isMissingVolumeRecord()).isFalse();
+ }
+
+ @Test
+ public void isMissingVolumeRecord_shouldReturnTrueForVolumeRecord() {
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry storage = new StorageEntry(volumeRecord);
+
+ assertThat(storage.isVolumeInfo()).isFalse();
+ assertThat(storage.isUnsupportedDiskInfo()).isFalse();
+ assertThat(storage.isMissingVolumeRecord()).isTrue();
+ }
+
+ @Test
+ public void isMounted_mountedOrMountedReadOnly_shouldReturnTrue() {
+ final VolumeInfo mountedVolumeInfo1 = mock(VolumeInfo.class);
+ final StorageEntry mountedStorage1 = new StorageEntry(mContext, mountedVolumeInfo1);
+ when(mountedVolumeInfo1.getState()).thenReturn(VolumeInfo.STATE_MOUNTED);
+ final VolumeInfo mountedVolumeInfo2 = mock(VolumeInfo.class);
+ when(mountedVolumeInfo2.getState()).thenReturn(VolumeInfo.STATE_MOUNTED_READ_ONLY);
+ final StorageEntry mountedStorage2 = new StorageEntry(mContext, mountedVolumeInfo2);
+
+ assertThat(mountedStorage1.isMounted()).isTrue();
+ assertThat(mountedStorage2.isMounted()).isTrue();
+ }
+
+ @Test
+ public void isMounted_nonVolumeInfo_shouldReturnFalse() {
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage2 = new StorageEntry(volumeRecord);
+
+ assertThat(diskStorage.isMounted()).isFalse();
+ assertThat(recordStorage2.isMounted()).isFalse();
+ }
+
+ @Test
+ public void isUnmountable_unmountableVolume_shouldReturnTrue() {
+ final VolumeInfo unmountableVolumeInfo = mock(VolumeInfo.class);
+ final StorageEntry mountedStorage = new StorageEntry(mContext, unmountableVolumeInfo);
+ when(unmountableVolumeInfo.getState()).thenReturn(VolumeInfo.STATE_UNMOUNTABLE);
+
+ assertThat(mountedStorage.isUnmountable()).isTrue();
+ }
+
+ @Test
+ public void isUnmountable_nonVolumeInfo_shouldReturnFalse() {
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage2 = new StorageEntry(volumeRecord);
+
+ assertThat(diskStorage.isUnmountable()).isFalse();
+ assertThat(recordStorage2.isUnmountable()).isFalse();
+ }
+
+ @Test
+ public void isPrivate_privateVolume_shouldReturnTrue() {
+ final VolumeInfo privateVolumeInfo = mock(VolumeInfo.class);
+ final StorageEntry privateStorage = new StorageEntry(mContext, privateVolumeInfo);
+ when(privateVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
+
+ assertThat(privateStorage.isPrivate()).isTrue();
+ }
+
+ @Test
+ public void isPrivate_nonVolumeInfo_shouldReturnFalse() {
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage2 = new StorageEntry(volumeRecord);
+
+ assertThat(diskStorage.isPrivate()).isFalse();
+ assertThat(recordStorage2.isPrivate()).isFalse();
+ }
+
+ @Test
+ public void getDescription_shouldReturnDescription() {
+ final String description = "description";
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(mStorageManager.getBestVolumeDescription(volumeInfo)).thenReturn(description);
+ final StorageEntry volumeStorage = new StorageEntry(mContext, volumeInfo);
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ when(diskInfo.getDescription()).thenReturn(description);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage = new StorageEntry(volumeRecord);
+ when(volumeRecord.getNickname()).thenReturn(description);
+
+ assertThat(volumeStorage.getDescription()).isEqualTo(description);
+ assertThat(diskStorage.getDescription()).isEqualTo(description);
+ assertThat(recordStorage.getDescription()).isEqualTo(description);
+ }
+
+ @Test
+ public void getDiskId_shouldReturnDiskId() {
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ final StorageEntry volumeStorage = new StorageEntry(mContext, volumeInfo);
+ when(volumeInfo.getDiskId()).thenReturn(VOLUME_INFO_ID);
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ when(diskInfo.getId()).thenReturn(DISK_INFO_ID);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage = new StorageEntry(volumeRecord);
+
+ assertThat(volumeStorage.getDiskId()).isEqualTo(VOLUME_INFO_ID);
+ assertThat(diskStorage.getDiskId()).isEqualTo(DISK_INFO_ID);
+ assertThat(recordStorage.getDiskId()).isEqualTo(null);
+ }
+
+ @Test
+ public void getFsUuid_shouldReturnFsUuid() {
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ final StorageEntry volumeStorage = new StorageEntry(mContext, volumeInfo);
+ when(volumeInfo.getFsUuid()).thenReturn(VOLUME_INFO_ID);
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage = new StorageEntry(volumeRecord);
+ when(volumeRecord.getFsUuid()).thenReturn(VOLUME_RECORD_UUID);
+
+ assertThat(volumeStorage.getFsUuid()).isEqualTo(VOLUME_INFO_ID);
+ assertThat(diskStorage.getFsUuid()).isEqualTo(null);
+ assertThat(recordStorage.getFsUuid()).isEqualTo(VOLUME_RECORD_UUID);
+ }
+
+ @Test
+ public void getPath_shouldReturnPath() {
+ final File file = new File("fakePath");
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ final StorageEntry volumeStorage = new StorageEntry(mContext, volumeInfo);
+ when(volumeInfo.getPath()).thenReturn(file);
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage = new StorageEntry(volumeRecord);
+
+ assertThat(volumeStorage.getPath()).isEqualTo(file);
+ assertThat(diskStorage.getPath()).isEqualTo(null);
+ assertThat(recordStorage.getPath()).isEqualTo(null);
+ }
+
+ @Test
+ public void getVolumeInfo_shouldVolumeInfo() {
+ final VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ final StorageEntry volumeStorage = new StorageEntry(mContext, volumeInfo);
+ final DiskInfo diskInfo = mock(DiskInfo.class);
+ final StorageEntry diskStorage = new StorageEntry(diskInfo);
+ final VolumeRecord volumeRecord = mock(VolumeRecord.class);
+ final StorageEntry recordStorage = new StorageEntry(volumeRecord);
+
+ assertThat(volumeStorage.getVolumeInfo()).isEqualTo(volumeInfo);
+ assertThat(diskStorage.getVolumeInfo()).isEqualTo(null);
+ assertThat(recordStorage.getVolumeInfo()).isEqualTo(null);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceControllerTest.java
new file mode 100644
index 00000000000..86351cb70b1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceControllerTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.storage.StorageManager;
+
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settingslib.widget.SettingsSpinnerPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class StorageSelectionPreferenceControllerTest {
+
+ private static final String PREFERENCE_KEY = "preference_key";
+
+ private Context mContext;
+ private StorageManager mStorageManager;
+ private StorageSelectionPreferenceController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = ApplicationProvider.getApplicationContext();
+ mStorageManager = mContext.getSystemService(StorageManager.class);
+ mController = new StorageSelectionPreferenceController(mContext, PREFERENCE_KEY);
+ }
+
+ @Test
+ public void setStorageEntries_fromStorageManager_correctAdapterItems() {
+ final List storageEntries = mStorageManager.getVolumes().stream()
+ .map(volumeInfo -> new StorageEntry(mContext, volumeInfo))
+ .collect(Collectors.toList());
+
+ mController.setStorageEntries(storageEntries);
+
+ final int adapterItemCount = mController.mStorageAdapter.getCount();
+ assertThat(adapterItemCount).isEqualTo(storageEntries.size());
+ for (int i = 0; i < adapterItemCount; i++) {
+ assertThat(storageEntries.get(i).getDescription())
+ .isEqualTo(mController.mStorageAdapter.getItem(i).getDescription());
+ }
+ }
+
+ @Test
+ public void setSelectedStorageEntry_primaryStorage_correctSelectedAdapterItem() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ final PreferenceManager preferenceManager = new PreferenceManager(mContext);
+ final PreferenceScreen preferenceScreen =
+ preferenceManager.createPreferenceScreen(mContext);
+ final SettingsSpinnerPreference spinnerPreference = new SettingsSpinnerPreference(mContext);
+ spinnerPreference.setKey(PREFERENCE_KEY);
+ preferenceScreen.addPreference(spinnerPreference);
+ mController.displayPreference(preferenceScreen);
+ final StorageEntry primaryStorageEntry =
+ StorageEntry.getDefaultInternalStorageEntry(mContext);
+ mController.setStorageEntries(mStorageManager.getVolumes().stream()
+ .map(volumeInfo -> new StorageEntry(mContext, volumeInfo))
+ .collect(Collectors.toList()));
+
+ mController.setSelectedStorageEntry(primaryStorageEntry);
+
+ assertThat((StorageEntry) mController.mSpinnerPreference.getSelectedItem())
+ .isEqualTo(primaryStorageEntry);
+ }
+}
+
diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceControllerTest.java
new file mode 100644
index 00000000000..6d9155a318a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceControllerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.deviceinfo.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.StorageStatsManager;
+import android.content.Context;
+import android.os.Looper;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settingslib.widget.UsageProgressBarPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+public class StorageUsageProgressBarPreferenceControllerTest {
+
+ private static final String FAKE_UUID = "95D9-B3A4";
+ private static final long WAIT_TIMEOUT = 10_000L;
+ private static final long FREE_BYTES = 123L;
+ private static final long TOTAL_BYTES = 456L;
+ private static final long USAGE_BYTES = TOTAL_BYTES - FREE_BYTES;
+
+ private Context mContext;
+ private FakeStorageUsageProgressBarPreferenceController mController;
+ private PreferenceScreen mPreferenceScreen;
+ @Mock
+ private StorageStatsManager mStorageStatsManager;
+
+ @Before
+ public void setUp() throws Exception {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ when(mContext.getSystemService(StorageStatsManager.class)).thenReturn(mStorageStatsManager);
+ mController = new FakeStorageUsageProgressBarPreferenceController(mContext, "key");
+ final PreferenceManager preferenceManager = new PreferenceManager(mContext);
+ mPreferenceScreen = preferenceManager.createPreferenceScreen(mContext);
+ final UsageProgressBarPreference usageProgressBarPreference =
+ new UsageProgressBarPreference(mContext);
+ usageProgressBarPreference.setKey(mController.getPreferenceKey());
+ mPreferenceScreen.addPreference(usageProgressBarPreference);
+ }
+
+ @Test
+ public void setSelectedStorageEntry_primaryStorage_getPrimaryStorageBytes() throws IOException {
+ final StorageEntry defaultInternalStorageEntry =
+ StorageEntry.getDefaultInternalStorageEntry(mContext);
+ when(mStorageStatsManager.getTotalBytes(defaultInternalStorageEntry.getFsUuid()))
+ .thenReturn(TOTAL_BYTES);
+ when(mStorageStatsManager.getFreeBytes(defaultInternalStorageEntry.getFsUuid()))
+ .thenReturn(FREE_BYTES);
+ mController.displayPreference(mPreferenceScreen);
+
+ synchronized (mController.mLock) {
+ mController.setSelectedStorageEntry(defaultInternalStorageEntry);
+ mController.waitUpdateState(WAIT_TIMEOUT);
+ }
+
+ assertThat(mController.mUsedBytes).isEqualTo(USAGE_BYTES);
+ assertThat(mController.mTotalBytes).isEqualTo(TOTAL_BYTES);
+ }
+
+ private class FakeStorageUsageProgressBarPreferenceController
+ extends StorageUsageProgressBarPreferenceController {
+ private final Object mLock = new Object();
+
+ FakeStorageUsageProgressBarPreferenceController(Context context, String key) {
+ super(context, key);
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ super.updateState(preference);
+ try {
+ mLock.notifyAll();
+ } catch (IllegalMonitorStateException e) {
+ // Catch it for displayPreference to prevent exception by object not locked by
+ // thread before notify. Do nothing.
+ }
+ }
+
+ public void waitUpdateState(long timeout) {
+ try {
+ mLock.wait(timeout);
+ } catch (InterruptedException e) {
+ // Do nothing.
+ }
+ }
+ }
+}
+
diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/VolumeSizesLoaderTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/VolumeSizesLoaderTest.java
index 79c5db8d831..77fd9636150 100644
--- a/tests/unit/src/com/android/settings/deviceinfo/storage/VolumeSizesLoaderTest.java
+++ b/tests/unit/src/com/android/settings/deviceinfo/storage/VolumeSizesLoaderTest.java
@@ -34,8 +34,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class VolumeSizesLoaderTest {
@Test
- public void getVolumeSize_getsValidSizes() throws Exception {
+ public void getVolumeSize_privateMountedVolume_getsValidSizes() throws Exception {
VolumeInfo info = mock(VolumeInfo.class);
+ when(info.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
+ when(info.getState()).thenReturn(VolumeInfo.STATE_MOUNTED);
StorageVolumeProvider storageVolumeProvider = mock(StorageVolumeProvider.class);
when(storageVolumeProvider.getTotalBytes(any(), any())).thenReturn(10000L);
when(storageVolumeProvider.getFreeBytes(any(), any())).thenReturn(1000L);
@@ -46,4 +48,19 @@ public class VolumeSizesLoaderTest {
assertThat(storageInfo.freeBytes).isEqualTo(1000L);
assertThat(storageInfo.totalBytes).isEqualTo(10000L);
}
+
+ @Test
+ public void getVolumeSize_unmountedVolume_getsValidSizes() throws Exception {
+ VolumeInfo info = mock(VolumeInfo.class);
+ when(info.getState()).thenReturn(VolumeInfo.STATE_UNMOUNTED);
+ StorageVolumeProvider storageVolumeProvider = mock(StorageVolumeProvider.class);
+ when(storageVolumeProvider.getTotalBytes(any(), any())).thenReturn(10000L);
+ when(storageVolumeProvider.getFreeBytes(any(), any())).thenReturn(1000L);
+
+ PrivateStorageInfo storageInfo =
+ VolumeSizesLoader.getVolumeSize(storageVolumeProvider, null, info);
+
+ assertThat(storageInfo.freeBytes).isEqualTo(0L);
+ assertThat(storageInfo.totalBytes).isEqualTo(0L);
+ }
}