Merge "Rework the location navigation flow" into pi-car-dev
diff --git a/Android.mk b/Android.mk
index 1fd2fc6..2e19d00 100644
--- a/Android.mk
+++ b/Android.mk
@@ -29,7 +29,8 @@
   LOCAL_USE_AAPT2 := true
 
   LOCAL_JAVA_LIBRARIES := \
-      android.car
+      android.car \
+      telephony-common
 
   LOCAL_STATIC_ANDROID_LIBRARIES := \
       androidx.car_car \
@@ -89,7 +90,8 @@
   LOCAL_USE_AAPT2 := true
 
   LOCAL_JAVA_LIBRARIES := \
-      android.car
+      android.car \
+      telephony-common
 
   LOCAL_STATIC_ANDROID_LIBRARIES := \
       androidx.car_car \
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 15a9ee8..e69f518 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -266,6 +266,11 @@
         <activity android:name=".security.CheckLockActivity"
                   android:configChanges="orientation|keyboardHidden|screenSize"
                   android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL" />
+                <action android:name="android.app.action.CONFIRM_FRP_CREDENTIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
 
         <activity android:name=".system.ThirdPartyLicensesActivity"
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..62cb3be
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 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.
+-->
+
+<resources>
+
+    <!-- Values for premium SMS permission selector [CHAR LIMIT=30] -->
+    <string-array name="premium_sms_access_values">
+        <!-- Ask user before sending to premium SMS short code. -->
+        <item>Ask</item>
+        <!-- Never allow app to send to premium SMS short code. -->
+        <item>Never allow</item>
+        <!-- Always allow app to send to premium SMS short code. -->
+        <item>Always allow</item>
+    </string-array>
+
+</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 1bfa3dc..21b7908 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,6 +24,8 @@
     <bool name="config_show_wifi_mac_address">true</bool>
     <!-- Whether to show regulatory info or not. -->
     <bool name="config_show_regulatory_info">true</bool>
+    <!-- Whether premium SMS should be shown or not. -->
+    <bool name="config_show_premium_sms">true</bool>
 
     <!-- The component which listens for the enabling of developer options. -->
     <string name="config_dev_options_module" translatable="false">com.android.car.developeroptions/.Settings$DevelopmentSettingsDashboardActivity</string>
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index 7a34f12..2690118 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -180,6 +180,12 @@
     <string name="pk_notification_access_entry" translatable="false">notification_access_entry
     </string>
     <string name="pk_notification_access" translatable="false">notification_access</string>
+    <string name="pk_premium_sms_access_entry" translatable="false">premium_sms_access_entry
+    </string>
+    <string name="pk_premium_sms_access" translatable="false">premium_sms_access</string>
+    <string name="pk_premium_sms_access_description" translatable="false">
+        premium_sms_access_description
+    </string>
     <string name="pk_usage_access_entry" translatable="false">usage_access_entry</string>
     <string name="pk_usage_access" translatable="false">usage_access</string>
     <string name="pk_usage_access_description" translatable="false">usage_access_description
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c5e510d..cfa2779 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -483,7 +483,7 @@
     <string name="show_system">Show system</string>
     <!-- Text for toggle button to control whether system processes are hidden in app lists. [CHAR_LIMIT=30] -->
     <string name="hide_system">Hide system</string>
-    <!-- Title for managing apps which can modify system settings. [CHAR_LIMIT=30] -->
+    <!-- Title for managing apps which can modify system settings. [CHAR_LIMIT=40] -->
     <string name="modify_system_settings_title">Modify system settings</string>
     <!-- Description of modifying system settings. [CHAR_LIMIT=NONE] -->
     <string name="modify_system_settings_description">This permission allows an app to modify system settings.</string>
@@ -499,6 +499,10 @@
     <string name="notification_listener_revoke_warning_confirm">Turn off</string>
     <!-- Negative button for a dialog warning message about revoking notification listener access. [CHAR_LIMIT=30] -->
     <string name="notification_listener_revoke_warning_cancel">Cancel</string>
+    <!-- Title for screen controlling which apps have access to send premium sms messages. [CHAR_LIMIT=60] -->
+    <string name="premium_sms_access_title">Premium SMS access</string>
+    <!-- Message shown for option to enable Premium SMS for an app. [CHAR_LIMIT=NONE] -->
+    <string name="premium_sms_access_description">Premium SMS may cost you money and will add up to your carrier bills. If you enable permission for an app, you will be able to send premium SMS using that app.</string>
     <!-- Title for managing apps which can query usage data. [CHAR_LIMIT=30] -->
     <string name="usage_access_title">Usage access</string>
     <!-- Description of the usage access permission. [CHAR_LIMIT=NONE] -->
@@ -506,7 +510,7 @@
     <!-- Title for managing apps which can change Wi-Fi state. [CHAR_LIMIT=30] -->
     <string name="wifi_control_title">Wi-Fi control</string>
     <!-- Description of the change wifi state permission. [CHAR_LIMIT=NONE] -->
-    <string name="wifi_control_description">Wi-Fi control allow an app to turn Wi-Fi on or off, scan and connect to Wi-Fi networks, add or remove networks, or start a local-only hotspot.</string>
+    <string name="wifi_control_description">Wi-Fi control allows an app to turn Wi-Fi on or off, scan and connect to Wi-Fi networks, add or remove networks, or start a local-only hotspot.</string>
 
     <!-- Location --><skip/>
     <!-- Main setting menu item to go into location settings [CHAR LIMIT=40] -->
diff --git a/res/xml/premium_sms_access_fragment.xml b/res/xml/premium_sms_access_fragment.xml
new file mode 100644
index 0000000..829cfd9
--- /dev/null
+++ b/res/xml/premium_sms_access_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/premium_sms_access_title">
+    <Preference
+        android:key="@string/pk_premium_sms_access_description"
+        android:selectable="false"
+        android:summary="@string/premium_sms_access_description"/>
+    <com.android.car.settings.common.LogicalPreferenceGroup
+        android:key="@string/pk_premium_sms_access"
+        settings:controller="com.android.car.settings.applications.specialaccess.PremiumSmsAccessPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/special_access_fragment.xml b/res/xml/special_access_fragment.xml
index 33c1706..a9abce1 100644
--- a/res/xml/special_access_fragment.xml
+++ b/res/xml/special_access_fragment.xml
@@ -30,6 +30,11 @@
         android:title="@string/notification_access_title"
         settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
     <Preference
+        android:fragment="com.android.car.settings.applications.specialaccess.PremiumSmsAccessFragment"
+        android:key="@string/pk_premium_sms_access_entry"
+        android:title="@string/premium_sms_access_title"
+        settings:controller="com.android.car.settings.applications.specialaccess.PremiumSmsAccessEntryPreferenceController"/>
+    <Preference
         android:fragment="com.android.car.settings.applications.specialaccess.UsageAccessFragment"
         android:key="@string/pk_usage_access_entry"
         android:title="@string/usage_access_title"
