method public final int getCurrentInterruptionFilter();
method public final int getCurrentListenerHints();
method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking();
+ method public final java.util.List<android.app.NotificationChannelGroup> getNotificationChannelGroups(java.lang.String);
+ method public final java.util.List<android.app.NotificationChannel> getNotificationChannels(java.lang.String);
method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications();
method public android.os.IBinder onBind(android.content.Intent);
method public void onInterruptionFilterChanged(int);
method public void onListenerConnected();
method public void onListenerDisconnected();
method public void onListenerHintsChanged(int);
+ method public void onNotificationChannelGroupModified(java.lang.String, android.app.NotificationChannelGroup, int);
+ method public void onNotificationChannelModified(java.lang.String, android.app.NotificationChannel, int);
method public void onNotificationPosted(android.service.notification.StatusBarNotification);
method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
method public final void requestUnbind();
method public final void setNotificationsShown(java.lang.String[]);
method public final void snoozeNotification(java.lang.String, long);
+ method public final void updateNotificationChannel(java.lang.String, android.app.NotificationChannel);
field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
field public static final int INTERRUPTION_FILTER_PRIORITY = 2; // 0x2
field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1; // 0x1
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; // 0x3
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2; // 0x2
field public static final int REASON_APP_CANCEL = 8; // 0x8
field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
field public static final int REASON_CANCEL = 2; // 0x2
method public final int getCurrentInterruptionFilter();
method public final int getCurrentListenerHints();
method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking();
+ method public final java.util.List<android.app.NotificationChannelGroup> getNotificationChannelGroups(java.lang.String);
+ method public final java.util.List<android.app.NotificationChannel> getNotificationChannels(java.lang.String);
method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications();
method public android.os.IBinder onBind(android.content.Intent);
method public void onInterruptionFilterChanged(int);
method public void onListenerConnected();
method public void onListenerDisconnected();
method public void onListenerHintsChanged(int);
+ method public void onNotificationChannelGroupModified(java.lang.String, android.app.NotificationChannelGroup, int);
+ method public void onNotificationChannelModified(java.lang.String, android.app.NotificationChannel, int);
method public void onNotificationPosted(android.service.notification.StatusBarNotification);
method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
method public final void snoozeNotification(java.lang.String, java.lang.String);
method public final void snoozeNotification(java.lang.String, long);
method public void unregisterAsSystemService() throws android.os.RemoteException;
+ method public final void updateNotificationChannel(java.lang.String, android.app.NotificationChannel);
field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
field public static final int INTERRUPTION_FILTER_PRIORITY = 2; // 0x2
field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1; // 0x1
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; // 0x3
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2; // 0x2
field public static final int REASON_APP_CANCEL = 8; // 0x8
field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
field public static final int REASON_CANCEL = 2; // 0x2
method public final int getCurrentInterruptionFilter();
method public final int getCurrentListenerHints();
method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking();
+ method public final java.util.List<android.app.NotificationChannelGroup> getNotificationChannelGroups(java.lang.String);
+ method public final java.util.List<android.app.NotificationChannel> getNotificationChannels(java.lang.String);
method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications();
method public android.os.IBinder onBind(android.content.Intent);
method public void onInterruptionFilterChanged(int);
method public void onListenerConnected();
method public void onListenerDisconnected();
method public void onListenerHintsChanged(int);
+ method public void onNotificationChannelGroupModified(java.lang.String, android.app.NotificationChannelGroup, int);
+ method public void onNotificationChannelModified(java.lang.String, android.app.NotificationChannel, int);
method public void onNotificationPosted(android.service.notification.StatusBarNotification);
method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
method public final void setNotificationsShown(java.lang.String[]);
method public final void snoozeNotification(java.lang.String, java.lang.String);
method public final void snoozeNotification(java.lang.String, long);
+ method public final void updateNotificationChannel(java.lang.String, android.app.NotificationChannel);
field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
field public static final int INTERRUPTION_FILTER_PRIORITY = 2; // 0x2
field public static final int INTERRUPTION_FILTER_UNKNOWN = 0; // 0x0
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1; // 0x1
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3; // 0x3
+ field public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2; // 0x2
field public static final int REASON_APP_CANCEL = 8; // 0x8
field public static final int REASON_APP_CANCEL_ALL = 9; // 0x9
field public static final int REASON_CANCEL = 2; // 0x2
void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
void setInterruptionFilter(String pkg, int interruptionFilter);
+ void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in NotificationChannel channel);
+ ParceledListSlice getNotificationChannelsFromPrivilegedListener(in INotificationListener token, String pkg);
+ ParceledListSlice getNotificationChannelGroupsFromPrivilegedListener(in INotificationListener token, String pkg);
+
void applyEnqueuedAdjustmentFromAssistant(in INotificationListener token, in Adjustment adjustment);
void applyAdjustmentFromAssistant(in INotificationListener token, in Adjustment adjustment);
void applyAdjustmentsFromAssistant(in INotificationListener token, in List<Adjustment> adjustments);
package android.service.notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
import android.service.notification.IStatusBarNotificationHolder;
import android.service.notification.StatusBarNotification;
import android.service.notification.NotificationRankingUpdate;
void onListenerHintsChanged(int hints);
void onInterruptionFilterChanged(int interruptionFilter);
+ // companion device managers only
+ void onNotificationChannelModification(String pkgName, in NotificationChannel channel, int modificationType);
+ void onNotificationChannelGroupModification(String pkgName, in NotificationChannelGroup group, int modificationType);
+
// rankers only
void onNotificationEnqueued(in IStatusBarNotificationHolder notificationHolder);
void onNotificationSnoozedUntilContext(in IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId);
}
}
-
private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
@Override
public void onNotificationEnqueued(IStatusBarNotificationHolder sbnHolder) {
package android.service.notification;
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.TestApi;
import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.companion.CompanionDeviceManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.SomeArgs;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
* A service that receives calls from the system when new notifications are
* posted or removed, or their ranking changed.
* <p>To extend this class, you must declare the service in your manifest file with
- * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
+ * the {@link Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
* and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
* <pre>
* <service android:name=".NotificationListener"
@SystemApi
public static final int TRIM_LIGHT = 1;
+
+ /** @hide */
+ @IntDef({NOTIFICATION_CHANNEL_OR_GROUP_ADDED, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
+ NOTIFICATION_CHANNEL_OR_GROUP_DELETED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ChannelOrGroupModificationTypes {}
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, NotificationChannelGroup, int)}- the
+ * provided object was created.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1;
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, NotificationChannelGroup, int)}- the
+ * provided object was updated.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2;
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, NotificationChannelGroup, int)}- the
+ * provided object was deleted.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3;
+
private final Object mLock = new Object();
private Handler mHandler;
}
/**
+ * Implement this method to learn about notification channel modifications.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to receive this callback.
+ *
+ * @param pkg The package the channel belongs to.
+ * @param channel The channel that has changed.
+ * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
+ */
+ public void onNotificationChannelModified(String pkg, NotificationChannel channel,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn about notification channel group modifications.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to receive this callback.
+ *
+ * @param pkg The package the group belongs to.
+ * @param group The group that has changed.
+ * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
+ */
+ public void onNotificationChannelGroupModified(String pkg, NotificationChannelGroup group,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ // optional
+ }
+
+ /**
* Implement this method to be notified when the
* {@link #getCurrentInterruptionFilter() interruption filter} changed.
*
}
}
+
+ /**
+ * Updates a notification channel for a given package. This should only be used to reflect
+ * changes a user has made to the channel via the listener's user interface.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to use this method.
+ *
+ * @param pkg The package the channel belongs to.
+ * @param channel the channel to update.
+ */
+ public final void updateNotificationChannel(@NonNull String pkg,
+ @NonNull NotificationChannel channel) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().updateNotificationChannelFromPrivilegedListener(
+ mWrapper, pkg, channel);
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channels belonging to the given package.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to use this method.
+ *
+ * @param pkg The package to retrieve channels for.
+ */
+ public final List<NotificationChannel> getNotificationChannels(@NonNull String pkg) {
+ if (!isBound()) return null;
+ try {
+
+ return getNotificationInterface().getNotificationChannelsFromPrivilegedListener(
+ mWrapper, pkg).getList();
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channel groups belonging to the given package.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to use this method.
+ *
+ * @param pkg The package to retrieve channel groups for.
+ */
+ public final List<NotificationChannelGroup> getNotificationChannelGroups(@NonNull String pkg) {
+ if (!isBound()) return null;
+ try {
+
+ return getNotificationInterface().getNotificationChannelGroupsFromPrivilegedListener(
+ mWrapper, pkg).getList();
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Sets the notification trim that will be received via {@link #onNotificationPosted}.
*
// no-op in the listener
}
+ @Override
+ public void onNotificationChannelModification(String pkgName, NotificationChannel channel,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = pkgName;
+ args.arg2 = channel;
+ args.arg3 = modificationType;
+ mHandler.obtainMessage(
+ MyHandler.MSG_ON_NOTIFICATION_CHANNEL_MODIFIED, args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationChannelGroupModification(String pkgName,
+ NotificationChannelGroup group,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = pkgName;
+ args.arg2 = group;
+ args.arg3 = modificationType;
+ mHandler.obtainMessage(
+ MyHandler.MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED, args).sendToTarget();
+ }
}
/**
public static final int MSG_ON_NOTIFICATION_RANKING_UPDATE = 4;
public static final int MSG_ON_LISTENER_HINTS_CHANGED = 5;
public static final int MSG_ON_INTERRUPTION_FILTER_CHANGED = 6;
+ public static final int MSG_ON_NOTIFICATION_CHANNEL_MODIFIED = 7;
+ public static final int MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED = 8;
public MyHandler(Looper looper) {
super(looper, null, false);
final int interruptionFilter = msg.arg1;
onInterruptionFilterChanged(interruptionFilter);
} break;
+
+ case MSG_ON_NOTIFICATION_CHANNEL_MODIFIED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String pkgName = (String) args.arg1;
+ NotificationChannel channel = (NotificationChannel) args.arg2;
+ int modificationType = (int) args.arg3;
+ onNotificationChannelModified(pkgName, channel, modificationType);
+ } break;
+
+ case MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String pkgName = (String) args.arg1;
+ NotificationChannelGroup group = (NotificationChannelGroup) args.arg2;
+ int modificationType = (int) args.arg3;
+ onNotificationChannelGroupModified(pkgName, group, modificationType);
+ } break;
}
}
}
abstract protected void onServiceAdded(ManagedServiceInfo info);
+ protected List<ManagedServiceInfo> getServices() {
+ synchronized (mMutex) {
+ List<ManagedServiceInfo> services = new ArrayList<>(mServices);
+ return services;
+ }
+ }
+
protected void onServiceRemovedLocked(ManagedServiceInfo removed) { }
private ManagedServiceInfo newServiceInfo(IInterface service,
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_TELEVISION;
+import static android.service.notification.NotificationListenerService
+ .NOTIFICATION_CHANNEL_OR_GROUP_ADDED;
+import static android.service.notification.NotificationListenerService
+ .NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
+import static android.service.notification.NotificationListenerService
+ .NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
import android.app.StatusBarManager;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
+import android.companion.ICompanionDeviceManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;
Vibrator mVibrator;
private WindowManagerInternal mWindowManagerInternal;
private AlarmManager mAlarmManager;
+ private ICompanionDeviceManager mCompanionManager;
final IBinder mForegroundToken = new Binder();
private Handler mHandler;
// TODO: Tests should call onStart instead once the methods above are removed.
@VisibleForTesting
void init(Looper looper, IPackageManager packageManager, PackageManager packageManagerClient,
- LightsManager lightsManager, NotificationListeners notificationListeners) {
+ LightsManager lightsManager, NotificationListeners notificationListeners,
+ ICompanionDeviceManager companionManager) {
Resources resources = getContext().getResources();
mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE,
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+ mCompanionManager = companionManager;
mHandler = new WorkerHandler(looper);
mRankingThread.start();
@Override
public void onStart() {
init(Looper.myLooper(), AppGlobals.getPackageManager(), getContext().getPackageManager(),
- getLocalService(LightsManager.class), new NotificationListeners());
+ getLocalService(LightsManager.class), new NotificationListeners(),
+ null);
publishBinderService(Context.NOTIFICATION_SERVICE, mService);
publishLocalService(NotificationManagerInternal.class, mInternalService);
}
sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
}
- private void updateNotificationChannelInt(String pkg, int uid, NotificationChannel channel) {
+ private void updateNotificationChannelInt(String pkg, int uid, NotificationChannel channel,
+ boolean fromListener) {
if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {
// cancel
cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, true,
}
mRankingHelper.updateNotificationChannel(pkg, uid, channel);
+ final NotificationChannel modifiedChannel =
+ mRankingHelper.getNotificationChannel(pkg, uid, channel.getId(), false);
+
+ if (!fromListener) {
+ mListeners.notifyNotificationChannelChanged(
+ pkg, modifiedChannel, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
+ }
+
synchronized (mNotificationLock) {
final int N = mNotificationList.size();
for (int i = N - 1; i >= 0; --i) {
&& r.sbn.getUid() == uid
&& channel.getId() != null
&& channel.getId().equals(r.getChannel().getId())) {
- r.updateNotificationChannel(mRankingHelper.getNotificationChannel(
- pkg, uid, channel.getId(), false));
+ r.updateNotificationChannel(modifiedChannel);
}
}
}
Preconditions.checkNotNull(group, "group in list is null");
mRankingHelper.createNotificationChannelGroup(pkg, Binder.getCallingUid(), group,
true /* fromTargetApp */);
+ mListeners.notifyNotificationChannelGroupChanged(pkg, group,
+ NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
}
savePolicyFile();
}
Preconditions.checkNotNull(channel, "channel in list is null");
mRankingHelper.createNotificationChannel(pkg, uid, channel,
true /* fromTargetApp */);
+ mListeners.notifyNotificationChannelChanged(pkg,
+ mRankingHelper.getNotificationChannel(pkg, uid, channel.getId(), false),
+ NOTIFICATION_CHANNEL_OR_GROUP_ADDED);
}
savePolicyFile();
}
@Override
public void deleteNotificationChannel(String pkg, String channelId) {
checkCallerIsSystemOrSameApp(pkg);
+ final int callingUid = Binder.getCallingUid();
if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {
throw new IllegalArgumentException("Cannot delete default channel");
}
cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true,
- UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, null);
- mRankingHelper.deleteNotificationChannel(pkg, Binder.getCallingUid(), channelId);
+ UserHandle.getUserId(callingUid), REASON_CHANNEL_BANNED, null);
+ mRankingHelper.deleteNotificationChannel(pkg, callingUid, channelId);
+ mListeners.notifyNotificationChannelChanged(pkg,
+ mRankingHelper.getNotificationChannel(pkg, callingUid, channelId, true),
+ NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
savePolicyFile();
}
}
@Override
- public void deleteNotificationChannelGroup(String pkg, String channelGroupId) {
+ public void deleteNotificationChannelGroup(String pkg, String groupId) {
checkCallerIsSystemOrSameApp(pkg);
- List<String> deletedChannelIds = mRankingHelper.deleteNotificationChannelGroup(
- pkg, Binder.getCallingUid(), channelGroupId);
- for (int i = 0; i < deletedChannelIds.size(); i++) {
- cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannelIds.get(i), 0, 0, true,
- UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED, null);
+ final int callingUid = Binder.getCallingUid();
+ NotificationChannelGroup groupToDelete =
+ mRankingHelper.getNotificationChannelGroup(groupId, pkg, callingUid);
+ if (groupToDelete != null) {
+ List<NotificationChannel> deletedChannels =
+ mRankingHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId);
+ for (int i = 0; i < deletedChannels.size(); i++) {
+ final NotificationChannel deletedChannel = deletedChannels.get(i);
+ cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannel.getId(), 0, 0,
+ true,
+ UserHandle.getUserId(Binder.getCallingUid()), REASON_CHANNEL_BANNED,
+ null);
+ mListeners.notifyNotificationChannelChanged(pkg, deletedChannel,
+ NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
+ }
+ mListeners.notifyNotificationChannelGroupChanged(
+ pkg, groupToDelete, NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
+ savePolicyFile();
}
- savePolicyFile();
}
@Override
NotificationChannel channel) {
enforceSystemOrSystemUI("Caller not system or systemui");
Preconditions.checkNotNull(channel);
- updateNotificationChannelInt(pkg, uid, channel);
+ updateNotificationChannelInt(pkg, uid, channel, false);
}
@Override
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void updateNotificationChannelFromPrivilegedListener(INotificationListener token,
+ String pkg, NotificationChannel channel) throws RemoteException {
+ Preconditions.checkNotNull(channel);
+
+ ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ checkHasCompanionDevice(info);
+
+ int uid = mPackageManager.getPackageUid(pkg, 0, info.userid);
+ updateNotificationChannelInt(pkg, uid, channel, true);
+ }
+
+ @Override
+ public ParceledListSlice<NotificationChannel> getNotificationChannelsFromPrivilegedListener(
+ INotificationListener token, String pkg) throws RemoteException {
+ ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ checkHasCompanionDevice(info);
+
+ int uid = mPackageManager.getPackageUid(pkg, 0, info.userid);
+ return mRankingHelper.getNotificationChannels(pkg, uid, false /* includeDeleted */);
+ }
+
+ @Override
+ public ParceledListSlice<NotificationChannelGroup>
+ getNotificationChannelGroupsFromPrivilegedListener(
+ INotificationListener token, String pkg) throws RemoteException {
+ ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ checkHasCompanionDevice(info);
+
+ List<NotificationChannelGroup> groups = new ArrayList<>();
+ int uid = mPackageManager.getPackageUid(pkg, 0, info.userid);
+ groups.addAll(mRankingHelper.getNotificationChannelGroups(pkg, uid));
+ return new ParceledListSlice<>(groups);
+ }
};
private void applyAdjustment(NotificationRecord n, Adjustment adjustment) {
return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
}
- private boolean isCallerSystem() {
+ protected boolean isCallerSystem() {
return isUidSystem(Binder.getCallingUid());
}
- protected void checkCallerIsSystem() {
+ private void checkCallerIsSystem() {
if (isCallerSystem()) {
return;
}
channels, overridePeople, snoozeCriteria, showBadge);
}
+ private void checkHasCompanionDevice(ManagedServiceInfo info) throws RemoteException {
+ if (mCompanionManager == null) {
+ mCompanionManager = ICompanionDeviceManager.Stub.asInterface(
+ ServiceManager.getService(Context.COMPANION_DEVICE_SERVICE));
+ }
+ if (ArrayUtils.isEmpty(mCompanionManager.getAssociations(
+ info.component.getPackageName(), info.userid))) {
+ throw new SecurityException("Disallowed call from " + info.component);
+ }
+ }
+
private boolean isVisibleToListener(StatusBarNotification sbn, ManagedServiceInfo listener) {
if (!listener.enabledAndUserMatches(sbn.getUserId())) {
return false;
final StatusBarNotification sbn = r.sbn;
TrimCache trimCache = new TrimCache(sbn);
- // mServices is the list inside ManagedServices of all the assistants,
// There should be only one, but it's a list, so while we enforce
// singularity elsewhere, we keep it general here, to avoid surprises.
for (final ManagedServiceInfo info : NotificationAssistants.this.mServices) {
}
}
+ protected void notifyNotificationChannelChanged(final String pkg,
+ final NotificationChannel channel, final int modificationType) {
+ if (channel == null) {
+ return;
+ }
+ for (final ManagedServiceInfo serviceInfo : getServices()) {
+ if (!serviceInfo.isEnabledForCurrentProfiles()) {
+ continue;
+ }
+ try {
+ checkHasCompanionDevice(serviceInfo);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyNotificationChannelChanged(serviceInfo, pkg, channel,
+ modificationType);
+ }
+ });
+ } catch (SecurityException se) {
+ // Not a privileged listener; do not notify
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Cannot reach companion device service", e);
+ }
+ }
+ }
+
+ protected void notifyNotificationChannelGroupChanged(final String pkg,
+ final NotificationChannelGroup group, final int modificationType) {
+ if (group == null) {
+ return;
+ }
+ for (final ManagedServiceInfo serviceInfo : getServices()) {
+ if (!serviceInfo.isEnabledForCurrentProfiles()) {
+ continue;
+ }
+ try {
+ checkHasCompanionDevice(serviceInfo);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyNotificationChannelGroupChanged(serviceInfo, pkg, group,
+ modificationType);
+ }
+ });
+ } catch (SecurityException se) {
+ // Not a privileged listener; do not notify
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Cannot reach companion device service", e);
+ }
+ }
+ }
+
private void notifyPosted(final ManagedServiceInfo info,
final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
final INotificationListener listener = (INotificationListener) info.service;
}
}
+ void notifyNotificationChannelChanged(ManagedServiceInfo info,
+ final String pkg, final NotificationChannel channel,
+ final int modificationType) {
+ final INotificationListener listener = (INotificationListener) info.service;
+ try {
+ listener.onNotificationChannelModification(pkg, channel, modificationType);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "unable to notify listener (channel changed): " + listener, ex);
+ }
+ }
+
+ private void notifyNotificationChannelGroupChanged(ManagedServiceInfo info,
+ final String pkg, final NotificationChannelGroup group,
+ final int modificationType) {
+ final INotificationListener listener = (INotificationListener) info.service;
+ try {
+ listener.onNotificationChannelGroupModification(pkg, group, modificationType);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "unable to notify listener (channel group changed): " + listener, ex);
+ }
+ }
+
public boolean isListenerPackage(String packageName) {
if (packageName == null) {
return false;
return new ParceledListSlice<>(new ArrayList<>(groups.values()));
}
- public List<String> deleteNotificationChannelGroup(String pkg, int uid,
+ public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid,
String groupId) {
- List<String> deletedChannelIds = new ArrayList<>();
+ List<NotificationChannel> deletedChannels = new ArrayList<>();
Record r = getRecord(pkg, uid);
if (r == null || TextUtils.isEmpty(groupId)) {
- return deletedChannelIds;
+ return deletedChannels;
}
r.groups.remove(groupId);
final NotificationChannel nc = r.channels.valueAt(i);
if (groupId.equals(nc.getGroup())) {
nc.setDeleted(true);
- deletedChannelIds.add(nc.getId());
+ deletedChannels.add(nc);
}
}
updateConfig();
- return deletedChannelIds;
+ return deletedChannels;
}
@Override
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<application>
<uses-library android:name="android.test.runner" />
package com.android.server.notification;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
+import android.companion.ICompanionDeviceManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
+import android.graphics.Color;
import android.os.Binder;
-import android.os.HandlerThread;
-import android.os.MessageQueue;
import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.test.annotation.UiThreadTest;
import android.support.test.InstrumentationRegistry;
import android.testing.TestableLooper;
+import java.util.ArrayList;
import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.List;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
private final RankingHelper mRankingHelper = mock(RankingHelper.class);
private NotificationChannel mTestNotificationChannel = new NotificationChannel(
TEST_CHANNEL_ID, TEST_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
+ private NotificationManagerService.NotificationListeners mNotificationListeners =
+ mock(NotificationManagerService.NotificationListeners.class);
+ private ManagedServices.ManagedServiceInfo mListener =
+ mNotificationListeners.new ManagedServiceInfo(
+ null, new ComponentName(PKG, "test_class"), uid, true, null, 0);
+ private ICompanionDeviceManager mCompanionMgr = mock(ICompanionDeviceManager.class);
// Use a Testable subclass so we can simulate calls from the system without failing.
private static class TestableNotificationManagerService extends NotificationManagerService {
public TestableNotificationManagerService(Context context) { super(context); }
@Override
- protected void checkCallerIsSystem() {}
+ protected boolean isCallerSystem() {
+ return true;
+ }
}
@Before
- @Test
@UiThreadTest
public void setUp() throws Exception {
mNotificationManagerService = new TestableNotificationManagerService(mContext);
// MockPackageManager - default returns ApplicationInfo with matching calling UID
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.uid = uid;
- when(mPackageManager.getApplicationInfo(any(), anyInt(), anyInt()))
+ when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
.thenReturn(applicationInfo);
when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
.thenReturn(applicationInfo);
when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class));
// Use this testable looper.
mTestableLooper = new TestableLooper(false);
- // Mock NotificationListeners to bypass security checks.
- final NotificationManagerService.NotificationListeners mockNotificationListeners =
- mock(NotificationManagerService.NotificationListeners.class);
- when(mockNotificationListeners.checkServiceTokenLocked(any())).thenReturn(
- mockNotificationListeners.new ManagedServiceInfo(null,
- new ComponentName(PKG, "test_class"), uid, true, null, 0));
+ when(mNotificationListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
mNotificationManagerService.init(mTestableLooper.getLooper(), mPackageManager,
- mPackageManagerClient, mockLightsManager, mockNotificationListeners);
+ mPackageManagerClient, mockLightsManager, mNotificationListeners, mCompanionMgr);
// Tests call directly into the Binder.
mBinderService = mNotificationManagerService.getBinderService();
verify(mRankingHelper, times(1)).getNotificationChannel(
anyString(), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean());
}
+
+ @Test
+ @UiThreadTest
+ public void testCreateChannelNotifyListener() throws Exception {
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(),
+ eq(mTestNotificationChannel.getId()), anyBoolean()))
+ .thenReturn(mTestNotificationChannel);
+ NotificationChannel channel2 = new NotificationChannel("a", "b", IMPORTANCE_LOW);
+ when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(),
+ eq(channel2.getId()), anyBoolean()))
+ .thenReturn(channel2);
+
+ reset(mNotificationListeners);
+ mBinderService.createNotificationChannels(PKG,
+ new ParceledListSlice(Arrays.asList(mTestNotificationChannel, channel2)));
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
+ eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED));
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
+ eq(channel2),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCreateChannelGroupNotifyListener() throws Exception {
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ NotificationChannelGroup group1 = new NotificationChannelGroup("a", "b");
+ NotificationChannelGroup group2 = new NotificationChannelGroup("n", "m");
+
+ reset(mNotificationListeners);
+ mBinderService.createNotificationChannelGroups(PKG,
+ new ParceledListSlice(Arrays.asList(group1, group2)));
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelGroupChanged(eq(PKG),
+ eq(group1),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED));
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelGroupChanged(eq(PKG),
+ eq(group2),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUpdateChannelNotifyListener() throws Exception {
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ mTestNotificationChannel.setLightColor(Color.CYAN);
+ when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(),
+ eq(mTestNotificationChannel.getId()), anyBoolean()))
+ .thenReturn(mTestNotificationChannel);
+
+ reset(mNotificationListeners);
+ mBinderService.updateNotificationChannelForPackage(PKG, 0, mTestNotificationChannel);
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
+ eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testDeleteChannelNotifyListener() throws Exception {
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ when(mRankingHelper.getNotificationChannel(eq(PKG), anyInt(),
+ eq(mTestNotificationChannel.getId()), anyBoolean()))
+ .thenReturn(mTestNotificationChannel);
+ reset(mNotificationListeners);
+ mBinderService.deleteNotificationChannel(PKG, mTestNotificationChannel.getId());
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelChanged(eq(PKG),
+ eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testDeleteChannelGroupNotifyListener() throws Exception {
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+ NotificationChannelGroup ncg = new NotificationChannelGroup("a", "b/c");
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ when(mRankingHelper.getNotificationChannelGroup(eq(ncg.getId()), eq(PKG), anyInt()))
+ .thenReturn(ncg);
+ reset(mNotificationListeners);
+ mBinderService.deleteNotificationChannelGroup(PKG, ncg.getId());
+ verify(mNotificationListeners, times(1)).notifyNotificationChannelGroupChanged(eq(PKG),
+ eq(ncg),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUpdateNotificationChannelFromPrivilegedListener_success() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ mBinderService.updateNotificationChannelFromPrivilegedListener(
+ null, PKG, mTestNotificationChannel);
+
+ verify(mRankingHelper, times(1)).updateNotificationChannel(anyString(), anyInt(), any());
+
+ verify(mNotificationListeners, never()).notifyNotificationChannelChanged(eq(PKG),
+ eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUpdateNotificationChannelFromPrivilegedListener_noAccess() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ try {
+ mBinderService.updateNotificationChannelFromPrivilegedListener(
+ null, PKG, mTestNotificationChannel);
+ fail("listeners that don't have a companion device shouldn't be able to call this");
+ } catch (SecurityException e) {
+ // pass
+ }
+
+ verify(mRankingHelper, never()).updateNotificationChannel(anyString(), anyInt(), any());
+
+ verify(mNotificationListeners, never()).notifyNotificationChannelChanged(eq(PKG),
+ eq(mTestNotificationChannel),
+ eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGetNotificationChannelFromPrivilegedListener_success() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ mBinderService.getNotificationChannelsFromPrivilegedListener(null, PKG);
+
+ verify(mRankingHelper, times(1)).getNotificationChannels(
+ anyString(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGetNotificationChannelFromPrivilegedListener_noAccess() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ try {
+ mBinderService.getNotificationChannelsFromPrivilegedListener(null, PKG);
+ fail("listeners that don't have a companion device shouldn't be able to call this");
+ } catch (SecurityException e) {
+ // pass
+ }
+
+ verify(mRankingHelper, never()).getNotificationChannels(
+ anyString(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGetNotificationChannelGroupsFromPrivilegedListener_success() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ associations.add("a");
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ mBinderService.getNotificationChannelGroupsFromPrivilegedListener(null, PKG);
+
+ verify(mRankingHelper, times(1)).getNotificationChannelGroups(anyString(), anyInt());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGetNotificationChannelGroupsFromPrivilegedListener_noAccess() throws Exception {
+ mNotificationManagerService.setRankingHelper(mRankingHelper);
+ List<String> associations = new ArrayList<>();
+ when(mCompanionMgr.getAssociations(PKG, uid)).thenReturn(associations);
+
+ try {
+ mBinderService.getNotificationChannelGroupsFromPrivilegedListener(null, PKG);
+ fail("listeners that don't have a companion device shouldn't be able to call this");
+ } catch (SecurityException e) {
+ // pass
+ }
+
+ verify(mRankingHelper, never()).getNotificationChannelGroups(anyString(), anyInt());
+ }
}