From 20814ed5c7e053c58032f5b185585db7cb2fa9ae Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 5 Feb 2021 18:30:52 -0500 Subject: [PATCH] Add package filtering to NLSes Now users can prevent a given notification listener from seeing notifications from particular apps. Test: settings unit tests Bug: 173052211 Change-Id: Ia868d6dc2da1ae7f75c0dca47034e28910584acd --- res/values/strings.xml | 16 ++ ...ification_access_bridged_apps_settings.xml | 25 ++ ...notification_access_permission_details.xml | 12 +- .../BridgedAppsPreferenceController.java | 218 +++++++++++++++++ .../BridgedAppsSettings.java | 137 +++++++++++ .../NotificationAccessDetails.java | 33 +++ .../notification/NotificationBackend.java | 1 + tests/unit/AndroidManifest.xml | 2 +- .../BridgedAppsPreferenceControllerTest.java | 221 ++++++++++++++++++ 9 files changed, 658 insertions(+), 7 deletions(-) create mode 100644 res/xml/notification_access_bridged_apps_settings.xml create mode 100644 src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceController.java create mode 100644 src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsSettings.java create mode 100644 tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index dda89ee30ba..ce8edf07561 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8774,6 +8774,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)); + } +}