diff --git a/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java b/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java
new file mode 100644
index 0000000..7359f80
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Manages a list of {@link ApplicationsState.AppEntry} instances by syncing in the background and
+ * providing updates via a {@link Callback}. Clients may provide an {@link ExtraInfoBridge} to
+ * populate the {@link ApplicationsState.AppEntry#extraInfo} field with use case sepecific data.
+ * Clients may also provide an {@link ApplicationsState.AppFilter} via an {@link AppFilterProvider}
+ * to determine which entries will appear in the list updates.
+ *
+ * <p>Clients should call {@link #init(ExtraInfoBridge, AppFilterProvider, Callback)} to specify
+ * behavior and then {@link #start()} to begin loading. {@link #stop()} will cancel loading, and
+ * {@link #destroy()} will clean up resources when this class will no longer be used.
+ */
+public class AppEntryListManager {
+
+    /** Callback for receiving events from {@link AppEntryListManager}. */
+    public interface Callback {
+        /**
+         * Called when the list of {@link ApplicationsState.AppEntry} instances or the {@link
+         * ApplicationsState.AppEntry#extraInfo} fields have changed.
+         */
+        void onAppEntryListChanged(List<ApplicationsState.AppEntry> entries);
+    }
+
+    /**
+     * Provides an {@link ApplicationsState.AppFilter} to tailor the entries in the list updates.
+     */
+    public interface AppFilterProvider {
+        /**
+         * Returns the filter that should be used to trim the entries list before callback delivery.
+         */
+        ApplicationsState.AppFilter getAppFilter();
+    }
+
+    /** Bridges extra information to {@link ApplicationsState.AppEntry#extraInfo}. */
+    public interface ExtraInfoBridge {
+        /**
+         * Populates the {@link ApplicationsState.AppEntry#extraInfo} field on the {@code enrties}
+         * with the relevant data for the implementation.
+         */
+        void loadExtraInfo(List<ApplicationsState.AppEntry> entries);
+    }
+
+    private final ApplicationsState.Callbacks mSessionCallbacks =
+            new ApplicationsState.Callbacks() {
+                @Override
+                public void onRunningStateChanged(boolean running) {
+                    // No op.
+                }
+
+                @Override
+                public void onPackageListChanged() {
+                    forceUpdate();
+                }
+
+                @Override
+                public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
+                    if (mCallback != null) {
+                        mCallback.onAppEntryListChanged(apps);
+                    }
+                }
+
+                @Override
+                public void onPackageIconChanged() {
+                    // No op.
+                }
+
+                @Override
+                public void onPackageSizeChanged(String packageName) {
+                    // No op.
+                }
+
+                @Override
+                public void onAllSizesComputed() {
+                    // No op.
+                }
+
+                @Override
+                public void onLauncherInfoChanged() {
+                    // No op.
+                }
+
+                @Override
+                public void onLoadEntriesCompleted() {
+                    mHasReceivedLoadEntries = true;
+                    forceUpdate();
+                }
+            };
+
+    private final ApplicationsState mApplicationsState;
+    private final BackgroundHandler mBackgroundHandler;
+    private final MainHandler mMainHandler;
+
+    private ExtraInfoBridge mExtraInfoBridge;
+    private AppFilterProvider mFilterProvider;
+    private Callback mCallback;
+    private ApplicationsState.Session mSession;
+
+    private boolean mHasReceivedLoadEntries;
+    private boolean mHasReceivedExtraInfo;
+
+    public AppEntryListManager(Context context) {
+        mApplicationsState = ApplicationsState.getInstance(
+                (Application) context.getApplicationContext());
+        // Run on the same background thread as the ApplicationsState to make sure updates don't
+        // conflict.
+        mBackgroundHandler = new BackgroundHandler(new WeakReference<>(this),
+                mApplicationsState.getBackgroundLooper());
+        mMainHandler = new MainHandler(new WeakReference<>(this));
+    }
+
+    /**
+     * Specifies the behavior of this manager.
+     *
+     * @param extraInfoBridge an optional bridge to load information into the entries.
+     * @param filterProvider  provides a filter to tailor the contents of the list updates.
+     * @param callback        callback to which updated lists are delivered.
+     */
+    public void init(@Nullable ExtraInfoBridge extraInfoBridge,
+            @Nullable AppFilterProvider filterProvider,
+            Callback callback) {
+        if (mSession != null) {
+            destroy();
+        }
+        mExtraInfoBridge = extraInfoBridge;
+        mFilterProvider = filterProvider;
+        mCallback = callback;
+        mSession = mApplicationsState.newSession(mSessionCallbacks);
+    }
+
+    /**
+     * Starts loading the information in the background. When loading is finished, the {@link
+     * Callback} will be notified on the main thread.
+     */
+    public void start() {
+        mSession.onResume();
+    }
+
+    /**
+     * Stops any pending loading.
+     */
+    public void stop() {
+        mSession.onPause();
+        clearHandlers();
+    }
+
+    /**
+     * Cleans up internal state when this will no longer be used.
+     */
+    public void destroy() {
+        mSession.onDestroy();
+        clearHandlers();
+        mExtraInfoBridge = null;
+        mFilterProvider = null;
+        mCallback = null;
+    }
+
+    /**
+     * Schedules updates for all {@link ApplicationsState.AppEntry} instances. When loading is
+     * finished, the {@link Callback} will be notified on the main thread.
+     */
+    public void forceUpdate() {
+        mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
+    }
+
+    /**
+     * Schedules an update for the given {@code entry}. When loading is finished, the {@link
+     * Callback} will be notified on the main thread.
+     */
+    public void forceUpdate(ApplicationsState.AppEntry entry) {
+        mBackgroundHandler.obtainMessage(BackgroundHandler.MSG_LOAD_PKG,
+                entry).sendToTarget();
+    }
+
+    private void rebuild() {
+        if (!mHasReceivedLoadEntries || !mHasReceivedExtraInfo) {
+            // Don't rebuild the list until all the app entries are loaded.
+            return;
+        }
+        mSession.rebuild((mFilterProvider != null) ? mFilterProvider.getAppFilter()
+                        : ApplicationsState.FILTER_EVERYTHING,
+                ApplicationsState.ALPHA_COMPARATOR, /* foreground= */ false);
+    }
+
+    private void clearHandlers() {
+        mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_ALL);
+        mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_PKG);
+        mMainHandler.removeMessages(MainHandler.MSG_INFO_UPDATED);
+    }
+
+    private void loadInfo(List<ApplicationsState.AppEntry> entries) {
+        if (mExtraInfoBridge != null) {
+            mExtraInfoBridge.loadExtraInfo(entries);
+        }
+        for (ApplicationsState.AppEntry entry : entries) {
+            mApplicationsState.ensureIcon(entry);
+        }
+    }
+
+    private static class BackgroundHandler extends Handler {
+        private static final int MSG_LOAD_ALL = 1;
+        private static final int MSG_LOAD_PKG = 2;
+
+        private final WeakReference<AppEntryListManager> mOuter;
+
+        BackgroundHandler(WeakReference<AppEntryListManager> outer, Looper looper) {
+            super(looper);
+            mOuter = outer;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            AppEntryListManager outer = mOuter.get();
+            if (outer == null) {
+                return;
+            }
+            switch (msg.what) {
+                case MSG_LOAD_ALL:
+                    outer.loadInfo(outer.mSession.getAllApps());
+                    outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+                    break;
+                case MSG_LOAD_PKG:
+                    ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) msg.obj;
+                    outer.loadInfo(Collections.singletonList(entry));
+                    outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+                    break;
+            }
+        }
+    }
+
+    private static class MainHandler extends Handler {
+        private static final int MSG_INFO_UPDATED = 1;
+
+        private final WeakReference<AppEntryListManager> mOuter;
+
+        MainHandler(WeakReference<AppEntryListManager> outer) {
+            mOuter = outer;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            AppEntryListManager outer = mOuter.get();
+            if (outer == null) {
+                return;
+            }
+            switch (msg.what) {
+                case MSG_INFO_UPDATED:
+                    outer.mHasReceivedExtraInfo = true;
+                    outer.rebuild();
+                    break;
+            }
+        }
+    }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
index a3fea69..043aec9 100644
--- a/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
+++ b/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
@@ -17,13 +17,13 @@
 package com.android.car.settings.applications.specialaccess;
 
 import android.app.AppOpsManager;
-import android.app.Application;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.SwitchPreference;
@@ -37,7 +37,7 @@
 import com.android.settingslib.applications.ApplicationsState.AppFilter;
 import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Displays a list of toggles for applications requesting permission to perform the operation with
@@ -60,60 +60,6 @@
     };
 
     private final AppOpsManager mAppOpsManager;
-    private final ApplicationsState mApplicationsState;
-
-    private final ApplicationsState.Callbacks mCallbacks =
-            new ApplicationsState.Callbacks() {
-                @Override
-                public void onRunningStateChanged(boolean running) {
-                    // TODO: show loading UI.
-                }
-
-                @Override
-                public void onPackageListChanged() {
-                    rebuild();
-                }
-
-                @Override
-                public void onRebuildComplete(ArrayList<AppEntry> entries) {
-                    mEntries = entries;
-                    refreshUi();
-                }
-
-                @Override
-                public void onPackageIconChanged() {
-                    // No op.
-                }
-
-                @Override
-                public void onPackageSizeChanged(String packageName) {
-                    // No op.
-                }
-
-                @Override
-                public void onAllSizesComputed() {
-                    // No op.
-                }
-
-                @Override
-                public void onLauncherInfoChanged() {
-                    rebuild();
-                }
-
-                @Override
-                public void onLoadEntriesCompleted() {
-                    mHasReceivedLoadEntries = true;
-                    rebuild();
-                }
-            };
-
-    private final AppStateBaseBridge.Callback mBridgeCallback = new AppStateBaseBridge.Callback() {
-        @Override
-        public void onExtraInfoUpdated() {
-            mHasReceivedBridgeCallback = true;
-            rebuild();
-        }
-    };
 
     private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
             new Preference.OnPreferenceChangeListener() {
@@ -128,24 +74,28 @@
                                 entry.info.packageName,
                                 allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode);
                         // Update the extra info of this entry so that it reflects the new mode.
-                        mExtraInfoBridge.forceUpdate(entry);
+                        mAppEntryListManager.forceUpdate(entry);
                         return true;
                     }
                     return false;
                 }
             };
 
+    private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
+        @Override
+        public void onAppEntryListChanged(List<AppEntry> entries) {
+            mEntries = entries;
+            refreshUi();
+        }
+    };
+
     private int mAppOpsOpCode = AppOpsManager.OP_NONE;
     private String mPermission;
     private int mNegativeOpMode = -1;
 
-    private ApplicationsState.Session mSession;
-    private AppStateAppOpsBridge mExtraInfoBridge;
-
-    private ArrayList<AppEntry> mEntries;
-
-    private boolean mHasReceivedLoadEntries;
-    private boolean mHasReceivedBridgeCallback;
+    @VisibleForTesting
+    AppEntryListManager mAppEntryListManager;
+    private List<AppEntry> mEntries;
 
     private boolean mShowSystem;
 
