| /* |
| * 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.media.IRingtonePlayer; |
| 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 com.android.internal.util.FunctionalUtils; |
| |
| 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" |
| + " 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"; |
| |
| 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 { |
| 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(); |
| 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); |
| } |
| } |
| |