/* * Copyright (C) 2016 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.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; 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.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController; 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; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; /** * 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 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 StorageManager mStorageManager; private UserManager mUserManager; private final List mStorageEntries = new ArrayList<>(); private StorageEntry mSelectedStorageEntry; private PrivateStorageInfo mStorageInfo; private SparseArray mAppsResult; private StorageItemPreferenceController mPreferenceController; private VolumeOptionMenuController mOptionMenuController; private StorageSelectionPreferenceController mStorageSelectionController; private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController; private List mSecondaryUsers; private boolean mIsWorkProfile; private int mUserId; private Preference mFreeUpSpacePreference; 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(); } } }; private void refreshUi() { mStorageSelectionController.setStorageEntries(mStorageEntries); mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry); mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry); mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry); getActivity().invalidateOptionsMenu(); // To prevent flicker, hides secondary users preference. // onReceivedSizes will set it visible for private storage. setSecondaryUsersVisible(false); if (!mSelectedStorageEntry.isMounted()) { // Set null volume to hide category stats. mPreferenceController.setVolume(null); return; } if (mSelectedStorageEntry.isPrivate()) { mStorageInfo = null; mAppsResult = null; maybeSetLoading(isQuotaSupported()); // To prevent flicker, sets null volume to hide category preferences. // onReceivedSizes will setVolume with the volume of selected storage. mPreferenceController.setVolume(null); // 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); 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); } initializePreference(); initializeOptionsMenu(activity); } 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 = false; mUserId = UserHandle.myUserId(); 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 onViewCreated(View v, Bundle savedInstanceState) { super.onViewCreated(v, savedInstanceState); EntityHeaderController.newInstance(getActivity(), this /*fragment*/, null /* header view */) .setRecyclerView(getListView(), getSettingsLifecycle()); } @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; } private void onReceivedSizes() { if (mStorageInfo == null || mAppsResult == null) { return; } if (getView().findViewById(R.id.loading_container).getVisibility() == View.VISIBLE) { setLoading(false /* loading */, true /* animate */); } 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); } } mPreferenceController.onLoadFinished(mAppsResult, mUserId); updateSecondaryUserControllers(mSecondaryUsers, mAppsResult); setSecondaryUsersVisible(true); } @Override public int getMetricsCategory() { return SettingsEnums.SETTINGS_STORAGE_CATEGORY; } @Override protected String getLogTag() { return TAG; } @Override protected int getPreferenceScreenResId() { return R.xml.storage_dashboard_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); } } } /** * For Search. */ public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override public List getXmlResourcesToIndex( Context context, boolean enabled) { final SearchIndexableResource sir = new SearchIndexableResource(context); sir.xmlResId = R.xml.storage_dashboard_fragment; return Arrays.asList(sir); } @Override public List createPreferenceControllers( Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); final UserManager userManager = context.getSystemService(UserManager.class); final List controllers = new ArrayList<>(); controllers.add(new StorageItemPreferenceController(context, null /* host */, null /* volume */, new StorageManagerVolumeProvider(sm), false /* isWorkProfile */)); controllers.addAll(SecondaryUserController.getSecondaryUserControllers( context, userManager, false /* isWorkProfileOnly */)); return controllers; } }; @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; 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); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivityAsUser(intent, new UserHandle(mUserId)); return true; } return false; } @VisibleForTesting public PrivateStorageInfo getPrivateStorageInfo() { return mStorageInfo; } @VisibleForTesting public void setPrivateStorageInfo(PrivateStorageInfo info) { mStorageInfo = info; } @VisibleForTesting public SparseArray getStorageResult() { return mAppsResult; } @VisibleForTesting public void setStorageResult(SparseArray info) { mAppsResult = info; } /** * 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 boolean isQuotaSupported() { return mSelectedStorageEntry.isMounted() && getActivity().getSystemService(StorageStatsManager.class) .isQuotaSupported(mSelectedStorageEntry.getFsUuid()); } private void setSecondaryUsersVisible(boolean visible) { final Optional secondaryUserController = mSecondaryUsers.stream() .filter(controller -> controller instanceof SecondaryUserController) .map(controller -> (SecondaryUserController) controller) .findAny(); if (secondaryUserController.isPresent()) { secondaryUserController.get().setPreferenceGroupVisible(visible); } } /** * IconLoaderCallbacks exists because StorageDashboardFragment 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; onReceivedSizes(); } } }