@@ -153,8 +103,7 @@
             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
         super(context, preferenceKey, fragmentController, uxRestrictions);
         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        mApplicationsState = ApplicationsState.getInstance(
-                (Application) context.getApplicationContext());
+        mAppEntryListManager = new AppEntryListManager(context);
     }
 
     @Override
@@ -185,7 +134,7 @@
     public void setShowSystem(boolean showSystem) {
         if (mShowSystem != showSystem) {
             mShowSystem = showSystem;
-            rebuild();
+            mAppEntryListManager.forceUpdate();
         }
     }
 
@@ -204,27 +153,24 @@
 
     @Override
     protected void onCreateInternal() {
-        mSession = mApplicationsState.newSession(mCallbacks);
-        mExtraInfoBridge = new AppStateAppOpsBridge(getContext(), mApplicationsState, mAppOpsOpCode,
-                mPermission, mBridgeCallback);
+        AppStateAppOpsBridge extraInfoBridge = new AppStateAppOpsBridge(getContext(), mAppOpsOpCode,
+                mPermission);
+        mAppEntryListManager.init(extraInfoBridge, this::getAppFilter, mCallback);
     }
 
     @Override
     protected void onStartInternal() {
-        mSession.onResume();
-        mExtraInfoBridge.start();
+        mAppEntryListManager.start();
     }
 
     @Override
     protected void onStopInternal() {
-        mSession.onPause();
-        mExtraInfoBridge.stop();
+        mAppEntryListManager.stop();
     }
 
     @Override
     protected void onDestroyInternal() {
-        mSession.onDestroy();
-        mExtraInfoBridge.destroy();
+        mAppEntryListManager.destroy();
     }
 
     @Override
@@ -235,7 +181,6 @@
         }
         preference.removeAll();
         for (AppEntry entry : mEntries) {
-            mApplicationsState.ensureIcon(entry);
             Preference appOpPreference = new AppOpPreference(getContext(), entry);
             appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
             preference.addPreference(appOpPreference);
@@ -253,15 +198,6 @@
         return filterObj;
     }
 
