diff --git a/res/xml/storage_category_fragment.xml b/res/xml/storage_category_fragment.xml new file mode 100644 index 00000000000..8ac9e940f86 --- /dev/null +++ b/res/xml/storage_category_fragment.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/storage_dashboard_fragment.xml b/res/xml/storage_dashboard_fragment.xml index 71d07127aa3..49db958cde3 100644 --- a/res/xml/storage_dashboard_fragment.xml +++ b/res/xml/storage_dashboard_fragment.xml @@ -18,7 +18,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/storage_settings" - android:orderingFromXml="false" settings:keywords="@string/keywords_storage"> + android:icon="@drawable/ic_files_go_round" + settings:allowDividerAbove="true"/> + + + + + + diff --git a/res/xml/storage_profile_fragment.xml b/res/xml/storage_profile_fragment.xml deleted file mode 100644 index a12bdd53c41..00000000000 --- a/res/xml/storage_profile_fragment.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java index 9e505e3607f..c4ff91b767b 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java @@ -16,32 +16,301 @@ package com.android.settings.dashboard.profileselector; +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; import android.os.Bundle; +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.text.TextUtils; +import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; -import com.android.settings.deviceinfo.StorageDashboardFragment; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.deviceinfo.StorageCategoryFragment; +import com.android.settings.deviceinfo.VolumeOptionMenuController; +import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController; +import com.android.settings.deviceinfo.storage.DiskInitFragment; +import com.android.settings.deviceinfo.storage.StorageEntry; +import com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController; +import com.android.settings.deviceinfo.storage.StorageUsageProgressBarPreferenceController; +import com.android.settings.deviceinfo.storage.StorageUtils; + +import java.util.ArrayList; +import java.util.List; /** - * Storage Settings page for personal/managed profile. + * Storage Settings main UI is composed by 3 fragments: + * + * StorageDashboardFragment only shows when there is only personal profile for current user. + * + * ProfileSelectStorageFragment (controls preferences above profile tab) and + * StorageCategoryFragment (controls preferences below profile tab) only show when current + * user has installed work profile. + * + * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same + * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to + * change Storage Settings. */ public class ProfileSelectStorageFragment extends ProfileSelectFragment { + private static final String TAG = "ProfileSelStorageFrag"; + private static final String SELECTED_STORAGE_ENTRY_KEY = "selected_storage_entry_key"; + + private StorageManager mStorageManager; + + private final List mStorageEntries = new ArrayList<>(); + private StorageEntry mSelectedStorageEntry; + private Fragment[] mFragments; + + private StorageSelectionPreferenceController mStorageSelectionController; + private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController; + private VolumeOptionMenuController mOptionMenuController; + + private final StorageEventListener mStorageEventListener = new StorageEventListener() { + @Override + public void onVolumeStateChanged(VolumeInfo volumeInfo, int oldState, int newState) { + if (!StorageUtils.isStorageSettingsInterestedVolume(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 (StorageUtils.isVolumeRecordMissed(mStorageManager, 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 (!StorageUtils.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(); + } + } + }; + @Override public Fragment[] getFragments() { + if (mFragments != null) { + return mFragments; + } + final Bundle workBundle = new Bundle(); workBundle.putInt(EXTRA_PROFILE, ProfileType.WORK); - final Fragment workFragment = new StorageDashboardFragment(); + final Fragment workFragment = new StorageCategoryFragment(); workFragment.setArguments(workBundle); final Bundle personalBundle = new Bundle(); personalBundle.putInt(EXTRA_PROFILE, ProfileType.PERSONAL); - final Fragment personalFragment = new StorageDashboardFragment(); + final Fragment personalFragment = new StorageCategoryFragment(); personalFragment.setArguments(personalBundle); - return new Fragment[] { + mFragments = new Fragment[] { personalFragment, workFragment }; + return mFragments; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.storage_dashboard_header_fragment; + } + + private void refreshUi() { + mStorageSelectionController.setStorageEntries(mStorageEntries); + mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); + mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); + + for (Fragment fragment : getFragments()) { + if (!(fragment instanceof StorageCategoryFragment)) { + throw new IllegalStateException("Wrong fragment type to refreshUi"); + } + ((StorageCategoryFragment) fragment).refreshUi(mSelectedStorageEntry); + } + + mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); + getActivity().invalidateOptionsMenu(); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + final Activity activity = getActivity(); + mStorageManager = activity.getSystemService(StorageManager.class); + + 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); + } + + initializeOptionsMenu(activity); + } + + @Override + public void onAttach(Context context) { + 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 VolumeOptionMenuController(activity, this, + mSelectedStorageEntry); + getSettingsLifecycle().addObserver(mOptionMenuController); + setHasOptionsMenu(true); + activity.invalidateOptionsMenu(); + } + + @Override + public void onResume() { + super.onResume(); + + mStorageEntries.clear(); + mStorageEntries.addAll(StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); + 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 + public int getHelpResource() { + return R.string.help_url_storage_dashboard; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_STORAGE_CATEGORY; + } + + @Override + protected String getLogTag() { + return TAG; } } diff --git a/src/com/android/settings/deviceinfo/StorageCategoryFragment.java b/src/com/android/settings/deviceinfo/StorageCategoryFragment.java new file mode 100644 index 00000000000..ba59498a795 --- /dev/null +++ b/src/com/android/settings/deviceinfo/StorageCategoryFragment.java @@ -0,0 +1,415 @@ +/* + * 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.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.StorageManager; +import android.util.SparseArray; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.dashboard.profileselector.ProfileSelectFragment; +import com.android.settings.deviceinfo.storage.CachedStorageValuesHelper; +import com.android.settings.deviceinfo.storage.SecondaryUserController; +import com.android.settings.deviceinfo.storage.StorageAsyncLoader; +import com.android.settings.deviceinfo.storage.StorageEntry; +import com.android.settings.deviceinfo.storage.StorageItemPreferenceController; +import com.android.settings.deviceinfo.storage.UserIconLoader; +import com.android.settings.deviceinfo.storage.VolumeSizesLoader; +import com.android.settings.overlay.FeatureFactory; +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 java.util.ArrayList; +import java.util.List; + +/** + * Storage Settings main UI is composed by 3 fragments: + * + * StorageDashboardFragment only shows when there is only personal profile for current user. + * + * ProfileSelectStorageFragment (controls preferences above profile tab) and + * StorageCategoryFragment (controls preferences below profile tab) only show when current + * user has installed work profile. + * + * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same + * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to + * change Storage Settings. + */ +public class StorageCategoryFragment extends DashboardFragment + implements + LoaderManager.LoaderCallbacks>, + Preference.OnPreferenceClickListener { + private static final String TAG = "StorageCategoryFrag"; + private static final String SUMMARY_PREF_KEY = "storage_summary"; + private static final String FREE_UP_SPACE_PREF_KEY = "free_up_space"; + 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 StorageManager mStorageManager; + private UserManager mUserManager; + private StorageEntry mSelectedStorageEntry; + private PrivateStorageInfo mStorageInfo; + private SparseArray mAppsResult; + private CachedStorageValuesHelper mCachedStorageValuesHelper; + + private StorageItemPreferenceController mPreferenceController; + private List mSecondaryUsers; + private boolean mIsWorkProfile; + private int mUserId; + private Preference mFreeUpSpacePreference; + + /** + * Refresh UI for specified storageEntry. + */ + public void refreshUi(StorageEntry storageEntry) { + mSelectedStorageEntry = storageEntry; + if (mPreferenceController == null) { + // Check null here because when onResume, StorageCategoryFragment may not + // onAttach to createPreferenceControllers and mPreferenceController will be null. + return; + } + + mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo()); + + if (!mSelectedStorageEntry.isMounted()) { + // Set null volume to hide category stats. + mPreferenceController.setVolume(null); + return; + } + if (mSelectedStorageEntry.isPrivate()) { + // Stats data is only available on private volumes. + 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.setVolume(mSelectedStorageEntry.getVolumeInfo()); + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mStorageManager = getActivity().getSystemService(StorageManager.class); + + initializePreference(); + } + + private void initializePreference() { + mFreeUpSpacePreference = getPreferenceScreen().findPreference(FREE_UP_SPACE_PREF_KEY); + mFreeUpSpacePreference.setOnPreferenceClickListener(this); + } + + @Override + public void onAttach(Context context) { + // These member variables are initialized befoer super.onAttach for + // createPreferenceControllers to work correctly. + mUserManager = context.getSystemService(UserManager.class); + mIsWorkProfile = getArguments().getInt(ProfileSelectFragment.EXTRA_PROFILE) + == ProfileSelectFragment.ProfileType.WORK; + mUserId = Utils.getCurrentUserId(mUserManager, mIsWorkProfile); + + super.onAttach(context); + } + + @Override + public void onViewCreated(View v, Bundle savedInstanceState) { + super.onViewCreated(v, savedInstanceState); + initializeCacheProvider(); + maybeSetLoading(isQuotaSupported()); + + EntityHeaderController.newInstance(getActivity(), this /*fragment*/, + null /* header view */) + .setRecyclerView(getListView(), getSettingsLifecycle()); + } + + @Override + public void onResume() { + super.onResume(); + + if (mSelectedStorageEntry != null) { + refreshUi(mSelectedStorageEntry); + } + } + + private void onReceivedSizes() { + boolean stopLoading = false; + if (mStorageInfo != null) { + final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes; + mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo()); + mPreferenceController.setUsedSize(privateUsedBytes); + mPreferenceController.setTotalSize(mStorageInfo.totalBytes); + for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) { + final AbstractPreferenceController controller = mSecondaryUsers.get(i); + if (controller instanceof SecondaryUserController) { + SecondaryUserController userController = (SecondaryUserController) controller; + userController.setTotalSize(mStorageInfo.totalBytes); + } + } + stopLoading = true; + } + + if (mAppsResult != null) { + mPreferenceController.onLoadFinished(mAppsResult, mUserId); + updateSecondaryUserControllers(mSecondaryUsers, mAppsResult); + stopLoading = true; + } + + // setLoading always causes a flicker, so let's avoid doing it. + if (stopLoading) { + if (getView().findViewById(R.id.loading_container).getVisibility() == View.VISIBLE) { + setLoading(false, true); + } + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_STORAGE_CATEGORY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.storage_category_fragment; + } + + @Override + protected List createPreferenceControllers(Context context) { + final List controllers = new ArrayList<>(); + final StorageManager sm = context.getSystemService(StorageManager.class); + mPreferenceController = new StorageItemPreferenceController(context, this, + null /* volume */, new StorageManagerVolumeProvider(sm), mIsWorkProfile); + controllers.add(mPreferenceController); + + mSecondaryUsers = SecondaryUserController.getSecondaryUserControllers(context, + mUserManager, mIsWorkProfile /* isWorkProfileOnly */); + controllers.addAll(mSecondaryUsers); + + return controllers; + } + + /** + * Updates the secondary user controller sizes. + */ + private void updateSecondaryUserControllers(List controllers, + SparseArray stats) { + for (int i = 0, size = controllers.size(); i < size; i++) { + final AbstractPreferenceController controller = controllers.get(i); + if (controller instanceof StorageAsyncLoader.ResultHandler) { + StorageAsyncLoader.ResultHandler userController = + (StorageAsyncLoader.ResultHandler) controller; + userController.handleResult(stats); + } + } + } + + @Override + public Loader> onCreateLoader(int id, + Bundle args) { + final Context context = getContext(); + return new StorageAsyncLoader(context, mUserManager, + mSelectedStorageEntry.getFsUuid(), + new StorageStatsSource(context), + context.getPackageManager()); + } + + @Override + public void onLoadFinished(Loader> loader, + SparseArray data) { + mAppsResult = data; + maybeCacheFreshValues(); + onReceivedSizes(); + } + + @Override + 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.startActivityAsUser(intent, new UserHandle(mUserId)); + return true; + } + return false; + } + + @VisibleForTesting + public void setCachedStorageValuesHelper(CachedStorageValuesHelper helper) { + mCachedStorageValuesHelper = helper; + } + + @VisibleForTesting + public PrivateStorageInfo getPrivateStorageInfo() { + return mStorageInfo; + } + + @VisibleForTesting + public void setPrivateStorageInfo(PrivateStorageInfo info) { + mStorageInfo = info; + } + + @VisibleForTesting + public SparseArray getAppsStorageResult() { + return mAppsResult; + } + + @VisibleForTesting + public void setAppsStorageResult(SparseArray info) { + mAppsResult = info; + } + + @VisibleForTesting + void initializeCachedValues() { + final PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo(); + final SparseArray loaderResult = + mCachedStorageValuesHelper.getCachedAppsStorageResult(); + if (info == null || loaderResult == null) { + return; + } + + mStorageInfo = info; + mAppsResult = loaderResult; + } + + /** + * Activate loading UI and animation if it's necessary. + */ + @VisibleForTesting + public void maybeSetLoading(boolean isQuotaSupported) { + // If we have fast stats, we load until both have loaded. + // If we have slow stats, we load when we get the total volume sizes. + if ((isQuotaSupported && (mStorageInfo == null || mAppsResult == null)) + || (!isQuotaSupported && mStorageInfo == null)) { + setLoading(true /* loading */, false /* animate */); + } + } + + private void initializeCacheProvider() { + mCachedStorageValuesHelper = new CachedStorageValuesHelper(getContext(), mUserId); + initializeCachedValues(); + onReceivedSizes(); + } + + private void maybeCacheFreshValues() { + if (mStorageInfo != null && mAppsResult != null) { + mCachedStorageValuesHelper.cacheResult(mStorageInfo, mAppsResult.get(mUserId)); + } + } + + private boolean isQuotaSupported() { + return mSelectedStorageEntry.isMounted() + && getActivity().getSystemService(StorageStatsManager.class) + .isQuotaSupported(mSelectedStorageEntry.getFsUuid()); + } + + /** + * IconLoaderCallbacks exists because StorageCategoryFragment already implements + * LoaderCallbacks for a different type. + */ + public final class IconLoaderCallbacks + implements LoaderManager.LoaderCallbacks> { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new UserIconLoader( + getContext(), + () -> UserIconLoader.loadUserIconsWithContext(getContext())); + } + + @Override + public void onLoadFinished( + Loader> loader, SparseArray data) { + mSecondaryUsers + .stream() + .filter(controller -> controller instanceof UserIconLoader.UserIconHandler) + .forEach( + controller -> + ((UserIconLoader.UserIconHandler) controller) + .handleUserIcons(data)); + } + + @Override + public void onLoaderReset(Loader> loader) { + } + } + + /** + * VolumeSizeCallbacks exists because StorageCategoryFragment already implements + * LoaderCallbacks for a different type. + */ + public final class VolumeSizeCallbacks + implements LoaderManager.LoaderCallbacks { + @Override + public Loader onCreateLoader(int id, Bundle args) { + final Context context = getContext(); + final StorageManagerVolumeProvider smvp = + new StorageManagerVolumeProvider(mStorageManager); + final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class); + return new VolumeSizesLoader(context, smvp, stats, + mSelectedStorageEntry.getVolumeInfo()); + } + + @Override + public void onLoaderReset(Loader loader) { + } + + @Override + public void onLoadFinished( + Loader loader, PrivateStorageInfo privateStorageInfo) { + if (privateStorageInfo == null) { + getActivity().finish(); + return; + } + + mStorageInfo = privateStorageInfo; + maybeCacheFreshValues(); + onReceivedSizes(); + } + } +} diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 6a3bb515e81..cc7eff6a27b 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -43,7 +43,6 @@ import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.Utils; 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; @@ -69,8 +68,20 @@ import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +/** + * Storage Settings main UI is composed by 3 fragments: + * + * StorageDashboardFragment only shows when there is only personal profile for current user. + * + * ProfileSelectStorageFragment (controls preferences above profile tab) and + * StorageCategoryFragment (controls preferences below profile tab) only show when current + * user has installed work profile. + * + * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same + * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to + * change Storage Settings. + */ @SearchIndexable public class StorageDashboardFragment extends DashboardFragment implements @@ -104,7 +115,7 @@ public class StorageDashboardFragment extends DashboardFragment private final StorageEventListener mStorageEventListener = new StorageEventListener() { @Override public void onVolumeStateChanged(VolumeInfo volumeInfo, int oldState, int newState) { - if (!isInteresting(volumeInfo)) { + if (!StorageUtils.isStorageSettingsInterestedVolume(volumeInfo)) { return; } @@ -146,7 +157,7 @@ public class StorageDashboardFragment extends DashboardFragment @Override public void onVolumeRecordChanged(VolumeRecord volumeRecord) { - if (isVolumeRecordMissed(volumeRecord)) { + if (StorageUtils.isVolumeRecordMissed(mStorageManager, 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. @@ -159,7 +170,7 @@ public class StorageDashboardFragment extends DashboardFragment // Find mapped VolumeInfo and replace with existing one for something changed. // (e.g., Renamed.) final VolumeInfo mappedVolumeInfo = - mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()); + mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid()); if (mappedVolumeInfo == null) { return; } @@ -190,7 +201,7 @@ public class StorageDashboardFragment extends DashboardFragment @Override public void onDiskScanned(DiskInfo disk, int volumeCount) { - if (!isDiskUnsupported(disk)) { + if (!StorageUtils.isDiskUnsupported(disk)) { return; } final StorageEntry storageEntry = new StorageEntry(disk); @@ -213,33 +224,6 @@ public class StorageDashboardFragment extends DashboardFragment } }; - 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); @@ -297,9 +281,8 @@ public class StorageDashboardFragment extends DashboardFragment // These member variables are initialized befoer super.onAttach for // createPreferenceControllers to work correctly. mUserManager = context.getSystemService(UserManager.class); - mIsWorkProfile = getArguments().getInt(ProfileSelectFragment.EXTRA_PROFILE) - == ProfileSelectFragment.ProfileType.WORK; - mUserId = Utils.getCurrentUserId(mUserManager, mIsWorkProfile); + mIsWorkProfile = false; + mUserId = UserHandle.myUserId(); super.onAttach(context); use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager( @@ -334,8 +317,7 @@ public class StorageDashboardFragment extends DashboardFragment initializeCacheProvider(); maybeSetLoading(isQuotaSupported()); - final Activity activity = getActivity(); - EntityHeaderController.newInstance(activity, this /*fragment*/, + EntityHeaderController.newInstance(getActivity(), this /*fragment*/, null /* header view */) .setRecyclerView(getListView(), getSettingsLifecycle()); } @@ -345,18 +327,7 @@ public class StorageDashboardFragment extends DashboardFragment super.onResume(); 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())); + mStorageEntries.addAll(StorageUtils.getAllStorageEntries(getContext(), mStorageManager)); refreshUi(); mStorageManager.registerListener(mStorageEventListener); } @@ -381,19 +352,18 @@ public class StorageDashboardFragment extends DashboardFragment private void onReceivedSizes() { boolean stopLoading = false; if (mStorageInfo != null) { - long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes; + final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes; mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo()); mPreferenceController.setUsedSize(privateUsedBytes); mPreferenceController.setTotalSize(mStorageInfo.totalBytes); for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) { - AbstractPreferenceController controller = mSecondaryUsers.get(i); + final AbstractPreferenceController controller = mSecondaryUsers.get(i); if (controller instanceof SecondaryUserController) { SecondaryUserController userController = (SecondaryUserController) controller; userController.setTotalSize(mStorageInfo.totalBytes); } } stopLoading = true; - } if (mAppsResult != null) { @@ -428,7 +398,7 @@ public class StorageDashboardFragment extends DashboardFragment @Override protected List createPreferenceControllers(Context context) { final List controllers = new ArrayList<>(); - StorageManager sm = context.getSystemService(StorageManager.class); + final StorageManager sm = context.getSystemService(StorageManager.class); mPreferenceController = new StorageItemPreferenceController(context, this, null /* volume */, new StorageManagerVolumeProvider(sm), mIsWorkProfile); controllers.add(mPreferenceController); @@ -440,18 +410,13 @@ public class StorageDashboardFragment extends DashboardFragment return controllers; } - @VisibleForTesting - protected void setVolume(VolumeInfo info) { - mSelectedStorageEntry = new StorageEntry(getContext(), info); - } - /** * Updates the secondary user controller sizes. */ private void updateSecondaryUserControllers(List controllers, SparseArray stats) { for (int i = 0, size = controllers.size(); i < size; i++) { - AbstractPreferenceController controller = controllers.get(i); + final AbstractPreferenceController controller = controllers.get(i); if (controller instanceof StorageAsyncLoader.ResultHandler) { StorageAsyncLoader.ResultHandler userController = (StorageAsyncLoader.ResultHandler) controller; @@ -552,9 +517,9 @@ public class StorageDashboardFragment extends DashboardFragment } @VisibleForTesting - public void initializeCachedValues() { - PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo(); - SparseArray loaderResult = + void initializeCachedValues() { + final PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo(); + final SparseArray loaderResult = mCachedStorageValuesHelper.getCachedAppsStorageResult(); if (info == null || loaderResult == null) { return; @@ -564,12 +529,15 @@ public class StorageDashboardFragment extends DashboardFragment mAppsResult = loaderResult; } + /** + * Activate loading UI and animation if it's necessary. + */ @VisibleForTesting public void maybeSetLoading(boolean isQuotaSupported) { // If we have fast stats, we load until both have loaded. // If we have slow stats, we load when we get the total volume sizes. - if ((isQuotaSupported && (mStorageInfo == null || mAppsResult == null)) || - (!isQuotaSupported && mStorageInfo == null)) { + if ((isQuotaSupported && (mStorageInfo == null || mAppsResult == null)) + || (!isQuotaSupported && mStorageInfo == null)) { setLoading(true /* loading */, false /* animate */); } } @@ -622,6 +590,10 @@ public class StorageDashboardFragment extends DashboardFragment } } + /** + * VolumeSizeCallbacks exists because StorageCategoryFragment already implements + * LoaderCallbacks for a different type. + */ public final class VolumeSizeCallbacks implements LoaderManager.LoaderCallbacks { @Override diff --git a/src/com/android/settings/deviceinfo/storage/StorageUtils.java b/src/com/android/settings/deviceinfo/storage/StorageUtils.java index 919908dbda1..549eef61f7f 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageUtils.java +++ b/src/com/android/settings/deviceinfo/storage/StorageUtils.java @@ -22,6 +22,7 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.storage.DiskInfo; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.os.storage.VolumeRecord; @@ -36,11 +37,67 @@ import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.deviceinfo.PrivateVolumeForget; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + /** Storage utilities */ public class StorageUtils { private static final String TAG = "StorageUtils"; + /** + * Collects and returns all kinds of StorageEntry which will show in Storage Settings. + */ + public static List getAllStorageEntries(Context context, + StorageManager storageManager) { + final List storageEntries = new ArrayList<>(); + storageEntries.addAll(storageManager.getVolumes().stream() + .filter(volumeInfo -> isStorageSettingsInterestedVolume(volumeInfo)) + .map(volumeInfo -> new StorageEntry(context, volumeInfo)) + .collect(Collectors.toList())); + storageEntries.addAll(storageManager.getDisks().stream() + .filter(disk -> isDiskUnsupported(disk)) + .map(disk -> new StorageEntry(disk)) + .collect(Collectors.toList())); + storageEntries.addAll(storageManager.getVolumeRecords().stream() + .filter(volumeRecord -> isVolumeRecordMissed(storageManager, volumeRecord)) + .map(volumeRecord -> new StorageEntry(volumeRecord)) + .collect(Collectors.toList())); + return storageEntries; + } + + /** + * Returns true if the volumeInfo may be displayed in Storage Settings. + */ + public static boolean isStorageSettingsInterestedVolume(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.) + */ + public static boolean isVolumeRecordMissed(StorageManager storageManager, + VolumeRecord volumeRecord) { + return volumeRecord.getType() == VolumeInfo.TYPE_PRIVATE + && storageManager.findVolumeByUuid(volumeRecord.getFsUuid()) == null; + } + + /** + * A unsupported disk is the disk of problem format, android is not able to mount automatically. + */ + public static boolean isDiskUnsupported(DiskInfo disk) { + return disk.volumeCount == 0 && disk.size > 0; + } + /** Launches the fragment to forget a specified missing volume record. */ public static void launchForgetMissingVolumeRecordFragment(Context context, StorageEntry storageEntry) { diff --git a/tests/robotests/assets/exempt_not_implementing_index_provider b/tests/robotests/assets/exempt_not_implementing_index_provider index 7815a486dac..d4a1c2e268d 100644 --- a/tests/robotests/assets/exempt_not_implementing_index_provider +++ b/tests/robotests/assets/exempt_not_implementing_index_provider @@ -37,6 +37,7 @@ com.android.settings.datausage.DataUsageSummary com.android.settings.datetime.timezone.TimeZoneSettings com.android.settings.development.compat.PlatformCompatDashboard com.android.settings.deviceinfo.PublicVolumeSettings +com.android.settings.deviceinfo.StorageDashboardNoHeaderFragment com.android.settings.deviceinfo.legal.ModuleLicensesDashboard com.android.settings.enterprise.ApplicationListFragment$AdminGrantedPermissionCamera com.android.settings.enterprise.ApplicationListFragment$AdminGrantedPermissionLocation diff --git a/tests/unit/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragmentTest.java b/tests/unit/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragmentTest.java new file mode 100644 index 00000000000..6eb9aa526fd --- /dev/null +++ b/tests/unit/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragmentTest.java @@ -0,0 +1,59 @@ +/* + * 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.dashboard.profileselector; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.os.Looper; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class ProfileSelectStorageFragmentTest { + + private ProfileSelectStorageFragment mFragment; + + @Before + @UiThreadTest + public void setUp() { + MockitoAnnotations.initMocks(this); + + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + mFragment = new ProfileSelectStorageFragment(); + } + + @Test + @UiThreadTest + public void test_initializeOptionsMenuInvalidatesExistingMenu() { + final Activity activity = mock(Activity.class); + + mFragment.initializeOptionsMenu(activity); + + verify(activity).invalidateOptionsMenu(); + } +}