blob: 6dbc3aa9a7e5492acbc8c3c12e8aac950dee940d [file] [log] [blame]
/*
* 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<SparseArray<StorageAsyncLoader.StorageResult>>,
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<StorageEntry> mStorageEntries = new ArrayList<>();
private StorageEntry mSelectedStorageEntry;
private PrivateStorageInfo mStorageInfo;
private SparseArray<StorageAsyncLoader.StorageResult> mAppsResult;
private StorageItemPreferenceController mPreferenceController;
private VolumeOptionMenuController mOptionMenuController;
private StorageSelectionPreferenceController mStorageSelectionController;
private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController;
private List<AbstractPreferenceController> 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<AbstractPreferenceController> createPreferenceControllers(Context context) {
final List<AbstractPreferenceController> 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<AbstractPreferenceController> controllers,
SparseArray<StorageAsyncLoader.StorageResult> 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<SearchIndexableResource> 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<AbstractPreferenceController> createPreferenceControllers(
Context context) {
final StorageManager sm = context.getSystemService(StorageManager.class);
final UserManager userManager = context.getSystemService(UserManager.class);
final List<AbstractPreferenceController> 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<SparseArray<StorageAsyncLoader.StorageResult>> 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<SparseArray<StorageAsyncLoader.StorageResult>> loader,
SparseArray<StorageAsyncLoader.StorageResult> data) {
mAppsResult = data;
onReceivedSizes();
}
@Override
public void onLoaderReset(Loader<SparseArray<StorageAsyncLoader.StorageResult>> 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 PrivateStorageInfo getPrivateStorageInfo() {
return mStorageInfo;
}
@VisibleForTesting
public void setPrivateStorageInfo(PrivateStorageInfo info) {
mStorageInfo = info;
}
@VisibleForTesting
public SparseArray<StorageAsyncLoader.StorageResult> getStorageResult() {
return mAppsResult;
}
@VisibleForTesting
public void setStorageResult(SparseArray<StorageAsyncLoader.StorageResult> 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> 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<SparseArray<Drawable>> {
@Override
public Loader<SparseArray<Drawable>> onCreateLoader(int id, Bundle args) {
return new UserIconLoader(
getContext(),
() -> UserIconLoader.loadUserIconsWithContext(getContext()));
}
@Override
public void onLoadFinished(
Loader<SparseArray<Drawable>> loader, SparseArray<Drawable> data) {
mSecondaryUsers
.stream()
.filter(controller -> controller instanceof UserIconLoader.UserIconHandler)
.forEach(
controller ->
((UserIconLoader.UserIconHandler) controller)
.handleUserIcons(data));
}
@Override
public void onLoaderReset(Loader<SparseArray<Drawable>> loader) {
}
}
/**
* VolumeSizeCallbacks exists because StorageCategoryFragment already implements
* LoaderCallbacks for a different type.
*/
public final class VolumeSizeCallbacks
implements LoaderManager.LoaderCallbacks<PrivateStorageInfo> {
@Override
public Loader<PrivateStorageInfo> 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<PrivateStorageInfo> loader) {
}
@Override
public void onLoadFinished(
Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo) {
if (privateStorageInfo == null) {
getActivity().finish();
return;
}
mStorageInfo = privateStorageInfo;
onReceivedSizes();
}
}
}