Storage Settings shift profile selection tab position

In this change, 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
StorageDashboardNoHeaderFragment (controls preferences below profile tab)
only show when current user has installed work profile.

Bug: 174964885
Test: atest com.android.settings.deviceinfo
      atest com.android.settings.deviceinfo.storage
      make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.deviceinfo
      make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.deviceinfo.storage
Change-Id: I50dea2fbcae39a1488e648f3ca615f54840e19d5
This commit is contained in:
Arc Wang
2021-05-06 21:20:21 +08:00
parent 15d75cf59a
commit c0b5ba5225
10 changed files with 967 additions and 125 deletions

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Layout for the storage breakdown for a profile of the primary user. -->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/storage_settings">
<Preference
android:key="free_up_space"
android:order="4"
android:title="@string/storage_free_up_space_title"
android:summary="@string/storage_free_up_space_summary"
android:icon="@drawable/ic_files_go_round"
settings:allowDividerAbove="true"/>
<!-- Preference order 100~200 are 'ONLY' for storage category preferences below. -->
<Preference
android:key="pref_public_storage"
android:title="@string/storage_files"
android:icon="@drawable/ic_folder_vd_theme_24"
android:order="100"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_images"
android:title="@string/storage_images"
android:icon="@drawable/ic_photo_library"
android:order="101"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_videos"
android:title="@string/storage_videos"
android:icon="@drawable/ic_local_movies"
android:order="102"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_audios"
android:title="@string/storage_audios"
android:icon="@drawable/ic_media_stream"
android:order="103"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_apps"
android:title="@string/storage_apps"
android:icon="@drawable/ic_storage_apps"
android:order="104"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_games"
android:title="@string/storage_games"
android:icon="@drawable/ic_videogame_vd_theme_24"
android:order="105"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_documents_and_other"
android:title="@string/storage_documents_and_other"
android:icon="@drawable/ic_folder_vd_theme_24"
android:order="106"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_system"
android:title="@string/storage_system"
android:icon="@drawable/ic_system_update"
android:order="107"/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_trash"
android:title="@string/storage_trash"
android:icon="@drawable/ic_trash_can"
android:order="108"/>
<!-- Preference order 100~200 are 'ONLY' for storage category preferences above. -->
<PreferenceCategory
android:key="pref_secondary_users"
android:title="@string/storage_other_users"
android:order="201" />
</PreferenceScreen>

View File

@@ -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">
<com.android.settingslib.widget.SettingsSpinnerPreference
android:key="storage_spinner"
@@ -45,8 +44,8 @@
android:order="4"
android:title="@string/storage_free_up_space_title"
android:summary="@string/storage_free_up_space_summary"
settings:allowDividerAbove="true"
android:icon="@drawable/ic_files_go_round"/>
android:icon="@drawable/ic_files_go_round"
settings:allowDividerAbove="true"/>
<!-- Preference order 100~200 are 'ONLY' for storage category preferences below. -->
<Preference
android:key="pref_public_storage"

View File

@@ -0,0 +1,42 @@
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="storage_header"
android:title="@string/storage_settings">
<com.android.settingslib.widget.SettingsSpinnerPreference
android:key="storage_spinner"
android:order="1"
settings:searchable="false"
settings:controller="com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController"/>
<com.android.settingslib.widget.UsageProgressBarPreference
android:key="storage_summary"
android:order="2"
android:selectable="false"
settings:searchable="false"
settings:controller="com.android.settings.deviceinfo.storage.StorageUsageProgressBarPreferenceController"
settings:allowDividerBelow="true"/>
<com.android.settings.widget.PrimarySwitchPreference
android:fragment="com.android.settings.deletionhelper.AutomaticStorageManagerSettings"
android:key="toggle_asm"
android:title="@string/automatic_storage_manager_preference_title"
android:icon="@drawable/ic_storage"
android:order="3"
settings:allowDividerAbove="true"
settings:controller="com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController"/>
</PreferenceScreen>

View File

@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Layout for the storage breakdown for a profile of the primary user. -->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/storage_settings">
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_photos_videos"
android:title="@string/storage_photos_videos"
android:icon="@drawable/ic_photo_library"
android:order="2" />
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_music_audio"
android:title="@string/storage_music_audio"
android:icon="@drawable/ic_media_stream"
android:order="3" />
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_games"
android:title="@string/storage_games"
android:icon="@drawable/ic_videogame_vd_theme_24"
android:order="4" />
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_movies"
android:title="@string/storage_movies_tv"
android:icon="@drawable/ic_local_movies"
android:order="5"
/>
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_other_apps"
android:title="@string/storage_other_apps"
android:icon="@drawable/ic_storage_apps"
android:order="6" />
<com.android.settings.deviceinfo.StorageItemPreference
android:key="pref_files"
android:title="@string/storage_files"
android:icon="@drawable/ic_folder_vd_theme_24"
android:order="7" />
</PreferenceScreen>

View File

@@ -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<StorageEntry> 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;
}
}

View File

@@ -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<SparseArray<StorageAsyncLoader.AppsStorageResult>>,
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<StorageAsyncLoader.AppsStorageResult> mAppsResult;
private CachedStorageValuesHelper mCachedStorageValuesHelper;
private StorageItemPreferenceController mPreferenceController;
private List<AbstractPreferenceController> 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<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.AppsStorageResult> 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<SparseArray<StorageAsyncLoader.AppsStorageResult>> 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.AppsStorageResult>> loader,
SparseArray<StorageAsyncLoader.AppsStorageResult> data) {
mAppsResult = data;
maybeCacheFreshValues();
onReceivedSizes();
}
@Override
public void onLoaderReset(Loader<SparseArray<StorageAsyncLoader.AppsStorageResult>> 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<StorageAsyncLoader.AppsStorageResult> getAppsStorageResult() {
return mAppsResult;
}
@VisibleForTesting
public void setAppsStorageResult(SparseArray<StorageAsyncLoader.AppsStorageResult> info) {
mAppsResult = info;
}
@VisibleForTesting
void initializeCachedValues() {
final PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo();
final SparseArray<StorageAsyncLoader.AppsStorageResult> 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<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;
maybeCacheFreshValues();
onReceivedSizes();
}
}
}

View File

@@ -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<AbstractPreferenceController> createPreferenceControllers(Context context) {
final List<AbstractPreferenceController> 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<AbstractPreferenceController> controllers,
SparseArray<StorageAsyncLoader.AppsStorageResult> 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<StorageAsyncLoader.AppsStorageResult> loaderResult =
void initializeCachedValues() {
final PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo();
final SparseArray<StorageAsyncLoader.AppsStorageResult> 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<PrivateStorageInfo> {
@Override

View File

@@ -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<StorageEntry> getAllStorageEntries(Context context,
StorageManager storageManager) {
final List<StorageEntry> 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) {

View File

@@ -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

View File

@@ -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();
}
}