Merge "Add package filtering to NLSes" into sc-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5c651b9..6d373fb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8820,6 +8820,22 @@
     <string name="notif_type_alerting">Alerting notifications</string>
     <string name="notif_type_silent">Silent notifications</string>
 
+    <!-- Per notification listener, launches a list of apps whose notifications this listener cannot see -->
+    <string name="notif_listener_excluded_title">Apps that are not bridged to this listener</string>
+
+    <!-- Per notification listener, when the listener can see notifications from all apps -->
+    <string name="notif_listener_excluded_summary_zero">All apps are bridged</string>
+
+    <!-- Per notification listener, a summary of how many apps this listener cannot see
+     notifications from -->
+    <plurals name="notif_listener_excluded_summary_nonzero">
+        <item quantity="one">%d app is not bridged</item>
+        <item quantity="other">%d apps are not bridged</item>
+    </plurals>
+
+    <!-- Per notification listener, a list of apps whose notifications this listener cannot see -->
+    <string name="notif_listener_excluded_app_title">Bridged apps</string>
+
     <!-- Title for managing VR (virtual reality) helper services. [CHAR LIMIT=50] -->
     <string name="vr_listeners_title">VR helper services</string>
 
diff --git a/res/xml/notification_access_bridged_apps_settings.xml b/res/xml/notification_access_bridged_apps_settings.xml
new file mode 100644
index 0000000..590a468
--- /dev/null
+++ b/res/xml/notification_access_bridged_apps_settings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2021 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:key="nonbridged_apps"
+    android:title="@string/notif_listener_excluded_app_title"
+    settings:controller="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsPreferenceController"
+    settings:searchable="false">
+</PreferenceScreen>
diff --git a/res/xml/notification_access_permission_details.xml b/res/xml/notification_access_permission_details.xml
index f7d928d..edac955 100644
--- a/res/xml/notification_access_permission_details.xml
+++ b/res/xml/notification_access_permission_details.xml
@@ -41,11 +41,11 @@
         style="@style/SettingsMultiSelectListPreference"
         settings:controller="com.android.settings.applications.specialaccess.notificationaccess.TypeFilterPreferenceController"/>/>
 
-    <PreferenceCategory
-        android:key="advanced"
-        android:order="50"
-        settings:initialExpandedChildrenCount="0">
+        <Preference
+            android:key="bridged_apps"
+            android:title="@string/notif_listener_excluded_app_title"
+            android:fragment="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsSettings"
+            settings:searchable="false"
+            settings:controller="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsPreferenceController" />
 
-
-    </PreferenceCategory>
 </PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceController.java