-    private void rebuild() {
-        if (!mHasReceivedLoadEntries || !mHasReceivedBridgeCallback) {
-            // Don't rebuild the list until all the app entries are loaded.
-            return;
-        }
-        mSession.rebuild(getAppFilter(), ApplicationsState.ALPHA_COMPARATOR, /* foreground= */
-                false);
-    }
-
     private static class AppOpPreference extends SwitchPreference {
 
         private final AppEntry mEntry;
diff --git a/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java b/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
index e278a3a..4a3d0a1 100644
--- a/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
+++ b/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
@@ -32,7 +32,6 @@
 
 import com.android.car.settings.common.Logger;
 import com.android.internal.util.ArrayUtils;
-import com.android.settingslib.applications.ApplicationsState;
 import com.android.settingslib.applications.ApplicationsState.AppEntry;
 
 import java.util.List;
@@ -42,7 +41,7 @@
  * Bridges {@link AppOpsManager} app operation permission information into {@link
  * AppEntry#extraInfo} as {@link PermissionState} objects.
  */
-public class AppStateAppOpsBridge extends AppStateBaseBridge {
+public class AppStateAppOpsBridge implements AppEntryListManager.ExtraInfoBridge {
 
     private static final Logger LOG = new Logger(AppStateAppOpsBridge.class);
 
@@ -59,18 +58,14 @@
      * @param appOpsOpCode the {@link AppOpsManager} op code constant to fetch information for.
      * @param permission   the {@link android.Manifest.permission} required to perform the
      *                     operation.
-     * @param callback     a {@link Callback} which will be notified when the information is
-     *                     finished loading.
      */
-    public AppStateAppOpsBridge(Context context, ApplicationsState appState, int appOpsOpCode,
-            String permission, Callback callback) {
-        this(context, appState, appOpsOpCode, permission, callback, AppGlobals.getPackageManager());
+    public AppStateAppOpsBridge(Context context, int appOpsOpCode, String permission) {
+        this(context, appOpsOpCode, permission, AppGlobals.getPackageManager());
     }
 
     @VisibleForTesting
-    AppStateAppOpsBridge(Context context, ApplicationsState appState, int appOpsOpCode,
-            String permission, Callback callback, IPackageManager packageManager) {
-        super(appState, callback);
+    AppStateAppOpsBridge(Context context, int appOpsOpCode, String permission,
+            IPackageManager packageManager) {
         mContext = context;
         mIPackageManager = packageManager;
         mProfiles = UserManager.get(context).getUserProfiles();
@@ -80,7 +75,7 @@
     }
 
     @Override
-    protected void loadExtraInfo(List<AppEntry> entries) {
+    public void loadExtraInfo(List<AppEntry> entries) {
         SparseArray<Map<String, PermissionState>> packageToStatesMapByProfileId =
                 getPackageToStateMapsByProfileId();
         loadAppOpModes(packageToStatesMapByProfileId);
diff --git a/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridge.java b/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridge.java
deleted file mode 100644
index 507516c..0000000
--- a/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridge.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright 2019 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.car.settings.applications.specialaccess;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-
-import com.android.settingslib.applications.ApplicationsState;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Common base class for bridging information to {@link ApplicationsState.AppEntry#extraInfo}.
- * Subclasses should implement {@link #loadExtraInfo(List)} and populate the fields in the given
- * {@link ApplicationsState.AppEntry} instances.
- */
-public abstract class AppStateBaseBridge {
-
-    /** Callback for receiving events from the bridge. */
-    public interface Callback {
-        /**
-         * Called when the bridge has finished updating all
-         * {@link ApplicationsState.AppEntry#extraInfo} in {@link ApplicationsState}.
-         */
-        void onExtraInfoUpdated();
-    }
-
-    private final ApplicationsState.Session mSession;
-    private final Callback mCallback;
-    private final BackgroundHandler mBackgroundHandler;
-    private final MainHandler mMainHandler;
-
-    private final ApplicationsState.Callbacks mSessionCallbacks =
-            new ApplicationsState.Callbacks() {
-                @Override
-                public void onRunningStateChanged(boolean running) {
-                    // No op.
-                }
-
-                @Override
-                public void onPackageListChanged() {
-                    mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
-                }
-
-                @Override
-                public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
-                    // No op.
-                }
-
-                @Override
-                public void onPackageIconChanged() {
-                    // No op.
-                }
-
-                @Override
-                public void onPackageSizeChanged(String packageName) {
-                    // No op.
-                }
-
-                @Override
-                public void onAllSizesComputed() {
-                    // No op.
-                }
-
-                @Override
-                public void onLauncherInfoChanged() {
-                    // No op.
-                }
-
-                @Override
-                public void onLoadEntriesCompleted() {
-                    mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
-                }
-            };
-
-    private boolean mIsStarted;
-
-    public AppStateBaseBridge(ApplicationsState applicationsState, Callback callback) {
-        mSession = applicationsState.newSession(mSessionCallbacks);
-        mCallback = callback;
-        // Run on the same background thread as the ApplicationsState to make sure updates don't
-        // conflict.
-        mBackgroundHandler = new BackgroundHandler(new WeakReference<>(this),
-                applicationsState.getBackgroundLooper());
-        mMainHandler = new MainHandler(new WeakReference<>(this));
-    }
-
-    /**
-     * Starts loading the information in the background. When loading is finished, the {@link
-     * Callback} will be notified on the main thread.
-     */
-    public void start() {
-        mIsStarted = true;
-        mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
-        mSession.onResume();
-    }
-
-    /**
-     * Stops any pending loading. In progress loading may still complete, but no {@link Callback}
-     * notifications will be delivered.
-     */
-    public void stop() {
-        mIsStarted = false;
-        mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_ALL);
-        mSession.onPause();
-    }
-
-    /**
-     * Cleans up internal state when this bridge will no longer be used.
-     */
-    public void destroy() {
-        mSession.onDestroy();
-    }
-
-    /**
-     * Updates the {@link ApplicationsState.AppEntry#extraInfo} of the given {@code entry}. When
-     * loading is finished, the {@link Callback} will be notified on the main thread.
-     */
-    public void forceUpdate(ApplicationsState.AppEntry entry) {
-        mBackgroundHandler.obtainMessage(BackgroundHandler.MSG_FORCE_LOAD_PKG,
-                entry).sendToTarget();
-    }
-
-    /**
-     * Populates the {@link ApplicationsState.AppEntry#extraInfo} field on the {@code enrties} with
-     * the relevant data for the subclass.
-     */
-    protected abstract void loadExtraInfo(List<ApplicationsState.AppEntry> entries);
-
-    private static class BackgroundHandler extends Handler {
-        private static final int MSG_LOAD_ALL = 1;
-        private static final int MSG_FORCE_LOAD_PKG = 2;
-
-        private final WeakReference<AppStateBaseBridge> mOuter;
-
-        BackgroundHandler(WeakReference<AppStateBaseBridge> outer, Looper looper) {
-            super(looper);
-            mOuter = outer;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            AppStateBaseBridge outer = mOuter.get();
-            if (outer == null) {
-                return;
-            }
-            switch (msg.what) {
-                case MSG_LOAD_ALL:
-                    outer.loadExtraInfo(outer.mSession.getAllApps());
-                    outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
-                    break;
-                case MSG_FORCE_LOAD_PKG:
-                    ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) msg.obj;
-                    outer.loadExtraInfo(Collections.singletonList(entry));
-                    outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
-                    break;
-            }
-        }
-    }
-
-    private static class MainHandler extends Handler {
-        private static final int MSG_INFO_UPDATED = 1;
-
-        private final WeakReference<AppStateBaseBridge> mOuter;
-
-        MainHandler(WeakReference<AppStateBaseBridge> outer) {
-            mOuter = outer;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            AppStateBaseBridge outer = mOuter.get();
-            if (outer == null) {
-                return;
-            }
-            switch (msg.what) {
-                case MSG_INFO_UPDATED:
-                    if (outer.mIsStarted) {
-                        outer.mCallback.onExtraInfoUpdated();
-                    }
-                    break;
-            }
-        }
-    }
-}
diff --git a/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridge.java b/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridge.java
new file mode 100644
index 0000000..b13fedb
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridge.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.os.RemoteException;
+
+import com.android.internal.telephony.ISms;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.List;
+
+/**
+ * Bridges the value of {@link ISms#getPremiumSmsPermission(String)} into the {@link
+ * ApplicationsState.AppEntry#extraInfo} for each entry's package name.
+ */
+public class AppStatePremiumSmsBridge implements AppEntryListManager.ExtraInfoBridge {
+
+    private final ISms mSmsManager;
+
+    public AppStatePremiumSmsBridge(ISms smsManager) {
+        mSmsManager = smsManager;
+    }
+
+    @Override
+    public void loadExtraInfo(List<ApplicationsState.AppEntry> entries) {
+        for (ApplicationsState.AppEntry entry : entries) {
+            entry.extraInfo = getSmsState(entry.info.packageName);
+        }
+    }
+
+    private int getSmsState(String packageName) {
+        try {
+            return mSmsManager.getPremiumSmsPermission(packageName);
+        } catch (RemoteException e) {
+            return SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN;
+        }
+    }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessEntryPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessEntryPreferenceController.java
new file mode 100644
index 0000000..3270b0e
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessEntryPreferenceController.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Controller for the entry point to premium SMS access settings. It reads an overlayable config
+ * value to determine if premium SMS is supported and returns an appropriate availability status.
+ */
+public class PremiumSmsAccessEntryPreferenceController extends PreferenceController<Preference> {
+
+    public PremiumSmsAccessEntryPreferenceController(Context context, String preferenceKey,
+            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+        super(context, preferenceKey, fragmentController, uxRestrictions);
+    }
+
+    @Override
+    protected Class<Preference> getPreferenceType() {
+        return Preference.class;
+    }
+
+    @Override
+    protected int getAvailabilityStatus() {
+        return getContext().getResources().getBoolean(R.bool.config_show_premium_sms) ? AVAILABLE
+                : UNSUPPORTED_ON_DEVICE;
+    }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessFragment.java b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessFragment.java
new file mode 100644
index 0000000..d485581
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessFragment.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Displays controls for managing premium SMS access. */
+public class PremiumSmsAccessFragment extends SettingsFragment {
+
+    @Override
+    @XmlRes
+    protected int getPreferenceScreenResId() {
+        return R.xml.premium_sms_access_fragment;
+    }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceController.java
new file mode 100644
index 0000000..1673dcb
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceController.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.internal.telephony.ISms;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+
+import java.util.List;
+
+/**
+ * Displays the list of apps which have a known premium SMS access state. When a user selects an
+ * app, they are shown a dialog which allows them to configure the state to one of:
+ *
+ * <ul>
+ * <li>Ask - the user will be prompted before app sends premium SMS.
+ * <li>Never allow - app can never send premium SMS.
+ * <li>Always allow - app can automatically send premium SMS.
+ * </ul>
+ */
+public class PremiumSmsAccessPreferenceController extends PreferenceController<PreferenceGroup> {
+
+    private static final Logger LOG = new Logger(PremiumSmsAccessPreferenceController.class);
+
+    private static final AppFilter FILTER_SMS_STATE_KNOWN = new AppFilter() {
+        @Override
+        public void init() {
+            // No op.
+        }
+
+        @Override
+        public boolean filterApp(AppEntry info) {
+            return info.extraInfo != null
+                    && (Integer) info.extraInfo != SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN;
+        }
+    };
+
+    private final ISms mSmsManager;
+
+    private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
+            new Preference.OnPreferenceChangeListener() {
+                @Override
+                public boolean onPreferenceChange(Preference preference, Object newValue) {
+                    PremiumSmsPreference appPreference = (PremiumSmsPreference) preference;
+                    AppEntry entry = appPreference.mEntry;
+                    int smsState = Integer.parseInt((String) newValue);
+                    if (smsState != (Integer) entry.extraInfo) {
+                        try {
+                            mSmsManager.setPremiumSmsPermission(entry.info.packageName, smsState);
+                        } catch (RemoteException e) {
+                            LOG.w("Unable to set premium sms permission for "
+                                    + entry.info.packageName + " " + entry.info.uid, e);
+                            return false;
+                        }
+                        // Update the extra info of this entry so that it reflects the new state.
+                        mAppEntryListManager.forceUpdate(entry);
+                        return true;
+                    }
+                    return false;
+                }
+            };
+
+    private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
+        @Override
+        public void onAppEntryListChanged(List<AppEntry> entries) {
+            mEntries = entries;
+            refreshUi();
+        }
+    };
+
+    @VisibleForTesting
+    AppEntryListManager mAppEntryListManager;
+    private List<AppEntry> mEntries;
+
+    public PremiumSmsAccessPreferenceController(Context context, String preferenceKey,
+            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+        super(context, preferenceKey, fragmentController, uxRestrictions);
+        mSmsManager = ISms.Stub.asInterface(ServiceManager.getService("isms"));
+        mAppEntryListManager = new AppEntryListManager(context);
+    }
+
+    @Override
+    protected Class<PreferenceGroup> getPreferenceType() {
+        return PreferenceGroup.class;
+    }
+
+    @Override
+    protected void onCreateInternal() {
+        mAppEntryListManager.init(new AppStatePremiumSmsBridge(mSmsManager),
+                () -> FILTER_SMS_STATE_KNOWN, mCallback);
+    }
+
+    @Override
+    protected void onStartInternal() {
+        mAppEntryListManager.start();
+    }
+
+    @Override
+    protected void onStopInternal() {
+        mAppEntryListManager.stop();
+    }
+
+    @Override
+    protected void onDestroyInternal() {
+        mAppEntryListManager.destroy();
+    }
+
+    @Override
+    protected void updateState(PreferenceGroup preference) {
+        if (mEntries == null) {
+            // Still loading.
+            return;
+        }
+        preference.removeAll();
+        for (AppEntry entry : mEntries) {
+            Preference appPreference = new PremiumSmsPreference(getContext(), entry);
+            appPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
+            preference.addPreference(appPreference);
+        }
+    }
+
+    private static class PremiumSmsPreference extends ListPreference {
+
+        private final AppEntry mEntry;
+
+        PremiumSmsPreference(Context context, AppEntry entry) {
+            super(context);
+            String key = entry.info.packageName + "|" + entry.info.uid;
+            setKey(key);
+            setTitle(entry.label);
+            setIcon(entry.icon);
+            setPersistent(false);
+            setEntries(R.array.premium_sms_access_values);
+            setEntryValues(new CharSequence[]{
+                    String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER),
+                    String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW),
+                    String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW)
+            });
+            setValue(String.valueOf(entry.extraInfo));
+            setSummary("%s");
+            mEntry = entry;
+        }
+    }
+}
diff --git a/src/com/android/car/settings/datausage/AppDataUsageFragment.java b/src/com/android/car/settings/datausage/AppDataUsageFragment.java
index b613e68..2c756e2 100644
--- a/src/com/android/car/settings/datausage/AppDataUsageFragment.java
+++ b/src/com/android/car/settings/datausage/AppDataUsageFragment.java
@@ -97,14 +97,13 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        Bundle bundle = getBundleForNetworkStats();
+        mBundle = getBundleForNetworkStats();
 
         LoaderManager loaderManager = LoaderManager.getInstance(this);
-        mAppsNetworkStatsManager.startLoading(loaderManager, bundle);
+        mAppsNetworkStatsManager.startLoading(loaderManager, mBundle);
     }
 
-    @VisibleForTesting
-    Bundle getBundleForNetworkStats() {
+    private Bundle getBundleForNetworkStats() {
         long historyStart = System.currentTimeMillis();
         long historyEnd = historyStart + 1;
 
@@ -139,4 +138,9 @@
 
         return SummaryForAllUidLoader.buildArgs(mNetworkTemplate, start, end);
     }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    Bundle getBundle() {
+        return mBundle;
+    }
 }
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppEntryListManagerTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppEntryListManagerTest.java
new file mode 100644
index 0000000..96143f9
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppEntryListManagerTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Looper;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.testutils.ShadowApplicationsState;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit test for {@link AppEntryListManager}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowApplicationsState.class})
+public class AppEntryListManagerTest {
+
+    @Mock
+    private ApplicationsState mApplicationsState;
+    @Mock
+    private ApplicationsState.Session mSession;
+    @Mock
+    private AppEntryListManager.ExtraInfoBridge mExtraInfoBridge;
+    @Mock
+    private AppEntryListManager.AppFilterProvider mFilterProvider;
+    @Mock
+    private AppEntryListManager.Callback mCallback;
+    @Captor
+    private ArgumentCaptor<ApplicationsState.Callbacks> mSessionCallbacksCaptor;
+    @Captor
+    private ArgumentCaptor<List<AppEntry>> mEntriesCaptor;
+
+    private AppEntryListManager mAppEntryListManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowApplicationsState.setInstance(mApplicationsState);
+        when(mApplicationsState.newSession(mSessionCallbacksCaptor.capture())).thenReturn(mSession);
+        when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
+
+        mAppEntryListManager = new AppEntryListManager(RuntimeEnvironment.application);
+        mAppEntryListManager.init(mExtraInfoBridge, mFilterProvider, mCallback);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowApplicationsState.reset();
+    }
+
+    @Test
+    public void start_resumesSession() {
+        mAppEntryListManager.start();
+
+        verify(mSession).onResume();
+    }
+
+    @Test
+    public void onPackageListChanged_loadsExtraInfo() {
+        mSessionCallbacksCaptor.getValue().onPackageListChanged();
+
+        verify(mExtraInfoBridge).loadExtraInfo(any());
+    }
+
+    @Test
+    public void onLoadEntriesComplete_loadsExtraInfo() {
+        mSessionCallbacksCaptor.getValue().onLoadEntriesCompleted();
+
+        verify(mExtraInfoBridge).loadExtraInfo(any());
+    }
+
+    @Test
+    public void stop_pausesSession() {
+        mAppEntryListManager.stop();
+
+        verify(mSession).onPause();
+    }
+
+    @Test
+    public void destroy_destroysSession() {
+        mAppEntryListManager.destroy();
+
+        verify(mSession).onDestroy();
+    }
+
+    @Test
+    public void forceUpdate_loadsExtraInfo() {
+        ArrayList<AppEntry> entries = new ArrayList<>();
+        entries.add(mock(AppEntry.class));
+        when(mSession.getAllApps()).thenReturn(entries);
+
+        mAppEntryListManager.forceUpdate();
+
+        verify(mExtraInfoBridge).loadExtraInfo(entries);
+    }
+
+    @Test
+    public void forceUpdate_forEntry_loadsExtraInfo() {
+        AppEntry entry = mock(AppEntry.class);
+
+        mAppEntryListManager.forceUpdate(entry);
+
+        verify(mExtraInfoBridge).loadExtraInfo(mEntriesCaptor.capture());
+        assertThat(mEntriesCaptor.getValue()).containsExactly(entry);
+    }
+
+    @Test
+    public void loadingFinished_rebuildsSession() {
+        ApplicationsState.AppFilter appFilter = mock(ApplicationsState.AppFilter.class);
+        when(mFilterProvider.getAppFilter()).thenReturn(appFilter);
+
+        mSessionCallbacksCaptor.getValue().onLoadEntriesCompleted();
+
+        verify(mSession).rebuild(eq(appFilter),
+                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */ eq(false));
+    }
+
+    @Test
+    public void onRebuildComplete_callsCallback() {
+        ArrayList<AppEntry> entries = new ArrayList<>();
+        entries.add(mock(AppEntry.class));
+
+        mSessionCallbacksCaptor.getValue().onRebuildComplete(entries);
+
+        verify(mCallback).onAppEntryListChanged(entries);
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java
index 497bd95..b8e70f3 100644
--- a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java
@@ -19,9 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
@@ -30,13 +28,8 @@
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ParceledListSlice;
 import android.os.Looper;
 import android.os.RemoteException;
-import android.os.UserHandle;
 
 import androidx.lifecycle.Lifecycle;
 import androidx.preference.PreferenceGroup;
@@ -45,7 +38,6 @@
 import com.android.car.settings.CarSettingsRobolectricTestRunner;
 import com.android.car.settings.common.LogicalPreferenceGroup;
 import com.android.car.settings.common.PreferenceControllerTestHelper;
-import com.android.car.settings.testutils.ShadowActivityThread;
 import com.android.car.settings.testutils.ShadowAppOpsManager;
 import com.android.car.settings.testutils.ShadowApplicationsState;
 import com.android.settingslib.applications.ApplicationsState;
@@ -55,7 +47,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -64,13 +55,13 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
 
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 /** Unit test for {@link AppOpsPreferenceController}. */
 @RunWith(CarSettingsRobolectricTestRunner.class)
-@Config(shadows = {ShadowAppOpsManager.class, ShadowApplicationsState.class,
-        ShadowActivityThread.class})
+@Config(shadows = {ShadowAppOpsManager.class, ShadowApplicationsState.class})
 public class AppOpsPreferenceControllerTest {
 
     private static final int APP_OP_CODE = AppOpsManager.OP_WRITE_SETTINGS;
@@ -78,17 +69,11 @@
     private static final int NEGATIVE_MODE = AppOpsManager.MODE_ERRORED;
 
     @Mock
-    private IPackageManager mIPackageManager;
-    @Mock
-    private ParceledListSlice<PackageInfo> mParceledPackages;
+    private AppEntryListManager mAppEntryListManager;
     @Mock
     private ApplicationsState mApplicationsState;
-    @Mock
-    private ApplicationsState.Session mSession;
-    @Mock
-    private ApplicationsState.Session mBridgeSession;
     @Captor
-    private ArgumentCaptor<ApplicationsState.Callbacks> mCallbackCaptor;
+    private ArgumentCaptor<AppEntryListManager.Callback> mCallbackCaptor;
 
     private Context mContext;
     private PreferenceGroup mPreferenceGroup;
@@ -98,17 +83,7 @@
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
-        ShadowActivityThread.setPackageManager(mIPackageManager);
-        when(mIPackageManager.getPackagesHoldingPermissions(
-                AdditionalMatchers.aryEq(new String[]{PERMISSION}),
-                eq(PackageManager.GET_PERMISSIONS),
-                eq(UserHandle.myUserId())))
-                .thenReturn(mParceledPackages);
-        when(mParceledPackages.getList()).thenReturn(Collections.emptyList());
         ShadowApplicationsState.setInstance(mApplicationsState);
-        when(mApplicationsState.newSession(mCallbackCaptor.capture()))
-                .thenReturn(mSession)
-                .thenReturn(mBridgeSession);
         when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
 
         mContext = RuntimeEnvironment.application;
@@ -117,14 +92,16 @@
                 AppOpsPreferenceController.class);
         mController = mControllerHelper.getController();
         mController.init(APP_OP_CODE, PERMISSION, NEGATIVE_MODE);
+        mController.mAppEntryListManager = mAppEntryListManager;
         mControllerHelper.setPreference(mPreferenceGroup);
         mControllerHelper.markState(Lifecycle.State.CREATED);
+        verify(mAppEntryListManager).init(any(AppStateAppOpsBridge.class), any(),
+                mCallbackCaptor.capture());
     }
 
     @After
     public void tearDown() {
         ShadowApplicationsState.reset();
-        ShadowActivityThread.reset();
     }
 
     @Test
@@ -164,54 +141,35 @@
     }
 
     @Test
-    public void onStart_resumesSessions() {
+    public void onStart_startsListManager() {
         mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
 
-        verify(mSession).onResume();
-        verify(mBridgeSession).onResume();
+        verify(mAppEntryListManager).start();
     }
 
     @Test
-    public void onStop_pausesSessions() {
+    public void onStop_stopsListManager() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
         mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
 
-        verify(mSession).onPause();
-        verify(mBridgeSession).onPause();
+        verify(mAppEntryListManager).stop();
     }
 
     @Test
-    public void onDestroy_destroysSessions() {
+    public void onDestroy_destroysListManager() {
         mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_DESTROY);
 
-        verify(mSession).onDestroy();
-        verify(mBridgeSession).onDestroy();
+        verify(mAppEntryListManager).destroy();
     }
 
     @Test
-    public void onLoadEntriesCompleted_extraInfoUpdated_rebuildsEntries() {
+    public void onAppEntryListChanged_addsPreferencesForEntries() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-
-        // Extra info updated callback happens synchronously onStart since we are using the main
-        // looper for testing.
-        callbacks.onLoadEntriesCompleted();
-
-        verify(mSession).rebuild(any(), eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
-    }
-
-    @Test
-    public void onRebuildComplete_addsPreferencesForEntries() {
-        mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        ArrayList<AppEntry> entries = new ArrayList<>();
-        entries.add(createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */ true));
-        entries.add(
+        List<AppEntry> entries = Arrays.asList(
+                createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */ true),
                 createAppEntry("another.test.package", /* uid= */ 2, /* isOpPermissible= */ false));
-        callbacks.onLoadEntriesCompleted();
 
-        callbacks.onRebuildComplete(entries);
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
 
         assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(2);
         assertThat(((TwoStatePreference) mPreferenceGroup.getPreference(0)).isChecked()).isTrue();
@@ -221,13 +179,11 @@
     @Test
     public void onPreferenceChange_checkedState_setsAppOpModeAllowed() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        ArrayList<AppEntry> entries = new ArrayList<>();
         String packageName = "test.package";
         int uid = 1;
-        entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ false));
-        callbacks.onLoadEntriesCompleted();
-        callbacks.onRebuildComplete(entries);
+        List<AppEntry> entries = Collections.singletonList(
+                createAppEntry(packageName, uid, /* isOpPermissible= */ false));
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
         TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
 
         appPref.performClick();
@@ -239,13 +195,11 @@
     @Test
     public void onPreferenceChange_uncheckedState_setsNegativeAppOpMode() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        ArrayList<AppEntry> entries = new ArrayList<>();
         String packageName = "test.package";
         int uid = 1;
-        entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ true));
-        callbacks.onLoadEntriesCompleted();
-        callbacks.onRebuildComplete(entries);
+        List<AppEntry> entries = Collections.singletonList(
+                createAppEntry(packageName, uid, /* isOpPermissible= */ true));
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
         TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
 
         appPref.performClick();
