summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Dan Sandler <dsandler@android.com> 2018-11-26 09:56:26 -0500
committer Daniel Sandler <dsandler@android.com> 2019-01-02 16:37:59 +0000
commit7647f1d1b8189a6d7ab321ce67f60d115cb3598b (patch)
treea16645dd94696067cd99dc5560ca568c9e46dfae
parentff709020ab39d1ae4ee1658e23b6aa78f9dfec4a (diff)
Post a notification from the shell.
$ adb shell cmd notification post --help usage: post [flags] <tag> <text> flags: -h|--help -v|--verbose -t|--title <text> -i|--icon <iconspec> -I|--large-icon <iconspec> -S|--style <style> [styleargs] -c|--content-intent <intentspec> styles: (default none) bigtext bigpicture --picture <iconspec> inbox --line <text> --line <text> ... messaging --conversation <title> --message <who>:<text> ... media an <iconspec> is one of file:///data/local/tmp/<img.png> content://<provider>/<path> @[<package>:]drawable/<img> data:base64,<B64DATA==> an <intentspec> is (broadcast|service|activity) <args> <args> are as described in `am start` Test: atest services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java Change-Id: I68642b534b5a17a9ba406721fc1860879a6a0e8d
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java108
-rw-r--r--services/core/java/com/android/server/notification/NotificationShellCmd.java483
-rw-r--r--services/tests/uiservicestests/Android.bp1
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java270
4 files changed, 756 insertions, 106 deletions
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index d961bad1cf59..e2cb75e96882 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -144,7 +144,6 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
-import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -3852,7 +3851,8 @@ public class NotificationManagerService extends SystemService {
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver)
throws RemoteException {
- new ShellCmd().exec(this, in, out, err, args, callback, resultReceiver);
+ new NotificationShellCmd(NotificationManagerService.this)
+ .exec(this, in, out, err, args, callback, resultReceiver);
}
};
@@ -7715,110 +7715,6 @@ public class NotificationManagerService extends SystemService {
}
}
- private class ShellCmd extends ShellCommand {
- public static final String USAGE = "help\n"
- + "allow_listener COMPONENT [user_id]\n"
- + "disallow_listener COMPONENT [user_id]\n"
- + "allow_assistant COMPONENT\n"
- + "remove_assistant COMPONENT\n"
- + "allow_dnd PACKAGE\n"
- + "disallow_dnd PACKAGE\n"
- + "suspend_package PACKAGE\n"
- + "unsuspend_package PACKAGE";
-
- @Override
- public int onCommand(String cmd) {
- if (cmd == null) {
- return handleDefaultCommands(cmd);
- }
- final PrintWriter pw = getOutPrintWriter();
- try {
- switch (cmd) {
- case "allow_dnd": {
- getBinderService().setNotificationPolicyAccessGranted(
- getNextArgRequired(), true);
- }
- break;
-
- case "disallow_dnd": {
- getBinderService().setNotificationPolicyAccessGranted(
- getNextArgRequired(), false);
- }
- break;
- case "allow_listener": {
- ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
- if (cn == null) {
- pw.println("Invalid listener - must be a ComponentName");
- return -1;
- }
- String userId = getNextArg();
- if (userId == null) {
- getBinderService().setNotificationListenerAccessGranted(cn, true);
- } else {
- getBinderService().setNotificationListenerAccessGrantedForUser(
- cn, Integer.parseInt(userId), true);
- }
- }
- break;
- case "disallow_listener": {
- ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
- if (cn == null) {
- pw.println("Invalid listener - must be a ComponentName");
- return -1;
- }
- String userId = getNextArg();
- if (userId == null) {
- getBinderService().setNotificationListenerAccessGranted(cn, false);
- } else {
- getBinderService().setNotificationListenerAccessGrantedForUser(
- cn, Integer.parseInt(userId), false);
- }
- }
- break;
- case "allow_assistant": {
- ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
- if (cn == null) {
- pw.println("Invalid assistant - must be a ComponentName");
- return -1;
- }
- getBinderService().setNotificationAssistantAccessGranted(cn, true);
- }
- break;
- case "disallow_assistant": {
- ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
- if (cn == null) {
- pw.println("Invalid assistant - must be a ComponentName");
- return -1;
- }
- getBinderService().setNotificationAssistantAccessGranted(cn, false);
- }
- break;
- case "suspend_package": {
- // only use for testing
- simulatePackageSuspendBroadcast(true, getNextArgRequired());
- }
- break;
- case "unsuspend_package": {
- // only use for testing
- simulatePackageSuspendBroadcast(false, getNextArgRequired());
- }
- break;
- default:
- return handleDefaultCommands(cmd);
- }
- } catch (Exception e) {
- pw.println("Error occurred. Check logcat for details. " + e.getMessage());
- Slog.e(TAG, "Error running shell command", e);
- }
- return 0;
- }
-
- @Override
- public void onHelp() {
- getOutPrintWriter().println(USAGE);
- }
- }
-
private void writeSecureNotificationsPolicy(XmlSerializer out) throws IOException {
out.startTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG);
out.attribute(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_VALUE,
diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java
new file mode 100644
index 000000000000..3d88f20f0710
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java
@@ -0,0 +1,483 @@
+/*
+ * 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 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.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 USAGE =
+ "usage: cmd notification SUBCMD [args]\n\n"
+ + "SUBCMDs:\n"
+ + " allow_listener COMPONENT [user_id]\n"
+ + " disallow_listener COMPONENT [user_id]\n"
+ + " allow_assistant COMPONENT\n"
+ + " remove_assistant COMPONENT\n"
+ + " allow_dnd PACKAGE\n"
+ + " disallow_dnd PACKAGE\n"
+ + " suspend_package PACKAGE\n"
+ + " unsuspend_package PACKAGE\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 = 1138;
+ public static final String NOTIFICATION_PACKAGE = "com.android.shell";
+ public static final String CHANNEL_ID = "shellcmd";
+ 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;
+
+ public NotificationShellCmd(NotificationManagerService service) {
+ mDirectService = service;
+ mBinderService = service.getBinderService();
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd.replace('-', '_')) {
+ case "allow_dnd": {
+ mBinderService.setNotificationPolicyAccessGranted(
+ getNextArgRequired(), true);
+ }
+ break;
+
+ case "disallow_dnd": {
+ mBinderService.setNotificationPolicyAccessGranted(
+ getNextArgRequired(), false);
+ }
+ break;
+ case "allow_listener": {
+ ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
+ if (cn == null) {
+ pw.println("Invalid listener - must be a ComponentName");
+ return -1;
+ }
+ String userId = getNextArg();
+ if (userId == null) {
+ mBinderService.setNotificationListenerAccessGranted(cn, true);
+ } else {
+ mBinderService.setNotificationListenerAccessGrantedForUser(
+ cn, Integer.parseInt(userId), true);
+ }
+ }
+ break;
+ case "disallow_listener": {
+ ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
+ if (cn == null) {
+ pw.println("Invalid listener - must be a ComponentName");
+ return -1;
+ }
+ String userId = getNextArg();
+ if (userId == null) {
+ mBinderService.setNotificationListenerAccessGranted(cn, false);
+ } else {
+ mBinderService.setNotificationListenerAccessGrantedForUser(
+ cn, Integer.parseInt(userId), false);
+ }
+ }
+ break;
+ case "allow_assistant": {
+ ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
+ if (cn == null) {
+ pw.println("Invalid assistant - must be a ComponentName");
+ return -1;
+ }
+ mBinderService.setNotificationAssistantAccessGranted(cn, true);
+ }
+ break;
+ case "disallow_assistant": {
+ ComponentName cn = ComponentName.unflattenFromString(getNextArgRequired());
+ if (cn == null) {
+ pw.println("Invalid assistant - must be a ComponentName");
+ return -1;
+ }
+ mBinderService.setNotificationAssistantAccessGranted(cn, 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 "post":
+ case "notify":
+ doNotify(pw);
+ 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() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ final int userid = UserHandle.getCallingUserId();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (mBinderService.getNotificationChannelForPackage(NOTIFICATION_PACKAGE,
+ uid, CHANNEL_ID, false) == null) {
+ final NotificationChannel chan = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
+ CHANNEL_IMP);
+ Slog.v(NotificationManagerService.TAG,
+ "creating shell channel for user " + userid + " uid " + uid + ": " + chan);
+ mBinderService.createNotificationChannelsForPackage(NOTIFICATION_PACKAGE, uid,
+ new ParceledListSlice<NotificationChannel>(
+ Collections.singletonList(chan)));
+ Slog.v(NotificationManagerService.TAG, "created channel: "
+ + mBinderService.getNotificationChannelForPackage(NOTIFICATION_PACKAGE,
+ uid, CHANNEL_ID, false));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ 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) 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();
+
+ final Notification n = builder.build();
+ pw.println("posting:\n " + n);
+ Slog.v("NotificationManager", "posting: " + n);
+
+ final int userId = UserHandle.getCallingUserId();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mBinderService.enqueueNotificationWithTag(
+ NOTIFICATION_PACKAGE, "android",
+ tag, NOTIFICATION_ID,
+ n, userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ if (verbose) {
+ NotificationRecord nr = mDirectService.findNotificationLocked(
+ NOTIFICATION_PACKAGE, tag, NOTIFICATION_ID, userId);
+ 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(
+ NOTIFICATION_PACKAGE, tag, NOTIFICATION_ID, userId);
+ }
+ 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);
+ }
+}
+
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 7a5eaa852c34..f4443fe81a00 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -20,6 +20,7 @@ android_test {
"android-support-test",
"mockito-target-inline-minus-junit4",
"platform-test-annotations",
+ "hamcrest-library",
"testables",
],
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java
new file mode 100644
index 000000000000..fa90b291eeee
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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 junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+// this is a lazy way to do in/out/err but we're not particularly interested in the output
+import static java.io.FileDescriptor.err;
+import static java.io.FileDescriptor.in;
+import static java.io.FileDescriptor.out;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.UserHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class NotificationShellCmdTest extends UiServiceTestCase {
+ private final Binder mBinder = new Binder();
+ private final ShellCallback mCallback = new ShellCallback();
+ private final TestableContext mTestableContext = spy(getContext());
+ @Mock
+ NotificationManagerService mMockService;
+ @Mock
+ INotificationManager mMockBinderService;
+ private TestableLooper mTestableLooper;
+ private ResultReceiver mResultReceiver;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTestableLooper = TestableLooper.get(this);
+ mResultReceiver = new ResultReceiver(new Handler(mTestableLooper.getLooper()));
+
+ when(mMockService.getContext()).thenReturn(mTestableContext);
+ when(mMockService.getBinderService()).thenReturn(mMockBinderService);
+ }
+
+ private Bitmap createTestBitmap() {
+ final Bitmap bits = Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bits);
+ final GradientDrawable grad = new GradientDrawable(GradientDrawable.Orientation.TL_BR,
+ new int[]{Color.RED, Color.YELLOW, Color.GREEN,
+ Color.CYAN, Color.BLUE, Color.MAGENTA});
+ grad.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ grad.draw(canvas);
+ return bits;
+ }
+
+ private void doCmd(String... args) {
+ new NotificationShellCmd(mMockService)
+ .exec(mBinder, in, out, err, args, mCallback, mResultReceiver);
+ }
+
+ @Test
+ public void testNoArgs() throws Exception {
+ doCmd();
+ }
+
+ @Test
+ public void testHelp() throws Exception {
+ doCmd("--help");
+ }
+
+ Notification captureNotification(String aTag) throws Exception {
+ ArgumentCaptor<Notification> notificationCaptor =
+ ArgumentCaptor.forClass(Notification.class);
+ verify(mMockBinderService).enqueueNotificationWithTag(
+ eq(NotificationShellCmd.NOTIFICATION_PACKAGE),
+ eq("android"),
+ eq(aTag),
+ eq(NotificationShellCmd.NOTIFICATION_ID),
+ notificationCaptor.capture(),
+ eq(UserHandle.getCallingUserId()));
+ return notificationCaptor.getValue();
+ }
+
+ @Test
+ public void testBasic() throws Exception {
+ final String aTag = "aTag";
+ final String aText = "someText";
+ final String aTitle = "theTitle";
+ doCmd("notify",
+ "--title", aTitle,
+ aTag, aText);
+ final Notification captured = captureNotification(aTag);
+ assertEquals(aText, captured.extras.getString(Notification.EXTRA_TEXT));
+ assertEquals(aTitle, captured.extras.getString(Notification.EXTRA_TITLE));
+ }
+
+ @Test
+ public void testIcon() throws Exception {
+ final String aTag = "aTag";
+ final String aText = "someText";
+ doCmd("notify", "--icon", "@android:drawable/stat_sys_adb", aTag, aText);
+ final Notification captured = captureNotification(aTag);
+ final Icon icon = captured.getSmallIcon();
+ assertEquals("android", icon.getResPackage());
+ assertEquals(com.android.internal.R.drawable.stat_sys_adb, icon.getResId());
+ }
+
+ @Test
+ public void testBigText() throws Exception {
+ final String aTag = "aTag";
+ final String aText = "someText";
+ final String bigText = "someBigText";
+ doCmd("notify",
+ "--style", "bigtext",
+ "--big-text", bigText,
+ aTag, aText);
+ final Notification captured = captureNotification(aTag);
+ assertSame(captured.getNotificationStyle(), Notification.BigTextStyle.class);
+ assertEquals(aText, captured.extras.getString(Notification.EXTRA_TEXT));
+ assertEquals(bigText, captured.extras.getString(Notification.EXTRA_BIG_TEXT));
+ }
+
+ @Test
+ public void testBigPicture() throws Exception {
+ final String aTag = "aTag";
+ final String aText = "someText";
+ final String bigPicture = "@android:drawable/default_wallpaper";
+ doCmd("notify",
+ "--style", "bigpicture",
+ "--picture", bigPicture,
+ aTag, aText);
+ final Notification captured = captureNotification(aTag);
+ assertSame(captured.getNotificationStyle(), Notification.BigPictureStyle.class);
+ final Object pic = captured.extras.get(Notification.EXTRA_PICTURE);
+ assertThat(pic, instanceOf(Bitmap.class));
+ }
+
+ @Test
+ public void testInbox() throws Exception {
+ final int n = 25;
+ final String aTag = "inboxTag";
+ final String aText = "inboxText";
+ ArrayList<String> args = new ArrayList<>();
+ args.add("notify");
+ args.add("--style");
+ args.add("inbox");
+ final int startOfLineArgs = args.size();
+ for (int i = 0; i < n; i++) {
+ args.add("--line");
+ args.add(String.format("Line %02d", i));
+ }
+ args.add(aTag);
+ args.add(aText);
+
+ doCmd(args.toArray(new String[0]));
+ final Notification captured = captureNotification(aTag);
+ assertSame(captured.getNotificationStyle(), Notification.InboxStyle.class);
+ final Notification.Builder builder =
+ Notification.Builder.recoverBuilder(mContext, captured);
+ final ArrayList<CharSequence> lines =
+ ((Notification.InboxStyle) (builder.getStyle())).getLines();
+ for (int i = 0; i < n; i++) {
+ assertEquals(lines.get(i), args.get(1 + 2 * i + startOfLineArgs));
+ }
+ }
+
+ static final String[] PEOPLE = {
+ "Alice",
+ "Bob",
+ "Charlotte"
+ };
+ static final String[] MESSAGES = {
+ "Shall I compare thee to a summer's day?",
+ "Thou art more lovely and more temperate:",
+ "Rough winds do shake the darling buds of May,",
+ "And summer's lease hath all too short a date;",
+ "Sometime too hot the eye of heaven shines,",
+ "And often is his gold complexion dimm'd;",
+ "And every fair from fair sometime declines,",
+ "By chance or nature's changing course untrimm'd;",
+ "But thy eternal summer shall not fade,",
+ "Nor lose possession of that fair thou ow'st;",
+ "Nor shall death brag thou wander'st in his shade,",
+ "When in eternal lines to time thou grow'st:",
+ " So long as men can breathe or eyes can see,",
+ " So long lives this, and this gives life to thee.",
+ };
+
+ @Test
+ public void testMessaging() throws Exception {
+ final String aTag = "messagingTag";
+ final String aText = "messagingText";
+ ArrayList<String> args = new ArrayList<>();
+ args.add("notify");
+ args.add("--style");
+ args.add("messaging");
+ args.add("--conversation");
+ args.add("Sonnet 18");
+ final int startOfLineArgs = args.size();
+ for (int i = 0; i < MESSAGES.length; i++) {
+ args.add("--message");
+ args.add(String.format("%s:%s",
+ PEOPLE[i % PEOPLE.length],
+ MESSAGES[i % MESSAGES.length]));
+ }
+ args.add(aTag);
+ args.add(aText);
+
+ doCmd(args.toArray(new String[0]));
+ final Notification captured = captureNotification(aTag);
+ assertSame(Notification.MessagingStyle.class, captured.getNotificationStyle());
+ final Notification.Builder builder =
+ Notification.Builder.recoverBuilder(mContext, captured);
+ final Notification.MessagingStyle messagingStyle =
+ (Notification.MessagingStyle) (builder.getStyle());
+
+ assertEquals("Sonnet 18", messagingStyle.getConversationTitle());
+ final List<Notification.MessagingStyle.Message> messages = messagingStyle.getMessages();
+ for (int i = 0; i < messages.size(); i++) {
+ final Notification.MessagingStyle.Message m = messages.get(i);
+ assertEquals(MESSAGES[i], m.getText());
+ assertEquals(PEOPLE[i % PEOPLE.length], m.getSenderPerson().getName());
+ }
+
+ }
+}