Merge "Enable the backup settings activity before launching"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 34f3c51..da10faf 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1209,9 +1209,9 @@
     <string name="lock_profile_wipe_dismiss">Dismiss</string>
 
     <!-- Hint shown in dialog screen when password is too short -->
-    <string name="lockpassword_password_too_short">Must be at least %d characters</string>
+    <string name="lockpassword_password_too_short">Must be at least <xliff:g id="count" example="3">%d</xliff:g> characters</string>
     <!-- Hint shown in dialog screen when PIN is too short -->
-    <string name="lockpassword_pin_too_short">PIN must be at least %d digits</string>
+    <string name="lockpassword_pin_too_short">PIN must be at least <xliff:g id="count" example="3">%d</xliff:g> digits</string>
 
     <!-- Hint shown after minimum password criteria is met -->
     <string name="lockpassword_continue_label">Continue</string>
@@ -1242,37 +1242,37 @@
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of letters -->
     <plurals name="lockpassword_password_requires_letters">
         <item quantity="one">Must contain at least 1 letter</item>
-        <item quantity="other">Must contain at least %d letters</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> letters</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of lowercase letters -->
     <plurals name="lockpassword_password_requires_lowercase">
         <item quantity="one">Must contain at least 1 lowercase letter</item>
-        <item quantity="other">Must contain at least %d lowercase letters</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> lowercase letters</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of uppercase letters -->
     <plurals name="lockpassword_password_requires_uppercase">
         <item quantity="one">Must contain at least 1 uppercase letter</item>
-        <item quantity="other">Must contain at least %d uppercase letters</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> uppercase letters</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of numerical digits -->
     <plurals name="lockpassword_password_requires_numeric">
         <item quantity="one">Must contain at least 1 numerical digit</item>
-        <item quantity="other">Must contain at least %d numerical digits</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> numerical digits</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of special symbols -->
     <plurals name="lockpassword_password_requires_symbols">
         <item quantity="one">Must contain at least 1 special symbol</item>
-        <item quantity="other">Must contain at least %d special symbols</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> special symbols</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password doesn't contain the required number of non-letter characters -->
     <plurals name="lockpassword_password_requires_nonletter">
         <item quantity="one">Must contain at least 1 non-letter character</item>
-        <item quantity="other">Must contain at least %d non-letter characters</item>
+        <item quantity="other">Must contain at least <xliff:g id="count" example="3">%d</xliff:g> non-letter characters</item>
     </plurals>
 
     <!-- Error shown when in PASSWORD mode and password has been used recently. Please keep this string short! -->
diff --git a/res/xml/wifi_configure_settings.xml b/res/xml/wifi_configure_settings.xml
index 66c9140..ad5315d 100644
--- a/res/xml/wifi_configure_settings.xml
+++ b/res/xml/wifi_configure_settings.xml
@@ -17,12 +17,10 @@
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
         android:title="@string/wifi_configure_titlebar">
 
-    <!-- android:dependency="enable_wifi" -->
-    <ListPreference
-        android:key="sleep_policy"
-        android:title="@string/wifi_setting_sleep_policy_title"
-        android:entries="@array/wifi_sleep_policy_entries"
-        android:entryValues="@array/wifi_sleep_policy_values" />
+    <SwitchPreference
+            android:key="enable_wifi_wakeup"
+            android:title="@string/wifi_wakeup"
+            android:summary="@string/wifi_wakeup_summary" />
 
     <SwitchPreference
       android:key="allow_recommendations"
@@ -35,22 +33,15 @@
         android:summary="@string/wifi_notify_open_networks_summary" />
 
     <SwitchPreference
-        android:key="enable_wifi_wakeup"
-        android:title="@string/wifi_wakeup"
-        android:summary="@string/wifi_wakeup_summary" />
-
-    <SwitchPreference
         android:key="wifi_cellular_data_fallback"
         android:title="@string/wifi_cellular_data_fallback_title"
         android:summary="@string/wifi_cellular_data_fallback_summary"/>
 
-    <Preference
-        android:key="mac_address"
-        android:title="@string/wifi_advanced_mac_address_title"/>
-
-    <Preference
-        android:key="current_ip_address"
-        android:title="@string/wifi_advanced_ip_address_title"/>
+    <ListPreference
+            android:key="sleep_policy"
+            android:title="@string/wifi_setting_sleep_policy_title"
+            android:entries="@array/wifi_sleep_policy_entries"
+            android:entryValues="@array/wifi_sleep_policy_values" />
 
     <Preference
             android:key="install_credentials"
@@ -77,4 +68,12 @@
             android:key="wps_pin_entry"
             android:title="@string/wifi_menu_wps_pin" />
 
+    <Preference
+            android:key="mac_address"
+            android:title="@string/wifi_advanced_mac_address_title"/>
+
+    <Preference
+            android:key="current_ip_address"
+            android:title="@string/wifi_advanced_ip_address_title"/>
+
 </PreferenceScreen>
diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java
index abf0154..dc7b7aa 100644
--- a/src/com/android/settings/DevelopmentSettings.java
+++ b/src/com/android/settings/DevelopmentSettings.java
@@ -1434,7 +1434,7 @@
         Settings.Global.putInt(getActivity().getContentResolver(),
                 Settings.Global.DEVELOPMENT_FORCE_RTL, value ? 1 : 0);
         SystemProperties.set(Settings.Global.DEVELOPMENT_FORCE_RTL, value ? "1" : "0");
