/* * Copyright (C) 2015 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 java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; import com.android.internal.os.SomeArgs; import java.lang.annotation.Retention; import java.util.List; /** * A service that helps the user manage notifications. *

* Only one notification assistant can be active at a time. Unlike notification listener services, * assistant services can additionally modify certain aspects about notifications * (see {@link Adjustment}) before they are posted. *

* A note about managed profiles: Unlike {@link NotificationListenerService listener services}, * NotificationAssistantServices are allowed to run in managed profiles * (see {@link DevicePolicyManager#isManagedProfile(ComponentName)}), so they can access the * information they need to create good {@link Adjustment adjustments}. To maintain the contract * with {@link NotificationListenerService}, an assistant service will receive all of the * callbacks from {@link NotificationListenerService} for the current user, managed profiles of * that user, and ones that affect all users. However, * {@link #onNotificationEnqueued(StatusBarNotification)} will only be called for notifications * sent to the current user, and {@link Adjustment adjuments} will only be accepted for the * current user. *

* All callbacks are called on the main thread. *

* @hide */ @SystemApi public abstract class NotificationAssistantService extends NotificationListenerService { private static final String TAG = "NotificationAssistants"; /** @hide */ @Retention(SOURCE) @IntDef({SOURCE_FROM_APP, SOURCE_FROM_ASSISTANT}) public @interface Source {} /** * To indicate an adjustment is from an app. */ public static final int SOURCE_FROM_APP = 0; /** * To indicate an adjustment is from a {@link NotificationAssistantService}. */ public static final int SOURCE_FROM_ASSISTANT = 1; /** * The {@link Intent} that must be declared as handled by the service. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService"; /** * Activity Action: Show notification assistant detail setting page in NAS app. *

* In some cases, a matching Activity may not exist, so ensure you * safeguard against this. *

* Input: Nothing. *

* Output: Nothing. */ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS"; /** * Data type: int, the feedback rating score provided by user. The score can be any integer * value depends on the experimental and feedback UX design. */ public static final String FEEDBACK_RATING = "feedback.rating"; /** * @hide */ protected Handler mHandler; @SuppressLint("OnNameExpected") @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); mHandler = new MyHandler(getContext().getMainLooper()); } @Override public final @NonNull IBinder onBind(@Nullable Intent intent) { if (mWrapper == null) { mWrapper = new NotificationAssistantServiceWrapper(); } return mWrapper; } /** * A notification was snoozed until a context. For use with * {@link Adjustment#KEY_SNOOZE_CRITERIA}. When the device reaches the given context, the * assistant should restore the notification with {@link #unsnoozeNotification(String)}. * * @param sbn the notification to snooze * @param snoozeCriterionId the {@link SnoozeCriterion#getId()} representing a device context. */ abstract public void onNotificationSnoozedUntilContext(@NonNull StatusBarNotification sbn, @NonNull String snoozeCriterionId); /** * A notification was posted by an app. Called before post. * *

Note: this method is only called if you don't override * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel)} or * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel, RankingMap)}.

* * @param sbn the new notification * @return an adjustment or null to take no action, within 200ms. */ abstract public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn); /** * A notification was posted by an app. Called before post. * *

Note: this method is only called if you don't override * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel, RankingMap)}.

* * @param sbn the new notification * @param channel the channel the notification was posted to * @return an adjustment or null to take no action, within 200ms. */ public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn, @NonNull NotificationChannel channel) { return onNotificationEnqueued(sbn); } /** * A notification was posted by an app. Called before post. * * @param sbn the new notification * @param channel the channel the notification was posted to * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications. * @return an adjustment or null to take no action, within 200ms. */ public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn, @NonNull NotificationChannel channel, @NonNull RankingMap rankingMap) { return onNotificationEnqueued(sbn, channel); } /** * Implement this method to learn when notifications are removed, how they were interacted with * before removal, and why they were removed. *

* This might occur because the user has dismissed the notification using system UI (or another * notification listener) or because the app has withdrawn the notification. *

* NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight * fields such as {@link android.app.Notification#contentView} and * {@link android.app.Notification#largeIcon}. However, all other fields on * {@link StatusBarNotification}, sufficient to match this call with a prior call to * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. * ** @param sbn A data structure encapsulating at least the original information (tag and id) * and source (package name) used to post the {@link android.app.Notification} that * was just removed. * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications. * @param stats Stats about how the user interacted with the notification before it was removed. * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. */ @Override public void onNotificationRemoved(@NonNull StatusBarNotification sbn, @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) { onNotificationRemoved(sbn, rankingMap, reason); } /** * Implement this to know when a user has seen notifications, as triggered by * {@link #setNotificationsShown(String[])}. */ public void onNotificationsSeen(@NonNull List keys) { } /** * Implement this to know when the notification panel is revealed * * @param items Number of notifications on the panel at time of opening */ public void onPanelRevealed(int items) { } /** * Implement this to know when the notification panel is hidden */ public void onPanelHidden() { } /** * Implement this to know when a notification becomes visible or hidden from the user. * * @param key the notification key * @param isVisible whether the notification is visible. */ public void onNotificationVisibilityChanged(@NonNull String key, boolean isVisible) { } /** * Implement this to know when a notification change (expanded / collapsed) is visible to user. * * @param key the notification key * @param isUserAction whether the expanded change is caused by user action. * @param isExpanded whether the notification is expanded. */ public void onNotificationExpansionChanged( @NonNull String key, boolean isUserAction, boolean isExpanded) {} /** * Implement this to know when a direct reply is sent from a notification. * @param key the notification key */ public void onNotificationDirectReplied(@NonNull String key) {} /** * Implement this to know when a suggested reply is sent. * @param key the notification key * @param reply the reply that is just sent * @param source the source that provided the reply, e.g. SOURCE_FROM_APP */ public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply, @Source int source) { } /** * Implement this to know when an action is clicked. * @param key the notification key * @param action the action that is just clicked * @param source the source that provided the action, e.g. SOURCE_FROM_APP */ public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action, @Source int source) { } /** * Implement this to know when a notification is clicked by user. * @param key the notification key */ public void onNotificationClicked(@NonNull String key) { } /** * Implement this to know when a user has changed which features of * their notifications the assistant can modify. *

Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what * {@link Adjustment adjustments} you are currently allowed to make.

* * @deprecated changing allowed adjustments is no longer supported. */ @Deprecated public void onAllowedAdjustmentsChanged() { } /** * Implement this to know when user provides a feedback. * @param key the notification key * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications. * @param feedback the received feedback, such as {@link #FEEDBACK_RATING rating score} */ public void onNotificationFeedbackReceived(@NonNull String key, @NonNull RankingMap rankingMap, @NonNull Bundle feedback) { } /** * Updates a notification. N.B. this won’t cause * an existing notification to alert, but might allow a future update to * this notification to alert. * * @param adjustment the adjustment with an explanation */ public final void adjustNotification(@NonNull Adjustment adjustment) { if (!isBound()) return; try { setAdjustmentIssuer(adjustment); getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); throw ex.rethrowFromSystemServer(); } } /** * Updates existing notifications. Re-ranking won't occur until all adjustments are applied. * N.B. this won’t cause an existing notification to alert, but might allow a future update to * these notifications to alert. * * @param adjustments a list of adjustments with explanations */ public final void adjustNotifications(@NonNull List adjustments) { if (!isBound()) return; try { for (Adjustment adjustment : adjustments) { setAdjustmentIssuer(adjustment); } getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); throw ex.rethrowFromSystemServer(); } } /** * Inform the notification manager about un-snoozing a specific notification. *

* This should only be used for notifications snoozed because of a contextual snooze suggestion * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the * notification. * @param key The key of the notification to snooze */ public final void unsnoozeNotification(@NonNull String key) { if (!isBound()) return; try { getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); } } private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper { @Override public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder, NotificationChannel channel, NotificationRankingUpdate update) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); } catch (RemoteException e) { Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e); return; } if (sbn == null) { Log.w(TAG, "onNotificationEnqueuedWithChannel: " + "Error receiving StatusBarNotification"); return; } applyUpdateLocked(update); SomeArgs args = SomeArgs.obtain(); args.arg1 = sbn; args.arg2 = channel; args.arg3 = getCurrentRanking(); mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED, args).sendToTarget(); } @Override public void onNotificationSnoozedUntilContext( IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); } catch (RemoteException e) { Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e); return; } if (sbn == null) { Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification"); return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = sbn; args.arg2 = snoozeCriterionId; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED, args).sendToTarget(); } @Override public void onNotificationsSeen(List keys) { SomeArgs args = SomeArgs.obtain(); args.arg1 = keys; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN, args).sendToTarget(); } @Override public void onPanelRevealed(int items) { SomeArgs args = SomeArgs.obtain(); args.argi1 = items; mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_REVEALED, args).sendToTarget(); } @Override public void onPanelHidden() { SomeArgs args = SomeArgs.obtain(); mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_HIDDEN, args).sendToTarget(); } @Override public void onNotificationVisibilityChanged(String key, boolean isVisible) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; args.argi1 = isVisible ? 1 : 0; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_VISIBILITY_CHANGED, args).sendToTarget(); } @Override public void onNotificationExpansionChanged(String key, boolean isUserAction, boolean isExpanded) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; args.argi1 = isUserAction ? 1 : 0; args.argi2 = isExpanded ? 1 : 0; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args) .sendToTarget(); } @Override public void onNotificationDirectReply(String key) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args) .sendToTarget(); } @Override public void onSuggestedReplySent(String key, CharSequence reply, int source) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; args.arg2 = reply; args.argi2 = source; mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget(); } @Override public void onActionClicked(String key, Notification.Action action, int source) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; args.arg2 = action; args.argi2 = source; mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget(); } @Override public void onNotificationClicked(String key) { SomeArgs args = SomeArgs.obtain(); args.arg1 = key; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_CLICKED, args).sendToTarget(); } @Override public void onAllowedAdjustmentsChanged() { mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget(); } @Override public void onNotificationFeedbackReceived(String key, NotificationRankingUpdate update, Bundle feedback) { applyUpdateLocked(update); SomeArgs args = SomeArgs.obtain(); args.arg1 = key; args.arg2 = getCurrentRanking(); args.arg3 = feedback; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_FEEDBACK_RECEIVED, args).sendToTarget(); } } private void setAdjustmentIssuer(@Nullable Adjustment adjustment) { if (adjustment != null) { adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName()); } } private final class MyHandler extends Handler { public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1; public static final int MSG_ON_NOTIFICATION_SNOOZED = 2; public static final int MSG_ON_NOTIFICATIONS_SEEN = 3; public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4; public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5; public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6; public static final int MSG_ON_ACTION_INVOKED = 7; public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8; public static final int MSG_ON_PANEL_REVEALED = 9; public static final int MSG_ON_PANEL_HIDDEN = 10; public static final int MSG_ON_NOTIFICATION_VISIBILITY_CHANGED = 11; public static final int MSG_ON_NOTIFICATION_CLICKED = 12; public static final int MSG_ON_NOTIFICATION_FEEDBACK_RECEIVED = 13; public MyHandler(Looper looper) { super(looper, null, false); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ON_NOTIFICATION_ENQUEUED: { SomeArgs args = (SomeArgs) msg.obj; StatusBarNotification sbn = (StatusBarNotification) args.arg1; NotificationChannel channel = (NotificationChannel) args.arg2; RankingMap ranking = (RankingMap) args.arg3; args.recycle(); Adjustment adjustment = onNotificationEnqueued(sbn, channel, ranking); setAdjustmentIssuer(adjustment); if (adjustment != null) { if (!isBound()) { Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip."); return; } try { getNotificationInterface().applyEnqueuedAdjustmentFromAssistant( mWrapper, adjustment); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); throw ex.rethrowFromSystemServer(); } catch (SecurityException e) { // app cannot catch and recover from this, so do on their behalf Log.w(TAG, "Enqueue adjustment failed; no longer connected", e); } } break; } case MSG_ON_NOTIFICATION_SNOOZED: { SomeArgs args = (SomeArgs) msg.obj; StatusBarNotification sbn = (StatusBarNotification) args.arg1; String snoozeCriterionId = (String) args.arg2; args.recycle(); onNotificationSnoozedUntilContext(sbn, snoozeCriterionId); break; } case MSG_ON_NOTIFICATIONS_SEEN: { SomeArgs args = (SomeArgs) msg.obj; List keys = (List) args.arg1; args.recycle(); onNotificationsSeen(keys); break; } case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; boolean isUserAction = args.argi1 == 1; boolean isExpanded = args.argi2 == 1; args.recycle(); onNotificationExpansionChanged(key, isUserAction, isExpanded); break; } case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; args.recycle(); onNotificationDirectReplied(key); break; } case MSG_ON_SUGGESTED_REPLY_SENT: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; CharSequence reply = (CharSequence) args.arg2; int source = args.argi2; args.recycle(); onSuggestedReplySent(key, reply, source); break; } case MSG_ON_ACTION_INVOKED: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; Notification.Action action = (Notification.Action) args.arg2; int source = args.argi2; args.recycle(); onActionInvoked(key, action, source); break; } case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: { onAllowedAdjustmentsChanged(); break; } case MSG_ON_PANEL_REVEALED: { SomeArgs args = (SomeArgs) msg.obj; int items = args.argi1; args.recycle(); onPanelRevealed(items); break; } case MSG_ON_PANEL_HIDDEN: { onPanelHidden(); break; } case MSG_ON_NOTIFICATION_VISIBILITY_CHANGED: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; boolean isVisible = args.argi1 == 1; args.recycle(); onNotificationVisibilityChanged(key, isVisible); break; } case MSG_ON_NOTIFICATION_CLICKED: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; args.recycle(); onNotificationClicked(key); break; } case MSG_ON_NOTIFICATION_FEEDBACK_RECEIVED: { SomeArgs args = (SomeArgs) msg.obj; String key = (String) args.arg1; RankingMap ranking = (RankingMap) args.arg2; Bundle feedback = (Bundle) args.arg3; args.recycle(); onNotificationFeedbackReceived(key, ranking, feedback); break; } } } } }