diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index a8ce73d9bd270..697a377dd99ff 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -35,6 +35,7 @@ import android.service.notification.Condition; import android.service.notification.IConditionListener; import android.service.notification.IConditionProvider; import android.service.notification.INotificationListener; +import android.service.notification.NotificationListenerFilter; import android.service.notification.StatusBarNotification; import android.app.AutomaticZenRule; import android.service.notification.ZenModeConfig; @@ -224,4 +225,7 @@ interface INotificationManager boolean getPrivateNotificationsAllowed(); long pullStats(long startNs, int report, boolean doAgg, out List stats); + + NotificationListenerFilter getListenerFilter(in ComponentName cn, int userId); + void setListenerFilter(in ComponentName cn, int userId, in NotificationListenerFilter nlf); } diff --git a/core/java/android/service/notification/NotificationListenerFilter.aidl b/core/java/android/service/notification/NotificationListenerFilter.aidl new file mode 100644 index 0000000000000..c186b74d04f73 --- /dev/null +++ b/core/java/android/service/notification/NotificationListenerFilter.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020, 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 android.service.notification; + +parcelable NotificationListenerFilter; + diff --git a/core/java/android/service/notification/NotificationListenerFilter.java b/core/java/android/service/notification/NotificationListenerFilter.java new file mode 100644 index 0000000000000..c945c2d64519f --- /dev/null +++ b/core/java/android/service/notification/NotificationListenerFilter.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, 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 android.service.notification; + +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_SILENT; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArraySet; + +/** + * Specifies a filter for what types of notifications should be bridged to notification listeners. + * Each requested listener will have their own filter instance. + * @hide + */ +public class NotificationListenerFilter implements Parcelable { + private int mAllowedNotificationTypes; + private ArraySet mDisallowedPackages; + + public NotificationListenerFilter() { + mAllowedNotificationTypes = FLAG_FILTER_TYPE_CONVERSATIONS + | FLAG_FILTER_TYPE_ALERTING + | FLAG_FILTER_TYPE_SILENT; + mDisallowedPackages = new ArraySet<>(); + } + + public NotificationListenerFilter(int types, ArraySet pkgs) { + mAllowedNotificationTypes = types; + mDisallowedPackages = pkgs; + } + + /** + * @hide + */ + protected NotificationListenerFilter(Parcel in) { + mAllowedNotificationTypes = in.readInt(); + mDisallowedPackages = (ArraySet) in.readArraySet(String.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAllowedNotificationTypes); + dest.writeArraySet(mDisallowedPackages); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public NotificationListenerFilter createFromParcel(Parcel in) { + return new NotificationListenerFilter(in); + } + + @Override + public NotificationListenerFilter[] newArray(int size) { + return new NotificationListenerFilter[size]; + } + }; + + public boolean isTypeAllowed(int type) { + return (mAllowedNotificationTypes & type) != 0; + } + + public boolean isPackageAllowed(String pkg) { + return !mDisallowedPackages.contains(pkg); + } + + public int getTypes() { + return mAllowedNotificationTypes; + } + + public ArraySet getDisallowedPackages() { + return mDisallowedPackages; + } + + public void setTypes(int types) { + mAllowedNotificationTypes = types; + } + + public void setDisallowedPackages(ArraySet pkgs) { + mDisallowedPackages = pkgs; + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 440eeb1619f02..ccde0bcf71c3f 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -241,6 +241,23 @@ public abstract class NotificationListenerService extends Service { }) public @interface NotificationCancelReason{}; + /** + * A flag value indicating that this notification listener can see conversation type + * notifications. + * @hide + */ + public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; + /** + * A flag value indicating that this notification listener can see altering type notifications. + * @hide + */ + public static final int FLAG_FILTER_TYPE_ALERTING = 2; + /** + * A flag value indicating that this notification listener can see silent type notifications. + * @hide + */ + public static final int FLAG_FILTER_TYPE_SILENT = 4; + /** * The full trim of the StatusBarNotification including all its features. * diff --git a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java new file mode 100644 index 0000000000000..a43b238405d10 --- /dev/null +++ b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 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 android.service.notification; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +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_SILENT; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.NotificationChannel; +import android.os.Parcel; +import android.util.ArraySet; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NotificationListenerFilterTest { + + @Test + public void testEmptyConstructor() { + NotificationListenerFilter nlf = new NotificationListenerFilter(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_CONVERSATIONS)).isTrue(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_ALERTING)).isTrue(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_SILENT)).isTrue(); + assertThat(nlf.getTypes()).isEqualTo(FLAG_FILTER_TYPE_CONVERSATIONS + | FLAG_FILTER_TYPE_ALERTING + | FLAG_FILTER_TYPE_SILENT); + + assertThat(nlf.getDisallowedPackages()).isEmpty(); + assertThat(nlf.isPackageAllowed("pkg1")).isTrue(); + } + + + @Test + public void testConstructor() { + ArraySet pkgs = new ArraySet<>(new String[] {"pkg1", "pkg2"}); + NotificationListenerFilter nlf = + new NotificationListenerFilter(FLAG_FILTER_TYPE_ALERTING, pkgs); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_CONVERSATIONS)).isFalse(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_ALERTING)).isTrue(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_SILENT)).isFalse(); + assertThat(nlf.getTypes()).isEqualTo(FLAG_FILTER_TYPE_ALERTING); + + assertThat(nlf.getDisallowedPackages()).contains("pkg1"); + assertThat(nlf.getDisallowedPackages()).contains("pkg2"); + assertThat(nlf.isPackageAllowed("pkg1")).isFalse(); + assertThat(nlf.isPackageAllowed("pkg2")).isFalse(); + } + + @Test + public void testSetDisallowedPackages() { + NotificationListenerFilter nlf = new NotificationListenerFilter(); + + ArraySet pkgs = new ArraySet<>(new String[] {"pkg1"}); + nlf.setDisallowedPackages(pkgs); + + assertThat(nlf.isPackageAllowed("pkg1")).isFalse(); + } + + @Test + public void testSetTypes() { + NotificationListenerFilter nlf = new NotificationListenerFilter(); + + nlf.setTypes(FLAG_FILTER_TYPE_ALERTING | FLAG_FILTER_TYPE_SILENT); + + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_CONVERSATIONS)).isFalse(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_ALERTING)).isTrue(); + assertThat(nlf.isTypeAllowed(FLAG_FILTER_TYPE_SILENT)).isTrue(); + assertThat(nlf.getTypes()).isEqualTo(FLAG_FILTER_TYPE_ALERTING + | FLAG_FILTER_TYPE_SILENT); + } + + @Test + public void testDescribeContents() { + final int expected = 0; + ArraySet pkgs = new ArraySet<>(new String[] {"pkg1", "pkg2"}); + NotificationListenerFilter nlf = + new NotificationListenerFilter(FLAG_FILTER_TYPE_ALERTING, pkgs); + assertThat(nlf.describeContents()).isEqualTo(expected); + } + + @Test + public void testParceling() { + ArraySet pkgs = new ArraySet<>(new String[] {"pkg1", "pkg2"}); + NotificationListenerFilter nlf = + new NotificationListenerFilter(FLAG_FILTER_TYPE_ALERTING, pkgs); + + Parcel parcel = Parcel.obtain(); + nlf.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + NotificationListenerFilter nlf1 = + NotificationListenerFilter.CREATOR.createFromParcel(parcel); + assertThat(nlf1.isTypeAllowed(FLAG_FILTER_TYPE_CONVERSATIONS)).isFalse(); + assertThat(nlf1.isTypeAllowed(FLAG_FILTER_TYPE_ALERTING)).isTrue(); + assertThat(nlf1.isTypeAllowed(FLAG_FILTER_TYPE_SILENT)).isFalse(); + assertThat(nlf1.getTypes()).isEqualTo(FLAG_FILTER_TYPE_ALERTING); + + assertThat(nlf1.getDisallowedPackages()).contains("pkg1"); + assertThat(nlf1.getDisallowedPackages()).contains("pkg2"); + assertThat(nlf1.isPackageAllowed("pkg1")).isFalse(); + assertThat(nlf1.isPackageAllowed("pkg2")).isFalse(); + } +} diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 54e9b37be64a1..21537e6aaa223 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -73,7 +73,6 @@ import com.android.server.utils.TimingsTraceAndSlog; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.PrintWriter; @@ -544,7 +543,8 @@ abstract public class ManagedServices { /** * This is called to process tags other than {@link #TAG_MANAGED_SERVICES}. */ - protected void readExtraTag(String tag, TypedXmlPullParser parser) throws IOException {} + protected void readExtraTag(String tag, TypedXmlPullParser parser) + throws IOException, XmlPullParserException {} protected final void migrateToXml() { for (UserInfo user : mUm.getUsers()) { @@ -1613,6 +1613,7 @@ abstract public class ManagedServices { public boolean isSystem; public ServiceConnection connection; public int targetSdkVersion; + public Pair mKey; public ManagedServiceInfo(IInterface service, ComponentName component, int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion) { @@ -1622,6 +1623,7 @@ abstract public class ManagedServices { this.isSystem = isSystem; this.connection = connection; this.targetSdkVersion = targetSdkVersion; + mKey = Pair.create(component, userid); } public boolean isGuest(ManagedServices host) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c9ed5183bdb78..a146c8c53960f 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -66,6 +66,9 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; +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_SILENT; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS; @@ -163,6 +166,8 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutServiceInternal; import android.content.pm.UserInfo; @@ -209,6 +214,7 @@ import android.service.notification.INotificationListener; import android.service.notification.IStatusBarNotificationHolder; import android.service.notification.ListenersDisablingEffectsProto; import android.service.notification.NotificationAssistantService; +import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationRecordProto; @@ -285,6 +291,7 @@ import libcore.io.IoUtils; import org.json.JSONException; import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; @@ -301,6 +308,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -1021,7 +1029,7 @@ public class NotificationManagerService extends SystemService { nv.recycle(); } reportUserInteraction(r); - mAssistants.notifyAssistantActionClicked(r.getSbn(), action, generatedByAssistant); + mAssistants.notifyAssistantActionClicked(r, action, generatedByAssistant); } } @@ -1110,7 +1118,7 @@ public class NotificationManagerService extends SystemService { reportSeen(r); } r.setVisibility(true, nv.rank, nv.count, mNotificationRecordLogger); - mAssistants.notifyAssistantVisibilityChangedLocked(r.getSbn(), true); + mAssistants.notifyAssistantVisibilityChangedLocked(r, true); boolean isHun = (nv.location == NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP); // hasBeenVisiblyExpanded must be called after updating the expansion state of @@ -1129,7 +1137,7 @@ public class NotificationManagerService extends SystemService { NotificationRecord r = mNotificationsByKey.get(nv.key); if (r == null) continue; r.setVisibility(false, nv.rank, nv.count, mNotificationRecordLogger); - mAssistants.notifyAssistantVisibilityChangedLocked(r.getSbn(), false); + mAssistants.notifyAssistantVisibilityChangedLocked(r, false); nv.recycle(); } } @@ -1161,7 +1169,7 @@ public class NotificationManagerService extends SystemService { reportUserInteraction(r); } mAssistants.notifyAssistantExpansionChangedLocked( - r.getSbn(), userAction, expanded); + r.getSbn(), r.getNotificationType(), userAction, expanded); } } } @@ -1180,7 +1188,7 @@ public class NotificationManagerService extends SystemService { NotificationRecordLogger.NotificationEvent.NOTIFICATION_DIRECT_REPLIED, r); reportUserInteraction(r); - mAssistants.notifyAssistantNotificationDirectReplyLocked(r.getSbn()); + mAssistants.notifyAssistantNotificationDirectReplyLocked(r); } } } @@ -1227,7 +1235,8 @@ public class NotificationManagerService extends SystemService { // Treat clicking on a smart reply as a user interaction. reportUserInteraction(r); mAssistants.notifyAssistantSuggestedReplySent( - r.getSbn(), reply, r.getSuggestionsGeneratedByAssistant()); + r.getSbn(), r.getNotificationType(), reply, + r.getSuggestionsGeneratedByAssistant()); } } } @@ -2241,7 +2250,8 @@ public class NotificationManagerService extends SystemService { init(handler, new RankingHandlerWorker(mRankingThread.getLooper()), AppGlobals.getPackageManager(), getContext().getPackageManager(), getLocalService(LightsManager.class), - new NotificationListeners(AppGlobals.getPackageManager()), + new NotificationListeners(getContext(), mNotificationLock, mUserProfiles, + AppGlobals.getPackageManager()), new NotificationAssistants(getContext(), mNotificationLock, mUserProfiles, AppGlobals.getPackageManager()), new ConditionProviders(getContext(), mUserProfiles, AppGlobals.getPackageManager()), @@ -3252,6 +3262,21 @@ public class NotificationManagerService extends SystemService { mHistoryManager.deleteNotificationHistoryItem(pkg, uid, postedTime); } + @Override + public NotificationListenerFilter getListenerFilter(ComponentName cn, int userId) { + checkCallerIsSystem(); + return mListeners.getNotificationListenerFilter(Pair.create(cn, userId)); + } + + @Override + public void setListenerFilter(ComponentName cn, int userId, + NotificationListenerFilter nlf) { + checkCallerIsSystem(); + mListeners.setNotificationListenerFilter(Pair.create(cn, userId), nlf); + // TODO (b/173052211): cancel notifications for listeners that can no longer see them + handleSavePolicyFile(); + } + @Override public int getPackageImportance(String pkg) { checkCallerIsSystemOrSameApp(pkg); @@ -4268,7 +4293,7 @@ public class NotificationManagerService extends SystemService { : mNotificationList.get(i); if (r == null) continue; StatusBarNotification sbn = r.getSbn(); - if (!isVisibleToListener(sbn, info)) continue; + if (!isVisibleToListener(sbn, r.getNotificationType(), info)) continue; StatusBarNotification sbnToSend = (trim == TRIM_FULL) ? sbn : sbn.cloneLight(); list.add(sbnToSend); @@ -4298,7 +4323,7 @@ public class NotificationManagerService extends SystemService { final NotificationRecord r = snoozedRecords.get(i); if (r == null) continue; StatusBarNotification sbn = r.getSbn(); - if (!isVisibleToListener(sbn, info)) continue; + if (!isVisibleToListener(sbn, r.getNotificationType(), info)) continue; StatusBarNotification sbnToSend = (trim == TRIM_FULL) ? sbn : sbn.cloneLight(); list.add(sbnToSend); @@ -6339,7 +6364,7 @@ public class NotificationManagerService extends SystemService { cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null); updateLightsLocked(); if (mSnoozeCriterionId != null) { - mAssistants.notifyAssistantSnoozedLocked(r.getSbn(), mSnoozeCriterionId); + mAssistants.notifyAssistantSnoozedLocked(r, mSnoozeCriterionId); mSnoozeHelper.snooze(r, mSnoozeCriterionId); } else { mSnoozeHelper.snooze(r, mDuration); @@ -8812,7 +8837,7 @@ public class NotificationManagerService extends SystemService { for (int i = 0; i < N; i++) { NotificationRecord record = mNotificationList.get(i); - if (!isVisibleToListener(record.getSbn(), info)) { + if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) { continue; } final String key = record.getSbn().getKey(); @@ -8886,11 +8911,21 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting - boolean isVisibleToListener(StatusBarNotification sbn, ManagedServiceInfo listener) { + boolean isVisibleToListener(StatusBarNotification sbn, int notificationType, + ManagedServiceInfo listener) { if (!listener.enabledAndUserMatches(sbn.getUserId())) { return false; } - return isInteractionVisibleToListener(listener, sbn.getUserId()); + if (!isInteractionVisibleToListener(listener, sbn.getUserId())) { + return false; + } + NotificationListenerFilter nls = mListeners.getNotificationListenerFilter(listener.mKey); + if (nls != null + && (!nls.isTypeAllowed(notificationType) + || !nls.isPackageAllowed(sbn.getPackageName()))) { + return false; + } + return true; } /** @@ -9126,7 +9161,8 @@ public class NotificationManagerService extends SystemService { for (final ManagedServiceInfo info : NotificationAssistants.this.getServices()) { ArrayList keys = new ArrayList<>(records.size()); for (NotificationRecord r : records) { - boolean sbnVisible = isVisibleToListener(r.getSbn(), info) + boolean sbnVisible = isVisibleToListener( + r.getSbn(), r.getNotificationType(), info) && info.isSameUser(r.getUserId()); if (sbnVisible) { keys.add(r.getKey()); @@ -9241,6 +9277,7 @@ public class NotificationManagerService extends SystemService { final StatusBarNotification sbn = r.getSbn(); notifyAssistantLocked( sbn, + r.getNotificationType(), true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9257,14 +9294,15 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyAssistantVisibilityChangedLocked( - final StatusBarNotification sbn, + final NotificationRecord r, final boolean isVisible) { - final String key = sbn.getKey(); + final String key = r.getSbn().getKey(); if (DBG) { Slog.d(TAG, "notifyAssistantVisibilityChangedLocked: " + key); } notifyAssistantLocked( - sbn, + r.getSbn(), + r.getNotificationType(), true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9278,11 +9316,13 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyAssistantExpansionChangedLocked( final StatusBarNotification sbn, + final int notificationType, final boolean isUserAction, final boolean isExpanded) { final String key = sbn.getKey(); notifyAssistantLocked( sbn, + notificationType, true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9295,10 +9335,11 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyAssistantNotificationDirectReplyLocked( - final StatusBarNotification sbn) { - final String key = sbn.getKey(); + final NotificationRecord r) { + final String key = r.getKey(); notifyAssistantLocked( - sbn, + r.getSbn(), + r.getNotificationType(), true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9311,10 +9352,12 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyAssistantSuggestedReplySent( - final StatusBarNotification sbn, CharSequence reply, boolean generatedByAssistant) { + final StatusBarNotification sbn, int notificationType, + CharSequence reply, boolean generatedByAssistant) { final String key = sbn.getKey(); notifyAssistantLocked( sbn, + notificationType, true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9332,11 +9375,12 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyAssistantActionClicked( - final StatusBarNotification sbn, Notification.Action action, + final NotificationRecord r, Notification.Action action, boolean generatedByAssistant) { - final String key = sbn.getKey(); + final String key = r.getSbn().getKey(); notifyAssistantLocked( - sbn, + r.getSbn(), + r.getNotificationType(), true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9358,9 +9402,10 @@ public class NotificationManagerService extends SystemService { */ @GuardedBy("mNotificationLock") private void notifyAssistantSnoozedLocked( - final StatusBarNotification sbn, final String snoozeCriterionId) { + final NotificationRecord r, final String snoozeCriterionId) { notifyAssistantLocked( - sbn, + r.getSbn(), + r.getNotificationType(), true /* sameUserOnly */, (assistant, sbnHolder) -> { try { @@ -9384,6 +9429,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") private void notifyAssistantLocked( final StatusBarNotification sbn, + int notificationType, boolean sameUserOnly, BiConsumer callback) { TrimCache trimCache = new TrimCache(sbn); @@ -9397,7 +9443,7 @@ public class NotificationManagerService extends SystemService { + sameUserOnly + "], callback = [" + callback + "]"); } for (final ManagedServiceInfo info : NotificationAssistants.this.getServices()) { - boolean sbnVisible = isVisibleToListener(sbn, info) + boolean sbnVisible = isVisibleToListener(sbn, notificationType, info) && (!sameUserOnly || info.isSameUser(sbn.getUserId())); if (debug) { Slog.v(TAG, "notifyAssistantLocked info=" + info + " snbVisible=" + sbnVisible); @@ -9451,11 +9497,22 @@ public class NotificationManagerService extends SystemService { public class NotificationListeners extends ManagedServices { static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners"; + static final String TAG_REQUESTED_LISTENERS = "requested_listeners"; + static final String TAG_REQUESTED_LISTENER = "listener"; + static final String ATT_COMPONENT = "component"; + static final String ATT_TYPES = "types"; + static final String ATT_PKGS = "pkgs"; + static final String TAG_APPROVED = "allowed"; + static final String TAG_DISALLOWED= "disallowed"; + static final String XML_SEPARATOR = ","; private final ArraySet mLightTrimListeners = new ArraySet<>(); + ArrayMap, NotificationListenerFilter> + mRequestedNotificationListeners = new ArrayMap<>(); - public NotificationListeners(IPackageManager pm) { - super(getContext(), mNotificationLock, mUserProfiles, pm); + public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, + IPackageManager pm) { + super(context, lock, userProfiles, pm); } @Override @@ -9551,6 +9608,59 @@ public class NotificationManagerService extends SystemService { mLightTrimListeners.remove(removed); } + @Override + public void onUserRemoved(int user) { + super.onUserRemoved(user); + for (int i = mRequestedNotificationListeners.size() - 1; i >= 0; i--) { + if (mRequestedNotificationListeners.keyAt(i).second == user) { + mRequestedNotificationListeners.removeAt(i); + } + } + } + + @Override + public void onUserUnlocked(int user) { + int flags = PackageManager.GET_SERVICES | PackageManager.GET_META_DATA; + + final PackageManager pmWrapper = mContext.getPackageManager(); + List installedServices = pmWrapper.queryIntentServicesAsUser( + new Intent(getConfig().serviceInterface), flags, user); + + for (ResolveInfo resolveInfo : installedServices) { + ServiceInfo info = resolveInfo.serviceInfo; + + if (!getConfig().bindPermission.equals(info.permission)) { + continue; + } + Pair key = Pair.create(info.getComponentName(), user); + if (!mRequestedNotificationListeners.containsKey(key)) { + mRequestedNotificationListeners.put(key, new NotificationListenerFilter()); + } + } + super.onUserUnlocked(user); + } + + @Override + public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) { + super.onPackagesChanged(removingPackage, pkgList, uidList); + + // Since the default behavior is to allow everything, we don't need to explicitly + // handle package add or update. they will be added to the xml file on next boot or + // when the user tries to change the settings. + if (removingPackage) { + for (int i = 0; i < pkgList.length; i++) { + String pkg = pkgList[i]; + int userId = UserHandle.getUserId(uidList[i]); + for (int j = mRequestedNotificationListeners.size() - 1; j >= 0; j--) { + Pair key = mRequestedNotificationListeners.keyAt(j); + if (key.second == userId && key.first.getPackageName().equals(pkg)) { + mRequestedNotificationListeners.removeAt(j); + } + } + } + } + } + @Override protected String getRequiredPermission() { return null; @@ -9563,6 +9673,75 @@ public class NotificationManagerService extends SystemService { return true; } + @Override + protected void readExtraTag(String tag, TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + if (TAG_REQUESTED_LISTENERS.equals(tag)) { + final int listenersOuterDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, listenersOuterDepth)) { + if (!TAG_REQUESTED_LISTENER.equals(parser.getName())) { + continue; + } + final int userId = XmlUtils.readIntAttribute(parser, ATT_USER_ID); + final ComponentName cn = ComponentName.unflattenFromString( + XmlUtils.readStringAttribute(parser, ATT_COMPONENT)); + int approved = FLAG_FILTER_TYPE_CONVERSATIONS | FLAG_FILTER_TYPE_ALERTING + | FLAG_FILTER_TYPE_SILENT; + + ArraySet disallowedPkgs = new ArraySet<>(); + final int listenerOuterDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, listenerOuterDepth)) { + if (TAG_APPROVED.equals(parser.getName())) { + approved = XmlUtils.readIntAttribute(parser, ATT_TYPES); + } else if (TAG_DISALLOWED.equals(parser.getName())) { + String pkgs = XmlUtils.readStringAttribute(parser, ATT_PKGS); + if (!TextUtils.isEmpty(pkgs)) { + disallowedPkgs = new ArraySet<>(pkgs.split(XML_SEPARATOR)); + } + } + } + NotificationListenerFilter nlf = + new NotificationListenerFilter(approved, disallowedPkgs); + mRequestedNotificationListeners.put(Pair.create(cn, userId), nlf); + } + } + } + + @Override + protected void writeExtraXmlTags(TypedXmlSerializer out) throws IOException { + out.startTag(null, TAG_REQUESTED_LISTENERS); + for (Pair listener : mRequestedNotificationListeners.keySet()) { + NotificationListenerFilter nlf = mRequestedNotificationListeners.get(listener); + out.startTag(null, TAG_REQUESTED_LISTENER); + XmlUtils.writeStringAttribute( + out, ATT_COMPONENT, listener.first.flattenToString()); + XmlUtils.writeIntAttribute(out, ATT_USER_ID, listener.second); + + out.startTag(null, TAG_APPROVED); + XmlUtils.writeIntAttribute(out, ATT_TYPES, nlf.getTypes()); + out.endTag(null, TAG_APPROVED); + + out.startTag(null, TAG_DISALLOWED); + XmlUtils.writeStringAttribute( + out, ATT_PKGS, String.join(XML_SEPARATOR, nlf.getDisallowedPackages())); + out.endTag(null, TAG_DISALLOWED); + + out.endTag(null, TAG_REQUESTED_LISTENER); + } + + out.endTag(null, TAG_REQUESTED_LISTENERS); + } + + protected @Nullable NotificationListenerFilter getNotificationListenerFilter( + Pair pair) { + return mRequestedNotificationListeners.get(pair); + } + + protected void setNotificationListenerFilter(Pair pair, + NotificationListenerFilter nlf) { + mRequestedNotificationListeners.put(pair, nlf); + } + @GuardedBy("mNotificationLock") public void setOnNotificationPostedTrimLocked(ManagedServiceInfo info, int trim) { if (trim == TRIM_LIGHT) { @@ -9618,8 +9797,9 @@ public class NotificationManagerService extends SystemService { TrimCache trimCache = new TrimCache(sbn); for (final ManagedServiceInfo info : getServices()) { - boolean sbnVisible = isVisibleToListener(sbn, info); - boolean oldSbnVisible = (oldSbn != null) && isVisibleToListener(oldSbn, info); + boolean sbnVisible = isVisibleToListener(sbn, r. getNotificationType(), info); + boolean oldSbnVisible = (oldSbn != null) + && isVisibleToListener(oldSbn, old.getNotificationType(), info); // This notification hasn't been and still isn't visible -> ignore. if (!oldSbnVisible && !sbnVisible) { continue; @@ -9672,7 +9852,7 @@ public class NotificationManagerService extends SystemService { for (final NotificationRecord r : mNotificationList) { // When granting permissions, ignore notifications which are invisible. // When revoking permissions, all notifications are invisible, so process all. - if (grant && !isVisibleToListener(r.getSbn(), info)) { + if (grant && !isVisibleToListener(r.getSbn(), r.getNotificationType(), info)) { continue; } // If the notification is hidden, permissions are not required by the listener. @@ -9714,7 +9894,7 @@ public class NotificationManagerService extends SystemService { // notification final StatusBarNotification sbnLight = sbn.cloneLight(); for (final ManagedServiceInfo info : getServices()) { - if (!isVisibleToListener(sbn, info)) { + if (!isVisibleToListener(sbn, r.getNotificationType(), info)) { continue; } @@ -9754,7 +9934,8 @@ public class NotificationManagerService extends SystemService { public void notifyRankingUpdateLocked(List changedHiddenNotifications) { boolean isHiddenRankingUpdate = changedHiddenNotifications != null && changedHiddenNotifications.size() > 0; - + // TODO (b/73052211): if the ranking update changed the notification type, + // cancel notifications for NLSes that can't see them anymore for (final ManagedServiceInfo serviceInfo : getServices()) { if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { @@ -9765,7 +9946,8 @@ public class NotificationManagerService extends SystemService { if (isHiddenRankingUpdate && serviceInfo.targetSdkVersion >= Build.VERSION_CODES.P) { for (NotificationRecord rec : changedHiddenNotifications) { - if (isVisibleToListener(rec.getSbn(), serviceInfo)) { + if (isVisibleToListener( + rec.getSbn(), rec.getNotificationType(), serviceInfo)) { notifyThisListener = true; break; } @@ -9979,6 +10161,7 @@ public class NotificationManagerService extends SystemService { } } + class RoleObserver implements OnRoleHoldersChangedListener { // Role name : user id : list of approved packages private ArrayMap>> mNonBlockableDefaultApps; diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index cff4f07fa9d86..6bc4f7e333e2b 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -1257,6 +1257,16 @@ public final class NotificationRecord { return !Objects.equals(getSbn().getPackageName(), getSbn().getOpPkg()); } + public int getNotificationType() { + if (isConversation()) { + return NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; + } else if (getImportance() >= IMPORTANCE_DEFAULT) { + return NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; + } else { + return NotificationListenerService.FLAG_FILTER_TYPE_SILENT; + } + } + /** * @return all {@link Uri} that should have permission granted to whoever * will be rendering it. This list has already been vetted to only diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java new file mode 100644 index 0000000000000..a093e0d4a7401 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2020 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.server.notification; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.INotificationManager; +import android.content.ComponentName; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.service.notification.NotificationListenerFilter; +import android.util.ArraySet; +import android.util.Pair; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class NotificationListenersTest extends UiServiceTestCase { + + @Mock + private PackageManager mPm; + @Mock + private IPackageManager miPm; + + @Mock + NotificationManagerService mNm; + @Mock + private INotificationManager mINm; + + NotificationManagerService.NotificationListeners mListeners; + + private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); + private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + getContext().setMockPackageManager(mPm); + + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm)); + when(mNm.getBinderService()).thenReturn(mINm); + } + + @Test + public void testReadExtraTag() throws Exception { + String xml = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(xml.getBytes())), null); + parser.nextTag(); + mListeners.readExtraTag("requested_listeners", parser); + + validateListenersFromXml(); + } + + @Test + public void testWriteExtraTag() throws Exception { + NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); + NotificationListenerFilter nlf2 = + new NotificationListenerFilter(4, new ArraySet<>(new String[] {"something"})); + mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); + mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); + + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + mListeners.writeExtraXmlTags(serializer); + serializer.endDocument(); + serializer.flush(); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mListeners.readExtraTag("requested_listeners", parser); + + validateListenersFromXml(); + } + + private void validateListenersFromXml() { + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0)).getTypes()) + .isEqualTo(7); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0)) + .getDisallowedPackages()) + .isEmpty(); + + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes()) + .isEqualTo(4); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)) + .getDisallowedPackages()) + .contains("something"); + } + + @Test + public void testOnUserRemoved() { + NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); + NotificationListenerFilter nlf2 = + new NotificationListenerFilter(4, new ArraySet<>(new String[] {"something"})); + mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); + mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); + + mListeners.onUserRemoved(0); + + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))).isNull(); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes()) + .isEqualTo(4); + } + + @Test + public void testOnUserUnlocked() { + // one exists already, say from xml + NotificationListenerFilter nlf = + new NotificationListenerFilter(4, new ArraySet<>(new String[] {"something"})); + mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf); + + // new service exists or backfilling on upgrade to S + ServiceInfo si = new ServiceInfo(); + si.permission = mListeners.getConfig().bindPermission; + si.packageName = "new"; + si.name = "comp"; + ResolveInfo ri = new ResolveInfo(); + ri.serviceInfo = si; + + // incorrect service + ServiceInfo si2 = new ServiceInfo(); + ResolveInfo ri2 = new ResolveInfo(); + ri2.serviceInfo = si2; + si2.packageName = "new2"; + si2.name = "comp2"; + + List ris = new ArrayList<>(); + ris.add(ri); + ris.add(ri2); + + when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(ris); + + mListeners.onUserUnlocked(0); + + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0)).getTypes()) + .isEqualTo(4); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0)) + .getDisallowedPackages()) + .contains("something"); + + assertThat(mListeners.getNotificationListenerFilter( + Pair.create(si.getComponentName(), 0)).getTypes()) + .isEqualTo(7); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(si.getComponentName(), 0)) + .getDisallowedPackages()) + .isEmpty(); + + assertThat(mListeners.getNotificationListenerFilter(Pair.create(si2.getComponentName(), 0))) + .isNull(); + + } + + @Test + public void testOnPackageChanged() { + NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); + NotificationListenerFilter nlf2 = + new NotificationListenerFilter(4, new ArraySet<>(new String[] {"something"})); + mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); + mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); + + String[] pkgs = new String[] {mCn1.getPackageName()}; + int[] uids = new int[] {1}; + mListeners.onPackagesChanged(false, pkgs, uids); + + // not removing; no change + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0)).getTypes()) + .isEqualTo(7); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 10)).getTypes()) + .isEqualTo(4); + } + + @Test + public void testOnPackageChanged_removing() { + NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); + NotificationListenerFilter nlf2 = + new NotificationListenerFilter(4, new ArraySet<>(new String[] {"something"})); + mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); + mListeners.setNotificationListenerFilter(Pair.create(mCn2, 0), nlf2); + + String[] pkgs = new String[] {mCn1.getPackageName()}; + int[] uids = new int[] {1}; + mListeners.onPackagesChanged(true, pkgs, uids); + + // only mCn1 removed + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn1, 0))).isNull(); + assertThat(mListeners.getNotificationListenerFilter(Pair.create(mCn2, 0)).getTypes()) + .isEqualTo(4); + } + +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index a18bce737753f..bd622e1bb0d46 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -55,6 +55,7 @@ import static android.os.Build.VERSION_CODES.P; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; +import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; @@ -143,6 +144,7 @@ import android.provider.MediaStore; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.ConversationChannelWrapper; +import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; @@ -269,6 +271,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock private NotificationListeners mListeners; + @Mock + private NotificationListenerFilter mNlf; @Mock private NotificationAssistants mAssistants; @Mock private ConditionProviders mConditionProviders; private ManagedServices.ManagedServiceInfo mListener; @@ -459,6 +463,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mPolicyFile.finishWrite(fos); // Setup managed services + when(mNlf.isTypeAllowed(anyInt())).thenReturn(true); + when(mNlf.isPackageAllowed(anyString())).thenReturn(true); + when(mNlf.isPackageAllowed(null)).thenReturn(true); + when(mListeners.getNotificationListenerFilter(any())).thenReturn(mNlf); mListener = mListeners.new ManagedServiceInfo( null, new ComponentName(PKG, "test_class"), UserHandle.getUserId(mUid), true, null, 0); @@ -3596,7 +3604,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mNotificationDelegate.onNotificationDirectReplied(r.getKey()); assertTrue(mService.getNotificationRecord(r.getKey()).getStats().hasDirectReplied()); - verify(mAssistants).notifyAssistantNotificationDirectReplyLocked(eq(r.getSbn())); + verify(mAssistants).notifyAssistantNotificationDirectReplyLocked(eq(r)); assertEquals(1, mNotificationRecordLogger.numCalls()); assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_DIRECT_REPLIED, @@ -3610,14 +3618,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mNotificationDelegate.onNotificationExpansionChanged(r.getKey(), true, true, NOTIFICATION_LOCATION_UNKNOWN); - verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), eq(true), - eq((true))); + verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), + eq(FLAG_FILTER_TYPE_ALERTING), eq(true), eq((true))); assertTrue(mService.getNotificationRecord(r.getKey()).getStats().hasExpanded()); mService.mNotificationDelegate.onNotificationExpansionChanged(r.getKey(), true, false, NOTIFICATION_LOCATION_UNKNOWN); - verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), eq(true), - eq((false))); + verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), + eq(FLAG_FILTER_TYPE_ALERTING), eq(true), eq((false))); assertTrue(mService.getNotificationRecord(r.getKey()).getStats().hasExpanded()); assertEquals(2, mNotificationRecordLogger.numCalls()); @@ -3635,14 +3643,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mNotificationDelegate.onNotificationExpansionChanged(r.getKey(), false, true, NOTIFICATION_LOCATION_UNKNOWN); assertFalse(mService.getNotificationRecord(r.getKey()).getStats().hasExpanded()); - verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), eq(false), - eq((true))); + verify(mAssistants).notifyAssistantExpansionChangedLocked(eq(r.getSbn()), + eq(FLAG_FILTER_TYPE_ALERTING), eq(false), eq((true))); mService.mNotificationDelegate.onNotificationExpansionChanged(r.getKey(), false, false, NOTIFICATION_LOCATION_UNKNOWN); assertFalse(mService.getNotificationRecord(r.getKey()).getStats().hasExpanded()); verify(mAssistants).notifyAssistantExpansionChangedLocked( - eq(r.getSbn()), eq(false), eq((false))); + eq(r.getSbn()), eq(FLAG_FILTER_TYPE_ALERTING), eq(false), eq((false))); } @Test @@ -3662,11 +3670,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationVisibility nv = NotificationVisibility.obtain(r.getKey(), 1, 2, true); mService.mNotificationDelegate.onNotificationVisibilityChanged( new NotificationVisibility[] {nv}, new NotificationVisibility[]{}); - verify(mAssistants).notifyAssistantVisibilityChangedLocked(eq(r.getSbn()), eq(true)); + verify(mAssistants).notifyAssistantVisibilityChangedLocked(eq(r), eq(true)); assertTrue(mService.getNotificationRecord(r.getKey()).getStats().hasSeen()); mService.mNotificationDelegate.onNotificationVisibilityChanged( new NotificationVisibility[] {}, new NotificationVisibility[]{nv}); - verify(mAssistants).notifyAssistantVisibilityChangedLocked(eq(r.getSbn()), eq(false)); + verify(mAssistants).notifyAssistantVisibilityChangedLocked(eq(r), eq(false)); assertTrue(mService.getNotificationRecord(r.getKey()).getStats().hasSeen()); } @@ -5324,7 +5332,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { r.getKey(), replyIndex, reply, NOTIFICATION_LOCATION_UNKNOWN, modifiedBeforeSending); verify(mAssistants).notifyAssistantSuggestedReplySent( - eq(r.getSbn()), eq(reply), eq(generatedByAssistant)); + eq(r.getSbn()), eq(FLAG_FILTER_TYPE_ALERTING), eq(reply), eq(generatedByAssistant)); assertEquals(1, mNotificationRecordLogger.numCalls()); assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_SMART_REPLIED, mNotificationRecordLogger.event(0)); @@ -5346,7 +5354,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 10, 10, r.getKey(), actionIndex, action, notificationVisibility, generatedByAssistant); verify(mAssistants).notifyAssistantActionClicked( - eq(r.getSbn()), eq(action), eq(generatedByAssistant)); + eq(r), eq(action), eq(generatedByAssistant)); assertEquals(1, mNotificationRecordLogger.numCalls()); assertEquals( @@ -5370,7 +5378,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 10, 10, r.getKey(), actionIndex, action, notificationVisibility, generatedByAssistant); verify(mAssistants).notifyAssistantActionClicked( - eq(r.getSbn()), eq(action), eq(generatedByAssistant)); + eq(r), eq(action), eq(generatedByAssistant)); assertEquals(1, mNotificationRecordLogger.numCalls()); assertEquals( @@ -7249,7 +7257,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(info.enabledAndUserMatches(info.userid)).thenReturn(false); when(mAssistants.checkServiceTokenLocked(any())).thenReturn(assistant); - assertFalse(mService.isVisibleToListener(sbn, info)); + assertFalse(mService.isVisibleToListener(sbn, 0, info)); } @Test @@ -7262,7 +7270,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(info.enabledAndUserMatches(info.userid)).thenReturn(true); when(mAssistants.checkServiceTokenLocked(any())).thenReturn(null); - assertTrue(mService.isVisibleToListener(sbn, info)); + assertTrue(mService.isVisibleToListener(sbn, 0, info)); } @Test @@ -7277,7 +7285,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(info.enabledAndUserMatches(info.userid)).thenReturn(true); when(mAssistants.checkServiceTokenLocked(any())).thenReturn(assistant); - assertFalse(mService.isVisibleToListener(sbn, info)); + assertFalse(mService.isVisibleToListener(sbn, 0, info)); } @Test @@ -7292,7 +7300,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(info.enabledAndUserMatches(info.userid)).thenReturn(true); when(mAssistants.checkServiceTokenLocked(any())).thenReturn(assistant); - assertTrue(mService.isVisibleToListener(sbn, info)); + assertTrue(mService.isVisibleToListener(sbn, 0, info)); + } + + @Test + public void testIsVisibleToListener_mismatchedType() { + when(mNlf.isTypeAllowed(anyInt())).thenReturn(false); + + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getUserId()).thenReturn(10); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + ManagedServices.ManagedServiceInfo assistant = mock(ManagedServices.ManagedServiceInfo.class); + info.userid = 10; + when(info.isSameUser(anyInt())).thenReturn(true); + when(assistant.isSameUser(anyInt())).thenReturn(true); + when(info.enabledAndUserMatches(info.userid)).thenReturn(true); + when(mAssistants.checkServiceTokenLocked(any())).thenReturn(assistant); + + assertFalse(mService.isVisibleToListener(sbn, 0, info)); + } + + @Test + public void testIsVisibleToListener_disallowedPackage() { + when(mNlf.isPackageAllowed(null)).thenReturn(false); + + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getUserId()).thenReturn(10); + ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); + ManagedServices.ManagedServiceInfo assistant = + mock(ManagedServices.ManagedServiceInfo.class); + info.userid = 10; + when(info.isSameUser(anyInt())).thenReturn(true); + when(assistant.isSameUser(anyInt())).thenReturn(true); + when(info.enabledAndUserMatches(info.userid)).thenReturn(true); + when(mAssistants.checkServiceTokenLocked(any())).thenReturn(assistant); + + assertFalse(mService.isVisibleToListener(sbn, 0, info)); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 976f40896b78f..da613e6f4d1d6 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -21,6 +21,9 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION; +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_SILENT; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; @@ -910,11 +913,13 @@ public class NotificationRecordTest extends UiServiceTestCase { record.setAssistantImportance(IMPORTANCE_LOW); record.calculateImportance(); assertEquals(IMPORTANCE_LOW, record.getImportance()); + assertEquals(FLAG_FILTER_TYPE_SILENT, record.getNotificationType()); record.updateNotificationChannel( new NotificationChannel(channelId, "", IMPORTANCE_DEFAULT)); assertEquals(IMPORTANCE_LOW, record.getImportance()); + assertEquals(FLAG_FILTER_TYPE_SILENT, record.getNotificationType()); } @Test @@ -1125,6 +1130,7 @@ public class NotificationRecordTest extends UiServiceTestCase { record.setShortcutInfo(mock(ShortcutInfo.class)); assertTrue(record.isConversation()); + assertEquals(FLAG_FILTER_TYPE_CONVERSATIONS, record.getNotificationType()); } @Test @@ -1134,6 +1140,7 @@ public class NotificationRecordTest extends UiServiceTestCase { record.setShortcutInfo(null); assertTrue(record.isConversation()); + assertEquals(FLAG_FILTER_TYPE_CONVERSATIONS, record.getNotificationType()); } @Test @@ -1144,6 +1151,7 @@ public class NotificationRecordTest extends UiServiceTestCase { record.setHasSentValidMsg(true); assertFalse(record.isConversation()); + assertEquals(FLAG_FILTER_TYPE_ALERTING, record.getNotificationType()); } @Test