1 /*
2  * Copyright (C) 2023 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.Manifest.permission.POST_NOTIFICATIONS
19 import android.Manifest.permission.RECEIVE_SENSITIVE_NOTIFICATIONS
20 import android.app.AppOpsManager
21 import android.app.Notification
22 import android.app.Notification.CATEGORY_MESSAGE
23 import android.app.Notification.EXTRA_MESSAGES
24 import android.app.Notification.EXTRA_SUB_TEXT
25 import android.app.Notification.EXTRA_TEXT
26 import android.app.Notification.EXTRA_TEXT_LINES
27 import android.app.Notification.EXTRA_TITLE
28 import android.app.Notification.InboxStyle
29 import android.app.Notification.MessagingStyle
30 import android.app.Notification.MessagingStyle.Message
31 import android.app.NotificationManager
32 import android.app.PendingIntent
33 import android.app.Person
34 import android.app.stubs.R
35 import android.app.stubs.shared.NotificationHelper.SEARCH_TYPE
36 import android.companion.CompanionDeviceManager
37 import android.content.Intent
38 import android.content.pm.ApplicationInfo
39 import android.content.pm.PackageManager
40 import android.graphics.drawable.Icon
41 import android.net.MacAddress
42 import android.os.Bundle
43 import android.os.Parcelable
44 import android.os.Process
45 import android.permission.cts.PermissionUtils
46 import android.platform.test.annotations.RequiresFlagsDisabled
47 import android.platform.test.annotations.RequiresFlagsEnabled
48 import android.platform.test.flag.junit.DeviceFlagsValueProvider
49 import android.service.notification.Adjustment
50 import android.service.notification.Adjustment.KEY_IMPORTANCE
51 import android.service.notification.Adjustment.KEY_RANKING_SCORE
52 import android.service.notification.Flags
53 import android.service.notification.NotificationListenerService
54 import android.service.notification.StatusBarNotification
55 import androidx.test.runner.AndroidJUnit4
56 import com.android.compatibility.common.util.CddTest
57 import com.android.compatibility.common.util.SystemUtil.runShellCommand
58 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
59 import com.google.common.truth.Truth.assertWithMessage
60 import org.junit.Assert
61 import org.junit.Assert.assertEquals
62 import org.junit.Assert.assertTrue
63 import org.junit.Assume.assumeFalse
64 import org.junit.Before
65 import org.junit.Rule
66 import org.junit.Test
67 import org.junit.runner.RunWith
68 
69 // TODO: b/301960090: Add tests with real NAS
70 /**
71  * These tests ensure that untrusted notification listeners get a redacted version of notifications,
72  * if said notifications have sensitive content.
73  */
74 @RunWith(AndroidJUnit4::class)
75 class SensitiveNotificationRedactionTest : BaseNotificationManagerTest() {
76     private val groupKey = "SensitiveNotificationRedactionTest begun at " +
77             System.currentTimeMillis()
78 
79     @JvmField
80     @Rule
81     val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()!!
82 
83     @Before
84     @Throws(Exception::class)
setUpnull85     fun setUp() {
86         PermissionUtils.grantPermission(STUB_PACKAGE_NAME, POST_NOTIFICATIONS)
87 
88         setUpNotifListener()
89         mAssistant = mNotificationHelper.enableAssistant(mContext.packageName)
90         mAssistant.mMarkSensitiveContent = true
91         mAssistant.mSmartReplies =
92             ArrayList<CharSequence>(listOf(OTP_MESSAGE_BASIC as CharSequence))
93         mAssistant.mSmartActions = ArrayList<Notification.Action>(listOf(createAction()))
94     }
95 
sendNotificationnull96     fun sendNotification(
97         text: String = OTP_MESSAGE_BASIC,
98         title: String = OTP_MESSAGE_BASIC,
99         subtext: String = OTP_MESSAGE_BASIC,
100         category: String = CATEGORY_MESSAGE,
101         actions: List<Notification.Action>? = null,
102         style: Notification.Style? = null,
103         extras: Bundle? = null,
104         tag: String = groupKey
105     ) {
106         val intent = Intent(Intent.ACTION_MAIN)
107         intent.setFlags(
108             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
109                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
110         )
111         intent.setAction(Intent.ACTION_MAIN)
112         intent.setPackage(mContext.getPackageName())
113 
114         val nb = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
115         nb.setContentText(text)
116         nb.setContentTitle(title)
117         nb.setSubText(subtext)
118         nb.setCategory(category)
119         nb.setSmallIcon(R.drawable.black)
120         nb.setLargeIcon(Icon.createWithResource(mContext, R.drawable.black))
121         nb.setContentIntent(createTestPendingIntent())
122         nb.setGroup(groupKey)
123         if (actions != null) {
124             nb.setActions(*actions.toTypedArray())
125         }
126         if (style != null) {
127             nb.setStyle(style)
128         }
129         if (extras != null) {
130             nb.addExtras(extras)
131         }
132         mNotificationManager.notify(tag, NOTIFICATION_ID, nb.build())
133     }
134 
createTestPendingIntentnull135     private fun createTestPendingIntent(): PendingIntent {
136         val intent = Intent(Intent.ACTION_MAIN)
137         intent.setFlags(
138             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
139                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
140         )
141         intent.setAction(Intent.ACTION_MAIN)
142         intent.setPackage(mContext.getPackageName())
143 
144         return PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_MUTABLE)
145     }
146 
createActionnull147     private fun createAction(): Notification.Action {
148         val pendingIntent = createTestPendingIntent()
149         return Notification.Action.Builder(
150             Icon.createWithResource(mContext, R.drawable.black),
151             OTP_MESSAGE_BASIC,
152             pendingIntent
153         ).build()
154     }
155 
waitForNotificationnull156     private fun waitForNotification(
157         searchType: SEARCH_TYPE = SEARCH_TYPE.POSTED,
158         tag: String = groupKey
159     ): StatusBarNotification {
160         val sbn = mNotificationHelper.findPostedNotification(tag, NOTIFICATION_ID, searchType)
161         assertWithMessage("Expected to find a notification with tag $tag")
162                 .that(sbn).isNotNull()
163         return sbn!!
164     }
165 
166     @Test
167     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testTextFieldsRedactednull168     fun testTextFieldsRedacted() {
169         val style = InboxStyle()
170         style.addLine(OTP_MESSAGE_BASIC)
171 
172         sendNotification(style = style)
173         val sbn = waitForNotification()
174 
175         val title = sbn.notification.extras.getCharSequence(EXTRA_TITLE)!!
176         val aInfo: ApplicationInfo = mPackageManager
177                 .getApplicationInfo(mContext.packageName, 0)
178         val pkgLabel = aInfo.loadLabel(mPackageManager).toString()
179         assertWithMessage("Expected title to be $pkgLabel, but was $title")
180                 .that(title).isEqualTo(title)
181 
182         assertNotificationTextRedacted(sbn)
183 
184         val subtext = sbn.notification.extras.getCharSequence(EXTRA_SUB_TEXT)
185         assertWithMessage("Expected subtext to be null, but it was $subtext").that(subtext).isNull()
186 
187         val textLines = sbn.notification.extras.getCharSequenceArray(EXTRA_TEXT_LINES)
188         assertWithMessage("Expected text lines to be null, but it was ${textLines?.toList()}")
189                 .that(textLines).isNull()
190     }
191 
192     @Test
193     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testActionsRedactednull194     fun testActionsRedacted() {
195         val intent = Intent(Intent.ACTION_MAIN)
196         intent.setFlags(
197             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
198                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
199         )
200         intent.setAction(Intent.ACTION_MAIN)
201         intent.setPackage(mContext.getPackageName())
202 
203         val pendingIntent = PendingIntent.getActivity(
204             mContext,
205             0,
206             intent,
207             PendingIntent.FLAG_MUTABLE
208         )
209         sendNotification(actions = listOf(createAction()))
210         val sbn = waitForNotification()
211         val action = sbn.notification.actions.firstOrNull()
212         assertWithMessage("expected notification to have an action").that(action).isNotNull()
213         assertWithMessage("expected notification action title not to contain otp:${action!!.title}")
214                 .that(action.title.toString()).doesNotContain(OTP_CODE)
215     }
216 
217     @Test
218     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testMessagesRedactednull219     fun testMessagesRedacted() {
220         val empty = Person.Builder().setName(PERSON_NAME).build()
221         val message = Message(OTP_MESSAGE_BASIC, System.currentTimeMillis(), empty)
222         val style = MessagingStyle(empty).apply {
223             addMessage(message)
224             addMessage(message)
225         }
226         sendNotification(style = style)
227         val sbn = waitForNotification()
228         val messages = Message.getMessagesFromBundleArray(
229             sbn.notification.extras.getParcelableArray(EXTRA_MESSAGES, Parcelable::class.java)
230         )
231         assertWithMessage("expected notification to have exactly one message")
232                 .that(messages.size).isEqualTo(1)
233         assertWithMessage("expected single message not to contain otp: ${messages[0].text}")
234                 .that(messages[0].text.toString()).doesNotContain(OTP_CODE)
235         assertWithMessage("expected message person to be redacted: ${messages[0].senderPerson}")
236                 .that(messages[0].senderPerson?.name.toString()).isNotEqualTo(PERSON_NAME)
237     }
238 
239     @Test
240     @RequiresFlagsEnabled(
241         Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS,
242         Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_BIG_TEXT_STYLE
243     )
testBigTextRedactednull244     fun testBigTextRedacted() {
245         val style = Notification.BigTextStyle()
246         val bigText = "BIG TEXT"
247         val bigTitleText = "BIG TITLE TEXT"
248         val summaryText = "summary text"
249         style.bigText(bigText)
250         style.setBigContentTitle(bigTitleText)
251         style.setSummaryText(summaryText)
252         sendNotification(style = style)
253         val sbn = waitForNotification()
254         val extras = sbn.notification.extras
255         val testBigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT).toString()
256         val testBigTitleText = extras.getCharSequence(Notification.EXTRA_TITLE_BIG).toString()
257         val testSummaryText = extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT).toString()
258         assertWithMessage("expected big text to be redacted: $testBigText")
259             .that(testBigText).doesNotContain(bigText)
260         assertWithMessage("expected big title text to be redacted: $testBigTitleText")
261             .that(testBigTitleText).doesNotContain(bigTitleText)
262         assertWithMessage("expected summary text to be redacted: $testSummaryText")
263             .that(testSummaryText).doesNotContain(summaryText)
264     }
265 
266     @Test
267     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testCustomExtrasNotRedactednull268     fun testCustomExtrasNotRedacted() {
269         val customExtra = Bundle()
270         customExtra.putBoolean(groupKey, true)
271         sendNotification(extras = customExtra)
272         val sbn = waitForNotification()
273 
274         // Assert the notification is redacted
275         assertNotificationTextRedacted(sbn)
276 
277         // Assert the custom extra is still present
278 
279         assertWithMessage("Expected custom extra to still be present, but it wasn't")
280                 .that(sbn.notification.extras.getBoolean(groupKey, false)).isTrue()
281     }
282 
283     @Test
284     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testRankingRedactedInPostnull285     fun testRankingRedactedInPost() {
286         mListener.mRankingMap = null
287         sendNotification()
288         val sbn = waitForNotification()
289         assertWithMessage("Expected to receive a ranking map")
290                 .that(mListener.mRankingMap).isNotNull()
291         assertRankingRedacted(sbn.key, mListener.mRankingMap)
292     }
293 
294     @Test
295     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testRankingRedactedInUpdatenull296     fun testRankingRedactedInUpdate() {
297         sendNotification()
298         val sbn = waitForNotification()
299         for (key in mListener.mRankingMap.orderedKeys) {
300             val ranking = NotificationListenerService.Ranking()
301             mListener.mRankingMap.getRanking(key, ranking)
302         }
303         mListener.mRankingMap = null
304         val b = Bundle().apply {
305             putInt(KEY_IMPORTANCE, NotificationManager.IMPORTANCE_MAX)
306             putFloat(KEY_RANKING_SCORE, 1.0f)
307         }
308         val latch = mListener.setRankingUpdateCountDown(1)
309         mAssistant.adjustNotification(Adjustment(sbn.packageName, sbn.key, b, "", sbn.user))
310         latch.await()
311         assertWithMessage("Expected to receive a ranking map")
312                 .that(mListener.mRankingMap).isNotNull()
313         assertRankingRedacted(sbn.key, mListener.mRankingMap)
314     }
315 
assertRankingRedactednull316     private fun assertRankingRedacted(
317         key: String,
318         rankingMap: NotificationListenerService.RankingMap
319     ) {
320         val ranking = NotificationListenerService.Ranking()
321         val foundPostedNotifRanking = rankingMap.getRanking(key, ranking)
322         assertWithMessage("Expected to find a ranking with key $key")
323                 .that(foundPostedNotifRanking).isTrue()
324         assertWithMessage("Expected smart actions to be empty").that(ranking.smartActions)
325                 .isEmpty()
326         assertWithMessage("Expected smart replies to be empty").that(ranking.smartReplies)
327                 .isEmpty()
328     }
329 
330     @Test
331     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testGetActiveNotificationsRedactednull332     fun testGetActiveNotificationsRedacted() {
333         sendNotification()
334         val postedSbn = waitForNotification()
335         val activeSbn = mListener.getActiveNotifications(arrayOf(postedSbn.key)).first()
336         assertNotificationTextRedacted(activeSbn)
337     }
338 
339     @Test
340     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testGetSnoozedNotificationsRedactednull341     fun testGetSnoozedNotificationsRedacted() {
342         sendNotification()
343         val postedSbn = waitForNotification()
344         mListener.snoozeNotification(postedSbn.key, SHORT_SLEEP_TIME_MS)
345         val snoozedSbn = waitForNotification(SEARCH_TYPE.SNOOZED)
346         // Allow the notification to be unsnoozed
347         Thread.sleep(SHORT_SLEEP_TIME_MS * 2)
348         assertNotificationTextRedacted(snoozedSbn)
349     }
350 
351     @Test
352     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testListenerWithCdmAssociationGetsUnredactednull353     fun testListenerWithCdmAssociationGetsUnredacted() {
354         assumeFalse(
355             mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) ||
356                 mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
357         )
358         val cdmManager = mContext.getSystemService(CompanionDeviceManager::class.java)!!
359         val macAddress = MacAddress.fromString("00:00:00:00:00:AA")
360         try {
361             runShellCommand(
362                 "cmd companiondevice associate " +
363                         "${mContext.userId} ${mContext.packageName} $macAddress"
364             )
365             // Trusted status is cached on helper enable, so disable + enable the listener
366             mNotificationHelper.disableListener(STUB_PACKAGE_NAME)
367             mNotificationHelper.enableListener(STUB_PACKAGE_NAME)
368             assertNotificationNotRedacted()
369         } finally {
370             runWithShellPermissionIdentity {
371                 val assocInfo = cdmManager.allAssociations.find {
372                     mContext.packageName.equals(it.packageName)
373                 }
374                 assertWithMessage("Expected to have an active cdm association")
375                         .that(assocInfo).isNotNull()
376                 cdmManager.disassociate(assocInfo!!.id)
377             }
378         }
379     }
380 
381     @Test
382     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testListenerWithReceiveSensitiveNotificationsPermissionsGetsUnredactednull383     fun testListenerWithReceiveSensitiveNotificationsPermissionsGetsUnredacted() {
384         runWithShellPermissionIdentity(
385             {
386                 // Trusted status is cached on helper enable, so disable + enable the listener
387                 mNotificationHelper.disableListener(STUB_PACKAGE_NAME)
388                 mNotificationHelper.enableListener(STUB_PACKAGE_NAME)
389                 assertNotificationNotRedacted()
390             },
391             RECEIVE_SENSITIVE_NOTIFICATIONS
392         )
393     }
394 
395     @Test
396     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testListenerWithReceiveSensitiveNotificationsAppOpGetsUnredactednull397     fun testListenerWithReceiveSensitiveNotificationsAppOpGetsUnredacted() {
398         val appOpsManager = mContext.getSystemService(AppOpsManager::class.java)!!
399         try {
400             runWithShellPermissionIdentity {
401                 assertEquals(
402                     AppOpsManager.MODE_IGNORED,
403                     appOpsManager.checkOp(
404                         AppOpsManager.OPSTR_RECEIVE_SENSITIVE_NOTIFICATIONS,
405                         Process.myUid(),
406                         STUB_PACKAGE_NAME
407                     )
408                 )
409                 appOpsManager.setUidMode(
410                     AppOpsManager.OPSTR_RECEIVE_SENSITIVE_NOTIFICATIONS,
411                     Process.myUid(),
412                     AppOpsManager.MODE_ALLOWED
413                 )
414             }
415             // Trusted status is cached on helper enable, so disable + enable the listener
416             mNotificationHelper.disableListener(STUB_PACKAGE_NAME)
417             mNotificationHelper.enableListener(STUB_PACKAGE_NAME)
418             assertNotificationNotRedacted()
419         } finally {
420             runWithShellPermissionIdentity {
421                 appOpsManager.setUidMode(
422                     AppOpsManager.OPSTR_RECEIVE_SENSITIVE_NOTIFICATIONS,
423                     Process.myUid(),
424                     AppOpsManager.MODE_IGNORED
425                 )
426             }
427         }
428     }
429 
430     @Test
431     @RequiresFlagsDisabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testStandardListenerGetsUnredactedWhenFlagDisablednull432     fun testStandardListenerGetsUnredactedWhenFlagDisabled() {
433         assertNotificationNotRedacted()
434     }
435 
436     // see packages/modules/ExtServices/java/tests/src/android/ext/services/notification/
437     // NotificationOtpDetectionHelperTest.kt for more granular tests of these otp messages
438     @Test
439     @CddTest(requirement = "3.8.3.4/C-1-1")
440     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testE2ERedaction_shouldRedactnull441     fun testE2ERedaction_shouldRedact() {
442         assertTrue(
443             "Expected a notification assistant to be present",
444             mPreviousEnabledAssistant != null
445         )
446         mNotificationHelper.disableAssistant(STUB_PACKAGE_NAME)
447         mNotificationHelper.enableOtherPkgAssistantIfNeeded(mPreviousEnabledAssistant)
448         // We just re-enabled the NAS. send one notification in order to start its process
449         sendNotification(text = "staring NAS process", title = "", subtext = "", tag = "start")
450         waitForNotification(tag = "start")
451 
452         val shouldRedact = mutableListOf(
453             "123G5",
454             "123456F8",
455             "123ķ4",
456             "123Ŀ4",
457             "1-1-01 is the date of your code T3425",
458             "your code 54-234-3 was sent on 1-1-01",
459             "34-58-30",
460             "12-1-3089",
461             "G-3d523",
462             "G-FD-745",
463             "your code is:G-345821",
464             "your code is (G-345821",
465             "your code is \nG-345821",
466             "you code is G-345821.",
467             "you code is (G-345821)",
468             "c'est g4zy75",
469             "2109",
470             "3035",
471             "1899")
472         var notifNum = 0
473         val notRedactedFailures = StringBuilder("")
474         for (otp in shouldRedact) {
475             val tag = "$groupKey #$notifNum"
476             sendNotification(text = otp, title = "", subtext = "", tag = tag)
477             val sbn = waitForNotification(tag = tag)
478             val text = sbn.notification.extras.getCharSequence(EXTRA_TEXT)!!.toString()
479             if (text.contains(otp)) {
480                 notRedactedFailures.append("otp \"$otp\" is in notification text \"$text\"\n")
481             }
482             notifNum += 1
483         }
484 
485         if (notRedactedFailures.toString() != "") {
486             Assert.fail(
487                 "The following codes were not redacted, but should have been:" +
488                         "\n$notRedactedFailures"
489             )
490         }
491     }
492 
493     // see packages/modules/ExtServices/java/tests/src/android/ext/services/notification/
494     // NotificationOtpDetectionHelperTest.kt for more granular tests of these otp messages
495     @Test
496     @CddTest(requirement = "3.8.3.4/C-1-1")
497     @RequiresFlagsEnabled(Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
testE2ERedaction_shouldNotRedactnull498     fun testE2ERedaction_shouldNotRedact() {
499         assertTrue(
500             "Expected a notification assistant to be present",
501             mPreviousEnabledAssistant != null
502         )
503         mNotificationHelper.disableAssistant(STUB_PACKAGE_NAME)
504         mNotificationHelper.enableOtherPkgAssistantIfNeeded(mPreviousEnabledAssistant)
505         // We just re-enabled the NAS. send one notification in order to start its process
506         sendNotification(text = "staring NAS process", title = "", subtext = "", tag = "start")
507         waitForNotification(tag = "start")
508 
509         val shouldNotRedact =
510             mutableListOf(
511                 "123G",
512                 "123",
513                 "12 345",
514                 "123T56789",
515                 "TEFHXES",
516                 "01-01-2001",
517                 "1-1-2001",
518                 "1-1-01",
519                 "6--7893",
520                 "------",
521                 "your code isG-345821",
522                 "your code is G-345821for real",
523                 "GVRXY 2",
524                 "2009",
525                 "1945",
526             )
527         var notifNum = 0
528         val redactedFailures = StringBuilder("")
529         for (notOtp in shouldNotRedact) {
530             val tag = "$groupKey #$notifNum"
531             sendNotification(text = notOtp, title = "", subtext = "", tag = tag)
532             val sbn = waitForNotification(tag = tag)
533             val text = sbn.notification.extras.getCharSequence(EXTRA_TEXT)!!.toString()
534             if (!text.contains(notOtp)) {
535                 redactedFailures.append(
536                     "non-otp message \"$notOtp\" is not in notification text " +
537                         "\"$text\"\n"
538                 )
539             }
540             notifNum += 1
541         }
542 
543         if (redactedFailures.toString() != "") {
544             Assert.fail(
545                 "The following codes were redacted, but should not have been:" +
546                         "\n$redactedFailures"
547             )
548         }
549     }
550 
assertNotificationNotRedactednull551     private fun assertNotificationNotRedacted() {
552         sendNotification()
553         val sbn = waitForNotification()
554         val text = sbn.notification.extras.getCharSequence(EXTRA_TEXT)!!.toString()
555         assertWithMessage("Expected notification text to contain OTP code, but it did not: $text")
556                 .that(text).contains(OTP_CODE)
557     }
558 
assertNotificationTextRedactednull559     private fun assertNotificationTextRedacted(sbn: StatusBarNotification) {
560         val text = sbn.notification.extras.getCharSequence(EXTRA_TEXT)!!.toString()
561         assertWithMessage("Expected notification text not to contain OTP code, but it did: $text")
562                 .that(text).doesNotContain(OTP_CODE)
563     }
564 
565     companion object {
566         private const val OTP_CODE = "123645"
567         private const val OTP_MESSAGE_BASIC = "your one time code is 123645"
568         private const val PERSON_NAME = "Alan Smithee"
569         private const val NOTIFICATION_ID = 42
570         private const val SHORT_SLEEP_TIME_MS: Long = 100
571     }
572 }
573