diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5c651b90c62..6d373fbda11 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8820,6 +8820,22 @@
Alerting notifications
Silent notifications
+
+ Apps that are not bridged to this listener
+
+
+ All apps are bridged
+
+
+
+ - %d app is not bridged
+ - %d apps are not bridged
+
+
+
+ Bridged apps
+
VR helper services
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 00000000000..590a468f07f
--- /dev/null
+++ b/res/xml/notification_access_bridged_apps_settings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/res/xml/notification_access_permission_details.xml b/res/xml/notification_access_permission_details.xml
index f7d928d5960..edac955c308 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"/>/>
-
+
-
-
\ 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 00000000000..9186bdb16a3
--- /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 apps) {
+ if (apps == null) {
+ return;
+ }
+ mNlf = mNm.getListenerFilter(mCn, mUserId);
+
+ // Create apps key set for removing useless preferences
+ final Set 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 apps = mSession.rebuild(mFilter,
+ ApplicationsState.ALPHA_COMPARATOR);
+ if (apps != null) {
+ onRebuildComplete(apps);
+ }
+ }
+
+ private void removeUselessPrefs(final Set 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 00000000000..d396a016684
--- /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 9f4b693a872..41a6efa82dc 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.Bundle;
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 @@ import java.util.Objects;
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 @@ public class NotificationAccessDetails extends DashboardFragment {
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 f4377eaae41..8a7e737dbb8 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -49,6 +49,7 @@ import android.service.notification.NotificationListenerFilter;
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 c5f082d3143..616e6a9076a 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -28,7 +28,7 @@
-
+
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 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 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 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 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 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 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 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));
+ }
+}