@@ -255,54 +209,35 @@
     }
 
     @Test
-    public void onPreferenceChange_rebuildsEntries() {
+    public void onPreferenceChange_updatesEntry() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        ArrayList<AppEntry> entries = new ArrayList<>();
-        String packageName = "test.package";
-        int uid = 1;
-        entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ false));
-        callbacks.onLoadEntriesCompleted();
-        callbacks.onRebuildComplete(entries);
+        List<AppEntry> entries = Collections.singletonList(
+                createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */ false));
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
         TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
 
         appPref.performClick();
 
-        // 2 times: onLoadEntriesCompleted, onPreferenceChange
-        verify(mSession, times(2)).rebuild(any(),
-                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
+        verify(mAppEntryListManager).forceUpdate(entries.get(0));
     }
 
     @Test
-    public void showSystem_rebuildsEntries() {
+    public void showSystem_updatesEntries() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        callbacks.onLoadEntriesCompleted();
 
         mController.setShowSystem(true);
 
-        // 2 times: onLoadEntriesCompleted, setShowSystem
-        verify(mSession, times(2)).rebuild(any(),
-                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
+        verify(mAppEntryListManager).forceUpdate();
     }
 
     @Test
-    public void rebuildFilter_showingSystemApps_keepsSystemEntries() {
+    public void appFilter_showingSystemApps_keepsSystemEntries() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        callbacks.onLoadEntriesCompleted();
         mController.setShowSystem(true);
-        ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
-                ApplicationsState.AppFilter.class);
-        // 2 times: onLoadEntriesCompleted, setShowSystem
-        verify(mSession, times(2)).rebuild(filterCaptor.capture(),
-                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
-
-        // Get the filter from setShowSystem.
-        ApplicationsState.AppFilter filter = filterCaptor.getAllValues().get(1);
+        ArgumentCaptor<AppEntryListManager.AppFilterProvider> filterCaptor =
+                ArgumentCaptor.forClass(AppEntryListManager.AppFilterProvider.class);
+        verify(mAppEntryListManager).init(any(), filterCaptor.capture(), any());
+        ApplicationsState.AppFilter filter = filterCaptor.getValue().getAppFilter();
 
         AppEntry systemApp = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
                 false);
@@ -312,18 +247,13 @@
     }
 
     @Test
