Merge "DO NOT MERGE Extract common pattern of loading app entries with extra info into a manager class." into pi-car-dev
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..1039a74
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java
@@ -0,0 +1,280 @@
+/*
+ * 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 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:
+                    if (outer.mExtraInfoBridge != null) {
+                        outer.mExtraInfoBridge.loadExtraInfo(outer.mSession.getAllApps());
+                    }
+                    outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+                    break;
+                case MSG_LOAD_PKG:
+                    ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) msg.obj;
+                    if (outer.mExtraInfoBridge != null) {
+                        outer.mExtraInfoBridge.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<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..2ce7aac 100644
--- a/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
+++ b/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
@@ -24,6 +24,7 @@
 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 +38,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
@@ -62,59 +63,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() {
                 @Override
@@ -128,24 +76,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;
 
@@ -155,6 +107,7 @@
         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
         mApplicationsState = ApplicationsState.getInstance(
                 (Application) context.getApplicationContext());
+        mAppEntryListManager = new AppEntryListManager(context);
     }
 
     @Override
@@ -185,7 +138,7 @@
     public void setShowSystem(boolean showSystem) {
         if (mShowSystem != showSystem) {
             mShowSystem = showSystem;
-            rebuild();
+            mAppEntryListManager.forceUpdate();
         }
     }
 
@@ -204,27 +157,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
@@ -253,15 +203,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/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/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 {