content: /*
   * 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.SystemApi;
  import android.annotation.TestApi;
  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.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.
   * <p>
   * 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.
   *<p>
   * 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.
   * <p>
   *     All callbacks are called on the main thread.
   * </p>
   * @hide
   */
  @SystemApi
  @TestApi
  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";
  
      /**
       * @hide
       */
      protected Handler mHandler;
  
      @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.
       *
       * <p>Note: this method is only called if you don't override
       * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel)}.</p>
       *
       * @param sbn the new notification
       * @return an adjustment or null to take no action, within 100ms.
       */
      abstract public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification 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
       * @return an adjustment or null to take no action, within 100ms.
       */
      public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn,
              @NonNull NotificationChannel channel) {
          return onNotificationEnqueued(sbn);
      }
  
      /**
       * Implement this method to learn when notifications are removed, how they were interacted with
       * before removal, and why they were removed.
       * <p>
       * 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.
       * <p>
       * 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<String> keys) {
  
      }
  
      /**
       * 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 user has changed which features of
       * their notifications the assistant can modify.
       * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what
       * {@link Adjustment adjustments} you are currently allowed to make.</p>
       */
      public void onAllowedAdjustmentsChanged() {
      }
  
      /**
       * 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<Adjustment> 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.
       * <p>
       * 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) {
              StatusBarNotification sbn;
              try {
                  sbn = sbnHolder.get();
              } catch (RemoteException e) {
                  Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e);
                  return;
              }
  
              SomeArgs args = SomeArgs.obtain();
              args.arg1 = sbn;
              args.arg2 = channel;
              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;
              }
  
              SomeArgs args = SomeArgs.obtain();
              args.arg1 = sbn;
              args.arg2 = snoozeCriterionId;
              mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
                      args).sendToTarget();
          }
  
          @Override
          public void onNotificationsSeen(List<String> keys) {
              SomeArgs args = SomeArgs.obtain();
              args.arg1 = keys;
              mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
                      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 onAllowedAdjustmentsChanged() {
              mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).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 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;
                      args.recycle();
                      Adjustment adjustment = onNotificationEnqueued(sbn, channel);
                      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<String> keys = (List<String>) 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;
                  }
              }
          }
      }
  }
  