diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 61f3e95b085..ea196ad1f08 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -17,7 +17,9 @@ package com.android.settings.deviceinfo; import android.content.Context; +import android.content.Loader; import android.os.Bundle; +import android.os.UserHandle; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.SearchIndexableResource; @@ -27,6 +29,7 @@ import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.PreferenceController; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.deviceinfo.storage.AppsAsyncLoader; import com.android.settings.deviceinfo.storage.StorageItemPreferenceController; import com.android.settings.deviceinfo.storage.StorageSummaryDonutPreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -42,6 +45,7 @@ import java.util.List; public class StorageDashboardFragment extends DashboardFragment { private static final String TAG = "StorageDashboardFrag"; + private static final int APPS_JOB_ID = 0; private VolumeInfo mVolume; @@ -53,6 +57,12 @@ public class StorageDashboardFragment extends DashboardFragment { && mVolume.isMountedReadable(); } + @Override + public void onResume() { + super.onResume(); + getLoaderManager().initLoader(APPS_JOB_ID, Bundle.EMPTY, mPreferenceController); + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); diff --git a/src/com/android/settings/deviceinfo/storage/AppsAsyncLoader.java b/src/com/android/settings/deviceinfo/storage/AppsAsyncLoader.java new file mode 100644 index 00000000000..cbedb08b1e8 --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/AppsAsyncLoader.java @@ -0,0 +1,86 @@ +/* + * 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. + */ + +package com.android.settings.deviceinfo.storage; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.util.ArraySet; + +import com.android.settings.applications.PackageManagerWrapper; +import com.android.settings.utils.AsyncLoader; + +import java.util.List; + +/** + * AppsAsyncLoader is a Loader which loads app storage information and categories it by the app's + * specified categorization. + */ +public class AppsAsyncLoader extends AsyncLoader { + private int mUserId; + private String mUuid; + private StorageStatsSource mStatsManager; + private PackageManagerWrapper mPackageManager; + + public AppsAsyncLoader(Context context, int userId, String uuid, StorageStatsSource source, + PackageManagerWrapper pm) { + super(context); + mUserId = userId; + mUuid = uuid; + mStatsManager = source; + mPackageManager = pm; + } + + @Override + public AppsStorageResult loadInBackground() { + return loadApps(); + } + + private AppsStorageResult loadApps() { + AppsStorageResult result = new AppsStorageResult(); + ArraySet seenUid = new ArraySet<>(); // some apps share a uid + + List applicationInfos = + mPackageManager.getInstalledApplicationsAsUser(0, mUserId); + int size = applicationInfos.size(); + for (int i = 0; i < size; i++) { + ApplicationInfo app = applicationInfos.get(i); + if (seenUid.contains(app.uid)) { + continue; + } + seenUid.add(app.uid); + + StorageStatsSource.AppStorageStats stats = mStatsManager.getStatsForUid(mUuid, app.uid); + // Note: This omits cache intentionally -- we are not attributing it to the apps. + long appSize = stats.getCodeBytes() + stats.getDataBytes(); + if (app.category == ApplicationInfo.CATEGORY_GAME) { + result.gamesSize += appSize; + } else { + result.otherAppsSize += appSize; + } + } + return result; + } + + @Override + protected void onDiscardResult(AppsStorageResult result) { + } + + public static class AppsStorageResult { + public long gamesSize; + public long otherAppsSize; + } +} diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index 16dcd18908f..5437dcb0fa3 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -17,9 +17,12 @@ package com.android.settings.deviceinfo.storage; import android.app.Fragment; +import android.app.LoaderManager; +import android.app.usage.StorageStatsManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.Loader; import android.os.Bundle; import android.os.Environment; import android.os.UserHandle; @@ -35,6 +38,7 @@ import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.Utils; import com.android.settings.applications.ManageApplications; +import com.android.settings.applications.PackageManagerWrapperImpl; import com.android.settings.core.PreferenceController; import com.android.settings.core.lifecycle.Lifecycle; import com.android.settings.core.lifecycle.LifecycleObserver; @@ -51,10 +55,12 @@ import java.util.HashMap; * categorization breakdown. */ public class StorageItemPreferenceController extends PreferenceController - implements StorageMeasurement.MeasurementReceiver, LifecycleObserver, OnDestroy { + implements StorageMeasurement.MeasurementReceiver, LifecycleObserver, OnDestroy, + LoaderManager.LoaderCallbacks { private static final String TAG = "StorageItemPreference"; private static final String IMAGE_MIME_TYPE = "image/*"; + @VisibleForTesting static final String PHOTO_KEY = "pref_photos_videos"; @VisibleForTesting @@ -179,15 +185,6 @@ public class StorageItemPreferenceController extends PreferenceController mAudioPreference.setStorageSize(audioSize); } - if (mGamePreference != null) { - mGamePreference.setStorageSize(0); - } - - final long appSize = details.appsSize.get(mUserId); - if (mAppPreference != null) { - mAppPreference.setStorageSize(appSize); - } - if (mSystemPreference != null) { mSystemPreference.setStorageSize(mSystemSize); } @@ -216,6 +213,25 @@ public class StorageItemPreferenceController extends PreferenceController mFilePreference = (StorageItemPreferenceAlternate) screen.findPreference(FILES_KEY); } + @Override + public Loader onCreateLoader(int id, + Bundle args) { + return new AppsAsyncLoader(mContext, UserHandle.myUserId(), mVolume.fsUuid, + new StorageStatsSource(mContext), + new PackageManagerWrapperImpl(mContext.getPackageManager())); + } + + @Override + public void onLoadFinished(Loader loader, + AppsAsyncLoader.AppsStorageResult data) { + mGamePreference.setStorageSize(data.gamesSize); + mAppPreference.setStorageSize(data.otherAppsSize); + } + + @Override + public void onLoaderReset(Loader loader) { + } + /** * Begins an asynchronous storage measurement task for the preferences. */ diff --git a/src/com/android/settings/deviceinfo/storage/StorageStatsSource.java b/src/com/android/settings/deviceinfo/storage/StorageStatsSource.java index b6e03fb2c78..98038fdc140 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageStatsSource.java +++ b/src/com/android/settings/deviceinfo/storage/StorageStatsSource.java @@ -16,6 +16,7 @@ package com.android.settings.deviceinfo.storage; +import android.app.usage.StorageStats; import android.app.usage.StorageStatsManager; import android.content.Context; import android.os.UserHandle; @@ -24,14 +25,19 @@ import android.os.UserHandle; * StorageStatsSource wraps the StorageStatsManager for testability purposes. */ public class StorageStatsSource { - private StorageStatsManager mSsm; + private StorageStatsManager mStorageStatsManager; public StorageStatsSource(Context context) { - mSsm = context.getSystemService(StorageStatsManager.class); + mStorageStatsManager = context.getSystemService(StorageStatsManager.class); } public ExternalStorageStats getExternalStorageStats(String volumeUuid, UserHandle user) { - return new ExternalStorageStats(mSsm.queryExternalStatsForUser(volumeUuid, user)); + return new ExternalStorageStats( + mStorageStatsManager.queryExternalStatsForUser(volumeUuid, user)); + } + + public AppStorageStats getStatsForUid(String volumeUuid, int uid) { + return new AppStorageStatsImpl(mStorageStatsManager.queryStatsForUid(volumeUuid, uid)); } public static class ExternalStorageStats { @@ -55,4 +61,30 @@ public class StorageStatsSource { imageBytes = stats.getImageBytes(); } } + + public interface AppStorageStats { + long getCodeBytes(); + long getDataBytes(); + long getCacheBytes(); + } + + public static class AppStorageStatsImpl implements AppStorageStats { + private StorageStats mStats; + + public AppStorageStatsImpl(StorageStats stats) { + mStats = stats; + } + + public long getCodeBytes() { + return mStats.getCodeBytes(); + } + + public long getDataBytes() { + return mStats.getDataBytes(); + } + + public long getCacheBytes() { + return mStats.getCacheBytes(); + } + } } diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java index f7baba365cb..cfec382bcc8 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceControllerTest.java @@ -215,11 +215,15 @@ public class StorageItemPreferenceControllerTest { details.mediaSize.put(0, mediaSizes); mController.setSystemSize(KILOBYTE * 6); mController.onDetailsChanged(details); + AppsAsyncLoader.AppsStorageResult result = new AppsAsyncLoader.AppsStorageResult(); + result.gamesSize = KILOBYTE * 8; + result.otherAppsSize = KILOBYTE * 9; + mController.onLoadFinished(null, result); assertThat(audio.getSummary().toString()).isEqualTo("4.00KB"); assertThat(image.getSummary().toString()).isEqualTo("5.00KB"); - assertThat(games.getSummary().toString()).isEqualTo("0"); - assertThat(apps.getSummary().toString()).isEqualTo("1.00KB"); + assertThat(games.getSummary().toString()).isEqualTo("8.00KB"); + assertThat(apps.getSummary().toString()).isEqualTo("9.00KB"); assertThat(system.getSummary().toString()).isEqualTo("6.00KB"); assertThat(files.getSummary().toString()).isEqualTo("5.00KB"); } diff --git a/tests/unit/src/com/android/settings/deviceinfo/storage/AppAsyncLoaderTest.java b/tests/unit/src/com/android/settings/deviceinfo/storage/AppAsyncLoaderTest.java new file mode 100644 index 00000000000..8d4dd2e3578 --- /dev/null +++ b/tests/unit/src/com/android/settings/deviceinfo/storage/AppAsyncLoaderTest.java @@ -0,0 +1,116 @@ +/* + * 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. + */ + +package com.android.settings.deviceinfo.storage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.settings.applications.PackageManagerWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AppAsyncLoaderTest { + @Mock + private StorageStatsSource mSource; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock + private PackageManagerWrapper mPackageManager; + ArrayList mInfo = new ArrayList<>(); + + private AppsAsyncLoader mLoader; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mInfo = new ArrayList<>(); + mLoader = new AppsAsyncLoader(mContext, 1, "id", mSource, mPackageManager); + when(mPackageManager.getInstalledApplicationsAsUser(anyInt(), anyInt())).thenReturn(mInfo); + } + + @Test + public void testLoadingApps() throws Exception { + addPackage(1001, 0, 1, 10, ApplicationInfo.CATEGORY_UNDEFINED); + addPackage(1002, 0, 100, 1000, ApplicationInfo.CATEGORY_UNDEFINED); + + AppsAsyncLoader.AppsStorageResult result = mLoader.loadInBackground(); + + assertThat(result.gamesSize).isEqualTo(0L); + assertThat(result.otherAppsSize).isEqualTo(1111L); + } + + @Test + public void testGamesAreFiltered() throws Exception { + addPackage(1001, 0, 1, 10, ApplicationInfo.CATEGORY_GAME); + + AppsAsyncLoader.AppsStorageResult result = mLoader.loadInBackground(); + + assertThat(result.gamesSize).isEqualTo(11L); + assertThat(result.otherAppsSize).isEqualTo(0); + } + + @Test + public void testDuplicateUidsAreSkipped() throws Exception { + addPackage(1001, 0, 1, 10, ApplicationInfo.CATEGORY_UNDEFINED); + addPackage(1001, 0, 1, 10, ApplicationInfo.CATEGORY_UNDEFINED); + + AppsAsyncLoader.AppsStorageResult result = mLoader.loadInBackground(); + + assertThat(result.otherAppsSize).isEqualTo(11L); + } + + @Test + public void testCacheIsIgnored() throws Exception { + addPackage(1001, 100, 1, 10, ApplicationInfo.CATEGORY_UNDEFINED); + + AppsAsyncLoader.AppsStorageResult result = mLoader.loadInBackground(); + + assertThat(result.otherAppsSize).isEqualTo(11L); + } + + private void addPackage(int uid, long cacheSize, long codeSize, long dataSize, int category) { + StorageStatsSource.AppStorageStats storageStats = + mock(StorageStatsSource.AppStorageStats.class); + when(storageStats.getCodeBytes()).thenReturn(codeSize); + when(storageStats.getDataBytes()).thenReturn(dataSize); + when(mSource.getStatsForUid(anyString(), eq(uid))).thenReturn(storageStats); + + ApplicationInfo info = new ApplicationInfo(); + info.uid = uid; + info.category = category; + mInfo.add(info); + } +}