Merge "Cache storage values for fast loading." into oc-mr1-dev
diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
index 33d7d36..dd0db9a 100644
--- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
+++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
@@ -40,6 +40,7 @@
 import com.android.settings.applications.UserManagerWrapperImpl;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController;
+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.StorageItemPreferenceController;
@@ -68,6 +69,7 @@
     private VolumeInfo mVolume;
     private PrivateStorageInfo mStorageInfo;
     private SparseArray<StorageAsyncLoader.AppsStorageResult> mAppsResult;
+    private CachedStorageValuesHelper mCachedStorageValuesHelper;
 
     private StorageSummaryDonutPreferenceController mSummaryController;
     private StorageItemPreferenceController mPreferenceController;
@@ -102,7 +104,10 @@
     @Override
     public void onViewCreated(View v, Bundle savedInstanceState) {
         super.onViewCreated(v, savedInstanceState);
-        setLoading(true, false);
+        initializeCacheProvider();
+        if (mAppsResult == null || mStorageInfo == null) {
+            setLoading(true, false);
+        }
     }
 
     @Override
@@ -249,6 +254,7 @@
     public void onLoadFinished(Loader<SparseArray<StorageAsyncLoader.AppsStorageResult>> loader,
             SparseArray<StorageAsyncLoader.AppsStorageResult> data) {
         mAppsResult = data;
+        maybeCacheFreshValues();
         onReceivedSizes();
     }
 
@@ -256,6 +262,48 @@
     public void onLoaderReset(Loader<SparseArray<StorageAsyncLoader.AppsStorageResult>> loader) {
     }
 
