From 89e6d99dafd9ddeb0a052e037a2b339bd886b00c Mon Sep 17 00:00:00 2001 From: Arc Wang Date: Thu, 25 Feb 2021 15:01:08 +0800 Subject: [PATCH 1/2] Revamp Storage Settings header part 1. Add new object StorageEntry to encapsulate VolumeInfo and unsupported DiskInfo and missing VolumeRecord. 2. Replaces StorageSummaryDonutPreference with UsageProgressBarPreference. 3. Add storage select spinner. 4. Add a "Free up storage" preference to replace "Manage storage" button. Bug: 174964885 Test: atest com.android.settings.deviceinfo.storage atest com.android.settings.deviceinfo manual Insert an USB drive, select the drive in StorageDashboardFragment and observe UI. Change-Id: I83877f76869414de4fb2788b6b18fe507aa5cfcf --- res/values/strings.xml | 8 + res/xml/storage_dashboard_fragment.xml | 21 +- .../PrivateVolumeOptionMenuController.java | 4 + .../deviceinfo/StorageDashboardFragment.java | 313 ++++++++++++++++-- .../deviceinfo/storage/StorageEntry.java | 283 ++++++++++++++++ .../StorageItemPreferenceController.java | 14 + .../StorageSelectionPreferenceController.java | 151 +++++++++ ...eUsageProgressBarPreferenceController.java | 124 +++++++ .../deviceinfo/storage/VolumeSizesLoader.java | 18 +- tests/unit/Android.bp | 2 + .../deviceinfo/storage/StorageEntryTest.java | 301 +++++++++++++++++ ...rageSelectionPreferenceControllerTest.java | 95 ++++++ ...geProgressBarPreferenceControllerTest.java | 123 +++++++ .../storage/VolumeSizesLoaderTest.java | 19 +- 14 files changed, 1443 insertions(+), 33 deletions(-) create mode 100644 src/com/android/settings/deviceinfo/storage/StorageEntry.java create mode 100644 src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceController.java create mode 100644 src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java create mode 100644 tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java create mode 100644 tests/unit/src/com/android/settings/deviceinfo/storage/StorageSelectionPreferenceControllerTest.java create mode 100644 tests/unit/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceControllerTest.java 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); + } } From ba79fe6676ec2569665e71d394d2cdc84ad36d96 Mon Sep 17 00:00:00 2001 From: Arc Wang Date: Mon, 1 Mar 2021 14:54:32 +0800 Subject: [PATCH 2/2] Add option menu in StorageDashboardFragment Add option menu for each kind of storage 1. Unsupported disk: Format. 2. Missing volume record: Forget. 3. Unmounted storage: Mount. 4. Default internal storage: Migrate. 5. Private volume: Rename / Unmount / Format as portable / Migrate. 6. Publuc volume: Rename / Unmount / Format / Format as internal. Bug: 174964885 Test: atest VolumeOptionMenuControllerTest Change-Id: I85fa117ff0a49ec7a53ba36580591c7ce7f5a8dc --- res/menu/storage_volume.xml | 12 + .../PrivateVolumeOptionMenuController.java | 89 ------ .../deviceinfo/StorageDashboardFragment.java | 154 +++++------ .../VolumeOptionMenuController.java | 257 ++++++++++++++++++ .../deviceinfo/storage/DiskInitFragment.java | 75 +++++ .../deviceinfo/storage/StorageEntry.java | 32 ++- .../storage/StorageRenameFragment.java | 80 ++++++ .../deviceinfo/storage/StorageUtils.java | 47 ++++ ...PrivateVolumeOptionMenuControllerTest.java | 139 ---------- .../VolumeOptionMenuControllerTest.java | 224 +++++++++++++++ .../deviceinfo/storage/StorageEntryTest.java | 16 +- 11 files changed, 791 insertions(+), 334 deletions(-) delete mode 100644 src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java create mode 100644 src/com/android/settings/deviceinfo/VolumeOptionMenuController.java create mode 100644 src/com/android/settings/deviceinfo/storage/DiskInitFragment.java create mode 100644 src/com/android/settings/deviceinfo/storage/StorageRenameFragment.java create mode 100644 src/com/android/settings/deviceinfo/storage/StorageUtils.java delete mode 100644 tests/unit/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuControllerTest.java create mode 100644 tests/unit/src/com/android/settings/deviceinfo/VolumeOptionMenuControllerTest.java diff --git a/res/menu/storage_volume.xml b/res/menu/storage_volume.xml index bf9f985aa63..87f75152ce3 100644 --- a/res/menu/storage_volume.xml +++ b/res/menu/storage_volume.xml @@ -27,10 +27,22 @@ + + + diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java deleted file mode 100644 index 549a02f18c4..00000000000 --- a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2017 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; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.storage.VolumeInfo; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import com.android.settings.R; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; -import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected; -import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu; - -import java.util.Objects; - -/** - * Handles the option menu on the Storage settings. - */ -public class PrivateVolumeOptionMenuController implements LifecycleObserver, OnCreateOptionsMenu, - OnPrepareOptionsMenu, OnOptionsItemSelected { - private static final int OPTIONS_MENU_MIGRATE_DATA = 100; - - private Context mContext; - private VolumeInfo mVolumeInfo; - private PackageManager mPm; - - public PrivateVolumeOptionMenuController( - Context context, VolumeInfo volumeInfo, PackageManager packageManager) { - mContext = context; - mVolumeInfo = volumeInfo; - mPm = packageManager; - } - - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - menu.add(Menu.NONE, OPTIONS_MENU_MIGRATE_DATA, 0, R.string.storage_menu_migrate); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - if (mVolumeInfo == null) { - return; - } - - // Only offer to migrate when not current storage - final VolumeInfo privateVol = mPm.getPrimaryStorageCurrentVolume(); - final MenuItem migrate = menu.findItem(OPTIONS_MENU_MIGRATE_DATA); - if (migrate != null) { - migrate.setVisible((privateVol != null) - && (privateVol.getType() == VolumeInfo.TYPE_PRIVATE) - && !Objects.equals(mVolumeInfo, privateVol) - && privateVol.isMountedWritable()); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (menuItem.getItemId() == OPTIONS_MENU_MIGRATE_DATA) { - final Intent intent = new Intent(mContext, StorageWizardMigrateConfirm.class); - intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolumeInfo.getId()); - mContext.startActivity(intent); - return true; - } - 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 c9d2c6d5083..7af4f0cfd02 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -17,11 +17,9 @@ 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; @@ -38,26 +36,24 @@ 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.DiskInitFragment; 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.StorageUtils; import com.android.settings.deviceinfo.storage.UserIconLoader; import com.android.settings.deviceinfo.storage.VolumeSizesLoader; import com.android.settings.overlay.FeatureFactory; @@ -96,7 +92,7 @@ public class StorageDashboardFragment extends DashboardFragment private CachedStorageValuesHelper mCachedStorageValuesHelper; private StorageItemPreferenceController mPreferenceController; - private PrivateVolumeOptionMenuController mOptionMenuController; + private VolumeOptionMenuController mOptionMenuController; private StorageSelectionPreferenceController mStorageSelectionController; private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController; private List mSecondaryUsers; @@ -110,22 +106,31 @@ public class StorageDashboardFragment extends DashboardFragment return; } - final StorageEntry storageEntry = new StorageEntry(getContext(), volumeInfo); + final StorageEntry changedStorageEntry = 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(); + // Add mounted or unmountable storage in the list and show it on spinner. + // Unmountable storages are the storages which has a problem format and android + // is not able to mount it automatically. + // Users can format an unmountable storage by the UI and then use the storage. + mStorageEntries.removeIf(storageEntry -> { + return storageEntry.equals(changedStorageEntry); + }); + mStorageEntries.add(changedStorageEntry); + if (changedStorageEntry.equals(mSelectedStorageEntry)) { + mSelectedStorageEntry = changedStorageEntry; } + 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)) { + // Remove removed storage from list and don't show it on spinner. + if (mStorageEntries.remove(changedStorageEntry)) { + if (changedStorageEntry.equals(mSelectedStorageEntry)) { mSelectedStorageEntry = StorageEntry.getDefaultInternalStorageEntry(getContext()); } @@ -139,10 +144,32 @@ public class StorageDashboardFragment extends DashboardFragment @Override public void onVolumeRecordChanged(VolumeRecord volumeRecord) { - final StorageEntry storageEntry = new StorageEntry(volumeRecord); - if (!mStorageEntries.contains(storageEntry)) { - mStorageEntries.add(storageEntry); - refreshUi(); + if (isVolumeRecordMissed(volumeRecord)) { + // VolumeRecord is a metadata of VolumeInfo, if a VolumeInfo is missing + // (e.g., internal SD card is removed.) show the missing storage to users, + // users can insert the SD card or manually forget the storage from the device. + final StorageEntry storageEntry = new StorageEntry(volumeRecord); + if (!mStorageEntries.contains(storageEntry)) { + mStorageEntries.add(storageEntry); + refreshUi(); + } + } else { + // Find mapped VolumeInfo and replace with existing one for something changed. + // (e.g., Renamed.) + final VolumeInfo mappedVolumeInfo = + mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()); + if (mappedVolumeInfo == null) { + return; + } + + final boolean removeMappedStorageEntry = mStorageEntries.removeIf(storageEntry -> + storageEntry.isVolumeInfo() + && TextUtils.equals(storageEntry.getFsUuid(), volumeRecord.getFsUuid()) + ); + if (removeMappedStorageEntry) { + mStorageEntries.add(new StorageEntry(getContext(), mappedVolumeInfo)); + refreshUi(); + } } } @@ -161,7 +188,7 @@ public class StorageDashboardFragment extends DashboardFragment @Override public void onDiskScanned(DiskInfo disk, int volumeCount) { - if (!isInteresting(disk)) { + if (!isDiskUnsupported(disk)) { return; } final StorageEntry storageEntry = new StorageEntry(disk); @@ -195,8 +222,19 @@ public class StorageDashboardFragment extends DashboardFragment } } - // Only interested in unsupported disk. - private static boolean isInteresting(DiskInfo disk) { + /** + * VolumeRecord is a metadata of VolumeInfo, this is the case where a VolumeInfo is missing. + * (e.g., internal SD card is removed.) + */ + private boolean isVolumeRecordMissed(VolumeRecord volumeRecord) { + return volumeRecord.getType() == VolumeInfo.TYPE_PRIVATE + && mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()) == null; + } + + /** + * A unsupported disk is the disk of problem format, android is not able to mount automatically. + */ + private static boolean isDiskUnsupported(DiskInfo disk) { return disk.volumeCount == 0 && disk.size > 0; } @@ -205,7 +243,8 @@ public class StorageDashboardFragment extends DashboardFragment mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); - mOptionMenuController.setVolumeInfo(mSelectedStorageEntry.getVolumeInfo()); + mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); + getActivity().invalidateOptionsMenu(); mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo()); @@ -263,18 +302,11 @@ public class StorageDashboardFragment extends DashboardFragment mSelectedStorageEntry = storageEntry; refreshUi(); - if (storageEntry.isUnsupportedDiskInfo() || storageEntry.isUnmountable()) { + if (storageEntry.isDiskInfoUnsupported() || 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(); + } else if (storageEntry.isVolumeRecordMissed()) { + StorageUtils.launchForgetMissingVolumeRecordFragment(getContext(), storageEntry); } }); mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class); @@ -282,9 +314,8 @@ public class StorageDashboardFragment extends DashboardFragment @VisibleForTesting void initializeOptionsMenu(Activity activity) { - mOptionMenuController = new PrivateVolumeOptionMenuController(activity, - mSelectedStorageEntry.getVolumeInfo(), - activity.getPackageManager()); + mOptionMenuController = new VolumeOptionMenuController(activity, this, + mSelectedStorageEntry); getSettingsLifecycle().addObserver(mOptionMenuController); setHasOptionsMenu(true); activity.invalidateOptionsMenu(); @@ -312,15 +343,12 @@ public class StorageDashboardFragment extends DashboardFragment .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)) + .filter(disk -> isDiskUnsupported(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) + .filter(volumeRecord -> isVolumeRecordMissed(volumeRecord)) .map(volumeRecord -> new StorageEntry(volumeRecord)) .collect(Collectors.toList())); refreshUi(); @@ -619,52 +647,4 @@ 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/VolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java new file mode 100644 index 00000000000..0932447bf20 --- /dev/null +++ b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java @@ -0,0 +1,257 @@ +/* + * 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; + +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.UserManager; +import android.os.storage.DiskInfo; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.deviceinfo.StorageSettings.MountTask; +import com.android.settings.deviceinfo.StorageSettings.UnmountTask; +import com.android.settings.deviceinfo.storage.StorageEntry; +import com.android.settings.deviceinfo.storage.StorageRenameFragment; +import com.android.settings.deviceinfo.storage.StorageUtils; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; +import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected; +import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu; + +import java.util.Objects; + +/** + * Handles the option menu on the Storage settings. + */ +public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOptionsMenu, + OnPrepareOptionsMenu, OnOptionsItemSelected { + + @VisibleForTesting + MenuItem mRename; + @VisibleForTesting + MenuItem mMount; + @VisibleForTesting + MenuItem mUnmount; + @VisibleForTesting + MenuItem mFormat; + @VisibleForTesting + MenuItem mFormatAsPortable; + @VisibleForTesting + MenuItem mFormatAsInternal; + @VisibleForTesting + MenuItem mMigrate; + @VisibleForTesting + MenuItem mFree; + @VisibleForTesting + MenuItem mForget; + + private final Context mContext; + private final Fragment mFragment; + private final PackageManager mPackageManager; + private final StorageManager mStorageManager; + private StorageEntry mStorageEntry; + + public VolumeOptionMenuController(Context context, Fragment parent, StorageEntry storageEntry) { + mContext = context; + mFragment = parent; + mPackageManager = context.getPackageManager(); + mStorageManager = context.getSystemService(StorageManager.class); + mStorageEntry = storageEntry; + } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.storage_volume, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + mRename = menu.findItem(R.id.storage_rename); + mMount = menu.findItem(R.id.storage_mount); + mUnmount = menu.findItem(R.id.storage_unmount); + mFormat = menu.findItem(R.id.storage_format); + mFormatAsPortable = menu.findItem(R.id.storage_format_as_portable); + mFormatAsInternal = menu.findItem(R.id.storage_format_as_internal); + mMigrate = menu.findItem(R.id.storage_migrate); + mFree = menu.findItem(R.id.storage_free); + mForget = menu.findItem(R.id.storage_forget); + + mRename.setVisible(false); + mMount.setVisible(false); + mUnmount.setVisible(false); + mFormat.setVisible(false); + mFormatAsPortable.setVisible(false); + mFormatAsInternal.setVisible(false); + mMigrate.setVisible(false); + mFree.setVisible(false); + mForget.setVisible(false); + + if (mStorageEntry.isDiskInfoUnsupported()) { + mFormat.setVisible(true); + return; + } + if (mStorageEntry.isVolumeRecordMissed()) { + mForget.setVisible(true); + return; + } + if (mStorageEntry.isUnmounted()) { + mMount.setVisible(true); + return; + } + if (!mStorageEntry.isMounted()) { + return; + } + + if (mStorageEntry.isPrivate()) { + if (!mStorageEntry.isDefaultInternalStorage()) { + mRename.setVisible(true); + mUnmount.setVisible(true); + mFormatAsPortable.setVisible(true); + } + + // Only offer to migrate when not current storage. + final VolumeInfo primaryVolumeInfo = mPackageManager.getPrimaryStorageCurrentVolume(); + final VolumeInfo selectedVolumeInfo = mStorageEntry.getVolumeInfo(); + mMigrate.setVisible(primaryVolumeInfo != null + && primaryVolumeInfo.getType() == VolumeInfo.TYPE_PRIVATE + && !Objects.equals(selectedVolumeInfo, primaryVolumeInfo) + && primaryVolumeInfo.isMountedWritable()); + return; + } + + if (mStorageEntry.isPublic()) { + mRename.setVisible(true); + mUnmount.setVisible(true); + mFormat.setVisible(true); + final DiskInfo diskInfo = mStorageManager.findDiskById(mStorageEntry.getDiskId()); + mFormatAsInternal.setVisible(diskInfo != null + && diskInfo.isAdoptable() + && UserManager.get(mContext).isAdminUser() + && !ActivityManager.isUserAMonkey()); + return; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if (!mFragment.isAdded()) { + return false; + } + + final int menuId = menuItem.getItemId(); + if (menuId == R.id.storage_mount) { + if (mStorageEntry.isUnmounted()) { + new MountTask(mFragment.getActivity(), mStorageEntry.getVolumeInfo()).execute(); + return true; + } + return false; + } + if (menuId == R.id.storage_unmount) { + if (mStorageEntry.isMounted()) { + if (mStorageEntry.isPublic()) { + new UnmountTask(mFragment.getActivity(), + mStorageEntry.getVolumeInfo()).execute(); + return true; + } + if (mStorageEntry.isPrivate() && !mStorageEntry.isDefaultInternalStorage()) { + final Bundle args = new Bundle(); + args.putString(VolumeInfo.EXTRA_VOLUME_ID, mStorageEntry.getId()); + new SubSettingLauncher(mContext) + .setDestination(PrivateVolumeUnmount.class.getCanonicalName()) + .setTitleRes(R.string.storage_menu_unmount) + .setSourceMetricsCategory(SettingsEnums.DEVICEINFO_STORAGE) + .setArguments(args) + .launch(); + return true; + } + } + return false; + } + if (menuId == R.id.storage_rename) { + if ((mStorageEntry.isPrivate() && !mStorageEntry.isDefaultInternalStorage()) + || mStorageEntry.isPublic()) { + StorageRenameFragment.show(mFragment, mStorageEntry.getVolumeInfo()); + return true; + } + return false; + } + if (menuId == R.id.storage_format) { + if (mStorageEntry.isDiskInfoUnsupported() || mStorageEntry.isPublic()) { + StorageWizardFormatConfirm.showPublic(mFragment.getActivity(), + mStorageEntry.getDiskId()); + return true; + } + return false; + } + if (menuId == R.id.storage_format_as_portable) { + if (mStorageEntry.isPrivate()) { + final Bundle args = new Bundle(); + args.putString(VolumeInfo.EXTRA_VOLUME_ID, mStorageEntry.getId()); + new SubSettingLauncher(mContext) + .setDestination(PrivateVolumeFormat.class.getCanonicalName()) + .setTitleRes(R.string.storage_menu_format) + .setSourceMetricsCategory(SettingsEnums.DEVICEINFO_STORAGE) + .setArguments(args) + .launch(); + return true; + } + return false; + } + if (menuId == R.id.storage_format_as_internal) { + if (mStorageEntry.isPublic()) { + StorageWizardFormatConfirm.showPrivate(mFragment.getActivity(), + mStorageEntry.getDiskId()); + return true; + } + return false; + } + if (menuId == R.id.storage_migrate) { + if (mStorageEntry.isPrivate()) { + final Intent intent = new Intent(mContext, StorageWizardMigrateConfirm.class); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mStorageEntry.getId()); + mContext.startActivity(intent); + return true; + } + return false; + } + if (menuId == R.id.storage_forget) { + if (mStorageEntry.isVolumeRecordMissed()) { + StorageUtils.launchForgetMissingVolumeRecordFragment(mContext, mStorageEntry); + return true; + } + return false; + } + return false; + } + + public void setSelectedStorageEntry(StorageEntry storageEntry) { + mStorageEntry = storageEntry; + } +} diff --git a/src/com/android/settings/deviceinfo/storage/DiskInitFragment.java b/src/com/android/settings/deviceinfo/storage/DiskInitFragment.java new file mode 100644 index 00000000000..1e6a98d175f --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/DiskInitFragment.java @@ -0,0 +1,75 @@ +/* + * 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.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.storage.DiskInfo; +import android.os.storage.StorageManager; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.deviceinfo.StorageWizardInit; + +/** A dialog which guides users to initialize a specified unsupported disk. */ +public class DiskInitFragment extends InstrumentedDialogFragment { + + private static final String TAG_DISK_INIT = "disk_init"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_VOLUME_INIT; + } + + /** Shows the dialog 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); + return builder.setMessage(TextUtils.expandTemplate(getText(resId), disk.getDescription())) + .setPositiveButton(R.string.storage_menu_set_up, (dialog, which) -> { + final Intent intent = new Intent(context, StorageWizardInit.class); + intent.putExtra(DiskInfo.EXTRA_DISK_ID, diskId); + startActivity(intent); }) + .setNegativeButton(R.string.cancel, null) + .create(); + } +} + diff --git a/src/com/android/settings/deviceinfo/storage/StorageEntry.java b/src/com/android/settings/deviceinfo/storage/StorageEntry.java index f86fa794d69..f71811602d5 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageEntry.java +++ b/src/com/android/settings/deviceinfo/storage/StorageEntry.java @@ -106,7 +106,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.equals(StorageEntry.mVolumeInfo); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.equals(StorageEntry.mUnsupportedDiskInfo); } return mMissingVolumeRecord.equals(StorageEntry.mMissingVolumeRecord); @@ -117,7 +117,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.hashCode(); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.hashCode(); } return mMissingVolumeRecord.hashCode(); @@ -128,7 +128,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.toString(); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.toString(); } return mMissingVolumeRecord.toString(); @@ -164,10 +164,10 @@ public class StorageEntry implements Comparable, Parcelable { return 1; } - if (!isMissingVolumeRecord() && other.isMissingVolumeRecord()) { + if (!isVolumeRecordMissed() && other.isVolumeRecordMissed()) { return -1; } - if (isMissingVolumeRecord() && !other.isMissingVolumeRecord()) { + if (isVolumeRecordMissed() && !other.isVolumeRecordMissed()) { return 1; } @@ -192,12 +192,12 @@ public class StorageEntry implements Comparable, Parcelable { } /** If it's an unsupported DiskInfo. */ - public boolean isUnsupportedDiskInfo() { + public boolean isDiskInfoUnsupported() { return mUnsupportedDiskInfo != null; } /** If it's a missing VolumeRecord. */ - public boolean isMissingVolumeRecord() { + public boolean isVolumeRecordMissed() { return mMissingVolumeRecord != null; } @@ -216,6 +216,11 @@ public class StorageEntry implements Comparable, Parcelable { || mVolumeInfo.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY); } + /** If it's an unmounted storage. */ + public boolean isUnmounted() { + return mVolumeInfo == null ? false : (mVolumeInfo.getState() == VolumeInfo.STATE_UNMOUNTED); + } + /** If it's an unmountable storage. */ public boolean isUnmountable() { return mVolumeInfo == null ? false : mVolumeInfo.getState() == VolumeInfo.STATE_UNMOUNTABLE; @@ -226,12 +231,17 @@ public class StorageEntry implements Comparable, Parcelable { return mVolumeInfo == null ? false : mVolumeInfo.getType() == VolumeInfo.TYPE_PRIVATE; } + /** If it's a public storage. */ + public boolean isPublic() { + return mVolumeInfo == null ? false : mVolumeInfo.getType() == VolumeInfo.TYPE_PUBLIC; + } + /** Returns description. */ public String getDescription() { if (isVolumeInfo()) { return mVolumeInfoDescription; } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.getDescription(); } return mMissingVolumeRecord.getNickname(); @@ -242,7 +252,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.getId(); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.getId(); } return mMissingVolumeRecord.getFsUuid(); @@ -253,7 +263,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.getDiskId(); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return mUnsupportedDiskInfo.getId(); } return null; @@ -264,7 +274,7 @@ public class StorageEntry implements Comparable, Parcelable { if (isVolumeInfo()) { return mVolumeInfo.getFsUuid(); } - if (isUnsupportedDiskInfo()) { + if (isDiskInfoUnsupported()) { return null; } return mMissingVolumeRecord.getFsUuid(); diff --git a/src/com/android/settings/deviceinfo/storage/StorageRenameFragment.java b/src/com/android/settings/deviceinfo/storage/StorageRenameFragment.java new file mode 100644 index 00000000000..c67fe338ffa --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/StorageRenameFragment.java @@ -0,0 +1,80 @@ +/* + * 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.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.os.storage.VolumeRecord; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Dialog that allows editing of volume nickname. + */ +public class StorageRenameFragment extends InstrumentedDialogFragment { + private static final String TAG_RENAME = "rename"; + + /** Shows the rename dialog. */ + public static void show(Fragment parent, VolumeInfo vol) { + final StorageRenameFragment dialog = new StorageRenameFragment(); + dialog.setTargetFragment(parent, 0 /* requestCode */); + final Bundle args = new Bundle(); + args.putString(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid()); + dialog.setArguments(args); + dialog.show(parent.getFragmentManager(), TAG_RENAME); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_VOLUME_RENAME; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + final StorageManager storageManager = context.getSystemService(StorageManager.class); + + final String fsUuid = getArguments().getString(VolumeRecord.EXTRA_FS_UUID); + final VolumeRecord rec = storageManager.findRecordByUuid(fsUuid); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + + final View view = dialogInflater.inflate(R.layout.dialog_edittext, null, false); + final EditText nickname = (EditText) view.findViewById(R.id.edittext); + nickname.setText(rec.getNickname()); + + return builder.setTitle(R.string.storage_rename_title) + .setView(view) + .setPositiveButton(R.string.save, (dialog, which) -> + // TODO: move to background thread + storageManager.setVolumeNickname(fsUuid, nickname.getText().toString())) + .setNegativeButton(R.string.cancel, null) + .create(); + } +} diff --git a/src/com/android/settings/deviceinfo/storage/StorageUtils.java b/src/com/android/settings/deviceinfo/storage/StorageUtils.java new file mode 100644 index 00000000000..26bdec0556c --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/StorageUtils.java @@ -0,0 +1,47 @@ +/* + * 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.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.os.storage.VolumeRecord; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.deviceinfo.PrivateVolumeForget; + +/** Storage utilities */ +public class StorageUtils { + + /** Launches the fragment to forget a specified missing volume record. */ + public static void launchForgetMissingVolumeRecordFragment(Context context, + StorageEntry storageEntry) { + if (storageEntry == null || !storageEntry.isVolumeRecordMissed()) { + return; + } + + final Bundle args = new Bundle(); + args.putString(VolumeRecord.EXTRA_FS_UUID, storageEntry.getFsUuid()); + new SubSettingLauncher(context) + .setDestination(PrivateVolumeForget.class.getCanonicalName()) + .setTitleRes(R.string.storage_menu_forget) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_STORAGE_CATEGORY) + .setArguments(args) + .launch(); + } +} diff --git a/tests/unit/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuControllerTest.java deleted file mode 100644 index e3affefda55..00000000000 --- a/tests/unit/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuControllerTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2017 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; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.storage.VolumeInfo; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settings.testutils.ResourcesUtils; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -public class PrivateVolumeOptionMenuControllerTest { - - @Mock - private MenuItem mMigrateMenuItem; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Menu mMenu; - @Mock - private MenuInflater mMenuInflater; - @Mock - private PackageManager mPm; - @Mock - private VolumeInfo mVolumeInfo; - @Mock - private VolumeInfo mPrimaryInfo; - - private Context mContext; - private PrivateVolumeOptionMenuController mController; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(ApplicationProvider.getApplicationContext()); - - when(mVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE); - when(mVolumeInfo.isMountedWritable()).thenReturn(true); - when(mPrimaryInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE); - when(mMenu.findItem(anyInt())).thenReturn(mMigrateMenuItem); - when(mMigrateMenuItem.getItemId()).thenReturn(100); - - mController = new PrivateVolumeOptionMenuController(mContext, mPrimaryInfo, mPm); - } - - @Test - public void testMigrateDataMenuItemIsAdded() { - mController.onCreateOptionsMenu(mMenu, mMenuInflater); - - verify(mMenu).add(Menu.NONE, 100, Menu.NONE, ResourcesUtils.getResourcesId( - mContext, "string", "storage_menu_migrate")); - } - - @Test - public void testMigrateDataIsNotVisibleNormally() { - when(mPm.getPrimaryStorageCurrentVolume()).thenReturn(mPrimaryInfo); - when(mPrimaryInfo.isMountedWritable()).thenReturn(true); - - mController.onCreateOptionsMenu(mMenu, mMenuInflater); - mController.onPrepareOptionsMenu(mMenu); - - verify(mMigrateMenuItem).setVisible(false); - } - - @Test - public void testMigrateDataIsVisibleWhenExternalVolumeIsPrimary() { - when(mPm.getPrimaryStorageCurrentVolume()).thenReturn(mVolumeInfo); - - mController.onCreateOptionsMenu(mMenu, mMenuInflater); - mController.onPrepareOptionsMenu(mMenu); - - verify(mMigrateMenuItem).setVisible(true); - } - - @Test - public void testMigrateDataIsNotVisibleWhenExternalVolumeIsNotMounted() { - when(mPm.getPrimaryStorageCurrentVolume()).thenReturn(mVolumeInfo); - when(mVolumeInfo.isMountedWritable()).thenReturn(false); - - mController.onCreateOptionsMenu(mMenu, mMenuInflater); - mController.onPrepareOptionsMenu(mMenu); - - verify(mMigrateMenuItem).setVisible(false); - } - - @Test - public void testMigrateDataGoesToMigrateWizard() { - when(mPm.getPrimaryStorageCurrentVolume()).thenReturn(mVolumeInfo); - doNothing().when(mContext).startActivity(any(Intent.class)); - - mController.onCreateOptionsMenu(mMenu, mMenuInflater); - mController.onPrepareOptionsMenu(mMenu); - - assertThat(mController.onOptionsItemSelected(mMigrateMenuItem)).isTrue(); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass( - Intent.class); - verify(mContext).startActivity(intentCaptor.capture()); - final Intent startedIntent = intentCaptor.getValue(); - assertThat(startedIntent.getComponent().getClassName()) - .isEqualTo(StorageWizardMigrateConfirm.class.getName()); - } -} diff --git a/tests/unit/src/com/android/settings/deviceinfo/VolumeOptionMenuControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/VolumeOptionMenuControllerTest.java new file mode 100644 index 00000000000..314f8c22138 --- /dev/null +++ b/tests/unit/src/com/android/settings/deviceinfo/VolumeOptionMenuControllerTest.java @@ -0,0 +1,224 @@ +/* + * 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; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.storage.DiskInfo; +import android.os.storage.StorageManager; +import android.os.storage.VolumeInfo; +import android.os.storage.VolumeRecord; +import android.view.Menu; + +import androidx.fragment.app.Fragment; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.deviceinfo.storage.StorageEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class VolumeOptionMenuControllerTest { + + private static final String INTERNAL_VOLUME_ID = "1"; + private static final String EXTERNAL_VOLUME_ID = "2"; + private static final String DISK_ID = "3"; + private static final String VOLUME_RECORD_FSUUID = "volume_record_fsuuid"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Menu mMenu; + @Mock private PackageManager mPackageManager; + @Mock private StorageManager mStorageManager; + @Mock private VolumeInfo mExternalVolumeInfo; + @Mock private VolumeInfo mInternalVolumeInfo; + + private Context mContext; + private VolumeOptionMenuController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager); + + when(mInternalVolumeInfo.getId()).thenReturn(INTERNAL_VOLUME_ID); + when(mInternalVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE); + when(mInternalVolumeInfo.getState()).thenReturn(VolumeInfo.STATE_MOUNTED); + when(mInternalVolumeInfo.isMountedWritable()).thenReturn(true); + when(mExternalVolumeInfo.getId()).thenReturn(EXTERNAL_VOLUME_ID); + + final StorageEntry selectedStorageEntry = new StorageEntry(mContext, mInternalVolumeInfo); + mController = new VolumeOptionMenuController(mContext, mock(Fragment.class), + selectedStorageEntry); + } + + @Test + public void onPrepareOptionsMenu_unSupportedDiskInfo_formatIsVisible() { + final StorageEntry unsupportedStorageEntry = + new StorageEntry(new DiskInfo(DISK_ID, 0 /* flags */)); + mController.setSelectedStorageEntry(unsupportedStorageEntry); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mFormat, atLeastOnce()).setVisible(true); + verify(mController.mRename, never()).setVisible(true); + verify(mController.mMount, never()).setVisible(true); + verify(mController.mUnmount, never()).setVisible(true); + verify(mController.mFormatAsPortable, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mMigrate, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + verify(mController.mForget, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_missingVolumeRecord_forgetIsVisible() { + final StorageEntry missingStorageEntry = + new StorageEntry(new VolumeRecord(0 /* type */, VOLUME_RECORD_FSUUID)); + mController.setSelectedStorageEntry(missingStorageEntry); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mForget, atLeastOnce()).setVisible(true); + verify(mController.mRename, never()).setVisible(true); + verify(mController.mMount, never()).setVisible(true); + verify(mController.mUnmount, never()).setVisible(true); + verify(mController.mFormat, never()).setVisible(true); + verify(mController.mFormatAsPortable, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mMigrate, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_unmountedStorage_mountIsVisible() { + when(mInternalVolumeInfo.getState()).thenReturn(VolumeInfo.STATE_UNMOUNTED); + mController.setSelectedStorageEntry(new StorageEntry(mContext, mInternalVolumeInfo)); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mMount, atLeastOnce()).setVisible(true); + verify(mController.mRename, never()).setVisible(true); + verify(mController.mUnmount, never()).setVisible(true); + verify(mController.mFormat, never()).setVisible(true); + verify(mController.mFormatAsPortable, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mMigrate, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + verify(mController.mForget, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_privateNotDefaultInternal_someMenusAreVisible() { + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mRename, atLeastOnce()).setVisible(true); + verify(mController.mUnmount, atLeastOnce()).setVisible(true); + verify(mController.mFormatAsPortable, atLeastOnce()).setVisible(true); + verify(mController.mMount, never()).setVisible(true); + verify(mController.mFormat, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + verify(mController.mForget, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_privateDefaultInternal_mostMenusAreNotVisible() { + when(mInternalVolumeInfo.getId()).thenReturn(VolumeInfo.ID_PRIVATE_INTERNAL); + when(mPackageManager.getPrimaryStorageCurrentVolume()).thenReturn(mInternalVolumeInfo); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mRename, never()).setVisible(true); + verify(mController.mUnmount, never()).setVisible(true); + verify(mController.mFormatAsPortable, never()).setVisible(true); + verify(mController.mMount, never()).setVisible(true); + verify(mController.mFormat, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + verify(mController.mForget, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_publicStorage_someMenusArcVisible() { + when(mExternalVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PUBLIC); + when(mExternalVolumeInfo.getState()).thenReturn(VolumeInfo.STATE_MOUNTED); + when(mExternalVolumeInfo.getDiskId()).thenReturn(DISK_ID); + final DiskInfo externalDiskInfo = mock(DiskInfo.class); + when(mStorageManager.findDiskById(DISK_ID)).thenReturn(externalDiskInfo); + mController.setSelectedStorageEntry(new StorageEntry(mContext, mExternalVolumeInfo)); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mRename, atLeastOnce()).setVisible(true); + verify(mController.mUnmount, atLeastOnce()).setVisible(true); + verify(mController.mFormat, atLeastOnce()).setVisible(true); + verify(mController.mMount, never()).setVisible(true); + verify(mController.mFormatAsPortable, never()).setVisible(true); + verify(mController.mFormatAsInternal, never()).setVisible(true); + verify(mController.mFree, never()).setVisible(true); + verify(mController.mForget, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_noExternalStorage_migrateNotVisible() { + when(mPackageManager.getPrimaryStorageCurrentVolume()).thenReturn(mInternalVolumeInfo); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mMigrate, atLeastOnce()).setVisible(false); + verify(mController.mMigrate, never()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_externalPrimaryStorageAvailable_migrateIsVisible() { + when(mExternalVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE); + when(mExternalVolumeInfo.isMountedWritable()).thenReturn(true); + when(mPackageManager.getPrimaryStorageCurrentVolume()).thenReturn(mExternalVolumeInfo); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mMigrate, atLeastOnce()).setVisible(true); + } + + @Test + public void onPrepareOptionsMenu_externalUnmounted_migrateIsVisible() { + when(mExternalVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE); + when(mExternalVolumeInfo.isMountedWritable()).thenReturn(false); + when(mPackageManager.getPrimaryStorageCurrentVolume()).thenReturn(mExternalVolumeInfo); + + mController.onPrepareOptionsMenu(mMenu); + + verify(mController.mMigrate, atLeastOnce()).setVisible(false); + verify(mController.mMigrate, never()).setVisible(true); + } +} diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java index 603d51e236f..cf1b6b2c8ec 100644 --- a/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java +++ b/tests/unit/src/com/android/settings/deviceinfo/storage/StorageEntryTest.java @@ -131,28 +131,28 @@ public class StorageEntryTest { final StorageEntry storage = new StorageEntry(mContext, volumeInfo); assertThat(storage.isVolumeInfo()).isTrue(); - assertThat(storage.isUnsupportedDiskInfo()).isFalse(); - assertThat(storage.isMissingVolumeRecord()).isFalse(); + assertThat(storage.isDiskInfoUnsupported()).isFalse(); + assertThat(storage.isVolumeRecordMissed()).isFalse(); } @Test - public void isUnsupportedDiskInfo_shouldReturnTrueForDiskInfo() { + public void isDiskInfoUnsupported_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(); + assertThat(storage.isDiskInfoUnsupported()).isTrue(); + assertThat(storage.isVolumeRecordMissed()).isFalse(); } @Test - public void isMissingVolumeRecord_shouldReturnTrueForVolumeRecord() { + public void isVolumeRecordMissed_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(); + assertThat(storage.isDiskInfoUnsupported()).isFalse(); + assertThat(storage.isVolumeRecordMissed()).isTrue(); } @Test