-        LocalePicker.updateLocale(getActivity().getResources().getConfiguration().locale);
+        LocalePicker.updateLocales(getActivity().getResources().getConfiguration().getLocales());
     }
 
     private void updateWifiDisplayCertificationOptions() {
@@ -1805,9 +1805,11 @@
         synchronized (mBluetoothA2dpLock) {
             if (mBluetoothA2dp != null) {
                 codecStatus = mBluetoothA2dp.getCodecStatus();
-                codecConfig = codecStatus.getCodecConfig();
-                codecsLocalCapabilities = codecStatus.getCodecsLocalCapabilities();
-                codecsSelectableCapabilities = codecStatus.getCodecsSelectableCapabilities();
+                if (codecStatus != null) {
+                    codecConfig = codecStatus.getCodecConfig();
+                    codecsLocalCapabilities = codecStatus.getCodecsLocalCapabilities();
+                    codecsSelectableCapabilities = codecStatus.getCodecsSelectableCapabilities();
+                }
             }
         }
         if (codecConfig == null)
diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
index 61f3e95..ea196ad 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.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 @@
 
 public class StorageDashboardFragment extends DashboardFragment {
     private static final String TAG = "StorageDashboardFrag";
+    private static final int APPS_JOB_ID = 0;
 
     private VolumeInfo mVolume;
 
@@ -54,6 +58,12 @@
     }
 
     @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 0000000..cbedb08
--- /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<AppsAsyncLoader.AppsStorageResult> {
+    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<Integer> seenUid = new ArraySet<>(); // some apps share a uid
+
+        List<ApplicationInfo> 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 16dcd18..5437dcb 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.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 @@
  * categorization breakdown.
  */
 public class StorageItemPreferenceController extends PreferenceController
-        implements StorageMeasurement.MeasurementReceiver, LifecycleObserver, OnDestroy {
+        implements StorageMeasurement.MeasurementReceiver, LifecycleObserver, OnDestroy,
+        LoaderManager.LoaderCallbacks<AppsAsyncLoader.AppsStorageResult> {
     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 @@
             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 @@
         mFilePreference = (StorageItemPreferenceAlternate) screen.findPreference(FILES_KEY);
     }
 
+    @Override
+    public Loader<AppsAsyncLoader.AppsStorageResult> 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<AppsAsyncLoader.AppsStorageResult> loader,
+            AppsAsyncLoader.AppsStorageResult data) {
+        mGamePreference.setStorageSize(data.gamesSize);
+        mAppPreference.setStorageSize(data.otherAppsSize);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<AppsAsyncLoader.AppsStorageResult> 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 b6e03fb..98038fd 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 @@
  * 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 @@
             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/src/com/android/settings/wifi/ConfigureWifiSettings.java b/src/com/android/settings/wifi/ConfigureWifiSettings.java
index 5fd3eeb..2b4743f 100644
--- a/src/com/android/settings/wifi/ConfigureWifiSettings.java
+++ b/src/com/android/settings/wifi/ConfigureWifiSettings.java
@@ -50,6 +50,12 @@
     }
 
     @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mProgressiveDisclosureMixin.setTileLimit(5);
+    }
+
+    @Override
     protected int getPreferenceScreenResId() {
         return R.xml.wifi_configure_settings;
     }
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 f7baba3..cfec382 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 @@
         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/robotests/src/com/android/settings/notification/WorkSoundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/WorkSoundPreferenceControllerTest.java
index 419fd00..acfd400 100644
--- a/tests/robotests/src/com/android/settings/notification/WorkSoundPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/WorkSoundPreferenceControllerTest.java
@@ -193,6 +193,30 @@
         verify(preference).setSummary(anyString());
     }
 
+    @Test
+    public void onResume_availableButLocked_shouldRedactPreferences() {
+        final String notAvailable = "(not available)";
+        when(mContext.getString(R.string.managed_profile_not_available_label))
+                .thenReturn(notAvailable);
+
+        // Given a device with a managed profile:
+        when(mAudioHelper.isSingleVolume()).thenReturn(false);
+        when(mFragment.getPreferenceScreen()).thenReturn(mScreen);
+        when(mAudioHelper.createPackageContextAsUser(anyInt())).thenReturn(mContext);
+        when(mAudioHelper.getManagedProfileId(any(UserManager.class)))
+                .thenReturn(UserHandle.myUserId());
+        when(mAudioHelper.isUserUnlocked(any(UserManager.class), anyInt())).thenReturn(false);
+        mockWorkCategory();
+
+        // When resumed:
+        mController.onResume();
+
+        // Sound preferences should explain that the profile isn't available yet.
+        verify(mScreen.findPreference(KEY_WORK_PHONE_RINGTONE)).setSummary(eq(notAvailable));
+        verify(mScreen.findPreference(KEY_WORK_NOTIFICATION_RINGTONE)).setSummary(eq(notAvailable));
+        verify(mScreen.findPreference(KEY_WORK_ALARM_RINGTONE)).setSummary(eq(notAvailable));
+    }
+
     private void mockWorkCategory() {
         when(mScreen.findPreference(KEY_WORK_CATEGORY))
             .thenReturn(mock(PreferenceGroup.class));
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 0000000..8d4dd2e
--- /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<ApplicationInfo> 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);
+    }
+}