+    @VisibleForTesting
+    public void setCachedStorageValuesHelper(CachedStorageValuesHelper helper) {
+        mCachedStorageValuesHelper = helper;
+    }
+
+    @VisibleForTesting
+    public PrivateStorageInfo getPrivateStorageInfo() {
+        return mStorageInfo;
+    }
+
+    @VisibleForTesting
+    public SparseArray<StorageAsyncLoader.AppsStorageResult> getAppsStorageResult() {
+        return mAppsResult;
+    }
+
+    @VisibleForTesting
+    public void initializeCachedValues() {
+        PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo();
+        SparseArray<StorageAsyncLoader.AppsStorageResult> loaderResult =
+                mCachedStorageValuesHelper.getCachedAppsStorageResult();
+        if (info == null || loaderResult == null) {
+            return;
+        }
+
+        mStorageInfo = info;
+        mAppsResult = loaderResult;
+    }
+
+    private void initializeCacheProvider() {
+        mCachedStorageValuesHelper =
+                new CachedStorageValuesHelper(getContext(), UserHandle.myUserId());
+        initializeCachedValues();
+        onReceivedSizes();
+    }
+
+    private void maybeCacheFreshValues() {
+        if (mStorageInfo != null && mAppsResult != null) {
+            mCachedStorageValuesHelper.cacheResult(
+                    mStorageInfo, mAppsResult.get(UserHandle.myUserId()));
+        }
+    }
+
     /**
      * IconLoaderCallbacks exists because StorageDashboardFragment already implements
      * LoaderCallbacks for a different type.
@@ -308,6 +356,7 @@
             }
 
             mStorageInfo = privateStorageInfo;
+            maybeCacheFreshValues();
             onReceivedSizes();
         }
     }
diff --git a/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java
new file mode 100644
index 0000000..8225db3
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java
@@ -0,0 +1,172 @@
+/*
+ * 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.SharedPreferences;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.util.SparseArray;
+
+import com.android.settingslib.applications.StorageStatsSource;
+import com.android.settingslib.deviceinfo.PrivateStorageInfo;
+
+import java.util.concurrent.TimeUnit;
+
+public class CachedStorageValuesHelper {
+
+    @VisibleForTesting public static final String SHARED_PREFERENCES_NAME = "CachedStorageValues";
+    public static final String TIMESTAMP_KEY = "last_query_timestamp";
+    public static final String FREE_BYTES_KEY = "free_bytes";
+    public static final String TOTAL_BYTES_KEY = "total_bytes";
+    public static final String GAME_APPS_SIZE_KEY = "game_apps_size";
+    public static final String MUSIC_APPS_SIZE_KEY = "music_apps_size";
+    public static final String VIDEO_APPS_SIZE_KEY = "video_apps_size";
+    public static final String PHOTO_APPS_SIZE_KEY = "photo_apps_size";
+    public static final String OTHER_APPS_SIZE_KEY = "other_apps_size";
+    public static final String CACHE_APPS_SIZE_KEY = "cache_apps_size";
+    public static final String EXTERNAL_TOTAL_BYTES = "external_total_bytes";
+    public static final String EXTERNAL_AUDIO_BYTES = "external_audio_bytes";
+    public static final String EXTERNAL_VIDEO_BYTES = "external_video_bytes";
+    public static final String EXTERNAL_IMAGE_BYTES = "external_image_bytes";
+    public static final String EXTERNAL_APP_BYTES = "external_apps_bytes";
+    public static final String USER_ID_KEY = "user_id";
+    private final Long mClobberThreshold;
+    private final SharedPreferences mSharedPreferences;
+    private final int mUserId;
+    // This clock is used to provide the time. By default, it uses the system clock, but can be
+    // replaced for test purposes.
+    protected Clock mClock;
+
+    public CachedStorageValuesHelper(Context context, int userId) {
+        mSharedPreferences =
+                context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        mClock = new Clock();
+        mUserId = userId;
+        mClobberThreshold =
+                Settings.Global.getLong(
+                        context.getContentResolver(),
+                        Settings.Global.STORAGE_SETTINGS_CLOBBER_THRESHOLD,
+                        TimeUnit.MINUTES.toMillis(5));
+    }
+
+    public PrivateStorageInfo getCachedPrivateStorageInfo() {
+        if (!isDataValid()) {
+            return null;
+        }
+        final long freeBytes = mSharedPreferences.getLong(FREE_BYTES_KEY, -1);
+        final long totalBytes = mSharedPreferences.getLong(TOTAL_BYTES_KEY, -1);
+        if (freeBytes < 0 || totalBytes < 0) {
+            return null;
+        }
+
+        return new PrivateStorageInfo(freeBytes, totalBytes);
+    }
+
+    public SparseArray<StorageAsyncLoader.AppsStorageResult> getCachedAppsStorageResult() {
+        if (!isDataValid()) {
+            return null;
+        }
+        final long gamesSize = mSharedPreferences.getLong(GAME_APPS_SIZE_KEY, -1);
+        final long musicAppsSize = mSharedPreferences.getLong(MUSIC_APPS_SIZE_KEY, -1);
+        final long videoAppsSize = mSharedPreferences.getLong(VIDEO_APPS_SIZE_KEY, -1);
+        final long photoAppSize = mSharedPreferences.getLong(PHOTO_APPS_SIZE_KEY, -1);
+        final long otherAppsSize = mSharedPreferences.getLong(OTHER_APPS_SIZE_KEY, -1);
+        final long cacheSize = mSharedPreferences.getLong(CACHE_APPS_SIZE_KEY, -1);
+        if (gamesSize < 0
+                || musicAppsSize < 0
+                || videoAppsSize < 0
+                || photoAppSize < 0
+                || otherAppsSize < 0
+                || cacheSize < 0) {
+            return null;
+        }
+
+        final long externalTotalBytes = mSharedPreferences.getLong(EXTERNAL_TOTAL_BYTES, -1);
+        final long externalAudioBytes = mSharedPreferences.getLong(EXTERNAL_AUDIO_BYTES, -1);
+        final long externalVideoBytes = mSharedPreferences.getLong(EXTERNAL_VIDEO_BYTES, -1);
+        final long externalImageBytes = mSharedPreferences.getLong(EXTERNAL_IMAGE_BYTES, -1);
+        final long externalAppBytes = mSharedPreferences.getLong(EXTERNAL_APP_BYTES, -1);
+        if (externalTotalBytes < 0
+                || externalAudioBytes < 0
+                || externalVideoBytes < 0
+                || externalImageBytes < 0
+                || externalAppBytes < 0) {
+            return null;
+        }
+
+        final StorageStatsSource.ExternalStorageStats externalStats =
+                new StorageStatsSource.ExternalStorageStats(
+                        externalTotalBytes,
+                        externalAudioBytes,
+                        externalVideoBytes,
+                        externalImageBytes,
+                        externalAppBytes);
+        final StorageAsyncLoader.AppsStorageResult result =
+                new StorageAsyncLoader.AppsStorageResult();
+        result.gamesSize = gamesSize;
+        result.musicAppsSize = musicAppsSize;
+        result.videoAppsSize = videoAppsSize;
+        result.photosAppsSize = photoAppSize;
+        result.otherAppsSize = otherAppsSize;
+        result.cacheSize = cacheSize;
+        result.externalStats = externalStats;
+        final SparseArray<StorageAsyncLoader.AppsStorageResult> resultArray = new SparseArray<>();
+        resultArray.append(mUserId, result);
+        return resultArray;
+    }
+
+    public void cacheResult(
+            PrivateStorageInfo storageInfo, StorageAsyncLoader.AppsStorageResult result) {
+        mSharedPreferences
+                .edit()
+                .putLong(FREE_BYTES_KEY, storageInfo.freeBytes)
+                .putLong(TOTAL_BYTES_KEY, storageInfo.totalBytes)
+                .putLong(GAME_APPS_SIZE_KEY, result.gamesSize)
+                .putLong(MUSIC_APPS_SIZE_KEY, result.musicAppsSize)
+                .putLong(VIDEO_APPS_SIZE_KEY, result.videoAppsSize)
+                .putLong(PHOTO_APPS_SIZE_KEY, result.photosAppsSize)
+                .putLong(OTHER_APPS_SIZE_KEY, result.otherAppsSize)
+                .putLong(CACHE_APPS_SIZE_KEY, result.cacheSize)
+                .putLong(EXTERNAL_TOTAL_BYTES, result.externalStats.totalBytes)
+                .putLong(EXTERNAL_AUDIO_BYTES, result.externalStats.audioBytes)
+                .putLong(EXTERNAL_VIDEO_BYTES, result.externalStats.videoBytes)
+                .putLong(EXTERNAL_IMAGE_BYTES, result.externalStats.imageBytes)
+                .putLong(EXTERNAL_APP_BYTES, result.externalStats.appBytes)
+                .putInt(USER_ID_KEY, mUserId)
+                .putLong(TIMESTAMP_KEY, mClock.getCurrentTime())
+                .apply();
+    }
+
+    private boolean isDataValid() {
+        final int cachedUserId = mSharedPreferences.getInt(USER_ID_KEY, -1);
+        if (cachedUserId != mUserId) {
+            return false;
+        }
+
+        final long lastQueryTime = mSharedPreferences.getLong(TIMESTAMP_KEY, Long.MAX_VALUE);
+        final long currentTime = mClock.getCurrentTime();
+        return currentTime - lastQueryTime < mClobberThreshold;
+    }
+
+    /** Clock provides the current time. */
+    static class Clock {
+        public long getCurrentTime() {
+            return System.currentTimeMillis();
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java
index b2d259a..a87f563 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java
@@ -20,13 +20,18 @@
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.os.storage.StorageManager;
 import android.provider.SearchIndexableResource;
+import android.util.SparseArray;
 
+import com.android.settings.deviceinfo.storage.CachedStorageValuesHelper;
+import com.android.settings.deviceinfo.storage.StorageAsyncLoader;
 import com.android.settings.testutils.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
+import com.android.settingslib.deviceinfo.PrivateStorageInfo;
 import com.android.settingslib.drawer.CategoryKey;
 
 import org.junit.Before;
@@ -69,6 +74,47 @@
     }
 
     @Test
+    public void test_cacheProviderProvidesValuesIfBothCached() {
+        CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class);
+        PrivateStorageInfo info = new PrivateStorageInfo(0, 0);
+        when(helper.getCachedPrivateStorageInfo()).thenReturn(info);
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result = new SparseArray<>();
+        when(helper.getCachedAppsStorageResult()).thenReturn(result);
+
+        mFragment.setCachedStorageValuesHelper(helper);
+        mFragment.initializeCachedValues();
+
+        assertThat(mFragment.getPrivateStorageInfo()).isEqualTo(info);
+        assertThat(mFragment.getAppsStorageResult()).isEqualTo(result);
+    }
+
+    @Test
+    public void test_cacheProviderDoesntProvideValuesIfAppsMissing() {
+        CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class);
+        PrivateStorageInfo info = new PrivateStorageInfo(0, 0);
+        when(helper.getCachedPrivateStorageInfo()).thenReturn(info);
+
+        mFragment.setCachedStorageValuesHelper(helper);
+        mFragment.initializeCachedValues();
+
+        assertThat(mFragment.getPrivateStorageInfo()).isNull();
+        assertThat(mFragment.getAppsStorageResult()).isNull();
+    }
+
+    @Test
+    public void test_cacheProviderDoesntProvideValuesIfVolumeInfoMissing() {
+        CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class);
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result = new SparseArray<>();
+        when(helper.getCachedAppsStorageResult()).thenReturn(result);
+
+        mFragment.setCachedStorageValuesHelper(helper);
+        mFragment.initializeCachedValues();
+
+        assertThat(mFragment.getPrivateStorageInfo()).isNull();
+        assertThat(mFragment.getAppsStorageResult()).isNull();
+    }
+
+    @Test
     public void testSearchIndexProvider_shouldIndexResource() {
         final List<SearchIndexableResource> indexRes =
                 StorageDashboardFragment.SEARCH_INDEX_DATA_PROVIDER.getXmlResourcesToIndex(
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java
new file mode 100644
index 0000000..154a7a1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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.android.settings.deviceinfo.storage.CachedStorageValuesHelper.CACHE_APPS_SIZE_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_APP_BYTES;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_AUDIO_BYTES;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_IMAGE_BYTES;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_TOTAL_BYTES;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_VIDEO_BYTES;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.FREE_BYTES_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.GAME_APPS_SIZE_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.MUSIC_APPS_SIZE_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.OTHER_APPS_SIZE_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.PHOTO_APPS_SIZE_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.SHARED_PREFERENCES_NAME;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.TIMESTAMP_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.TOTAL_BYTES_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.USER_ID_KEY;
+import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.VIDEO_APPS_SIZE_KEY;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.SparseArray;
+
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settingslib.applications.StorageStatsSource;
+import com.android.settingslib.deviceinfo.PrivateStorageInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CachedStorageValuesHelperTest {
+    private Context mContext;
+
+    @Mock private CachedStorageValuesHelper.Clock mMockClock;
+    private CachedStorageValuesHelper mCachedValuesHelper;
+    private SharedPreferences mSharedPreferences;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application.getApplicationContext();
+        mSharedPreferences = mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, 0);
+        mCachedValuesHelper = new CachedStorageValuesHelper(mContext, 0);
+        mCachedValuesHelper.mClock = mMockClock;
+    }
+
+    @Test
+    public void getCachedPrivateStorageInfo_cachedValuesAreLoaded() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10001L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 0)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 2)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 6000L)
+                .putInt(USER_ID_KEY, 0)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo();
+
+        assertThat(info.freeBytes).isEqualTo(1000L);
+        assertThat(info.totalBytes).isEqualTo(6000L);
+    }
+
+    @Test
+    public void getCachedAppsStorageResult_cachedValuesAreLoaded() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10001L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 1)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 222222)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 5000L)
+                .putInt(USER_ID_KEY, 0)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result =
+                mCachedValuesHelper.getCachedAppsStorageResult();
+
+        StorageAsyncLoader.AppsStorageResult primaryResult = result.get(0);
+        assertThat(primaryResult.gamesSize).isEqualTo(1L);
+        assertThat(primaryResult.musicAppsSize).isEqualTo(10L);
+        assertThat(primaryResult.videoAppsSize).isEqualTo(100L);
+        assertThat(primaryResult.photosAppsSize).isEqualTo(1000L);
+        assertThat(primaryResult.otherAppsSize).isEqualTo(10000L);
+        assertThat(primaryResult.cacheSize).isEqualTo(100000L);
+        assertThat(primaryResult.externalStats.totalBytes).isEqualTo(222222L);
+        assertThat(primaryResult.externalStats.audioBytes).isEqualTo(22L);
+        assertThat(primaryResult.externalStats.videoBytes).isEqualTo(222L);
+        assertThat(primaryResult.externalStats.imageBytes).isEqualTo(2222L);
+        assertThat(primaryResult.externalStats.appBytes).isEqualTo(22222L);
+    }
+
+    @Test
+    public void getCachedPrivateStorageInfo_nullIfDataIsStale() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10000000L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 0)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 2)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 5000L)
+                .putInt(USER_ID_KEY, 0)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo();
+        assertThat(info).isNull();
+    }
+
+    @Test
+    public void getCachedAppsStorageResult_nullIfDataIsStale() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10000000L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 0)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 2)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 5000L)
+                .putInt(USER_ID_KEY, 0)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result =
+                mCachedValuesHelper.getCachedAppsStorageResult();
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void getCachedPrivateStorageInfo_nullIfWrongUser() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10001L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 0)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 2)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 5000L)
+                .putInt(USER_ID_KEY, 1)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo();
+        assertThat(info).isNull();
+    }
+
+    @Test
+    public void getCachedAppsStorageResult_nullIfWrongUser() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10001L);
+        mSharedPreferences
+                .edit()
+                .putLong(GAME_APPS_SIZE_KEY, 0)
+                .putLong(MUSIC_APPS_SIZE_KEY, 10)
+                .putLong(VIDEO_APPS_SIZE_KEY, 100)
+                .putLong(PHOTO_APPS_SIZE_KEY, 1000)
+                .putLong(OTHER_APPS_SIZE_KEY, 10000)
+                .putLong(CACHE_APPS_SIZE_KEY, 100000)
+                .putLong(EXTERNAL_TOTAL_BYTES, 2)
+                .putLong(EXTERNAL_AUDIO_BYTES, 22)
+                .putLong(EXTERNAL_VIDEO_BYTES, 222)
+                .putLong(EXTERNAL_IMAGE_BYTES, 2222)
+                .putLong(EXTERNAL_APP_BYTES, 22222)
+                .putLong(FREE_BYTES_KEY, 1000L)
+                .putLong(TOTAL_BYTES_KEY, 5000L)
+                .putInt(USER_ID_KEY, 1)
+                .putLong(TIMESTAMP_KEY, 10000L)
+                .apply();
+
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result =
+                mCachedValuesHelper.getCachedAppsStorageResult();
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void getCachedPrivateStorageInfo_nullIfEmpty() throws Exception {
+        PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo();
+        assertThat(info).isNull();
+    }
+
+    @Test
+    public void getCachedAppsStorageResult_nullIfEmpty() throws Exception {
+        SparseArray<StorageAsyncLoader.AppsStorageResult> result =
+                mCachedValuesHelper.getCachedAppsStorageResult();
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void cacheResult_succeeds() throws Exception {
+        when(mMockClock.getCurrentTime()).thenReturn(10000L);
+        final StorageStatsSource.ExternalStorageStats externalStats =
+                new StorageStatsSource.ExternalStorageStats(22222l, 2l, 20L, 200L, 2000L);
+        final StorageAsyncLoader.AppsStorageResult result =
+                new StorageAsyncLoader.AppsStorageResult();
+        result.gamesSize = 1L;
+        result.musicAppsSize = 10l;
+        result.videoAppsSize = 100L;
+        result.photosAppsSize = 1000L;
+        result.otherAppsSize = 10000L;
+        result.cacheSize = 100000l;
+        result.externalStats = externalStats;
+        final PrivateStorageInfo info = new PrivateStorageInfo(1000L, 6000L);
+
+        mCachedValuesHelper.cacheResult(info, result);
+
+        assertThat(mSharedPreferences.getLong(GAME_APPS_SIZE_KEY, -1)).isEqualTo(1L);
+        assertThat(mSharedPreferences.getLong(MUSIC_APPS_SIZE_KEY, -1)).isEqualTo(10L);
+        assertThat(mSharedPreferences.getLong(VIDEO_APPS_SIZE_KEY, -1)).isEqualTo(100L);
+        assertThat(mSharedPreferences.getLong(PHOTO_APPS_SIZE_KEY, -1)).isEqualTo(1000L);
+        assertThat(mSharedPreferences.getLong(OTHER_APPS_SIZE_KEY, -1)).isEqualTo(10000L);
+        assertThat(mSharedPreferences.getLong(CACHE_APPS_SIZE_KEY, -1)).isEqualTo(100000L);
+        assertThat(mSharedPreferences.getLong(EXTERNAL_TOTAL_BYTES, -1)).isEqualTo(22222L);
+        assertThat(mSharedPreferences.getLong(EXTERNAL_AUDIO_BYTES, -1)).isEqualTo(2L);
+        assertThat(mSharedPreferences.getLong(EXTERNAL_VIDEO_BYTES, -1)).isEqualTo(20L);
+        assertThat(mSharedPreferences.getLong(EXTERNAL_IMAGE_BYTES, -1)).isEqualTo(200L);
+        assertThat(mSharedPreferences.getLong(EXTERNAL_APP_BYTES, -1)).isEqualTo(2000L);
+        assertThat(mSharedPreferences.getLong(FREE_BYTES_KEY, -1)).isEqualTo(1000L);
+        assertThat(mSharedPreferences.getLong(TOTAL_BYTES_KEY, -1)).isEqualTo(6000L);
+        assertThat(mSharedPreferences.getInt(USER_ID_KEY, -1)).isEqualTo(0);
+        assertThat(mSharedPreferences.getLong(TIMESTAMP_KEY, -1)).isEqualTo(10000L);
+    };
+}