-    public void rebuildFilter_notShowingSystemApps_removesSystemEntries() {
+    public void appFilter_notShowingSystemApps_removesSystemEntries() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        callbacks.onLoadEntriesCompleted();
-        ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
-                ApplicationsState.AppFilter.class);
-        verify(mSession).rebuild(filterCaptor.capture(),
-                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
-
-        // Not showing system by default
-        ApplicationsState.AppFilter filter = filterCaptor.getValue();
+        // Not showing system by default.
+        ArgumentCaptor<AppEntryListManager.AppFilterProvider> filterCaptor =
+                ArgumentCaptor.forClass(AppEntryListManager.AppFilterProvider.class);
+        verify(mAppEntryListManager).init(any(), filterCaptor.capture(), any());
+        ApplicationsState.AppFilter filter = filterCaptor.getValue().getAppFilter();
 
         AppEntry systemApp = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
                 false);
@@ -333,17 +263,12 @@
     }
 
     @Test
-    public void rebuildFilter_removesNullExtraInfoEntries() {
+    public void appFilter_removesNullExtraInfoEntries() {
         mControllerHelper.markState(Lifecycle.State.STARTED);
-        ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
-        callbacks.onLoadEntriesCompleted();
-        ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
-                ApplicationsState.AppFilter.class);
-        verify(mSession).rebuild(filterCaptor.capture(),
-                eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
-                eq(false));
-
-        ApplicationsState.AppFilter filter = filterCaptor.getValue();
+        ArgumentCaptor<AppEntryListManager.AppFilterProvider> filterCaptor =
+                ArgumentCaptor.forClass(AppEntryListManager.AppFilterProvider.class);
+        verify(mAppEntryListManager).init(any(), filterCaptor.capture(), any());
+        ApplicationsState.AppFilter filter = filterCaptor.getValue().getAppFilter();
 
         AppEntry appEntry = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
                 false);
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java
index 8c75c7c..718edde 100644
--- a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -31,7 +30,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -39,7 +37,6 @@
 import com.android.car.settings.CarSettingsRobolectricTestRunner;
 import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
 import com.android.car.settings.testutils.ShadowAppOpsManager;
-import com.android.settingslib.applications.ApplicationsState;
 import com.android.settingslib.applications.ApplicationsState.AppEntry;
 
 import org.junit.Before;
@@ -54,6 +51,7 @@
 import org.robolectric.shadows.ShadowUserManager;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -68,16 +66,11 @@
     @Mock
     private IPackageManager mIPackageManager;
     @Mock
-    private ApplicationsState mApplicationsState;
-    @Mock
-    private ApplicationsState.Session mSession;
-    @Mock
     private ParceledListSlice<PackageInfo> mParceledPackages;
     @Mock
     private ParceledListSlice<PackageInfo> mParceledPackagesOtherProfile;
 
     private List<PackageInfo> mPackages;
-    private ArrayList<AppEntry> mAppEntries;
 
     private Context mContext;
     private AppOpsManager mAppOpsManager;
@@ -94,15 +87,9 @@
                 .thenReturn(mParceledPackages);
         when(mParceledPackages.getList()).thenReturn(mPackages);
 
-        mAppEntries = new ArrayList<>();
-        when(mApplicationsState.newSession(any())).thenReturn(mSession);
-        when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
-        when(mSession.getAllApps()).thenReturn(mAppEntries);
-
         mContext = RuntimeEnvironment.application;
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
-        mBridge = new AppStateAppOpsBridge(mContext, mApplicationsState, APP_OP_CODE, PERMISSION,
-                mock(AppStateBaseBridge.Callback.class), mIPackageManager);
+        mBridge = new AppStateAppOpsBridge(mContext, APP_OP_CODE, PERMISSION, mIPackageManager);
     }
 
     @Test
@@ -111,11 +98,9 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo);
+        AppEntry entry = createAppEntry(packageInfo);
 
-        AppEntry entry = mAppEntries.get(0);
-
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNull();
     }
@@ -126,11 +111,9 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo);
+        AppEntry entry = createAppEntry(packageInfo);
 
-        AppEntry entry = mAppEntries.get(0);
-
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNull();
     }
@@ -144,11 +127,9 @@
         mPackages.add(packageInfo);
         when(mIPackageManager.isPackageAvailable(packageInfo.packageName,
                 UserHandle.myUserId())).thenReturn(true);
-        addEntry(packageInfo);
+        AppEntry entry = createAppEntry(packageInfo);
 
-        AppEntry entry = mAppEntries.get(0);
-
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNull();
     }
@@ -159,14 +140,11 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo);
-
         when(mIPackageManager.isPackageAvailable(packageInfo.packageName,
                 UserHandle.myUserId())).thenReturn(false);
+        AppEntry entry = createAppEntry(packageInfo);
 
-        AppEntry entry = mAppEntries.get(0);
-
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNull();
     }
@@ -177,12 +155,10 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo);
-
-        AppEntry entry = mAppEntries.get(0);
+        AppEntry entry = createAppEntry(packageInfo);
         assertThat(entry.extraInfo).isNull();
 
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNotNull();
         assertThat(((PermissionState) entry.extraInfo).isPermissible()).isTrue();
