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