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