@@ -194,12 +170,10 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_DEFAULT);
-        addEntry(packageInfo);
-
-        AppEntry entry = mAppEntries.get(0);
+        AppEntry entry = createAppEntry(packageInfo);
         assertThat(entry.extraInfo).isNull();
 
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNotNull();
         assertThat(((PermissionState) entry.extraInfo).isPermissible()).isTrue();
@@ -211,12 +185,10 @@
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
         addPackageWithPermission(packageInfo, AppOpsManager.MODE_IGNORED);
-        addEntry(packageInfo);
-
-        AppEntry entry = mAppEntries.get(0);
+        AppEntry entry = createAppEntry(packageInfo);
         assertThat(entry.extraInfo).isNull();
 
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNotNull();
         assertThat(((PermissionState) entry.extraInfo).isPermissible()).isFalse();
@@ -228,18 +200,15 @@
         int uid1 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo1 = createPackageInfo(packageName1, uid1);
         addPackageWithPermission(packageInfo1, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo1);
+        AppEntry entry1 = createAppEntry(packageInfo1);
 
         String packageName2 = "test.package2";
         int uid2 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 2);
         PackageInfo packageInfo2 = createPackageInfo(packageName2, uid2);
         addPackageWithPermission(packageInfo2, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo2);
+        AppEntry entry2 = createAppEntry(packageInfo2);
 
-        AppEntry entry1 = mAppEntries.get(0);
-        AppEntry entry2 = mAppEntries.get(1);
-
-        mBridge.start();
+        mBridge.loadExtraInfo(Arrays.asList(entry1, entry2));
 
         assertThat(entry1.extraInfo).isNotNull();
         assertThat(entry2.extraInfo).isNotNull();
@@ -251,7 +220,7 @@
         int uid1 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo1 = createPackageInfo(packageName1, uid1);
         addPackageWithPermission(packageInfo1, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo1);
+        AppEntry entry1 = createAppEntry(packageInfo1);
 
         // Add a package for another profile.
         int otherUserId = UserHandle.myUserId() + 1;
@@ -269,17 +238,13 @@
                 otherUserId)).thenReturn(true);
         mAppOpsManager.setMode(APP_OP_CODE, packageInfo2.applicationInfo.uid,
                 packageInfo2.packageName, AppOpsManager.MODE_ALLOWED);
-        addEntry(packageInfo2);
-
-        AppEntry entry1 = mAppEntries.get(0);
-        AppEntry entry2 = mAppEntries.get(1);
+        AppEntry entry2 = createAppEntry(packageInfo2);
 
         getShadowUserManager().addUserProfile(UserHandle.of(otherUserId));
         // Recreate the bridge so it has all user profiles.
-        mBridge = new AppStateAppOpsBridge(mContext, mApplicationsState, APP_OP_CODE, PERMISSION,
-                mock(AppStateBaseBridge.Callback.class), mIPackageManager);
+        mBridge = new AppStateAppOpsBridge(mContext, APP_OP_CODE, PERMISSION, mIPackageManager);
 
-        mBridge.start();
+        mBridge.loadExtraInfo(Arrays.asList(entry1, entry2));
 
         assertThat(entry1.extraInfo).isNotNull();
         assertThat(entry2.extraInfo).isNotNull();
@@ -290,12 +255,10 @@
         String packageName = "test.package";
         int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
         PackageInfo packageInfo = createPackageInfo(packageName, uid);
-        addEntry(packageInfo);
-
-        AppEntry entry = mAppEntries.get(0);
+        AppEntry entry = createAppEntry(packageInfo);
         entry.extraInfo = new Object();
 
-        mBridge.start();
+        mBridge.loadExtraInfo(Collections.singletonList(entry));
 
         assertThat(entry.extraInfo).isNull();
     }
@@ -322,10 +285,10 @@
                 packageInfo.packageName, mode);
     }
 
