blob: da7864ba32c47fe7bc1e25d54e6810de4d6fb9b3 [file] [log] [blame]
/*
* 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.Process;
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;
import java.util.Date;
/**
* 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"
+ " set_dnd [on|none (same as on)|priority|alarms|all|off (same as all)]"
+ " 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\n"
+ " set_bubbles PACKAGE PREFERENCE (0=none 1=all 2=selected) "
+ "[user_id (current user if not specified)]\n"
+ " set_bubbles_channel PACKAGE CHANNEL_ID ALLOW "
+ "[user_id (current user if not specified)]\n"
+ " list\n"
+ " get <notification-key>\n"
+ " snooze --for <msec> <notification-key>\n"
+ " unsnooze <notification-key>\n"
;
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;
private NotificationChannel mChannel;
public NotificationShellCmd(NotificationManagerService service) {
mDirectService = service;
mBinderService = service.getBinderService();
mPm = mDirectService.getContext().getPackageManager();
}
protected boolean checkShellCommandPermission(int callingUid) {
return (callingUid == Process.ROOT_UID || callingUid == Process.SHELL_UID);
}
@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 {
if (callingUid == Process.ROOT_UID) {
callingPackage = NotificationManagerService.ROOT_PKG;
} else {
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();
if (!checkShellCommandPermission(callingUid)) {
Slog.e(TAG, "error: permission denied: callingUid="
+ callingUid + " callingPackage=" + callingPackage);
pw.println("error: permission denied: callingUid="
+ callingUid + " callingPackage=" + callingPackage);
return 255;
}
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 "set_bubbles": {
// only use for testing
String packageName = getNextArgRequired();
int preference = Integer.parseInt(getNextArgRequired());
if (preference > 3 || preference < 0) {
pw.println("Invalid preference - must be between 0-3 "
+ "(0=none 1=all 2=selected)");
return -1;
}
int userId = ActivityManager.getCurrentUser();
if (peekNextArg() != null) {
userId = Integer.parseInt(getNextArgRequired());
}
int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0));
mBinderService.setBubblesAllowed(packageName, appUid, preference);
break;
}
case "set_bubbles_channel": {
// only use for testing
String packageName = getNextArgRequired();
String channelId = getNextArgRequired();
boolean allow = Boolean.parseBoolean(getNextArgRequired());
int userId = ActivityManager.getCurrentUser();
if (peekNextArg() != null) {
userId = Integer.parseInt(getNextArgRequired());
}
NotificationChannel channel = mBinderService.getNotificationChannel(
callingPackage, userId, packageName, channelId);
channel.setAllowBubbles(allow);
int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0));
mBinderService.updateNotificationChannelForPackage(packageName, appUid,
channel);
break;
}
case "post":
case "notify":
doNotify(pw, callingPackage, callingUid);
break;
case "list":
for (String key : mDirectService.mNotificationsByKey.keySet()) {
pw.println(key);
}
break;
case "get": {
final String key = getNextArgRequired();
final NotificationRecord nr = mDirectService.getNotificationRecord(key);
if (nr != null) {
nr.dump(pw, "", mDirectService.getContext(), false);
} else {
pw.println("error: no active notification matching key: " + key);
return 1;
}
break;
}
case "snoozed": {
final StringBuilder sb = new StringBuilder();
final SnoozeHelper sh = mDirectService.mSnoozeHelper;
for (NotificationRecord nr : sh.getSnoozed()) {
final String pkg = nr.getSbn().getPackageName();
final String key = nr.getKey();
pw.println(key + " snoozed, time="
+ sh.getSnoozeTimeForUnpostedNotification(
nr.getUserId(), pkg, key)
+ " context="
+ sh.getSnoozeContextForUnpostedNotification(
nr.getUserId(), pkg, key));
}
break;
}
case "unsnooze": {
boolean mute = false;
String key = getNextArgRequired();
if ("--mute".equals(key)) {
mute = true;
key = getNextArgRequired();
}
if (null != mDirectService.mSnoozeHelper.getNotification(key)) {
pw.println("unsnoozing: " + key);
mDirectService.unsnoozeNotificationInt(key, null, mute);
} else {
pw.println("error: no snoozed otification matching key: " + key);
return 1;
}
break;
}
case "snooze": {
String subflag = getNextArg();
if (subflag == null) {
subflag = "help";
} else if (subflag.startsWith("--")) {
subflag = subflag.substring(2);
}
String flagarg = getNextArg();
String key = getNextArg();
if (key == null) subflag = "help";
String criterion = null;
long duration = 0;
switch (subflag) {
case "context":
case "condition":
case "criterion":
criterion = flagarg;
break;
case "until":
case "for":
case "duration":
duration = Long.parseLong(flagarg);
break;
default:
pw.println("usage: cmd notification snooze (--for <msec> | "
+ "--context <snooze-criterion-id>) <key>");
return 1;
}
if (null == mDirectService.getNotificationRecord(key)) {
pw.println("error: no notification matching key: " + key);
return 1;
}
if (duration > 0 || criterion != null) {
if (duration > 0) {
pw.println(String.format("snoozing <%s> until time: %s", key,
new Date(System.currentTimeMillis() + duration)));
} else {
pw.println(String.format("snoozing <%s> until criterion: %s", key,
criterion));
}
mDirectService.snoozeNotificationInt(key, duration, criterion, null);
} else {
pw.println("error: invalid value for --" + subflag + ": " + flagarg);
return 1;
}
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);
}
}