new file mode 100644
index 0000000..9186bdb1
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceController.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.settings.applications.specialaccess.notificationaccess;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.VersionedPackage;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerFilter;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import com.android.settings.applications.AppStateBaseBridge;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.TreeSet;
+
+
+public class BridgedAppsPreferenceController extends BasePreferenceController implements
+        LifecycleObserver, ApplicationsState.Callbacks,
+        AppStateBaseBridge.Callback {
+
+    private ApplicationsState mApplicationsState;
+    private ApplicationsState.Session mSession;
+    private AppFilter mFilter;
+    private PreferenceScreen mScreen;
+
+    private ComponentName mCn;
+    private int mUserId;
+    private NotificationBackend mNm;
+    private NotificationListenerFilter mNlf;
+
+    public BridgedAppsPreferenceController(Context context, String key) {
+        super(context, key);
+    }
+
+    public BridgedAppsPreferenceController setAppState(ApplicationsState appState) {
+        mApplicationsState = appState;
+        return this;
+    }
+
+    public BridgedAppsPreferenceController setCn(ComponentName cn) {
+        mCn = cn;
+        return this;
+    }
+
+    public BridgedAppsPreferenceController setUserId(int userId) {
+        mUserId = userId;
+        return this;
+    }
+
+    public BridgedAppsPreferenceController setNm(NotificationBackend nm) {
+        mNm = nm;
+        return this;
+    }
+
+    public BridgedAppsPreferenceController setFilter(AppFilter filter) {
+        mFilter = filter;
+        return this;
+    }
+
+    public BridgedAppsPreferenceController setSession(Lifecycle lifecycle) {
+        mSession = mApplicationsState.newSession(this, lifecycle);
+        return this;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        mScreen = screen;
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+
+    @Override
+    public void onExtraInfoUpdated() {
+        rebuild();
+    }
+
+    @Override
+    public void onRunningStateChanged(boolean running) {
+
+    }
+
+    @Override
+    public void onPackageListChanged() {
+        rebuild();
+    }
+
+    @Override
+    public void onRebuildComplete(ArrayList<AppEntry> apps) {
+        if (apps == null) {
+            return;
+        }
+        mNlf = mNm.getListenerFilter(mCn, mUserId);
+
+        // Create apps key set for removing useless preferences
+        final Set<String> appsKeySet = new TreeSet<>();
+        // Add or update preferences
+        final int N = apps.size();
+        for (int i = 0; i < N; i++) {
+            final AppEntry entry = apps.get(i);
+            if (!shouldAddPreference(entry)) {
+                continue;
+            }
+            final String prefKey = entry.info.packageName + "|" + entry.info.uid;
+            appsKeySet.add(prefKey);
+            SwitchPreference preference = mScreen.findPreference(prefKey);
+            if (preference == null) {
+                preference = new SwitchPreference(mScreen.getContext());
+                preference.setIcon(entry.icon);
+                preference.setTitle(entry.label);
+                preference.setKey(prefKey);
+                mScreen.addPreference(preference);
+            }
+            preference.setOrder(i);
+            preference.setChecked(mNlf.isPackageAllowed(
+                    new VersionedPackage(entry.info.packageName, entry.info.uid)));
+            preference.setOnPreferenceChangeListener(this::onPreferenceChange);
+        }
+
+        // Remove preferences that are no longer existing in the updated list of apps
+        removeUselessPrefs(appsKeySet);
+    }
+
+    @Override
+    public void onPackageIconChanged() {
+        rebuild();
+    }
+
+    @Override
+    public void onPackageSizeChanged(String packageName) {
+
+    }
+
+    @Override
+    public void onAllSizesComputed() {
+    }
+
+    @Override
+    public void onLauncherInfoChanged() {
+    }
+
+    @Override
+    public void onLoadEntriesCompleted() {
+        rebuild();
+    }
+
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        if (preference instanceof SwitchPreference) {
+            String packageName = preference.getKey().substring(0, preference.getKey().indexOf("|"));
+            int uid = Integer.parseInt(preference.getKey().substring(
+                    preference.getKey().indexOf("|") + 1));
+            boolean allowlisted = newValue == Boolean.TRUE;
+            mNlf = mNm.getListenerFilter(mCn, mUserId);
+            if (allowlisted) {
+                mNlf.removePackage(new VersionedPackage(packageName, uid));
+            } else {
+                mNlf.addPackage(new VersionedPackage(packageName, uid));
+            }
+            mNm.setListenerFilter(mCn, mUserId, mNlf);
+            return true;
+        }
+        return false;
+    }
+
+    public void rebuild() {
+        final ArrayList<AppEntry> apps = mSession.rebuild(mFilter,
+                ApplicationsState.ALPHA_COMPARATOR);
+        if (apps != null) {
+            onRebuildComplete(apps);
+        }
+    }
+
+    private void removeUselessPrefs(final Set<String> appsKeySet) {
+        final int prefCount = mScreen.getPreferenceCount();
+        String prefKey;
+        if (prefCount > 0) {
+            for (int i = prefCount - 1; i >= 0; i--) {
+                Preference pref = mScreen.getPreference(i);
+                prefKey = pref.getKey();
+                if (!appsKeySet.contains(prefKey)) {
+                    mScreen.removePreference(pref);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static boolean shouldAddPreference(AppEntry app) {
+        return app != null && UserHandle.isApp(app.info.uid);
+    }
+}
diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsSettings.java b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsSettings.java
new file mode 100644
index 0000000..d396a01
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsSettings.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.settings.applications.specialaccess.notificationaccess;
+
+import static com.android.settings.applications.AppInfoBase.ARG_PACKAGE_NAME;
+
+import android.app.Application;
+import android.app.settings.SettingsEnums;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+
+public class BridgedAppsSettings extends DashboardFragment {
+
+    private static final String TAG = "BridgedAppsSettings";
+
+    private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 42;
+    private static final String EXTRA_SHOW_SYSTEM = "show_system";
+
+    private boolean mShowSystem;
+    private AppFilter mFilter;
+
+    private int mUserId;
+    private ComponentName mComponentName;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        mShowSystem = icicle != null && icicle.getBoolean(EXTRA_SHOW_SYSTEM);
+
+        use(BridgedAppsPreferenceController.class).setNm(new NotificationBackend());
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
+                mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system);
+        super.onCreateOptionsMenu(menu, inflater);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case MENU_SHOW_SYSTEM:
+                mShowSystem = !mShowSystem;
+                item.setTitle(mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system);
+                mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED
+                        : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER;
+
+                use(BridgedAppsPreferenceController.class).setFilter(mFilter).rebuild();
+
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(EXTRA_SHOW_SYSTEM, mShowSystem);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED
+                : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER;
+
+        final Bundle args = getArguments();
+        Intent intent = (args == null) ?
+                getIntent() : (Intent) args.getParcelable("intent");
+        String cn = args.getString(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME);
+        if (cn != null) {
+            mComponentName = ComponentName.unflattenFromString(cn);
+        }
+        if (intent != null && intent.hasExtra(Intent.EXTRA_USER_HANDLE)) {
+            mUserId = ((UserHandle) intent.getParcelableExtra(
+                    Intent.EXTRA_USER_HANDLE)).getIdentifier();
+        } else {
+            mUserId = UserHandle.myUserId();
+        }
+
+        use(BridgedAppsPreferenceController.class)
+                .setAppState(ApplicationsState.getInstance(
+                        (Application) context.getApplicationContext()))
+                .setCn(mComponentName)
+                .setUserId(mUserId)
+                .setSession(getSettingsLifecycle())
+                .setFilter(mFilter)
+                .rebuild();
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.NOTIFICATION_ACCESS_BRIDGED_APPS;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.notification_access_bridged_apps_settings;
+    }
+}
diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
index 9f4b693..41a6efa 100644
--- a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
+++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
@@ -32,15 +32,19 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.service.notification.NotificationListenerFilter;
 import android.service.notification.NotificationListenerService;
 import android.util.Log;
 import android.util.Slog;
 
+import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
 import com.android.settings.SettingsActivity;
+import com.android.settings.applications.AppInfoBase;
 import com.android.settings.applications.manageapplications.ManageApplications;
+import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.notification.NotificationBackend;
 import com.android.settingslib.RestrictedLockUtils;
@@ -52,6 +56,8 @@
 public class NotificationAccessDetails extends DashboardFragment {
     private static final String TAG = "NotifAccessDetails";
 
+    private NotificationBackend mNm = new NotificationBackend();
+    private NotificationListenerFilter mNlf;
     private ComponentName mComponentName;
     private CharSequence mServiceName;
     protected PackageInfo mPackageInfo;
@@ -131,6 +137,33 @@
         if (!refreshUi()) {
             setIntentAndFinish(true /* appChanged */);
         }
+        Preference apps = getPreferenceScreen().findPreference(
+                use(BridgedAppsPreferenceController.class).getPreferenceKey());
+        if (apps != null) {
+            mNlf = mNm.getListenerFilter(mComponentName, mUserId);
+            int nonBridgedCount = mNlf.getDisallowedPackages().size();
+            apps.setSummary(nonBridgedCount == 0 ?
+                    getString(R.string.notif_listener_excluded_summary_zero)
+                    : getResources().getQuantityString(
+                            R.plurals.notif_listener_excluded_summary_nonzero,
+                            nonBridgedCount, nonBridgedCount));
+
+            apps.setOnPreferenceClickListener(preference -> {
+                final Bundle args = new Bundle();
+                args.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName);
+                args.putString(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
+                        mComponentName.flattenToString());
+
+                new SubSettingLauncher(getContext())
+                        .setDestination(BridgedAppsSettings.class.getName())
+                        .setSourceMetricsCategory(getMetricsCategory())
+                        .setTitleRes(R.string.notif_listener_excluded_app_title)
+                        .setArguments(args)
+                        .setUserHandle(UserHandle.of(mUserId))
+                        .launch();
+                return true;
+            });
+        }
     }
 
     protected void setIntentAndFinish(boolean appChanged) {
diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java
index f4377ea..8a7e737 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -49,6 +49,7 @@
 import android.text.format.DateUtils;
 import android.util.IconDrawableFactory;
 import android.util.Log;
+import android.util.Slog;
 
 import androidx.annotation.VisibleForTesting;
 
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index c5f082d..616e6a9 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -28,7 +28,7 @@
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
         <activity android:name="com.android.settings.tests.BluetoothRequestPermissionTest"
             android:exported="true"
diff --git a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceControllerTest.java
new file mode 100644
index 0000000..3cdbd3a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceControllerTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.specialaccess.notificationaccess;
+
+import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
+import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.VersionedPackage;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.service.notification.NotificationListenerFilter;
+import android.util.ArraySet;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.notification.NotificationBackend;
+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.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class BridgedAppsPreferenceControllerTest {
+
+    private Context mContext;
+    private BridgedAppsPreferenceController mController;
+    @Mock
+    NotificationBackend mNm;
+    ComponentName mCn = new ComponentName("a", "b");
+    PreferenceScreen mScreen;
+    @Mock
+    ApplicationsState mAppState;
+    private ApplicationsState.AppEntry mAppEntry;
+    private ApplicationsState.AppEntry mAppEntry2;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        PreferenceManager preferenceManager = new PreferenceManager(mContext);
+        mScreen = preferenceManager.createPreferenceScreen(mContext);
+
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = "pkg";
+        ai.uid = 12300;
+        ai.sourceDir = "";
+        ApplicationInfo ai2 = new ApplicationInfo();
+        ai2.packageName = "another";
+        ai2.uid = 18800;
+        ai2.sourceDir = "";
+        mAppEntry = new ApplicationsState.AppEntry(mContext, ai, 0);
+        mAppEntry2 = new ApplicationsState.AppEntry(mContext, ai2, 1);
+
+        mAppEntry.info = ai;
+        mAppEntry.label = "hi";
+        Drawable icon = mock(Drawable.class);
+        mAppEntry.icon = icon;
+
+        mController = new BridgedAppsPreferenceController(mContext, "key");
+        mController.setCn(mCn);
+        mController.setNm(mNm);
+        mController.setUserId(0);
+        mController.setAppState(mAppState);
+        mController.displayPreference(mScreen);
+    }
+
+    @Test
+    public void onRebuildComplete_AddsToScreen() {
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
+
+        ArrayList<ApplicationsState.AppEntry> entries = new ArrayList<>();
+        entries.add(mAppEntry);
+        entries.add(mAppEntry2);
+
+        mController.onRebuildComplete(entries);
+
+        assertThat(mScreen.getPreferenceCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void onRebuildComplete_doesNotReaddToScreen() {
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
+
+        SwitchPreference p = mock(SwitchPreference.class);
+        when(p.getKey()).thenReturn("pkg|12300");
+        mScreen.addPreference(p);
+
+        ArrayList<ApplicationsState.AppEntry> entries = new ArrayList<>();
+        entries.add(mAppEntry);
+        entries.add(mAppEntry2);
+
+        mController.onRebuildComplete(entries);
+
+        assertThat(mScreen.getPreferenceCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void onRebuildComplete_removesExtras() {
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
+
+        Preference p = mock(Preference.class);
+        when(p.getKey()).thenReturn("pkg|123");
+        mScreen.addPreference(p);
+
+        ArrayList<ApplicationsState.AppEntry> entries = new ArrayList<>();
+        entries.add(mAppEntry);
+        entries.add(mAppEntry2);
+
+        mController.onRebuildComplete(entries);
+
+        assertThat((Preference) mScreen.findPreference("pkg|123")).isNull();
+    }
+
+    @Test
+    public void onRebuildComplete_buildsSetting() {
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
+
+        ArrayList<ApplicationsState.AppEntry> entries = new ArrayList<>();
+        entries.add(mAppEntry);
+
+        mController.onRebuildComplete(entries);
+
+        SwitchPreference actual = mScreen.findPreference("pkg|12300");
+
+        assertThat(actual.isChecked()).isTrue();
+        assertThat(actual.getTitle()).isEqualTo("hi");
+        assertThat(actual.getIcon()).isEqualTo(mAppEntry.icon);
+    }
+
+    @Test
+    public void onPreferenceChange_false() {
+        VersionedPackage vp = new VersionedPackage("pkg", 10567);
+        ArraySet<VersionedPackage> vps = new ArraySet<>();
+        vps.add(vp);
+        NotificationListenerFilter nlf = new NotificationListenerFilter(FLAG_FILTER_TYPE_ONGOING
+                | FLAG_FILTER_TYPE_CONVERSATIONS, vps);
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf);
+
+        SwitchPreference pref = new SwitchPreference(mContext);
+        pref.setKey("pkg|567");
+
+        mController.onPreferenceChange(pref, false);
+
+        ArgumentCaptor<NotificationListenerFilter> captor =
+                ArgumentCaptor.forClass(NotificationListenerFilter.class);
+        verify(mNm).setListenerFilter(eq(mCn), eq(0), captor.capture());
+        assertThat(captor.getValue().getDisallowedPackages()).contains(
+                new VersionedPackage("pkg", 567));
+        assertThat(captor.getValue().getDisallowedPackages()).contains(
+                new VersionedPackage("pkg", 10567));
+    }
+
+    @Test
+    public void onPreferenceChange_true() {
+        VersionedPackage vp = new VersionedPackage("pkg", 567);
+        VersionedPackage vp2 = new VersionedPackage("pkg", 10567);
+        ArraySet<VersionedPackage> vps = new ArraySet<>();
+        vps.add(vp);
+        vps.add(vp2);
+        NotificationListenerFilter nlf = new NotificationListenerFilter(FLAG_FILTER_TYPE_ONGOING
+                | FLAG_FILTER_TYPE_CONVERSATIONS, vps);
+        when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true);
+        when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf);
+
+        SwitchPreference pref = new SwitchPreference(mContext);
+        pref.setKey("pkg|567");
+
+        mController.onPreferenceChange(pref, true);
+
+        ArgumentCaptor<NotificationListenerFilter> captor =
+                ArgumentCaptor.forClass(NotificationListenerFilter.class);
+        verify(mNm).setListenerFilter(eq(mCn), eq(0), captor.capture());
+        assertThat(captor.getValue().getDisallowedPackages().size()).isEqualTo(1);
+        assertThat(captor.getValue().getDisallowedPackages()).contains(
+                new VersionedPackage("pkg", 10567));
+    }
+}