-    private void addEntry(PackageInfo packageInfo) {
+    private AppEntry createAppEntry(PackageInfo packageInfo) {
         AppEntry appEntry = mock(AppEntry.class);
         appEntry.info = packageInfo.applicationInfo;
-        mAppEntries.add(appEntry);
+        return appEntry;
     }
 
     private ShadowUserManager getShadowUserManager() {
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridgeTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridgeTest.java
deleted file mode 100644
index 1f910dc..0000000
--- a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateBaseBridgeTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright 2019 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.car.settings.applications.specialaccess;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.os.Looper;
-
-import com.android.car.settings.CarSettingsRobolectricTestRunner;
-import com.android.settingslib.applications.ApplicationsState;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** Unit test for {@link AppStateBaseBridge}. */
-@RunWith(CarSettingsRobolectricTestRunner.class)
-public class AppStateBaseBridgeTest {
-
-    @Mock
-    private ApplicationsState mApplicationsState;
-    @Mock
-    private ApplicationsState.Session mSession;
-    @Mock
-    private AppStateBaseBridge.Callback mCallback;
-    @Captor
-    private ArgumentCaptor<ApplicationsState.Callbacks> mSessionCallbacksCaptor;
-
-    private TestAppStateBaseBridge mBridge;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mApplicationsState.newSession(mSessionCallbacksCaptor.capture())).thenReturn(mSession);
-        when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
-
-        mBridge = new TestAppStateBaseBridge(mApplicationsState, mCallback);
-    }
-
-    @Test
-    public void start_resumesSession() {
-        mBridge.start();
-
-        verify(mSession).onResume();
-    }
-
-    @Test
-    public void start_beginsLoadingExtraInfo() {
-        mBridge.start();
-
-        assertThat(mBridge.getLoadExtraInfoCalledCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void onPackageListChanged_beginsLoadingExtraInfo() {
-        mSessionCallbacksCaptor.getValue().onPackageListChanged();
-
-        assertThat(mBridge.getLoadExtraInfoCalledCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void onLoadEntriesCompleted_beginsLoadingExtraInfo() {
-        mSessionCallbacksCaptor.getValue().onLoadEntriesCompleted();
-
-        assertThat(mBridge.getLoadExtraInfoCalledCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void stop_pausesSession() {
-        mBridge.stop();
-
-        verify(mSession).onPause();
-    }
-
-    @Test
-    public void destroy_destroysSession() {
-        mBridge.destroy();
-
-        verify(mSession).onDestroy();
-    }
-
-    @Test
-    public void forceUpdate_updatesEntryExtraInfo() {
-        ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
-        mBridge.forceUpdate(entry);
-
-        assertThat(mBridge.getArgsForLoadExtraInfo(/* forNthCall= */ 0)).containsExactly(entry);
-    }
-
-    @Test
-    public void extraInfoLoaded_callbackNotified() {
-        // Start loading.
-        mBridge.start();
-
-        // Everything is on the same looper in the test env, so loading will finish immediately.
-        verify(mCallback).onExtraInfoUpdated();
-    }
-
-    /** Concrete impl of base class for testing. */
-    private static class TestAppStateBaseBridge extends AppStateBaseBridge {
-
-        private int mLoadExtraInfoCalledCount;
-        private List<List<ApplicationsState.AppEntry>> mLoadExtraInfoArgs = new ArrayList<>();
-
-        TestAppStateBaseBridge(ApplicationsState applicationsState, Callback callback) {
-            super(applicationsState, callback);
-        }
-
-        @Override
-        protected void loadExtraInfo(List<ApplicationsState.AppEntry> entries) {
-            mLoadExtraInfoCalledCount++;
-            mLoadExtraInfoArgs.add(entries);
-        }
-
-        int getLoadExtraInfoCalledCount() {
-            return mLoadExtraInfoCalledCount;
-        }
-
-        List<ApplicationsState.AppEntry> getArgsForLoadExtraInfo(int forNthCall) {
-            return mLoadExtraInfoArgs.get(forNthCall);
-        }
-    }
-}
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridgeTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridgeTest.java
new file mode 100644
index 0000000..382917c
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStatePremiumSmsBridgeTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.ApplicationInfo;
+import android.os.RemoteException;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.internal.telephony.ISms;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+
+/** Unit test for {@link AppStatePremiumSmsBridge}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+public class AppStatePremiumSmsBridgeTest {
+
+    @Mock
+    private ISms mSmsManager;
+    private AppStatePremiumSmsBridge mBridge;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mBridge = new AppStatePremiumSmsBridge(mSmsManager);
+    }
+
+    @Test
+    public void loadExtraInfo() throws RemoteException {
+        String package1 = "test.package1";
+        AppEntry appEntry1 = createAppEntry(package1);
+        int value1 = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW;
+        when(mSmsManager.getPremiumSmsPermission(package1)).thenReturn(value1);
+
+        String package2 = "test.package2";
+        AppEntry appEntry2 = createAppEntry(package2);
+        int value2 = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW;
+        when(mSmsManager.getPremiumSmsPermission(package2)).thenReturn(value2);
+
+        mBridge.loadExtraInfo(Arrays.asList(appEntry1, appEntry2));
+
+        assertThat(appEntry1.extraInfo).isEqualTo(value1);
+        assertThat(appEntry2.extraInfo).isEqualTo(value2);
+    }
+
+    private AppEntry createAppEntry(String packageName) {
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = packageName;
+
+        AppEntry appEntry = mock(AppEntry.class);
+        appEntry.info = info;
+        appEntry.label = packageName;
+
+        return appEntry;
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceControllerTest.java
new file mode 100644
index 0000000..38c1678
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/PremiumSmsAccessPreferenceControllerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Looper;
+import android.os.RemoteException;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.LogicalPreferenceGroup;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowApplicationsState;
+import com.android.car.settings.testutils.ShadowISms;
+import com.android.internal.telephony.ISms;
+import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Unit test for {@link PremiumSmsAccessPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowApplicationsState.class, ShadowISms.class})
+public class PremiumSmsAccessPreferenceControllerTest {
+
+    @Mock
+    private AppEntryListManager mAppEntryListManager;
+    @Mock
+    private ApplicationsState mApplicationsState;
+    @Mock
+    private ISms mISms;
+    @Captor
+    private ArgumentCaptor<AppEntryListManager.Callback> mCallbackCaptor;
+
+    private PreferenceGroup mPreferenceGroup;
+    private PreferenceControllerTestHelper<PremiumSmsAccessPreferenceController> mControllerHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowApplicationsState.setInstance(mApplicationsState);
+        when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
+        ShadowISms.setISms(mISms);
+
+        Context context = RuntimeEnvironment.application;
+        mPreferenceGroup = new LogicalPreferenceGroup(context);
+        mControllerHelper = new PreferenceControllerTestHelper<>(context,
+                PremiumSmsAccessPreferenceController.class, mPreferenceGroup);
+        mControllerHelper.getController().mAppEntryListManager = mAppEntryListManager;
+        mControllerHelper.markState(Lifecycle.State.CREATED);
+        verify(mAppEntryListManager).init(any(AppStatePremiumSmsBridge.class), any(),
+                mCallbackCaptor.capture());
+    }
+
+    @After
+    public void tearDown() {
+        ShadowApplicationsState.reset();
+        ShadowISms.reset();
+    }
+
+    @Test
+    public void onStart_startsListManager() {
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+        verify(mAppEntryListManager).start();
+    }
+
+    @Test
+    public void onStop_stopsListManager() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        verify(mAppEntryListManager).stop();
+    }
+
+    @Test
+    public void onDestroy_destroysListManager() {
+        mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+        verify(mAppEntryListManager).destroy();
+    }
+
+    @Test
+    public void onAppEntryListChanged_addsPreferencesForEntries() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        List<AppEntry> entries = Arrays.asList(
+                createAppEntry("test.package", /* uid= */ 1,
+                        SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW),
+                createAppEntry("another.test.package", /* uid= */ 2,
+                        SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW));
+
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
+
+        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(2);
+        assertThat(((ListPreference) mPreferenceGroup.getPreference(0)).getValue()).isEqualTo(
+                String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW));
+        assertThat(((ListPreference) mPreferenceGroup.getPreference(1)).getValue()).isEqualTo(
+                String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW));
+    }
+
+    @Test
+    public void onPreferenceChange_setsPremiumSmsPermission() throws RemoteException {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        String packageName = "test.package";
+        List<AppEntry> entries = Collections.singletonList(
+                createAppEntry(packageName, /* uid= */ 1,
+                        SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW));
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
+        Preference appPref = mPreferenceGroup.getPreference(0);
+        int updatedValue = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER;
+
+        appPref.getOnPreferenceChangeListener().onPreferenceChange(appPref,
+                String.valueOf(updatedValue));
+
+        verify(mISms).setPremiumSmsPermission(packageName, updatedValue);
+    }
+
+    @Test
+    public void onPreferenceChange_updatesEntry() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        List<AppEntry> entries = Collections.singletonList(
+                createAppEntry("test.package", /* uid= */ 1,
+                        SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW));
+        mCallbackCaptor.getValue().onAppEntryListChanged(entries);
+        Preference appPref = mPreferenceGroup.getPreference(0);
+
+        appPref.getOnPreferenceChangeListener().onPreferenceChange(appPref,
+                String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER));
+
+        verify(mAppEntryListManager).forceUpdate(entries.get(0));
+    }
+
+    @Test
+    public void appFilter_removesUnknownStates() {
+        mControllerHelper.markState(Lifecycle.State.STARTED);
+        ArgumentCaptor<AppEntryListManager.AppFilterProvider> filterCaptor =
+                ArgumentCaptor.forClass(AppEntryListManager.AppFilterProvider.class);
+        verify(mAppEntryListManager).init(any(), filterCaptor.capture(), any());
+        ApplicationsState.AppFilter filter = filterCaptor.getValue().getAppFilter();
+        AppEntry unknownStateApp = createAppEntry("test.package", /* uid= */ 1,
+                SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN);
+
+        assertThat(filter.filterApp(unknownStateApp)).isFalse();
+    }
+
+    private AppEntry createAppEntry(String packageName, int uid, int smsState) {
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = packageName;
+        info.uid = uid;
+
+        AppEntry appEntry = mock(AppEntry.class);
+        appEntry.info = info;
+        appEntry.label = packageName;
+        appEntry.extraInfo = smsState;
+
+        return appEntry;
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/datausage/AppDataUsageFragmentTest.java b/tests/robotests/src/com/android/car/settings/datausage/AppDataUsageFragmentTest.java
index e4cf647..c94cfa8 100644
--- a/tests/robotests/src/com/android/car/settings/datausage/AppDataUsageFragmentTest.java
+++ b/tests/robotests/src/com/android/car/settings/datausage/AppDataUsageFragmentTest.java
@@ -85,7 +85,7 @@
     public void onActivityCreated_policyIsNull_startAndEndDateShouldHaveFourWeeksDifference() {
         mFragmentController.create();
 
-        Bundle bundle = mFragment.getBundleForNetworkStats();
+        Bundle bundle = mFragment.getBundle();
         long start = bundle.getLong(KEY_START);
         long end = bundle.getLong(KEY_END);
         long timeDiff = end - start;
@@ -102,7 +102,7 @@
         ShadowNetworkPolicyManager.setCycleIterator(iterator);
         mFragmentController.create();
 
-        Bundle bundle = mFragment.getBundleForNetworkStats();
+        Bundle bundle = mFragment.getBundle();
         long start = bundle.getLong(KEY_START);
         long end = bundle.getLong(KEY_END);
         long timeDiff = end - start;
@@ -130,7 +130,7 @@
         ShadowNetworkPolicyManager.setCycleIterator(iterator);
         mFragmentController.create();
 
-        Bundle bundle = mFragment.getBundleForNetworkStats();
+        Bundle bundle = mFragment.getBundle();
         long start = bundle.getLong(KEY_START);
         long end = bundle.getLong(KEY_END);
 
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
index 2d51a86..5cd1ac6 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
@@ -25,29 +25,14 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.Resetter;
 
 import java.lang.reflect.Proxy;
 
 @Implements(ActivityThread.class)
 public class ShadowActivityThread {
 
-    private static IPackageManager sIPackageManager;
-
-    public static void setPackageManager(IPackageManager packageManager) {
-        sIPackageManager = packageManager;
-    }
-
-    @Resetter
-    public static void reset() {
-        sIPackageManager = null;
-    }
-
     @Implementation
     protected static IPackageManager getPackageManager() {
-        if (sIPackageManager != null) {
-            return sIPackageManager;
-        }
         ClassLoader classLoader = ShadowActivityThread.class.getClassLoader();
         Class<?> iPackageManagerClass;
         try {
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
index 8215a8b..e07116a 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
@@ -29,7 +29,7 @@
 
 import java.util.List;
 
-/**TODO: Refactor all methods to run without relying on sInstance. */
+/** TODO: Refactor all methods to run without relying on sInstance. */
 @Implements(CarWifiManager.class)
 public class ShadowCarWifiManager {
 
@@ -126,6 +126,12 @@
     }
 
     @Implementation
+    protected void connectToSavedWifi(AccessPoint accessPoint,
+            WifiManager.ActionListener listener) {
+        sInstance.connectToSavedWifi(accessPoint, listener);
+    }
+
+    @Implementation
     protected boolean isDualModeSupported() {
         return sIsDualModeSupported;
     }
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowISms.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowISms.java
new file mode 100644
index 0000000..17a5073
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowISms.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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.car.settings.testutils;
+
+import android.os.IBinder;
+
+import com.android.internal.telephony.ISms;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(value = ISms.Stub.class)
+public class ShadowISms {
+
+    private static ISms sISms;
+
+    @Resetter
+    public static void reset() {
+        sISms = null;
+    }
+
+    public static void setISms(ISms iSms) {
+        sISms = iSms;
+    }
+
+    @Implementation
+    protected static ISms asInterface(IBinder obj) {
+        return sISms;
+    }
+}