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/res/values/strings.xml b/res/values/strings.xml index 993d33b7c4d..4257be04b1c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3368,6 +3368,10 @@ Manage storage clean, storage + + Free up space + + Go to Files app to manage and free up space USB computer connection @@ -11584,6 +11588,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 deleted file mode 100644 index 00a79a03f18..00000000000 --- a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java +++ /dev/null @@ -1,85 +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; - } -} diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 10c3a43fe8e..7af4f0cfd02 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -20,13 +20,18 @@ import android.app.Activity; import android.app.settings.SettingsEnums; import android.app.usage.StorageStatsManager; import android.content.Context; +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; @@ -41,15 +46,22 @@ 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; 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 +69,227 @@ 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 VolumeOptionMenuController 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 changedStorageEntry = new StorageEntry(getContext(), volumeInfo); + switch (volumeInfo.getState()) { + case VolumeInfo.STATE_MOUNTED: + case VolumeInfo.STATE_MOUNTED_READ_ONLY: + case VolumeInfo.STATE_UNMOUNTABLE: + // 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: + // Remove removed storage from list and don't show it on spinner. + if (mStorageEntries.remove(changedStorageEntry)) { + if (changedStorageEntry.equals(mSelectedStorageEntry)) { + mSelectedStorageEntry = + StorageEntry.getDefaultInternalStorageEntry(getContext()); + } + refreshUi(); + } + break; + default: + // Do nothing. + } + } + + @Override + public void onVolumeRecordChanged(VolumeRecord volumeRecord) { + 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(); + } + } + } + + @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 (!isDiskUnsupported(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; + } + } + + /** + * 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; + } + + private void refreshUi() { + mStorageSelectionController.setStorageEntries(mStorageEntries); + mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); + mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); + + mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); + getActivity().invalidateOptionsMenu(); + + 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 +297,25 @@ 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.isDiskInfoUnsupported() || storageEntry.isUnmountable()) { + DiskInitFragment.show(this, R.string.storage_dialog_unmountable, + storageEntry.getDiskId()); + } else if (storageEntry.isVolumeRecordMissed()) { + StorageUtils.launchForgetMissingVolumeRecordFragment(getContext(), storageEntry); + } + }); + mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class); } @VisibleForTesting void initializeOptionsMenu(Activity activity) { - mOptionMenuController = new PrivateVolumeOptionMenuController( - activity, mVolume, activity.getPackageManager()); + mOptionMenuController = new VolumeOptionMenuController(activity, this, + mSelectedStorageEntry); getSettingsLifecycle().addObserver(mOptionMenuController); setHasOptionsMenu(true); activity.invalidateOptionsMenu(); @@ -133,10 +337,34 @@ 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())); + mStorageEntries.addAll(mStorageManager.getDisks().stream() + .filter(disk -> isDiskUnsupported(disk)) + .map(disk -> new StorageEntry(disk)) + .collect(Collectors.toList())); + mStorageEntries.addAll(mStorageManager.getVolumeRecords().stream() + .filter(volumeRecord -> isVolumeRecordMissed(volumeRecord)) + .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 +376,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 +425,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 +437,7 @@ public class StorageDashboardFragment extends DashboardFragment @VisibleForTesting protected void setVolume(VolumeInfo info) { - mVolume = info; + mSelectedStorageEntry = new StorageEntry(getContext(), info); } /** @@ -260,7 +488,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 +505,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 +583,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 +622,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 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 new file mode 100644 index 00000000000..f71811602d5 --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/StorageEntry.java @@ -0,0 +1,293 @@ +/* + * 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 (isDiskInfoUnsupported()) { + return mUnsupportedDiskInfo.equals(StorageEntry.mUnsupportedDiskInfo); + } + return mMissingVolumeRecord.equals(StorageEntry.mMissingVolumeRecord); + } + + @Override + public int hashCode() { + if (isVolumeInfo()) { + return mVolumeInfo.hashCode(); + } + if (isDiskInfoUnsupported()) { + return mUnsupportedDiskInfo.hashCode(); + } + return mMissingVolumeRecord.hashCode(); + } + + @Override + public String toString() { + if (isVolumeInfo()) { + return mVolumeInfo.toString(); + } + if (isDiskInfoUnsupported()) { + 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 (!isVolumeRecordMissed() && other.isVolumeRecordMissed()) { + return -1; + } + if (isVolumeRecordMissed() && !other.isVolumeRecordMissed()) { + 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 isDiskInfoUnsupported() { + return mUnsupportedDiskInfo != null; + } + + /** If it's a missing VolumeRecord. */ + public boolean isVolumeRecordMissed() { + 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 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; + } + + /** If it's a private storage. */ + public boolean isPrivate() { + 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 (isDiskInfoUnsupported()) { + return mUnsupportedDiskInfo.getDescription(); + } + return mMissingVolumeRecord.getNickname(); + } + + /** Returns ID. */ + public String getId() { + if (isVolumeInfo()) { + return mVolumeInfo.getId(); + } + if (isDiskInfoUnsupported()) { + return mUnsupportedDiskInfo.getId(); + } + return mMissingVolumeRecord.getFsUuid(); + } + + /** Returns disk ID. */ + public String getDiskId() { + if (isVolumeInfo()) { + return mVolumeInfo.getDiskId(); + } + if (isDiskInfoUnsupported()) { + return mUnsupportedDiskInfo.getId(); + } + return null; + } + + /** Returns fsUuid. */ + public String getFsUuid() { + if (isVolumeInfo()) { + return mVolumeInfo.getFsUuid(); + } + if (isDiskInfoUnsupported()) { + 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/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/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/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/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/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 new file mode 100644 index 00000000000..cf1b6b2c8ec --- /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.isDiskInfoUnsupported()).isFalse(); + assertThat(storage.isVolumeRecordMissed()).isFalse(); + } + + @Test + public void isDiskInfoUnsupported_shouldReturnTrueForDiskInfo() { + final DiskInfo diskInfo = mock(DiskInfo.class); + final StorageEntry storage = new StorageEntry(diskInfo); + + assertThat(storage.isVolumeInfo()).isFalse(); + assertThat(storage.isDiskInfoUnsupported()).isTrue(); + assertThat(storage.isVolumeRecordMissed()).isFalse(); + } + + @Test + public void isVolumeRecordMissed_shouldReturnTrueForVolumeRecord() { + final VolumeRecord volumeRecord = mock(VolumeRecord.class); + final StorageEntry storage = new StorageEntry(volumeRecord); + + assertThat(storage.isVolumeInfo()).isFalse(); + assertThat(storage.isDiskInfoUnsupported()).isFalse(); + assertThat(storage.isVolumeRecordMissed()).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); + } }