From eb2b36a5e2c4acad9ffbaba6384c7ad4189b0e4e Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 5 Jan 2021 16:54:54 -0500 Subject: [PATCH] Allow notification filtering per listener. Test: atest Bug: 173052211 Change-Id: I54c740e9755f18339c59aad4f1f5aecd8c734892 --- res/values/arrays.xml | 17 +++ res/values/strings.xml | 5 + ...notification_access_permission_details.xml | 17 +++ .../NotificationAccessDetails.java | 11 +- .../TypeFilterPreferenceController.java | 144 ++++++++++++++++++ .../notification/NotificationBackend.java | 27 ++++ .../TypeFilterPreferenceControllerTest.java | 136 +++++++++++++++++ 7 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceController.java create mode 100644 tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceControllerTest.java diff --git a/res/values/arrays.xml b/res/values/arrays.xml index f8df9206510..3528f9f17b2 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -1532,4 +1532,21 @@ 8 12 + + + + @string/notif_type_ongoing + @string/notif_type_conversation + @string/notif_type_alerting + @string/notif_type_silent + + + + + 8 + 1 + 2 + 4 + diff --git a/res/values/strings.xml b/res/values/strings.xml index 02752697f69..21bf8b1fd49 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8756,6 +8756,11 @@ Turn off Cancel + Allowed notification types + Important ongoing notifications + Conversation notifications + Alerting notifications + Silent notifications VR helper services diff --git a/res/xml/notification_access_permission_details.xml b/res/xml/notification_access_permission_details.xml index e0c7c1a2549..f7d928d5960 100644 --- a/res/xml/notification_access_permission_details.xml +++ b/res/xml/notification_access_permission_details.xml @@ -31,4 +31,21 @@ android:title="@string/notification_access_detail_switch" settings:controller="com.android.settings.applications.specialaccess.notificationaccess.ApprovalPreferenceController"/> + /> + + + + + \ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java index c0416a3858c..9f4b693a872 100644 --- a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java @@ -42,11 +42,10 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.notification.NotificationBackend; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; -import com.android.settingslib.applications.ApplicationsState; -import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -91,6 +90,10 @@ public class NotificationAccessDetails extends DashboardFragment { .setPackageInfo(mPackageInfo) .setPm(context.getPackageManager()) .setServiceName(mServiceName); + use(TypeFilterPreferenceController.class) + .setNm(new NotificationBackend()) + .setCn(mComponentName) + .setUserId(mUserId); } @Override @@ -172,6 +175,8 @@ public class NotificationAccessDetails extends DashboardFragment { ApprovalPreferenceController controller = use(ApprovalPreferenceController.class); controller.disable(cn); controller.updateState(screen.findPreference(controller.getPreferenceKey())); + TypeFilterPreferenceController dependent1 = use(TypeFilterPreferenceController.class); + dependent1.updateState(screen.findPreference(dependent1.getPreferenceKey())); } protected void enable(ComponentName cn) { @@ -179,6 +184,8 @@ public class NotificationAccessDetails extends DashboardFragment { ApprovalPreferenceController controller = use(ApprovalPreferenceController.class); controller.enable(cn); controller.updateState(screen.findPreference(controller.getPreferenceKey())); + TypeFilterPreferenceController dependent1 = use(TypeFilterPreferenceController.class); + dependent1.updateState(screen.findPreference(dependent1.getPreferenceKey())); } // To save binder calls, load this in the fragment rather than each preference controller diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceController.java new file mode 100644 index 00000000000..9d7fcc1aa0b --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceController.java @@ -0,0 +1,144 @@ +/* + * 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_ALERTING; +import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; +import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; +import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_SILENT; + +import android.content.ComponentName; +import android.content.Context; +import android.service.notification.NotificationListenerFilter; + +import androidx.preference.MultiSelectListPreference; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.notification.NotificationBackend; + +import java.util.HashSet; +import java.util.Set; + +public class TypeFilterPreferenceController extends BasePreferenceController implements + PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String TAG = "TypeFilterPrefCntlr"; + + private ComponentName mCn; + private int mUserId; + private NotificationBackend mNm; + private NotificationListenerFilter mNlf; + + public TypeFilterPreferenceController(Context context, String key) { + super(context, key); + } + + public TypeFilterPreferenceController setCn(ComponentName cn) { + mCn = cn; + return this; + } + + public TypeFilterPreferenceController setUserId(int userId) { + mUserId = userId; + return this; + } + + public TypeFilterPreferenceController setNm(NotificationBackend nm) { + mNm = nm; + return this; + } + + @Override + public int getAvailabilityStatus() { + if (mNm.isNotificationListenerAccessGranted(mCn)) { + return AVAILABLE; + } else { + return DISABLED_DEPENDENT_SETTING; + } + } + + @Override + public void updateState(Preference pref) { + mNlf = mNm.getListenerFilter(mCn, mUserId); + Set values = new HashSet<>(); + Set entries = new HashSet<>(); + + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_ONGOING)) { + values.add(String.valueOf(FLAG_FILTER_TYPE_ONGOING)); + entries.add(mContext.getString(R.string.notif_type_ongoing)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_CONVERSATIONS)) { + values.add(String.valueOf(FLAG_FILTER_TYPE_CONVERSATIONS)); + entries.add(mContext.getString(R.string.notif_type_conversation)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_ALERTING)) { + values.add(String.valueOf(FLAG_FILTER_TYPE_ALERTING)); + entries.add(mContext.getString(R.string.notif_type_alerting)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_SILENT)) { + values.add(String.valueOf(FLAG_FILTER_TYPE_SILENT)); + entries.add(mContext.getString(R.string.notif_type_silent)); + } + + final MultiSelectListPreference preference = (MultiSelectListPreference) pref; + preference.setValues(values); + super.updateState(preference); + pref.setEnabled(getAvailabilityStatus() == AVAILABLE); + } + + private boolean hasFlag(int value, int flag) { + return (value & flag) != 0; + } + + public CharSequence getSummary() { + Set entries = new HashSet<>(); + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_ONGOING)) { + entries.add(mContext.getString(R.string.notif_type_ongoing)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_CONVERSATIONS)) { + entries.add(mContext.getString(R.string.notif_type_conversation)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_ALERTING)) { + entries.add(mContext.getString(R.string.notif_type_alerting)); + } + if (hasFlag(mNlf.getTypes(), FLAG_FILTER_TYPE_SILENT)) { + entries.add(mContext.getString(R.string.notif_type_silent)); + } + return String.join(System.lineSeparator(), entries); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + // retrieve latest in case the package filter has changed + mNlf = mNm.getListenerFilter(mCn, mUserId); + + Set set = (Set) newValue; + + int newFilter = 0; + for (String filterType : set) { + newFilter |= Integer.parseInt(filterType); + } + mNlf.setTypes(newFilter); + preference.setSummary(getSummary()); + mNm.setListenerFilter(mCn, mUserId, mNlf); + return true; + } + +} \ No newline at end of file diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index 6d51eb3e870..43d170070fc 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -45,6 +45,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.service.notification.ConversationChannelWrapper; +import android.service.notification.NotificationListenerFilter; import android.text.format.DateUtils; import android.util.IconDrawableFactory; import android.util.Log; @@ -593,6 +594,32 @@ public class NotificationBackend { } } + public NotificationListenerFilter getListenerFilter(ComponentName cn, int userId) { + try { + return sINM.getListenerFilter(cn, userId); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return new NotificationListenerFilter(); + } + + public void setListenerFilter(ComponentName cn, int userId, NotificationListenerFilter nlf) { + try { + sINM.setListenerFilter(cn, userId, nlf); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + + public boolean isNotificationListenerAccessGranted(ComponentName cn) { + try { + return sINM.isNotificationListenerAccessGranted(cn); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return false; + } + /** * NotificationsSentState contains how often an app sends notifications and how recently it sent * one. diff --git a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceControllerTest.java new file mode 100644 index 00000000000..3014066f477 --- /dev/null +++ b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/TypeFilterPreferenceControllerTest.java @@ -0,0 +1,136 @@ +/* + * 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 android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_SILENT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.service.notification.NotificationListenerFilter; +import android.util.ArraySet; + +import androidx.preference.MultiSelectListPreference; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.notification.NotificationBackend; + +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.Set; + +@RunWith(AndroidJUnit4.class) +public class TypeFilterPreferenceControllerTest { + + private Context mContext; + private TypeFilterPreferenceController mController; + @Mock + NotificationBackend mNm; + ComponentName mCn = new ComponentName("a", "b"); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + + mController = new TypeFilterPreferenceController(mContext, "key"); + mController.setCn(mCn); + mController.setNm(mNm); + mController.setUserId(0); + } + + @Test + public void updateState_enabled() { + when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true); + when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter()); + MultiSelectListPreference pref = new MultiSelectListPreference(mContext); + + mController.updateState(pref); + assertThat(pref.isEnabled()).isTrue(); + } + + @Test + public void updateState_disabled() { + when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(false); + when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter()); + MultiSelectListPreference pref = new MultiSelectListPreference(mContext); + + mController.updateState(pref); + assertThat(pref.isEnabled()).isFalse(); + } + + @Test + public void updateState() { + NotificationListenerFilter nlf = new NotificationListenerFilter(FLAG_FILTER_TYPE_ONGOING + | FLAG_FILTER_TYPE_SILENT, new ArraySet<>()); + when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true); + when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf); + + MultiSelectListPreference pref = new MultiSelectListPreference(mContext); + mController.updateState(pref); + + assertThat(pref.getValues()).containsExactlyElementsIn( + new String[] {String.valueOf(FLAG_FILTER_TYPE_ONGOING), + String.valueOf(FLAG_FILTER_TYPE_SILENT)}); + assertThat(pref.getSummary()).isNotNull(); + } + + @Test + public void getSummary() { + NotificationListenerFilter nlf = new NotificationListenerFilter(FLAG_FILTER_TYPE_ONGOING + | FLAG_FILTER_TYPE_CONVERSATIONS, new ArraySet<>()); + when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true); + when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf); + + MultiSelectListPreference pref = new MultiSelectListPreference(mContext); + mController.updateState(pref); + + assertThat(mController.getSummary().toString()).ignoringCase().contains("ongoing"); + assertThat(mController.getSummary().toString()).ignoringCase().contains("conversation"); + } + + @Test + public void onPreferenceChange() { + NotificationListenerFilter nlf = new NotificationListenerFilter(FLAG_FILTER_TYPE_ONGOING + | FLAG_FILTER_TYPE_CONVERSATIONS, new ArraySet<>()); + when(mNm.isNotificationListenerAccessGranted(mCn)).thenReturn(true); + when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf); + + MultiSelectListPreference pref = new MultiSelectListPreference(mContext); + + mController.onPreferenceChange(pref, Set.of("8", "1", "4")); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(NotificationListenerFilter.class); + verify(mNm).setListenerFilter(eq(mCn), eq(0), captor.capture()); + assertThat(captor.getValue().getTypes()).isEqualTo(FLAG_FILTER_TYPE_CONVERSATIONS + | FLAG_FILTER_TYPE_SILENT | FLAG_FILTER_TYPE_ONGOING); + } +}