diff options
| author | 2018-11-26 09:56:26 -0500 | |
|---|---|---|
| committer | 2019-01-02 16:37:59 +0000 | |
| commit | 7647f1d1b8189a6d7ab321ce67f60d115cb3598b (patch) | |
| tree | a16645dd94696067cd99dc5560ca568c9e46dfae | |
| parent | ff709020ab39d1ae4ee1658e23b6aa78f9dfec4a (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
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()); + } + + } +} |