Cache mechanism for Storage page

- Use SharedPreference to cache the size info
- Improve the flicker problem on Documents & other preference
- The jobs are destroied on onPause to prevent the jobs being
  restarting when back to Storage page
- Enable progress bar animation for each storage item

Bug: 191117970
Test: manual test
1) The loading spinner will be shown when entering Storage page
   at first time.
2) Back to Settings homepage and switch back to Storage page, the
   loading spinner shouldn't be shown.
3) Click each preference in the Storage page and switch between these
   pages, the size info should be updated if something removed and the
   order of preference shouldn't be changed.

Change-Id: I75533742a025dc61116207285a894ee728d0af68
This commit is contained in:
Mill Chen
2022-02-08 15:37:53 +08:00
parent 05e713bcc5
commit 77775a66f2
7 changed files with 351 additions and 46 deletions

View File

@@ -47,6 +47,7 @@ import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchP
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.StorageCacheHelper;
import com.android.settings.deviceinfo.storage.StorageEntry;
import com.android.settings.deviceinfo.storage.StorageItemPreferenceController;
import com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController;
@@ -109,6 +110,8 @@ public class StorageDashboardFragment extends DashboardFragment
private boolean mIsWorkProfile;
private int mUserId;
private Preference mFreeUpSpacePreference;
private boolean mIsLoadedFromCache;
private StorageCacheHelper mStorageCacheHelper;
private final StorageEventListener mStorageEventListener = new StorageEventListener() {
@Override
@@ -239,15 +242,27 @@ public class StorageDashboardFragment extends DashboardFragment
mPreferenceController.setVolume(null);
return;
}
if (mStorageCacheHelper.hasCachedSizeInfo() && mSelectedStorageEntry.isPrivate()) {
StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
mPreferenceController.setUsedSize(cachedData.usedSize);
mPreferenceController.setTotalSize(cachedData.totalSize);
}
if (mSelectedStorageEntry.isPrivate()) {
mStorageInfo = null;
mAppsResult = null;
// Hide the loading spinner if there is cached data.
if (mStorageCacheHelper.hasCachedSizeInfo()) {
//TODO(b/220259287): apply cache mechanism to secondary user
mPreferenceController.onLoadFinished(mAppsResult, mUserId);
} else {
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()
@@ -277,6 +292,16 @@ public class StorageDashboardFragment extends DashboardFragment
initializePreference();
initializeOptionsMenu(activity);
if (mStorageCacheHelper.hasCachedSizeInfo()) {
mIsLoadedFromCache = true;
mStorageEntries.clear();
mStorageEntries.addAll(
StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
refreshUi();
updateSecondaryUserControllers(mSecondaryUsers, mAppsResult);
setSecondaryUsersVisible(true);
}
}
private void initializePreference() {
@@ -291,6 +316,7 @@ public class StorageDashboardFragment extends DashboardFragment
mUserManager = context.getSystemService(UserManager.class);
mIsWorkProfile = false;
mUserId = UserHandle.myUserId();
mStorageCacheHelper = new StorageCacheHelper(getContext(), mUserId);
super.onAttach(context);
use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager(
@@ -323,9 +349,14 @@ public class StorageDashboardFragment extends DashboardFragment
public void onResume() {
super.onResume();
if (mIsLoadedFromCache) {
mIsLoadedFromCache = false;
} else {
mStorageEntries.clear();
mStorageEntries.addAll(StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
mStorageEntries.addAll(
StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
refreshUi();
}
mStorageManager.registerListener(mStorageEventListener);
}
@@ -333,6 +364,11 @@ public class StorageDashboardFragment extends DashboardFragment
public void onPause() {
super.onPause();
mStorageManager.unregisterListener(mStorageEventListener);
// Destroy the data loaders to prevent unnecessary data loading when switching back to the
// page.
getLoaderManager().destroyLoader(STORAGE_JOB_ID);
getLoaderManager().destroyLoader(ICON_JOB_ID);
getLoaderManager().destroyLoader(VOLUME_SIZE_JOB_ID);
}
@Override
@@ -359,6 +395,8 @@ public class StorageDashboardFragment extends DashboardFragment
mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
mPreferenceController.setUsedSize(privateUsedBytes);
mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
// Cache total size and used size
mStorageCacheHelper.cacheTotalSizeAndUsedSize(mStorageInfo.totalBytes, privateUsedBytes);
for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) {
final AbstractPreferenceController controller = mSecondaryUsers.get(i);
if (controller instanceof SecondaryUserController) {

View File

@@ -66,7 +66,7 @@ public class StorageItemPreference extends Preference {
return;
mProgressBar.setMax(PROGRESS_MAX);
mProgressBar.setProgress(mProgressPercent);
mProgressBar.setProgress(mProgressPercent, true /* animate */);
}
@Override

View File

@@ -183,6 +183,9 @@ public class SecondaryUserController extends AbstractPreferenceController implem
@Override
public void handleResult(SparseArray<StorageAsyncLoader.StorageResult> stats) {
if (stats == null) {
return;
}
final StorageAsyncLoader.StorageResult result = stats.get(getUser().id);
if (result != null) {
setSize(result.externalStats.totalBytes);

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.deviceinfo.storage;
import android.content.Context;
import android.content.SharedPreferences;
/**
* A utility class to cache and restore the storage size information.
*/
public class StorageCacheHelper {
private static final String SHARED_PREFERENCE_NAME = "StorageCache";
private static final String TOTAL_SIZE_KEY = "total_size_key";
private static final String USED_SIZE_KEY = "used_size_key";
private static final String IMAGES_SIZE_KEY = "images_size_key";
private static final String VIDEOS_SIZE_KEY = "videos_size_key";
private static final String AUDIO_SIZE_KEY = "audio_size_key";
private static final String APPS_SIZE_KEY = "apps_size_key";
private static final String GAMES_SIZE_KEY = "games_size_key";
private static final String DOCUMENTS_AND_OTHER_SIZE_KEY = "documents_and_other_size_key";
private static final String TRASH_SIZE_KEY = "trash_size_key";
private static final String SYSTEM_SIZE_KEY = "system_size_key";
private final SharedPreferences mSharedPreferences;
public StorageCacheHelper(Context context, int userId) {
String sharedPrefName = SHARED_PREFERENCE_NAME + userId;
mSharedPreferences = context.getSharedPreferences(sharedPrefName, Context.MODE_PRIVATE);
}
/**
* Returns true if there's a cached size info.
*/
public boolean hasCachedSizeInfo() {
return mSharedPreferences.getAll().size() > 0;
}
/**
* Cache the size info
* @param data a data about the file size info.
*/
public void cacheSizeInfo(StorageCache data) {
mSharedPreferences
.edit()
.putLong(IMAGES_SIZE_KEY, data.imagesSize)
.putLong(VIDEOS_SIZE_KEY, data.videosSize)
.putLong(AUDIO_SIZE_KEY, data.audioSize)
.putLong(APPS_SIZE_KEY, data.allAppsExceptGamesSize)
.putLong(GAMES_SIZE_KEY, data.gamesSize)
.putLong(DOCUMENTS_AND_OTHER_SIZE_KEY, data.documentsAndOtherSize)
.putLong(TRASH_SIZE_KEY, data.trashSize)
.putLong(SYSTEM_SIZE_KEY, data.systemSize)
.apply();
}
/**
* Cache total size and used size
*/
public void cacheTotalSizeAndUsedSize(long totalSize, long usedSize) {
mSharedPreferences
.edit()
.putLong(TOTAL_SIZE_KEY, totalSize)
.putLong(USED_SIZE_KEY, usedSize)
.apply();
}
/**
* Returns a cached data about all file size information.
*/
public StorageCache retrieveCachedSize() {
StorageCache result = new StorageCache();
result.totalSize = mSharedPreferences.getLong(TOTAL_SIZE_KEY, 0);
result.usedSize = mSharedPreferences.getLong(USED_SIZE_KEY, 0);
result.imagesSize = mSharedPreferences.getLong(IMAGES_SIZE_KEY, 0);
result.videosSize = mSharedPreferences.getLong(VIDEOS_SIZE_KEY, 0);
result.audioSize = mSharedPreferences.getLong(AUDIO_SIZE_KEY, 0);
result.allAppsExceptGamesSize = mSharedPreferences.getLong(APPS_SIZE_KEY, 0);
result.gamesSize = mSharedPreferences.getLong(GAMES_SIZE_KEY, 0);
result.documentsAndOtherSize = mSharedPreferences.getLong(DOCUMENTS_AND_OTHER_SIZE_KEY, 0);
result.trashSize = mSharedPreferences.getLong(TRASH_SIZE_KEY, 0);
result.systemSize = mSharedPreferences.getLong(SYSTEM_SIZE_KEY, 0);
return result;
}
/**
* All the cached data about the file size information.
*/
public static class StorageCache {
public long totalSize;
public long usedSize;
public long gamesSize;
public long allAppsExceptGamesSize;
public long audioSize;
public long imagesSize;
public long videosSize;
public long documentsAndOtherSize;
public long trashSize;
public long systemSize;
}
}

View File

@@ -34,6 +34,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
@@ -135,7 +136,11 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
private boolean mIsWorkProfile;
private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
private StorageCacheHelper mStorageCacheHelper;
// The mIsDocumentsPrefShown being used here is to prevent a flicker problem from displaying
// the Document entry.
private boolean mIsDocumentsPrefShown;
private boolean mIsPreferenceOrderedBySize;
public StorageItemPreferenceController(Context context, Fragment hostFragment,
VolumeInfo volume, StorageVolumeProvider svp, boolean isWorkProfile) {
@@ -148,6 +153,8 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
mIsWorkProfile = isWorkProfile;
mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
mUserId = getCurrentUserId();
mIsDocumentsPrefShown = isDocumentsPrefShown();
mStorageCacheHelper = new StorageCacheHelper(mContext, mUserId);
mImagesUri = Uri.parse(context.getResources()
.getString(R.string.config_images_storage_category_uri));
@@ -267,14 +274,17 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
// If we don't have a shared volume for our internal storage (or the shared volume isn't
// mounted as readable for whatever reason), we should hide the File preference.
if (visible) {
final VolumeInfo sharedVolume = mSvp.findEmulatedForPrivate(mVolume);
mDocumentsAndOtherPreference.setVisible(sharedVolume != null
&& sharedVolume.isMountedReadable());
mDocumentsAndOtherPreference.setVisible(mIsDocumentsPrefShown);
} else {
mDocumentsAndOtherPreference.setVisible(false);
}
}
private boolean isDocumentsPrefShown() {
VolumeInfo sharedVolume = mSvp.findEmulatedForPrivate(mVolume);
return sharedVolume != null && sharedVolume.isMountedReadable();
}
private void updatePrivateStorageCategoryPreferencesOrder() {
if (mScreen == null || !isValidPrivateVolume()) {
return;
@@ -360,19 +370,54 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
mTrashPreference = screen.findPreference(TRASH_KEY);
}
/** Fragments use it to set storage result and update UI of this controller. */
public void onLoadFinished(SparseArray<StorageAsyncLoader.StorageResult> result, int userId) {
final StorageAsyncLoader.StorageResult data = result.get(userId);
mImagesPreference.setStorageSize(data.imagesSize, mTotalSize);
mVideosPreference.setStorageSize(data.videosSize, mTotalSize);
mAudioPreference.setStorageSize(data.audioSize, mTotalSize);
mAppsPreference.setStorageSize(data.allAppsExceptGamesSize, mTotalSize);
mGamesPreference.setStorageSize(data.gamesSize, mTotalSize);
mDocumentsAndOtherPreference.setStorageSize(data.documentsAndOtherSize, mTotalSize);
mTrashPreference.setStorageSize(data.trashSize, mTotalSize);
/**
* Fragments use it to set storage result and update UI of this controller.
* @param result The StorageResult from StorageAsyncLoader. This allows a nullable result.
* When it's null, the cached storage size info will be used instead.
* @param userId User ID to get the storage size info
*/
public void onLoadFinished(@Nullable SparseArray<StorageAsyncLoader.StorageResult> result,
int userId) {
// Calculate the size info for each category
StorageCacheHelper.StorageCache storageCache = getSizeInfo(result, userId);
// Set size info to each preference
mImagesPreference.setStorageSize(storageCache.imagesSize, mTotalSize);
mVideosPreference.setStorageSize(storageCache.videosSize, mTotalSize);
mAudioPreference.setStorageSize(storageCache.audioSize, mTotalSize);
mAppsPreference.setStorageSize(storageCache.allAppsExceptGamesSize, mTotalSize);
mGamesPreference.setStorageSize(storageCache.gamesSize, mTotalSize);
mDocumentsAndOtherPreference.setStorageSize(storageCache.documentsAndOtherSize, mTotalSize);
mTrashPreference.setStorageSize(storageCache.trashSize, mTotalSize);
if (mSystemPreference != null) {
mSystemPreference.setStorageSize(storageCache.systemSize, mTotalSize);
}
// Cache the size info
if (result != null) {
mStorageCacheHelper.cacheSizeInfo(storageCache);
}
// Sort the preference according to size info in descending order
if (!mIsPreferenceOrderedBySize) {
updatePrivateStorageCategoryPreferencesOrder();
mIsPreferenceOrderedBySize = true;
}
setPrivateStorageCategoryPreferencesVisibility(true);
}
private StorageCacheHelper.StorageCache getSizeInfo(
SparseArray<StorageAsyncLoader.StorageResult> result, int userId) {
if (result == null) {
return mStorageCacheHelper.retrieveCachedSize();
}
StorageAsyncLoader.StorageResult data = result.get(userId);
StorageCacheHelper.StorageCache storageCache = new StorageCacheHelper.StorageCache();
storageCache.imagesSize = data.imagesSize;
storageCache.videosSize = data.videosSize;
storageCache.audioSize = data.audioSize;
storageCache.allAppsExceptGamesSize = data.allAppsExceptGamesSize;
storageCache.gamesSize = data.gamesSize;
storageCache.documentsAndOtherSize = data.documentsAndOtherSize;
storageCache.trashSize = data.trashSize;
// Everything else that hasn't already been attributed is tracked as
// belonging to system.
long attributedSize = 0;
@@ -388,14 +433,9 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle
+ otherData.allAppsExceptGamesSize;
attributedSize -= otherData.duplicateCodeSize;
}
final long systemSize = Math.max(DataUnit.GIBIBYTES.toBytes(1),
storageCache.systemSize = Math.max(DataUnit.GIBIBYTES.toBytes(1),
mUsedBytes - attributedSize);
mSystemPreference.setStorageSize(systemSize, mTotalSize);
}
updatePrivateStorageCategoryPreferencesOrder();
setPrivateStorageCategoryPreferencesVisibility(true);
return storageCache;
}
public void setUsedSize(long usedSizeBytes) {

View File

@@ -18,6 +18,7 @@ package com.android.settings.deviceinfo.storage;
import android.app.usage.StorageStatsManager;
import android.content.Context;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -47,11 +48,13 @@ public class StorageUsageProgressBarPreferenceController extends BasePreferenceC
private UsageProgressBarPreference mUsageProgressBarPreference;
private StorageEntry mStorageEntry;
boolean mIsUpdateStateFromSelectedStorageEntry;
private StorageCacheHelper mStorageCacheHelper;
public StorageUsageProgressBarPreferenceController(Context context, String key) {
super(context, key);
mStorageStatsManager = context.getSystemService(StorageStatsManager.class);
mStorageCacheHelper = new StorageCacheHelper(context, UserHandle.myUserId());
}
/** Set StorageEntry to display. */
@@ -71,6 +74,15 @@ public class StorageUsageProgressBarPreferenceController extends BasePreferenceC
}
private void getStorageStatsAndUpdateUi() {
// Use cached data for both total size and used size.
if (mStorageEntry != null && mStorageEntry.isMounted() && mStorageEntry.isPrivate()) {
StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
mTotalBytes = cachedData.totalSize;
mUsedBytes = cachedData.usedSize;
mIsUpdateStateFromSelectedStorageEntry = true;
updateState(mUsageProgressBarPreference);
}
// Get the latest data from StorageStatsManager.
ThreadUtils.postOnBackgroundThread(() -> {
try {
if (mStorageEntry == null || !mStorageEntry.isMounted()) {

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.deviceinfo.storage;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.UserHandle;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class StorageCacheHelperTest {
private static final long FAKE_IMAGES_SIZE = 7000L;
private static final long FAKE_VIDEOS_SIZE = 8900L;
private static final long FAKE_AUDIO_SIZE = 3500L;
private static final long FAKE_APPS_SIZE = 4000L;
private static final long FAKE_GAMES_SIZE = 5000L;
private static final long FAKE_DOCS_SIZE = 1500L;
private static final long FAKE_TRASH_SIZE = 500L;
private static final long FAKE_SYSTEM_SIZE = 2300L;
private static final long FAKE_TOTAL_SIZE = 256000L;
private static final long FAKE_USED_SIZE = 50000L;
private Context mContext;
private StorageCacheHelper mHelper;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mHelper = new StorageCacheHelper(mContext, UserHandle.myUserId());
}
@Test
public void hasCachedSizeInfo_noCacheData_shouldReturnFalse() {
assertThat(mHelper.hasCachedSizeInfo()).isFalse();
}
@Test
public void hasCachedSizeInfo_hasCacheData_shouldReturnTrue() {
mHelper.cacheSizeInfo(getFakeStorageCache());
assertThat(mHelper.hasCachedSizeInfo()).isTrue();
}
@Test
public void cacheSizeInfo_shouldSaveToSharedPreference() {
mHelper.cacheSizeInfo(getFakeStorageCache());
StorageCacheHelper.StorageCache storageCache = mHelper.retrieveCachedSize();
assertThat(storageCache.imagesSize).isEqualTo(FAKE_IMAGES_SIZE);
assertThat(storageCache.totalSize).isEqualTo(0);
}
@Test
public void cacheTotalSizeAndUsedSize_shouldSaveToSharedPreference() {
mHelper.cacheTotalSizeAndUsedSize(FAKE_TOTAL_SIZE, FAKE_USED_SIZE);
StorageCacheHelper.StorageCache storageCache = mHelper.retrieveCachedSize();
assertThat(storageCache.totalSize).isEqualTo(FAKE_TOTAL_SIZE);
assertThat(storageCache.usedSize).isEqualTo(FAKE_USED_SIZE);
}
private StorageCacheHelper.StorageCache getFakeStorageCache() {
StorageCacheHelper.StorageCache result = new StorageCacheHelper.StorageCache();
result.trashSize = FAKE_TRASH_SIZE;
result.systemSize = FAKE_SYSTEM_SIZE;
result.imagesSize = FAKE_IMAGES_SIZE;
result.documentsAndOtherSize = FAKE_DOCS_SIZE;
result.audioSize = FAKE_AUDIO_SIZE;
result.gamesSize = FAKE_GAMES_SIZE;
result.videosSize = FAKE_VIDEOS_SIZE;
result.allAppsExceptGamesSize = FAKE_APPS_SIZE;
return result;
}
}