1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.app; 18 19 import static com.android.internal.util.NotificationColorUtil.satisfiesTextContrast; 20 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertFalse; 23 import static org.junit.Assert.assertNotSame; 24 import static org.junit.Assert.assertSame; 25 import static org.junit.Assert.assertTrue; 26 27 import android.annotation.Nullable; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.graphics.BitmapFactory; 31 import android.graphics.drawable.Icon; 32 import android.media.session.MediaSession; 33 import android.os.Build; 34 import android.os.Parcel; 35 import android.os.Parcelable; 36 import android.support.test.InstrumentationRegistry; 37 import android.support.test.filters.SmallTest; 38 import android.support.test.runner.AndroidJUnit4; 39 import android.widget.RemoteViews; 40 41 import org.junit.Before; 42 import org.junit.Test; 43 import org.junit.runner.RunWith; 44 45 import java.util.function.Consumer; 46 47 @RunWith(AndroidJUnit4.class) 48 @SmallTest 49 public class NotificationTest { 50 51 private Context mContext; 52 53 @Before 54 public void setUp() { 55 mContext = InstrumentationRegistry.getContext(); 56 } 57 58 @Test 59 public void testColorizedByPermission() { 60 Notification n = new Notification.Builder(mContext, "test") 61 .setFlag(Notification.FLAG_CAN_COLORIZE, true) 62 .setColorized(true) 63 .build(); 64 assertTrue(n.isColorized()); 65 66 n = new Notification.Builder(mContext, "test") 67 .setFlag(Notification.FLAG_CAN_COLORIZE, true) 68 .build(); 69 assertFalse(n.isColorized()); 70 71 n = new Notification.Builder(mContext, "test") 72 .setFlag(Notification.FLAG_CAN_COLORIZE, false) 73 .setColorized(true) 74 .build(); 75 assertFalse(n.isColorized()); 76 } 77 78 @Test 79 public void testColorizedByForeground() { 80 Notification n = new Notification.Builder(mContext, "test") 81 .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) 82 .setColorized(true) 83 .build(); 84 assertTrue(n.isColorized()); 85 86 n = new Notification.Builder(mContext, "test") 87 .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) 88 .build(); 89 assertFalse(n.isColorized()); 90 91 n = new Notification.Builder(mContext, "test") 92 .setFlag(Notification.FLAG_FOREGROUND_SERVICE, false) 93 .setColorized(true) 94 .build(); 95 assertFalse(n.isColorized()); 96 } 97 98 @Test 99 public void testColorSatisfiedWhenBgDarkTextDarker() { 100 Notification.Builder builder = getMediaNotification(); 101 Notification n = builder.build(); 102 103 assertTrue(n.isColorized()); 104 105 // An initial guess where the foreground color is actually darker than an already dark bg 106 int backgroundColor = 0xff585868; 107 int initialForegroundColor = 0xff505868; 108 builder.setColorPalette(backgroundColor, initialForegroundColor); 109 int primaryTextColor = builder.getPrimaryTextColor(); 110 assertTrue(satisfiesTextContrast(primaryTextColor, backgroundColor)); 111 int secondaryTextColor = builder.getSecondaryTextColor(); 112 assertTrue(satisfiesTextContrast(secondaryTextColor, backgroundColor)); 113 } 114 115 @Test 116 public void testHasCompletedProgress_noProgress() { 117 Notification n = new Notification.Builder(mContext).build(); 118 119 assertFalse(n.hasCompletedProgress()); 120 } 121 122 @Test 123 public void testHasCompletedProgress_complete() { 124 Notification n = new Notification.Builder(mContext) 125 .setProgress(100, 100, true) 126 .build(); 127 Notification n2 = new Notification.Builder(mContext) 128 .setProgress(10, 10, false) 129 .build(); 130 assertTrue(n.hasCompletedProgress()); 131 assertTrue(n2.hasCompletedProgress()); 132 } 133 134 @Test 135 public void testHasCompletedProgress_notComplete() { 136 Notification n = new Notification.Builder(mContext) 137 .setProgress(100, 99, true) 138 .build(); 139 Notification n2 = new Notification.Builder(mContext) 140 .setProgress(10, 4, false) 141 .build(); 142 assertFalse(n.hasCompletedProgress()); 143 assertFalse(n2.hasCompletedProgress()); 144 } 145 146 @Test 147 public void testHasCompletedProgress_zeroMax() { 148 Notification n = new Notification.Builder(mContext) 149 .setProgress(0, 0, true) 150 .build(); 151 assertFalse(n.hasCompletedProgress()); 152 } 153 154 @Test 155 public void largeIconMultipleReferences_keptAfterParcelling() { 156 Icon originalIcon = Icon.createWithBitmap(BitmapFactory.decodeResource( 157 mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); 158 159 Notification n = new Notification.Builder(mContext).setLargeIcon(originalIcon).build(); 160 assertSame(n.getLargeIcon(), originalIcon); 161 162 Notification q = writeAndReadParcelable(n); 163 assertNotSame(q.getLargeIcon(), n.getLargeIcon()); 164 165 assertTrue(q.getLargeIcon().getBitmap().sameAs(n.getLargeIcon().getBitmap())); 166 assertSame(q.getLargeIcon(), q.extras.getParcelable(Notification.EXTRA_LARGE_ICON)); 167 } 168 169 @Test 170 public void largeIconReferenceInExtrasOnly_keptAfterParcelling() { 171 Icon originalIcon = Icon.createWithBitmap(BitmapFactory.decodeResource( 172 mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); 173 174 Notification n = new Notification.Builder(mContext).build(); 175 n.extras.putParcelable(Notification.EXTRA_LARGE_ICON, originalIcon); 176 assertSame(n.getLargeIcon(), null); 177 178 Notification q = writeAndReadParcelable(n); 179 assertSame(q.getLargeIcon(), null); 180 assertTrue(((Icon) q.extras.getParcelable(Notification.EXTRA_LARGE_ICON)).getBitmap() 181 .sameAs(originalIcon.getBitmap())); 182 } 183 184 @Test 185 public void allPendingIntents_recollectedAfterReusingBuilder() { 186 PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), 0); 187 PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), 0); 188 189 Notification.Builder builder = new Notification.Builder(mContext, "channel"); 190 builder.setContentIntent(intent1); 191 192 Parcel p = Parcel.obtain(); 193 194 Notification n1 = builder.build(); 195 n1.writeToParcel(p, 0); 196 197 builder.setContentIntent(intent2); 198 Notification n2 = builder.build(); 199 n2.writeToParcel(p, 0); 200 201 assertTrue(n2.allPendingIntents.contains(intent2)); 202 } 203 204 @Test 205 public void allPendingIntents_containsCustomRemoteViews() { 206 PendingIntent intent = PendingIntent.getActivity(mContext, 0, new Intent("test"), 0); 207 208 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 0 /* layoutId */); 209 contentView.setOnClickPendingIntent(1 /* id */, intent); 210 211 Notification.Builder builder = new Notification.Builder(mContext, "channel"); 212 builder.setCustomContentView(contentView); 213 214 Parcel p = Parcel.obtain(); 215 216 Notification n = builder.build(); 217 n.writeToParcel(p, 0); 218 219 assertTrue(n.allPendingIntents.contains(intent)); 220 } 221 222 @Test 223 public void messagingStyle_isGroupConversation() { 224 mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P; 225 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name") 226 .setGroupConversation(true) 227 .setConversationTitle("test conversation title"); 228 Notification notification = new Notification.Builder(mContext, "test id") 229 .setSmallIcon(1) 230 .setContentTitle("test title") 231 .setStyle(messagingStyle) 232 .build(); 233 234 assertTrue(messagingStyle.isGroupConversation()); 235 assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION)); 236 } 237 238 @Test 239 public void messagingStyle_isGroupConversation_noConversationTitle() { 240 mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P; 241 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name") 242 .setGroupConversation(true) 243 .setConversationTitle(null); 244 Notification notification = new Notification.Builder(mContext, "test id") 245 .setSmallIcon(1) 246 .setContentTitle("test title") 247 .setStyle(messagingStyle) 248 .build(); 249 250 assertTrue(messagingStyle.isGroupConversation()); 251 assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION)); 252 } 253 254 @Test 255 public void messagingStyle_isGroupConversation_withConversationTitle_legacy() { 256 // In legacy (version < P), isGroupConversation is controlled by conversationTitle. 257 mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.O; 258 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name") 259 .setGroupConversation(false) 260 .setConversationTitle("test conversation title"); 261 Notification notification = new Notification.Builder(mContext, "test id") 262 .setSmallIcon(1) 263 .setContentTitle("test title") 264 .setStyle(messagingStyle) 265 .build(); 266 267 assertTrue(messagingStyle.isGroupConversation()); 268 assertFalse(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION)); 269 } 270 271 @Test 272 public void messagingStyle_isGroupConversation_withoutConversationTitle_legacy() { 273 // In legacy (version < P), isGroupConversation is controlled by conversationTitle. 274 mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.O; 275 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name") 276 .setGroupConversation(true) 277 .setConversationTitle(null); 278 Notification notification = new Notification.Builder(mContext, "test id") 279 .setSmallIcon(1) 280 .setContentTitle("test title") 281 .setStyle(messagingStyle) 282 .build(); 283 284 assertFalse(messagingStyle.isGroupConversation()); 285 assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION)); 286 } 287 288 @Test 289 public void action_builder_hasDefault() { 290 Notification.Action action = makeNotificationAction(null); 291 assertEquals(Notification.Action.SEMANTIC_ACTION_NONE, action.getSemanticAction()); 292 } 293 294 @Test 295 public void action_builder_setSemanticAction() { 296 Notification.Action action = makeNotificationAction( 297 builder -> builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY)); 298 assertEquals(Notification.Action.SEMANTIC_ACTION_REPLY, action.getSemanticAction()); 299 } 300 301 @Test 302 public void action_parcel() { 303 Notification.Action action = writeAndReadParcelable( 304 makeNotificationAction(builder -> { 305 builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_ARCHIVE); 306 builder.setAllowGeneratedReplies(true); 307 })); 308 309 assertEquals(Notification.Action.SEMANTIC_ACTION_ARCHIVE, action.getSemanticAction()); 310 assertTrue(action.getAllowGeneratedReplies()); 311 } 312 313 @Test 314 public void action_clone() { 315 Notification.Action action = makeNotificationAction( 316 builder -> builder.setSemanticAction(Notification.Action.SEMANTIC_ACTION_DELETE)); 317 assertEquals( 318 Notification.Action.SEMANTIC_ACTION_DELETE, 319 action.clone().getSemanticAction()); 320 } 321 322 private Notification.Builder getMediaNotification() { 323 MediaSession session = new MediaSession(mContext, "test"); 324 return new Notification.Builder(mContext, "color") 325 .setSmallIcon(com.android.internal.R.drawable.emergency_icon) 326 .setContentTitle("Title") 327 .setContentText("Text") 328 .setStyle(new Notification.MediaStyle().setMediaSession(session.getSessionToken())); 329 } 330 331 /** 332 * Writes an arbitrary {@link Parcelable} into a {@link Parcel} using its writeToParcel 333 * method before reading it out again to check that it was sent properly. 334 */ 335 private static <T extends Parcelable> T writeAndReadParcelable(T original) { 336 Parcel p = Parcel.obtain(); 337 p.writeParcelable(original, /* flags */ 0); 338 p.setDataPosition(0); 339 return p.readParcelable(/* classLoader */ null); 340 } 341 342 /** 343 * Creates a Notification.Action by mocking initial dependencies and then applying 344 * transformations if they're defined. 345 */ 346 private Notification.Action makeNotificationAction( 347 @Nullable Consumer<Notification.Action.Builder> transformation) { 348 Notification.Action.Builder actionBuilder = 349 new Notification.Action.Builder(null, "Test Title", null); 350 if (transformation != null) { 351 transformation.accept(actionBuilder); 352 } 353 return actionBuilder.build(); 354 } 355 } 356