1 /*
<lambda>null2  * Copyright (C) 2020 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 package android.app.notification.current.cts
17 
18 import android.R
19 import android.app.Notification
20 import android.app.PendingIntent
21 import android.app.Person
22 import android.app.cts.CtsAppTestUtils.platformNull
23 import android.app.cts.NotificationTemplateTestBase
24 import android.content.Intent
25 import android.content.pm.PackageManager
26 import android.graphics.Bitmap
27 import android.graphics.Canvas
28 import android.graphics.Color
29 import android.graphics.drawable.Icon
30 import android.net.Uri
31 import android.util.Log
32 import android.view.View
33 import android.widget.ImageView
34 import android.widget.TextView
35 import androidx.annotation.ColorInt
36 import androidx.test.filters.SmallTest
37 import androidx.test.runner.AndroidJUnit4
38 import com.android.compatibility.common.util.CddTest
39 import com.google.common.truth.Truth.assertThat
40 import kotlin.test.assertFailsWith
41 import org.junit.Before
42 import org.junit.Test
43 import org.junit.runner.RunWith
44 
45 @RunWith(AndroidJUnit4::class)
46 class NotificationTemplateTest : NotificationTemplateTestBase() {
47 
48     @Before
49     public fun setUp() {
50         assertThat(context.applicationInfo.targetSdkVersion).isGreaterThan(30)
51     }
52 
53     @Test
54     fun testWideIcon_inCollapsedState_cappedTo16By9() {
55         val icon = createBitmap(200, 100)
56         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
57                 .setSmallIcon(R.drawable.ic_media_play)
58                 .setContentTitle("Title")
59                 .setLargeIcon(icon)
60                 .createContentView()
61         checkIconView(views) { iconView ->
62             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
63             assertThat(iconView.width.toFloat())
64                     .isWithin(1f)
65                     .of((iconView.height * 16 / 9).toFloat())
66         }
67     }
68 
69     @Test
70     fun testWideIcon_inCollapsedState_canShowExact4By3() {
71         val icon = createBitmap(40, 30)
72         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
73                 .setSmallIcon(R.drawable.ic_media_play)
74                 .setContentTitle("Title")
75                 .setLargeIcon(icon)
76                 .createContentView()
77         checkIconView(views) { iconView ->
78             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
79             assertThat(iconView.width.toFloat())
80                     .isWithin(1f)
81                     .of((iconView.height * 4 / 3).toFloat())
82         }
83     }
84 
85     @Test
86     fun testWideIcon_inCollapsedState_canShowUriIcon() {
87         val uri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
88         val icon = Icon.createWithContentUri(uri)
89         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
90                 .setSmallIcon(R.drawable.ic_media_play)
91                 .setContentTitle("Title")
92                 .setLargeIcon(icon)
93                 .createContentView()
94         checkIconView(views) { iconView ->
95             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
96             assertThat(iconView.width.toFloat())
97                     .isWithin(1f)
98                     .of((iconView.height * 4 / 3).toFloat())
99         }
100     }
101 
102     @Test
103     fun testWideIcon_inCollapsedState_neverNarrowerThanSquare() {
104         val icon = createBitmap(200, 300)
105         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
106                 .setSmallIcon(R.drawable.ic_media_play)
107                 .setContentTitle("Title")
108                 .setLargeIcon(icon)
109                 .createContentView()
110         checkIconView(views) { iconView ->
111             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
112             assertThat(iconView.width).isEqualTo(iconView.height)
113         }
114     }
115 
116     @Test
117     fun testWideIcon_inBigBaseState_cappedTo16By9() {
118         val icon = createBitmap(200, 100)
119         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
120                 .setSmallIcon(R.drawable.ic_media_play)
121                 .setContentTitle("Title")
122                 .setLargeIcon(icon)
123                 .createBigContentView()
124         checkIconView(views) { iconView ->
125             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
126             assertThat(iconView.width.toFloat())
127                     .isWithin(1f)
128                     .of((iconView.height * 16 / 9).toFloat())
129         }
130     }
131 
132     @Test
133     fun testWideIcon_inBigBaseState_canShowExact4By3() {
134         val icon = createBitmap(40, 30)
135         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
136                 .setSmallIcon(R.drawable.ic_media_play)
137                 .setContentTitle("Title")
138                 .setLargeIcon(icon)
139                 .createBigContentView()
140         checkIconView(views) { iconView ->
141             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
142             assertThat(iconView.width.toFloat())
143                     .isWithin(1f)
144                     .of((iconView.height * 4 / 3).toFloat())
145         }
146     }
147 
148     @Test
149     fun testWideIcon_inBigBaseState_neverNarrowerThanSquare() {
150         val icon = createBitmap(200, 300)
151         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
152                 .setSmallIcon(R.drawable.ic_media_play)
153                 .setContentTitle("Title")
154                 .setLargeIcon(icon)
155                 .createBigContentView()
156         checkIconView(views) { iconView ->
157             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
158             assertThat(iconView.width).isEqualTo(iconView.height)
159         }
160     }
161 
162     @Test
163     fun testWideIcon_inBigPicture_cappedTo16By9() {
164         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
165             return
166         }
167 
168         val picture = createBitmap(40, 30)
169         val icon = createBitmap(200, 100)
170         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
171                 .setSmallIcon(R.drawable.ic_media_play)
172                 .setContentTitle("Title")
173                 .setLargeIcon(icon)
174                 .setStyle(Notification.BigPictureStyle().bigPicture(picture))
175                 .createBigContentView()
176         checkIconView(views) { iconView ->
177             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
178             assertThat(iconView.width.toFloat())
179                     .isWithin(1f)
180                     .of((iconView.height * 16 / 9).toFloat())
181         }
182     }
183 
184     @Test
185     fun testWideIcon_inBigPicture_canShowExact4By3() {
186         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
187             return
188         }
189 
190         val picture = createBitmap(40, 30)
191         val icon = createBitmap(40, 30)
192         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
193                 .setSmallIcon(R.drawable.ic_media_play)
194                 .setContentTitle("Title")
195                 .setLargeIcon(icon)
196                 .setStyle(Notification.BigPictureStyle().bigPicture(picture))
197                 .createBigContentView()
198         checkIconView(views) { iconView ->
199             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
200             assertThat(iconView.width.toFloat())
201                     .isWithin(1f)
202                     .of((iconView.height * 4 / 3).toFloat())
203         }
204     }
205 
206     @Test
207     fun testWideIcon_inBigPicture_neverNarrowerThanSquare() {
208         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
209             return
210         }
211 
212         val picture = createBitmap(40, 30)
213         val icon = createBitmap(200, 300)
214         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
215                 .setSmallIcon(R.drawable.ic_media_play)
216                 .setContentTitle("Title")
217                 .setLargeIcon(icon)
218                 .setStyle(Notification.BigPictureStyle().bigPicture(picture))
219                 .createBigContentView()
220         checkIconView(views) { iconView ->
221             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
222             assertThat(iconView.width).isEqualTo(iconView.height)
223         }
224     }
225 
226     @Test
227     fun testWideIcon_inBigText_cappedTo16By9() {
228         val icon = createBitmap(200, 100)
229         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
230                 .setSmallIcon(R.drawable.ic_media_play)
231                 .setContentTitle("Title")
232                 .setLargeIcon(icon)
233                 .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
234                 .createBigContentView()
235         checkIconView(views) { iconView ->
236             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
237             assertThat(iconView.width.toFloat())
238                     .isWithin(1f)
239                     .of((iconView.height * 16 / 9).toFloat())
240         }
241     }
242 
243     @Test
244     fun testWideIcon_inBigText_canShowExact4By3() {
245         val icon = createBitmap(40, 30)
246         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
247                 .setSmallIcon(R.drawable.ic_media_play)
248                 .setContentTitle("Title")
249                 .setLargeIcon(icon)
250                 .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
251                 .createBigContentView()
252         checkIconView(views) { iconView ->
253             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
254             assertThat(iconView.width.toFloat())
255                     .isWithin(1f)
256                     .of((iconView.height * 4 / 3).toFloat())
257         }
258     }
259 
260     @Test
261     fun testWideIcon_inBigText_neverNarrowerThanSquare() {
262         val icon = createBitmap(200, 300)
263         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
264                 .setSmallIcon(R.drawable.ic_media_play)
265                 .setContentTitle("Title")
266                 .setLargeIcon(icon)
267                 .setStyle(Notification.BigTextStyle().bigText("Big\nText\nContent"))
268                 .createBigContentView()
269         checkIconView(views) { iconView ->
270             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
271             assertThat(iconView.width).isEqualTo(iconView.height)
272         }
273     }
274 
275     @Test
276     fun testBigPictureStyle_populatesExtrasCompatibly() {
277         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
278             return
279         }
280 
281         val bitmap = createBitmap(40, 30)
282         val uri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
283         val iconWithUri = Icon.createWithContentUri(uri)
284         val iconWithBitmap = Icon.createWithBitmap(bitmap)
285         val style = Notification.BigPictureStyle()
286         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
287                 .setSmallIcon(R.drawable.ic_media_play)
288                 .setContentTitle("Title")
289                 .setStyle(style)
290 
291         style.bigPicture(bitmap)
292         builder.build().let {
293             assertThat(
294                 it.extras.getParcelable<Bitmap>(Notification.EXTRA_PICTURE)!!.sameAs(bitmap)
295             ).isTrue()
296             assertThat(it.extras.get(Notification.EXTRA_PICTURE_ICON)).isNull()
297         }
298 
299         style.bigPicture(iconWithUri)
300         builder.build().let {
301             assertThat(it.extras.get(Notification.EXTRA_PICTURE)).isNull()
302             assertThat(it.extras.getParcelable<Icon>(Notification.EXTRA_PICTURE_ICON))
303                     .isSameInstanceAs(iconWithUri)
304         }
305 
306         style.bigPicture(iconWithBitmap)
307         builder.build().let {
308             assertThat(
309                 it.extras.getParcelable<Bitmap>(Notification.EXTRA_PICTURE)!!.sameAs(bitmap)
310             ).isTrue()
311             assertThat(it.extras.get(Notification.EXTRA_PICTURE_ICON)).isNull()
312         }
313     }
314 
315     @Test
316     @CddTest(requirement = "3.8.3.1/C-2-1")
317     fun testBigPictureStyle_bigPictureUriIcon() {
318         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
319             return
320         }
321 
322         val pictureUri = Uri.parse("content://android.app.stubs.assets/picture_400_by_300.png")
323         val pictureIcon = Icon.createWithContentUri(pictureUri)
324         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
325                 .setSmallIcon(R.drawable.ic_media_play)
326                 .setContentTitle("Title")
327                 .setStyle(Notification.BigPictureStyle().bigPicture(pictureIcon))
328         checkViews(builder.createBigContentView()) {
329             val pictureView = requireViewByIdName<ImageView>("big_picture")
330             assertThat(pictureView.visibility).isEqualTo(View.VISIBLE)
331             assertThat(pictureView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
332         }
333     }
334 
335     @Test
336     @CddTest(requirement = "3.8.3.1/C-2-1")
337     fun testPromoteBigPicture_withBigPictureUriIcon() {
338         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
339             return
340         }
341 
342         val pictureUri = Uri.parse("content://android.app.stubs.assets/picture_800_by_600.png")
343         val pictureIcon = Icon.createWithContentUri(pictureUri)
344         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
345                 .setSmallIcon(R.drawable.ic_media_play)
346                 .setContentTitle("Title")
347                 .setStyle(
348                     Notification.BigPictureStyle()
349                         .bigPicture(pictureIcon)
350                         .showBigPictureWhenCollapsed(true)
351                 )
352         checkIconView(builder.createContentView()) { iconView ->
353             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
354             assertThat(iconView.width.toFloat())
355                     .isWithin(1f)
356                     .of((iconView.height * 4 / 3).toFloat())
357             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
358         }
359     }
360 
361     @Test
362     fun testPromoteBigPicture_withoutLargeIcon() {
363         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
364             return
365         }
366 
367         val picture = createBitmap(40, 30)
368         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
369                 .setSmallIcon(R.drawable.ic_media_play)
370                 .setContentTitle("Title")
371                 .setStyle(
372                     Notification.BigPictureStyle()
373                         .bigPicture(picture)
374                         .showBigPictureWhenCollapsed(true)
375                 )
376         checkIconView(builder.createContentView()) { iconView ->
377             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
378             assertThat(iconView.width.toFloat())
379                     .isWithin(1f)
380                     .of((iconView.height * 4 / 3).toFloat())
381             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
382         }
383         checkIconView(builder.createBigContentView()) { iconView ->
384             assertThat(iconView.visibility).isEqualTo(View.GONE)
385         }
386     }
387 
388     @Test
389     fun testPromoteBigPicture_withLargeIcon() {
390         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
391             return
392         }
393 
394         val picture = createBitmap(40, 30)
395         val icon = createBitmap(80, 65)
396         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
397                 .setSmallIcon(R.drawable.ic_media_play)
398                 .setContentTitle("Title")
399                 .setLargeIcon(icon)
400                 .setStyle(
401                     Notification.BigPictureStyle()
402                         .bigPicture(picture)
403                         .showBigPictureWhenCollapsed(true)
404                 )
405 
406         checkIconView(builder.createContentView()) { iconView ->
407             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
408             assertThat(iconView.width.toFloat())
409                     .isWithin(1f)
410                     .of((iconView.height * 4 / 3).toFloat())
411             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
412         }
413         checkIconView(builder.createBigContentView()) { iconView ->
414             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
415             assertThat(iconView.width.toFloat())
416                     .isWithin(1f)
417                     .of((iconView.height * 80 / 65).toFloat())
418             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
419         }
420     }
421 
422     @Test
423     @CddTest(requirement = "3.8.3.1/C-2-1")
424     fun testPromoteBigPicture_withBigLargeIcon() {
425         if (skipIfPlatformDoesNotSupportNotificationStyles()) {
426             return
427         }
428 
429         val picture = createBitmap(40, 30)
430         val inputWidth = 400
431         val inputHeight = 300
432         val bigIcon = createBitmap(inputWidth, inputHeight)
433         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
434                 .setSmallIcon(R.drawable.ic_media_play)
435                 .setContentTitle("Title")
436                 .setStyle(
437                     Notification.BigPictureStyle()
438                         .bigPicture(picture)
439                         .bigLargeIcon(bigIcon)
440                         .showBigPictureWhenCollapsed(true)
441                 )
442 
443         checkIconView(builder.createContentView()) { iconView ->
444             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
445             assertThat(iconView.width.toFloat())
446                     .isWithin(1f)
447                     .of((iconView.height * 4 / 3).toFloat())
448             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
449         }
450         checkIconView(builder.createBigContentView()) { iconView ->
451             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
452             assertThat(iconView.width.toFloat())
453                     .isWithin(1f)
454                     .of((iconView.height * 4 / 3).toFloat())
455             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
456         }
457         assertThat(
458             builder.build().extras.getParcelable<Bitmap>(
459                 Notification.EXTRA_PICTURE
460             )!!.sameAs(picture)
461         ).isTrue()
462     }
463 
464     @Test
465     @CddTest(requirement = "3.8.3.1/C-2-1")
466     fun testBigPicture_withBigLargeIcon_withContentUri() {
467     if (skipIfPlatformDoesNotSupportNotificationStyles()) {
468             return
469         }
470 
471         val iconUri = Uri.parse("content://android.app.stubs.assets/picture_800_by_600.png")
472         val icon = Icon.createWithContentUri(iconUri)
473         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
474                 .setSmallIcon(R.drawable.ic_media_play)
475                 .setContentTitle("Title")
476                 .setStyle(Notification.BigPictureStyle().bigLargeIcon(icon))
477         checkIconView(builder.createBigContentView()) { iconView ->
478             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
479             assertThat(iconView.width.toFloat())
480                     .isWithin(1f)
481                     .of((iconView.height * 4 / 3).toFloat())
482             assertThat(iconView.scaleType).isEqualTo(ImageView.ScaleType.CENTER_CROP)
483         }
484     }
485 
486     @Test
487     @SmallTest
488     fun testBaseTemplate_hasExpandedStateWithoutActions() {
489         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
490                 .setSmallIcon(R.drawable.ic_media_play)
491                 .setContentTitle("Title")
492                 .createBigContentView()
493         assertThat(views).isNotNull()
494     }
495 
496     @Test
497     fun testDecoratedCustomViewStyle_collapsedState() {
498         val customContent = makeCustomContent()
499         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
500                 .setSmallIcon(R.drawable.ic_media_play)
501                 .setContentTitle("Title")
502                 .setCustomContentView(customContent)
503                 .setStyle(Notification.DecoratedCustomViewStyle())
504                 .createContentView()
505         checkViews(views) {
506             // first check that the custom view is actually shown
507             val customTextView = requireViewByIdName<TextView>("text1")
508             assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
509             assertThat(customTextView.text).isEqualTo("Example Text")
510 
511             // check that the icon shows
512             val iconView = requireViewByIdName<ImageView>("icon")
513             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
514         }
515     }
516 
517     @Test
518     fun testDecoratedCustomViewStyle_expandedState() {
519         val customContent = makeCustomContent()
520         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
521                 .setSmallIcon(R.drawable.ic_media_play)
522                 .setContentTitle("Title")
523                 .setCustomBigContentView(customContent)
524                 .setStyle(Notification.DecoratedCustomViewStyle())
525                 .createBigContentView()
526         checkViews(views) {
527             // first check that the custom view is actually shown
528             val customTextView = requireViewByIdName<TextView>("text1")
529             assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
530             assertThat(customTextView.text).isEqualTo("Example Text")
531 
532             // check that the app name text shows
533             val appNameView = requireViewByIdName<TextView>("app_name_text")
534             assertThat(appNameView.visibility).isEqualTo(View.VISIBLE)
535 
536             // check that the icon shows
537             val iconView = requireViewByIdName<ImageView>("icon")
538             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
539         }
540     }
541 
542     @Test
543     fun testCustomViewNotification_collapsedState_isDecorated() {
544         val customContent = makeCustomContent()
545         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
546                 .setSmallIcon(R.drawable.ic_media_play)
547                 .setContentTitle("Title")
548                 .setCustomContentView(customContent)
549                 .createContentView()
550         checkViews(views) {
551             // first check that the custom view is actually shown
552             val customTextView = requireViewByIdName<TextView>("text1")
553             assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
554 
555             assertThat(customTextView.text).isEqualTo("Example Text")
556 
557             // check that the icon shows
558             val iconView = requireViewByIdName<ImageView>("icon")
559             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
560         }
561     }
562 
563     @Test
564     fun testCustomViewNotification_expandedState_isDecorated() {
565         val customContent = makeCustomContent()
566         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
567                 .setSmallIcon(R.drawable.ic_media_play)
568                 .setContentTitle("Title")
569                 .setCustomBigContentView(customContent)
570                 .createBigContentView()
571         checkViews(views) {
572             // first check that the custom view is actually shown
573             val customTextView = requireViewByIdName<TextView>("text1")
574             assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
575             assertThat(customTextView.text).isEqualTo("Example Text")
576 
577             // check that the app name text shows
578             val appNameView = requireViewByIdName<TextView>("app_name_text")
579             assertThat(appNameView.visibility).isEqualTo(View.VISIBLE)
580 
581             // check that the icon shows
582             val iconView = requireViewByIdName<ImageView>("icon")
583             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
584         }
585     }
586 
587     @Test
588     fun testCustomViewNotification_headsUpState_isDecorated() {
589         val customContent = makeCustomContent()
590         val views = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
591                 .setSmallIcon(R.drawable.ic_media_play)
592                 .setContentTitle("Title")
593                 .setCustomHeadsUpContentView(customContent)
594                 .createHeadsUpContentView()
595         checkViews(views) {
596             // first check that the custom view is actually shown
597             val customTextView = requireViewByIdName<TextView>("text1")
598             assertThat(customTextView.visibility).isEqualTo(View.VISIBLE)
599             assertThat(customTextView.text).isEqualTo("Example Text")
600 
601             // check that the icon shows
602             val iconView = requireViewByIdName<ImageView>("icon")
603             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
604         }
605     }
606 
607     @Test
608     @SmallTest
609     fun testCallStyle_forIncomingCall_validatesArguments() {
610         val namedPerson = Person.Builder().setName("Named Person").build()
611         val namelessPerson = Person.Builder().setName("").build()
612         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
613             Notification.CallStyle.forIncomingCall(platformNull(), pendingIntent, pendingIntent)
614         }
615         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
616             Notification.CallStyle.forIncomingCall(namelessPerson, pendingIntent, pendingIntent)
617         }
618         assertFailsWith(NullPointerException::class, "declineIntent is required") {
619             Notification.CallStyle.forIncomingCall(namedPerson, platformNull(), pendingIntent)
620         }
621         assertFailsWith(NullPointerException::class, "answerIntent is required") {
622             Notification.CallStyle.forIncomingCall(namedPerson, pendingIntent, platformNull())
623         }
624         Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
625                 .setStyle(
626                     Notification.CallStyle
627                         .forIncomingCall(namedPerson, pendingIntent, pendingIntent)
628                 )
629                 .build()
630     }
631 
632     @Test
633     fun testCallStyle_forIncomingCall_hasCorrectActions() {
634         val namedPerson = Person.Builder().setName("Named Person").build()
635         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
636                 .setSmallIcon(R.drawable.ic_media_play)
637                 .setStyle(
638                     Notification.CallStyle
639                         .forIncomingCall(namedPerson, pendingIntent, pendingIntent)
640                 )
641         assertThat(builder.build()).isNotNull()
642         val answerText = context.getString(getAndroidRString("call_notification_answer_action"))
643         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
644         val hangUpText = context.getString(getAndroidRString("call_notification_hang_up_action"))
645         val views = builder.createBigContentView()
646         checkViews(views) {
647             assertThat(requireViewWithTextContaining(answerText).visibility).isEqualTo(View.VISIBLE)
648             assertThat(
649                 requireViewWithTextContaining(declineText).visibility
650             ).isEqualTo(View.VISIBLE)
651             assertThat(findViewWithTextContaining(hangUpText)).isNull()
652         }
653     }
654 
655     @Test
656     fun testCallStyle_forIncomingCall_isVideo_hasCorrectActions() {
657         val namedPerson = Person.Builder().setName("Named Person").build()
658         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
659                 .setSmallIcon(R.drawable.ic_media_play)
660                 .setStyle(
661                     Notification.CallStyle
662                         .forIncomingCall(namedPerson, pendingIntent, pendingIntent)
663                         .setIsVideo(true)
664                 )
665         val notification = builder.build()
666         assertThat(notification).isNotNull()
667         assertThat(notification.extras.getBoolean(Notification.EXTRA_CALL_IS_VIDEO)).isTrue()
668         val answerText = context.getString(
669                 getAndroidRString("call_notification_answer_video_action")
670         )
671         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
672         val hangUpText = context.getString(getAndroidRString("call_notification_hang_up_action"))
673         val views = builder.createBigContentView()
674         checkViews(views) {
675             assertThat(requireViewWithTextContaining(answerText).visibility).isEqualTo(View.VISIBLE)
676             assertThat(
677                 requireViewWithTextContaining(declineText).visibility
678             ).isEqualTo(View.VISIBLE)
679             assertThat(findViewWithTextContaining(hangUpText)).isNull()
680         }
681     }
682 
683     @Test
684     @SmallTest
685     fun testCallStyle_forOngoingCall_validatesArguments() {
686         val namedPerson = Person.Builder().setName("Named Person").build()
687         val namelessPerson = Person.Builder().setName("").build()
688         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
689             Notification.CallStyle.forOngoingCall(platformNull(), pendingIntent)
690         }
691         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
692             Notification.CallStyle.forOngoingCall(namelessPerson, pendingIntent)
693         }
694         assertFailsWith(NullPointerException::class, "hangUpIntent is required") {
695             Notification.CallStyle.forOngoingCall(namedPerson, platformNull())
696         }
697         Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
698                 .setStyle(Notification.CallStyle.forOngoingCall(namedPerson, pendingIntent))
699                 .build()
700     }
701 
702     @Test
703     fun testCallStyle_forOngoingCall_hasCorrectActions() {
704         val namedPerson = Person.Builder().setName("Named Person").build()
705         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
706                 .setSmallIcon(R.drawable.ic_media_play)
707                 .setStyle(Notification.CallStyle.forOngoingCall(namedPerson, pendingIntent))
708         assertThat(builder.build()).isNotNull()
709         val answerText = context.getString(getAndroidRString("call_notification_answer_action"))
710         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
711         val hangUpText = context.getString(getAndroidRString("call_notification_hang_up_action"))
712         val views = builder.createBigContentView()
713         checkViews(views) {
714             assertThat(findViewWithTextContaining(answerText)).isNull()
715             assertThat(findViewWithTextContaining(declineText)).isNull()
716             assertThat(requireViewWithTextContaining(hangUpText).visibility).isEqualTo(View.VISIBLE)
717         }
718     }
719 
720     @Test
721     @SmallTest
722     fun testCallStyle_forScreeningCall_validatesArguments() {
723         val namedPerson = Person.Builder().setName("Named Person").build()
724         val namelessPerson = Person.Builder().setName("").build()
725         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
726             Notification.CallStyle.forScreeningCall(platformNull(), pendingIntent, pendingIntent)
727         }
728         assertFailsWith(IllegalArgumentException::class, "person must have a non-empty a name") {
729             Notification.CallStyle.forScreeningCall(namelessPerson, pendingIntent, pendingIntent)
730         }
731         assertFailsWith(NullPointerException::class, "hangUpIntent is required") {
732             Notification.CallStyle.forScreeningCall(namedPerson, platformNull(), pendingIntent)
733         }
734         assertFailsWith(NullPointerException::class, "answerIntent is required") {
735             Notification.CallStyle.forScreeningCall(namedPerson, pendingIntent, platformNull())
736         }
737         Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
738                 .setStyle(
739                     Notification.CallStyle
740                         .forScreeningCall(namedPerson, pendingIntent, pendingIntent)
741                 )
742                 .build()
743     }
744 
745     @Test
746     fun testCallStyle_forScreeningCall_hasCorrectActions() {
747         val namedPerson = Person.Builder().setName("Named Person").build()
748         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
749                 .setSmallIcon(R.drawable.ic_media_play)
750                 .setStyle(
751                     Notification.CallStyle
752                         .forScreeningCall(namedPerson, pendingIntent, pendingIntent)
753                 )
754         assertThat(builder.build()).isNotNull()
755         val answerText = context.getString(getAndroidRString("call_notification_answer_action"))
756         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
757         val hangUpText = context.getString(getAndroidRString("call_notification_hang_up_action"))
758         val views = builder.createBigContentView()
759         checkViews(views) {
760             assertThat(requireViewWithTextContaining(answerText).visibility).isEqualTo(View.VISIBLE)
761             assertThat(findViewWithTextContaining(declineText)).isNull()
762             assertThat(requireViewWithTextContaining(hangUpText).visibility).isEqualTo(View.VISIBLE)
763         }
764     }
765 
766     @Test
767     fun testCallStyle_hidesVerification_whenNotProvided() {
768         val person = Person.Builder().setName("Person").build()
769         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
770                 .setSmallIcon(R.drawable.ic_media_play)
771                 .setStyle(
772                     Notification.CallStyle
773                         .forIncomingCall(person, pendingIntent, pendingIntent)
774                 )
775 
776         val notification = builder.build()
777         val extras = notification.extras
778         assertThat(extras.containsKey(Notification.EXTRA_VERIFICATION_TEXT)).isFalse()
779         assertThat(extras.containsKey(Notification.EXTRA_VERIFICATION_ICON)).isFalse()
780 
781         val views = builder.createBigContentView()
782         checkViews(views) {
783             val textView = requireViewByIdName<TextView>("verification_text")
784             assertThat(textView.visibility).isEqualTo(View.GONE)
785 
786             val iconView = requireViewByIdName<ImageView>("verification_icon")
787             assertThat(iconView.visibility).isEqualTo(View.GONE)
788         }
789     }
790 
791     @Test
792     fun testCallStyle_showsVerification_whenProvided() {
793         val person = Person.Builder().setName("Person").build()
794         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
795                 .setSmallIcon(R.drawable.ic_media_play)
796                 .setStyle(
797                     Notification.CallStyle
798                         .forIncomingCall(person, pendingIntent, pendingIntent)
799                         .setVerificationIcon(Icon.createWithResource(context, R.drawable.ic_info))
800                         .setVerificationText("Verified!")
801                 )
802 
803         val notification = builder.build()
804         val extras = notification.extras
805         assertThat(extras.getCharSequence(Notification.EXTRA_VERIFICATION_TEXT))
806                 .isEqualTo("Verified!")
807         assertThat(extras.getParcelable<Icon>(Notification.EXTRA_VERIFICATION_ICON)?.resId)
808                 .isEqualTo(R.drawable.ic_info)
809 
810         val views = builder.createBigContentView()
811         checkViews(views) {
812             val textView = requireViewByIdName<TextView>("verification_text")
813             assertThat(textView.visibility).isEqualTo(View.VISIBLE)
814             assertThat(textView.text).isEqualTo("Verified!")
815 
816             val iconView = requireViewByIdName<ImageView>("verification_icon")
817             assertThat(iconView.visibility).isEqualTo(View.VISIBLE)
818         }
819     }
820 
821     @Test
822     fun testCallStyle_ignoresCustomColors_whenNotColorized() {
823         if (!context.resources.getBoolean(getAndroidRBool(
824                         "config_callNotificationActionColorsRequireColorized"
825             ))) {
826             Log.i(
827                 TAG,
828                 "Skipping: testCallStyle_ignoresCustomColors_whenNotColorized" +
829                     " - Test will not run when config disabled."
830             )
831             return
832         }
833         val person = Person.Builder().setName("Person").build()
834         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
835                 .setSmallIcon(R.drawable.ic_media_play)
836                 .setColor(Color.WHITE)
837                 .setStyle(
838                     Notification.CallStyle
839                         .forIncomingCall(person, pendingIntent, pendingIntent)
840                         .setAnswerButtonColorHint(Color.BLUE)
841                         .setDeclineButtonColorHint(Color.MAGENTA)
842                 )
843 
844         val notification = builder.build()
845         assertThat(notification.extras.getInt(Notification.EXTRA_ANSWER_COLOR, -1))
846                 .isEqualTo(Color.BLUE)
847         assertThat(notification.extras.getInt(Notification.EXTRA_DECLINE_COLOR, -1))
848                 .isEqualTo(Color.MAGENTA)
849 
850         val answerText = context.getString(getAndroidRString("call_notification_answer_action"))
851         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
852         val views = builder.createBigContentView()
853         checkViews(views) {
854             assertThat(
855                 requireViewWithTextContaining(answerText).bgContainsColor(Color.BLUE)
856             ).isFalse()
857             assertThat(
858                 requireViewWithTextContaining(declineText).bgContainsColor(Color.MAGENTA)
859             ).isFalse()
860         }
861     }
862 
863     @Test
864     fun testCallStyle_usesCustomColors_whenColorized() {
865         val person = Person.Builder().setName("Person").build()
866         val builder = Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
867                 .setSmallIcon(R.drawable.ic_media_play)
868                 .setColorized(true)
869                 .setColor(Color.WHITE)
870                 .setStyle(
871                     Notification.CallStyle
872                         .forIncomingCall(person, pendingIntent, pendingIntent)
873                         .setAnswerButtonColorHint(Color.BLUE)
874                         .setDeclineButtonColorHint(Color.MAGENTA)
875                 )
876 
877         val notification = builder.build()
878         assertThat(notification.extras.getInt(Notification.EXTRA_ANSWER_COLOR, -1))
879                 .isEqualTo(Color.BLUE)
880         assertThat(notification.extras.getInt(Notification.EXTRA_DECLINE_COLOR, -1))
881                 .isEqualTo(Color.MAGENTA)
882 
883         // Setting this flag ensures that createBigContentView allows colorization.
884         notification.flags = notification.flags or Notification.FLAG_FOREGROUND_SERVICE
885         val answerText = context.getString(getAndroidRString("call_notification_answer_action"))
886         val declineText = context.getString(getAndroidRString("call_notification_decline_action"))
887         val views = builder.createBigContentView()
888         checkViews(views) {
889             // TODO(b/184896890): diagnose/fix flaky bgContainsColor method
890             assertThat(
891                 requireViewWithTextContaining(answerText).bgContainsColor(Color.BLUE)
892             ) // .isTrue()
893             assertThat(
894                 requireViewWithTextContaining(declineText).bgContainsColor(Color.MAGENTA)
895             ) // .isTrue()
896         }
897     }
898 
899     private fun View.bgContainsColor(@ColorInt color: Int): Boolean {
900         val background = background ?: return false
901         val bitmap = createBitmap(width, height)
902         val canvas = Canvas(bitmap)
903         background.draw(canvas)
904         val maskedColor = color and 0x00ffffff
905         for (x in 0 until bitmap.width) {
906             for (y in 0 until bitmap.height) {
907                 if (bitmap.getPixel(x, y) and 0x00ffffff == maskedColor) {
908                     return true
909                 }
910             }
911         }
912         return false
913     }
914 
915     private val pendingIntent by lazy {
916         PendingIntent.getBroadcast(context, 0, Intent("test"), PendingIntent.FLAG_IMMUTABLE)
917     }
918 
919     /**
920      * Assume that we're running on the platform that supports styled notifications.
921      *
922      * If the current platform does not support notification styles, skip this test without failure.
923      */
924     private fun skipIfPlatformDoesNotSupportNotificationStyles(): Boolean {
925         return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) ||
926                         context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
927     }
928 
929     companion object {
930         val TAG = NotificationTemplateTest::class.java.simpleName
931         const val NOTIFICATION_CHANNEL_ID = "NotificationTemplateTest"
932     }
933 }
934