content: /*
   * Copyright (C) 2018 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 android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
  import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
  import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
  import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
  import static android.app.NotificationManager.INTERRUPTION_FILTER_UNKNOWN;
  
  import android.app.ActivityManager;
  import android.app.INotificationManager;
  import android.app.Notification;
  import android.app.NotificationChannel;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.app.Person;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.content.pm.ParceledListSlice;
  import android.content.res.Resources;
  import android.graphics.drawable.BitmapDrawable;
  import android.graphics.drawable.Drawable;
  import android.graphics.drawable.Icon;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.RemoteException;
  import android.os.ShellCommand;
  import android.os.UserHandle;
  import android.text.TextUtils;
  import android.util.Slog;
  
  import java.io.PrintWriter;
  import java.net.URISyntaxException;
  import java.util.Collections;
  
  /**
   * Implementation of `cmd notification` in NotificationManagerService.
   */
  public class NotificationShellCmd extends ShellCommand {
      private static final String TAG = "NotifShellCmd";
      private static final String USAGE = "usage: cmd notification SUBCMD [args]\n\n"
              + "SUBCMDs:\n"
              + "  allow_listener COMPONENT [user_id (current user if not specified)]\n"
              + "  disallow_listener COMPONENT [user_id (current user if not specified)]\n"
              + "  allow_assistant COMPONENT [user_id (current user if not specified)]\n"
              + "  remove_assistant COMPONENT [user_id (current user if not specified)]\n"
              + "  allow_dnd PACKAGE [user_id (current user if not specified)]\n"
              + "  disallow_dnd PACKAGE [user_id (current user if not specified)]\n"
              + "  suspend_package PACKAGE\n"
              + "  unsuspend_package PACKAGE\n"
              + "  reset_assistant_user_set [user_id (current user if not specified)]\n"
              + "  get_approved_assistant [user_id (current user if not specified)]\n"
              + "  post [--help | flags] TAG TEXT";
  
      private static final String NOTIFY_USAGE =
                "usage: cmd notification post [flags] <tag> <text>\n\n"
              + "flags:\n"
              + "  -h|--help\n"
              + "  -v|--verbose\n"
              + "  -t|--title <text>\n"
              + "  -i|--icon <iconspec>\n"
              + "  -I|--large-icon <iconspec>\n"
              + "  -S|--style <style> [styleargs]\n"
              + "  -c|--content-intent <intentspec>\n"
              + "\n"
              + "styles: (default none)\n"
              + "  bigtext\n"
              + "  bigpicture --picture <iconspec>\n"
              + "  inbox --line <text> --line <text> ...\n"
              + "  messaging --conversation <title> --message <who>:<text> ...\n"
              + "  media\n"
              + "\n"
              + "an <iconspec> is one of\n"
              + "  file:///data/local/tmp/<img.png>\n"
              + "  content://<provider>/<path>\n"
              + "  @[<package>:]drawable/<img>\n"
              + "  data:base64,<B64DATA==>\n"
              + "\n"
              + "an <intentspec> is (broadcast|service|activity) <args>\n"
              + "  <args> are as described in `am start`";
  
      public static final int NOTIFICATION_ID = 2020;
      public static final String CHANNEL_ID = "shell_cmd";
      public static final String CHANNEL_NAME = "Shell command";
      public static final int CHANNEL_IMP = NotificationManager.IMPORTANCE_DEFAULT;
  
      private final NotificationManagerService mDirectService;
      private final INotificationManager mBinderService;
      private final PackageManager mPm;
  
      public NotificationShellCmd(NotificationManagerService service) {
          mDirectService = service;
          mBinderService = service.getBinderService();
          mPm = mDirectService.getContext().getPackageManager();
      }
  
      @Override
      public int onCommand(String cmd) {
          if (cmd == null) {
              return handleDefaultCommands(cmd);
          }
          String callingPackage = null;
          final int callingUid = Binder.getCallingUid();
          long identity = Binder.clearCallingIdentity();
          try {
              String[] packages = mPm.getPackagesForUid(callingUid);
              if (packages != null && packages.length > 0) {
                  callingPackage = packages[0];
              }
          } catch (Exception e) {
              Slog.e(TAG, "failed to get caller pkg", e);
          } finally {
              Binder.restoreCallingIdentity(identity);
          }
          final PrintWriter pw = getOutPrintWriter();
          try {
              switch (cmd.replace('-', '_')) {
                  case "set_dnd": {
                      String mode = getNextArgRequired();
                      int interruptionFilter = INTERRUPTION_FILTER_UNKNOWN;
                      switch(mode) {
                          case "none":
                          case "on":
                              interruptionFilter = INTERRUPTION_FILTER_NONE;
                              break;
                          case "priority":
                              interruptionFilter = INTERRUPTION_FILTER_PRIORITY;
                              break;
                          case "alarms":
                              interruptionFilter = INTERRUPTION_FILTER_ALARMS;
                              break;
                          case "all":
                          case "off":
                              interruptionFilter = INTERRUPTION_FILTER_ALL;
                      }
                      final int filter = interruptionFilter;
                      mBinderService.setInterruptionFilter(callingPackage, filter);
                  }
                  break;
                  case "allow_dnd": {
                      String packageName = getNextArgRequired();
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationPolicyAccessGrantedForUser(
                              packageName, userId, true);
                  }
                  break;
  
                  case "disallow_dnd": {
                      String packageName = getNextArgRequired();
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationPolicyAccessGrantedForUser(
                              packageName, userId, false);
                  }
                  break;
                  case "allow_listener": {
                      ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
                      if (cn == null) {
                          pw.println("Invalid listener - must be a ComponentName");
                          return -1;
                      }
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationListenerAccessGrantedForUser(cn, userId, true);
                  }
                  break;
                  case "disallow_listener": {
                      ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
                      if (cn == null) {
                          pw.println("Invalid listener - must be a ComponentName");
                          return -1;
                      }
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationListenerAccessGrantedForUser(cn, userId, false);
                  }
                  break;
                  case "allow_assistant": {
                      ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
                      if (cn == null) {
                          pw.println("Invalid assistant - must be a ComponentName");
                          return -1;
                      }
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, true);
                  }
                  break;
                  case "disallow_assistant": {
                      ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
                      if (cn == null) {
                          pw.println("Invalid assistant - must be a ComponentName");
                          return -1;
                      }
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mBinderService.setNotificationAssistantAccessGrantedForUser(cn, userId, false);
                  }
                  break;
                  case "suspend_package": {
                      // only use for testing
                      mDirectService.simulatePackageSuspendBroadcast(true, getNextArgRequired());
                  }
                  break;
                  case "unsuspend_package": {
                      // only use for testing
                      mDirectService.simulatePackageSuspendBroadcast(false, getNextArgRequired());
                  }
                  break;
                  case "distract_package": {
                      // only use for testing
                      // Flag values are in
                      // {@link android.content.pm.PackageManager.DistractionRestriction}.
                      mDirectService.simulatePackageDistractionBroadcast(
                              Integer.parseInt(getNextArgRequired()),
                              getNextArgRequired().split(","));
                      break;
                  }
                  case "reset_assistant_user_set": {
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      mDirectService.resetAssistantUserSet(userId);
                      break;
                  }
                  case "get_approved_assistant": {
                      int userId = ActivityManager.getCurrentUser();
                      if (peekNextArg() != null) {
                          userId = Integer.parseInt(getNextArgRequired());
                      }
                      ComponentName approvedAssistant = mDirectService.getApprovedAssistant(userId);
                      if (approvedAssistant == null) {
                          pw.println("null");
                      } else {
                          pw.println(approvedAssistant.flattenToString());
                      }
                      break;
                  }
                  case "post":
                  case "notify":
                      doNotify(pw, callingPackage, callingUid);
                      break;
                  default:
                      return handleDefaultCommands(cmd);
              }
          } catch (Exception e) {
              pw.println("Error occurred. Check logcat for details. " + e.getMessage());
              Slog.e(NotificationManagerService.TAG, "Error running shell command", e);
          }
          return 0;
      }
  
      void ensureChannel(String callingPackage, int callingUid) throws RemoteException {
          final NotificationChannel channel =
                  new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, CHANNEL_IMP);
          mBinderService.createNotificationChannels(callingPackage,
                  new ParceledListSlice<>(Collections.singletonList(channel)));
          Slog.v(NotificationManagerService.TAG, "created channel: "
                  + mBinderService.getNotificationChannel(callingPackage,
                  UserHandle.getUserId(callingUid), callingPackage, CHANNEL_ID));
      }
  
      Icon parseIcon(Resources res, String encoded) throws IllegalArgumentException {
          if (TextUtils.isEmpty(encoded)) return null;
          if (encoded.startsWith("/")) {
              encoded = "file://" + encoded;
          }
          if (encoded.startsWith("http:")
                  || encoded.startsWith("https:")
                  || encoded.startsWith("content:")
                  || encoded.startsWith("file:")
                  || encoded.startsWith("android.resource:")) {
              Uri asUri = Uri.parse(encoded);
              return Icon.createWithContentUri(asUri);
          } else if (encoded.startsWith("@")) {
              final int resid = res.getIdentifier(encoded.substring(1),
                      "drawable", "android");
              if (resid != 0) {
                  return Icon.createWithResource(res, resid);
              }
          } else if (encoded.startsWith("data:")) {
              encoded = encoded.substring(encoded.indexOf(',') + 1);
              byte[] bits = android.util.Base64.decode(encoded, android.util.Base64.DEFAULT);
              return Icon.createWithData(bits, 0, bits.length);
          }
          return null;
      }
  
      private int doNotify(PrintWriter pw, String callingPackage, int callingUid)
              throws RemoteException, URISyntaxException {
          final Context context = mDirectService.getContext();
          final Resources res = context.getResources();
          final Notification.Builder builder = new Notification.Builder(context, CHANNEL_ID);
          String opt;
  
          boolean verbose = false;
          Notification.BigPictureStyle bigPictureStyle = null;
          Notification.BigTextStyle bigTextStyle = null;
          Notification.InboxStyle inboxStyle = null;
          Notification.MediaStyle mediaStyle = null;
          Notification.MessagingStyle messagingStyle = null;
  
          Icon smallIcon = null;
          while ((opt = getNextOption()) != null) {
              boolean large = false;
              switch (opt) {
                  case "-v":
                  case "--verbose":
                      verbose = true;
                      break;
                  case "-t":
                  case "--title":
                  case "title":
                      builder.setContentTitle(getNextArgRequired());
                      break;
                  case "-I":
                  case "--large-icon":
                  case "--largeicon":
                  case "largeicon":
                  case "large-icon":
                      large = true;
                      // fall through
                  case "-i":
                  case "--icon":
                  case "icon":
                      final String iconSpec = getNextArgRequired();
                      final Icon icon = parseIcon(res, iconSpec);
                      if (icon == null) {
                          pw.println("error: invalid icon: " + iconSpec);
                          return -1;
                      }
                      if (large) {
                          builder.setLargeIcon(icon);
                          large = false;
                      } else {
                          smallIcon = icon;
                      }
                      break;
                  case "-c":
                  case "--content-intent":
                  case "content-intent":
                  case "--intent":
                  case "intent":
                      String intentKind = null;
                      switch (peekNextArg()) {
                          case "broadcast":
                          case "service":
                          case "activity":
                              intentKind = getNextArg();
                      }
                      final Intent intent = Intent.parseCommandArgs(this, null);
                      if (intent.getData() == null) {
                          // force unique intents unless you know what you're doing
                          intent.setData(Uri.parse("xyz:" + System.currentTimeMillis()));
                      }
                      final PendingIntent pi;
                      if ("broadcast".equals(intentKind)) {
                          pi = PendingIntent.getBroadcastAsUser(
                                  context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT,
                                  UserHandle.CURRENT);
                      } else if ("service".equals(intentKind)) {
                          pi = PendingIntent.getService(
                                  context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
                      } else {
                          pi = PendingIntent.getActivityAsUser(
                                  context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, null,
                                  UserHandle.CURRENT);
                      }
                      builder.setContentIntent(pi);
                      break;
                  case "-S":
                  case "--style":
                      final String styleSpec = getNextArgRequired().toLowerCase();
                      switch (styleSpec) {
                          case "bigtext":
                              bigTextStyle = new Notification.BigTextStyle();
                              builder.setStyle(bigTextStyle);
                              break;
                          case "bigpicture":
                              bigPictureStyle = new Notification.BigPictureStyle();
                              builder.setStyle(bigPictureStyle);
                              break;
                          case "inbox":
                              inboxStyle = new Notification.InboxStyle();
                              builder.setStyle(inboxStyle);
                              break;
                          case "messaging":
                              String name = "You";
                              if ("--user".equals(peekNextArg())) {
                                  getNextArg();
                                  name = getNextArgRequired();
                              }
                              messagingStyle = new Notification.MessagingStyle(
                                      new Person.Builder().setName(name).build());
                              builder.setStyle(messagingStyle);
                              break;
                          case "media":
                              mediaStyle = new Notification.MediaStyle();
                              builder.setStyle(mediaStyle);
                              break;
                          default:
                              throw new IllegalArgumentException(
                                      "unrecognized notification style: " + styleSpec);
                      }
                      break;
                  case "--bigText": case "--bigtext": case "--big-text":
                      if (bigTextStyle == null) {
                          throw new IllegalArgumentException("--bigtext requires --style bigtext");
                      }
                      bigTextStyle.bigText(getNextArgRequired());
                      break;
                  case "--picture":
                      if (bigPictureStyle == null) {
                          throw new IllegalArgumentException("--picture requires --style bigpicture");
                      }
                      final String pictureSpec = getNextArgRequired();
                      final Icon pictureAsIcon = parseIcon(res, pictureSpec);
                      if (pictureAsIcon == null) {
                          throw new IllegalArgumentException("bad picture spec: " + pictureSpec);
                      }
                      final Drawable d = pictureAsIcon.loadDrawable(context);
                      if (d instanceof BitmapDrawable) {
                          bigPictureStyle.bigPicture(((BitmapDrawable) d).getBitmap());
                      } else {
                          throw new IllegalArgumentException("not a bitmap: " + pictureSpec);
                      }
                      break;
                  case "--line":
                      if (inboxStyle == null) {
                          throw new IllegalArgumentException("--line requires --style inbox");
                      }
                      inboxStyle.addLine(getNextArgRequired());
                      break;
                  case "--message":
                      if (messagingStyle == null) {
                          throw new IllegalArgumentException(
                                  "--message requires --style messaging");
                      }
                      String arg = getNextArgRequired();
                      String[] parts = arg.split(":", 2);
                      if (parts.length > 1) {
                          messagingStyle.addMessage(parts[1], System.currentTimeMillis(),
                                  parts[0]);
                      } else {
                          messagingStyle.addMessage(parts[0], System.currentTimeMillis(),
                                  new String[]{
                                          messagingStyle.getUserDisplayName().toString(),
                                          "Them"
                                  }[messagingStyle.getMessages().size() % 2]);
                      }
                      break;
                  case "--conversation":
                      if (messagingStyle == null) {
                          throw new IllegalArgumentException(
                                  "--conversation requires --style messaging");
                      }
                      messagingStyle.setConversationTitle(getNextArgRequired());
                      break;
                  case "-h":
                  case "--help":
                  case "--wtf":
                  default:
                      pw.println(NOTIFY_USAGE);
                      return 0;
              }
          }
  
          final String tag = getNextArg();
          final String text = getNextArg();
          if (tag == null || text == null) {
              pw.println(NOTIFY_USAGE);
              return -1;
          }
  
          builder.setContentText(text);
  
          if (smallIcon == null) {
              // uh oh, let's substitute something
              builder.setSmallIcon(com.android.internal.R.drawable.stat_notify_chat);
          } else {
              builder.setSmallIcon(smallIcon);
          }
  
          ensureChannel(callingPackage, callingUid);
  
          final Notification n = builder.build();
          pw.println("posting:\n  " + n);
          Slog.v("NotificationManager", "posting: " + n);
  
          mBinderService.enqueueNotificationWithTag(callingPackage, callingPackage, tag,
                  NOTIFICATION_ID, n, UserHandle.getUserId(callingUid));
  
          if (verbose) {
              NotificationRecord nr = mDirectService.findNotificationLocked(
                      callingPackage, tag, NOTIFICATION_ID, UserHandle.getUserId(callingUid));
              for (int tries = 3; tries-- > 0; ) {
                  if (nr != null) break;
                  try {
                      pw.println("waiting for notification to post...");
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                  }
                  nr = mDirectService.findNotificationLocked(
                          callingPackage, tag, NOTIFICATION_ID, UserHandle.getUserId(callingUid));
              }
              if (nr == null) {
                  pw.println("warning: couldn't find notification after enqueueing");
              } else {
                  pw.println("posted: ");
                  nr.dump(pw, "  ", context, false);
              }
          }
  
          return 0;
      }
  
      @Override
      public void onHelp() {
          getOutPrintWriter().println(USAGE);
      }
  }
  
  