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 
17 package android.ext.services.notification
18 
19 import android.app.Notification
20 import android.app.Notification.CATEGORY_EMAIL
21 import android.app.Notification.CATEGORY_MESSAGE
22 import android.app.Notification.CATEGORY_SOCIAL
23 import android.app.Notification.EXTRA_TEXT
24 import android.app.PendingIntent
25 import android.app.Person
26 import android.content.Intent
27 import android.icu.util.ULocale
28 import androidx.test.platform.app.InstrumentationRegistry
29 import com.android.modules.utils.build.SdkLevel
30 import android.platform.test.flag.junit.SetFlagsRule
31 import android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_BIG_TEXT_STYLE
32 import android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS
33 import android.view.textclassifier.TextClassifier
34 import android.view.textclassifier.TextLanguage
35 import android.view.textclassifier.TextLinks
36 import com.google.common.truth.Truth.assertWithMessage
37 import org.junit.After
38 import org.junit.Assume.assumeTrue
39 import org.junit.Before
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.rules.TestRule
43 import org.junit.runner.RunWith
44 import org.junit.runners.JUnit4
45 import org.mockito.ArgumentMatchers.any
46 import org.mockito.Mockito
47 
48 @RunWith(JUnit4::class)
49 class NotificationOtpDetectionHelperTest {
50     val context = InstrumentationRegistry.getInstrumentation().targetContext!!
51     val localeWithRegex = ULocale.ENGLISH
52     val invalidLocale = ULocale.ROOT
53 
54     @get:Rule
55     val setFlagsRule = if (SdkLevel.isAtLeastV()) {
56         SetFlagsRule()
57     } else {
58         // On < V, have a test rule that does nothing
59         TestRule { statement, _ -> statement}
60     }
61 
62     private data class TestResult(
63         val expected: Boolean,
64         val actual: Boolean,
65         val failureMessage: String
66     )
67 
68     private val results = mutableListOf<TestResult>()
69 
70     @Before
71     fun enableFlag() {
72         assumeTrue(SdkLevel.isAtLeastV())
73         (setFlagsRule as SetFlagsRule).enableFlags(
74             FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS,
75             FLAG_REDACT_SENSITIVE_NOTIFICATIONS_BIG_TEXT_STYLE)
76         results.clear()
77     }
78 
79     @After
80     fun verifyResults() {
81         val allFailuresMessage = StringBuilder("")
82         var numFailures = 0;
83         results.forEach { (expected, actual, failureMessage) ->
84             if (expected != actual) {
85                 numFailures += 1
86                 allFailuresMessage.append("$failureMessage\n")
87             }
88         }
89         assertWithMessage("Found $numFailures failures:\n$allFailuresMessage")
90             .that(numFailures).isEqualTo(0)
91     }
92 
93     private fun addResult(expected: Boolean, actual: Boolean, failureMessage: String) {
94         results.add(TestResult(expected, actual, failureMessage))
95     }
96 
97     @Test
98     fun testGetTextForDetection_emptyIfFlagDisabled() {
99         (setFlagsRule as SetFlagsRule)
100             .disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
101         val text = "text"
102         val title = "title"
103         val subtext = "subtext"
104         val sensitive = NotificationOtpDetectionHelper.getTextForDetection(
105             createNotification(text = text, title = title, subtext = subtext))
106         assertWithMessage("expected sensitive text to be empty").that(sensitive).isEmpty()
107     }
108 
109 
110     @Test
111     fun testGetTextForDetection_textFieldsIncluded() {
112         val text = "text"
113         val title = "title"
114         val subtext = "subtext"
115         val sensitive = NotificationOtpDetectionHelper.getTextForDetection(
116             createNotification(text = text, title = title, subtext = subtext))
117         addResult(expected = true, sensitive.contains(text),"expected sensitive text to contain $text")
118         addResult(expected = true, sensitive.contains(title), "expected sensitive text to contain $title")
119         addResult(expected = true, sensitive.contains(subtext), "expected sensitive text to contain $subtext")
120     }
121 
122     @Test
123     fun testGetTextForDetection_nullTextFields() {
124         val text = "text"
125         val title = "title"
126         val subtext = "subtext"
127         var sensitive = NotificationOtpDetectionHelper.getTextForDetection(
128             createNotification(text = text, title = null, subtext = null))
129         addResult(expected = true, sensitive.contains(text), "expected sensitive text to contain $text")
130         addResult(expected = false, sensitive.contains(title), "expected sensitive text not to contain $title")
131         addResult(expected = false, sensitive.contains("subtext"), "expected sensitive text not to contain $subtext")
132         sensitive = NotificationOtpDetectionHelper.getTextForDetection(
133             createNotification(text = null, title = null, subtext = null))
134         addResult(expected = true, sensitive != null, "expected to get a nonnull string")
135         val nullExtras = createNotification(text = null, title = null, subtext = null).apply {
136             this.extras = null
137         }
138         sensitive = NotificationOtpDetectionHelper.getTextForDetection(nullExtras)
139         addResult(expected = true, sensitive != null, "expected to get a nonnull string")
140     }
141 
142     @Test
143     fun testGetTextForDetection_messagesIncludedSorted() {
144         val empty = Person.Builder().setName("test name").build()
145         val messageText1 = "message text 1"
146         val messageText2 = "message text 2"
147         val messageText3 = "message text 3"
148         val timestamp1 = 0L
149         val timestamp2 = 1000L
150         val timestamp3 = 50L
151         val message1 =
152             Notification.MessagingStyle.Message(messageText1,
153                 timestamp1,
154                 empty)
155         val message2 =
156             Notification.MessagingStyle.Message(messageText2,
157                 timestamp2,
158                 empty)
159         val message3 =
160             Notification.MessagingStyle.Message(messageText3,
161                 timestamp3,
162                 empty)
163         val style = Notification.MessagingStyle(empty).apply {
164             addMessage(message1)
165             addMessage(message2)
166             addMessage(message3)
167         }
168         val notif = createNotification(style = style)
169         val sensitive = NotificationOtpDetectionHelper.getTextForDetection(notif)
170         addResult(expected = true, sensitive.contains(messageText1), "expected sensitive text to contain $messageText1")
171         addResult(expected = true, sensitive.contains(messageText2), "expected sensitive text to contain $messageText2")
172         addResult(expected = true, sensitive.contains(messageText3), "expected sensitive text to contain $messageText3")
173 
174         // MessagingStyle notifications get their main text set automatically to their first
175         // message, so we should skip to the end of that to find the message text
176         val notifText = notif.extras.getCharSequence(EXTRA_TEXT)?.toString() ?: ""
177         val messagesSensitiveStartIdx = sensitive.indexOf(notifText) + notifText.length
178         val sensitiveSub = sensitive.substring(messagesSensitiveStartIdx)
179         val text1Position = sensitiveSub.indexOf(messageText1)
180         val text2Position = sensitiveSub.indexOf(messageText2)
181         val text3Position = sensitiveSub.indexOf(messageText3)
182         // The messages should be sorted by timestamp, newest first, so 2 -> 3 -> 1
183         addResult(expected = true, text2Position < text1Position, "expected the newest message (2) to be first in \"$sensitiveSub\"")
184         addResult(expected = true, text2Position < text3Position, "expected the newest message (2) to be first in \"$sensitiveSub\"")
185         addResult(expected = true, text3Position < text1Position, "expected the middle message (3) to be center in \"$sensitiveSub\"")
186     }
187 
188     @Test
189     fun testGetTextForDetection_textLinesIncluded() {
190         val style = Notification.InboxStyle()
191         val extraLine = "extra line"
192         style.addLine(extraLine)
193         val sensitive = NotificationOtpDetectionHelper
194                 .getTextForDetection(createNotification(style = style))
195         addResult(expected = true, sensitive.contains(extraLine), "expected sensitive text to contain $extraLine")
196     }
197 
198     @Test
199     fun testGetTextForDetection_bigTextStyleTextsIncluded() {
200         val style = Notification.BigTextStyle()
201         val bigText = "BIG TEXT"
202         val bigTitleText = "BIG TITLE TEXT"
203         val summaryText = "summary text"
204         style.bigText(bigText)
205         style.setBigContentTitle(bigTitleText)
206         style.setSummaryText(summaryText)
207         val sensitive = NotificationOtpDetectionHelper
208             .getTextForDetection(createNotification(style = style))
209         addResult(expected = true, sensitive.contains(bigText), "expected sensitive text to contain $bigText")
210         addResult(expected =
211             true,
212             sensitive.contains(bigTitleText),
213             "expected sensitive text to contain $bigTitleText"
214         )
215         addResult(expected =
216             true,
217             sensitive.contains(summaryText),
218             "expected sensitive text to contain $summaryText"
219         )
220     }
221 
222     @Test
223     fun testGetTextForDetection_maxLen() {
224         val text = "0123456789".repeat(70) // 700 chars
225         val sensitive =
226             NotificationOtpDetectionHelper.getTextForDetection(createNotification(text = text))
227         addResult(expected = true, sensitive.length <= 600, "Expected to be 600 chars or fewer")
228     }
229 
230     @Test
231     fun testShouldCheckForOtp_falseIfFlagDisabled() {
232         (setFlagsRule as SetFlagsRule)
233             .disableFlags(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS)
234         val shouldCheck = NotificationOtpDetectionHelper
235             .shouldCheckForOtp(createNotification(category = CATEGORY_MESSAGE))
236         addResult(expected = false, shouldCheck, "$CATEGORY_MESSAGE should not be checked")
237     }
238 
239 
240     @Test
241     fun testShouldCheckForOtp_styles() {
242         val style = Notification.InboxStyle()
243         var shouldCheck = NotificationOtpDetectionHelper
244                 .shouldCheckForOtp(createNotification(style = style))
245         addResult(expected = true, shouldCheck, "InboxStyle should be checked")
246         val empty = Person.Builder().setName("test").build()
247         val style2 = Notification.MessagingStyle(empty)
248         val style3 = Notification.BigPictureStyle()
249         shouldCheck = NotificationOtpDetectionHelper
250                 .shouldCheckForOtp(createNotification(style = style2))
251         addResult(expected = true, shouldCheck, "MessagingStyle should be checked")
252         shouldCheck = NotificationOtpDetectionHelper
253                 .shouldCheckForOtp(createNotification())
254         addResult(expected = false, shouldCheck, "No style should not be checked")
255         shouldCheck = NotificationOtpDetectionHelper
256                 .shouldCheckForOtp(createNotification(style = style3))
257         addResult(expected = false, shouldCheck, "Valid non-messaging non-inbox style should not be checked")
258     }
259 
260     @Test
261     fun testShouldCheckForOtp_categories() {
262         var shouldCheck = NotificationOtpDetectionHelper
263                 .shouldCheckForOtp(createNotification(category = CATEGORY_MESSAGE))
264         addResult(expected = true, shouldCheck, "$CATEGORY_MESSAGE should be checked")
265         shouldCheck = NotificationOtpDetectionHelper
266             .shouldCheckForOtp(createNotification(category = CATEGORY_SOCIAL))
267         addResult(expected = true, shouldCheck, "$CATEGORY_SOCIAL should be checked")
268         shouldCheck = NotificationOtpDetectionHelper
269             .shouldCheckForOtp(createNotification(category = CATEGORY_EMAIL))
270         addResult(expected = true, shouldCheck, "$CATEGORY_EMAIL should be checked")
271         shouldCheck = NotificationOtpDetectionHelper
272             .shouldCheckForOtp(createNotification(category = ""))
273         addResult(expected = false, shouldCheck, "Empty string category should not be checked")
274     }
275 
276     @Test
277     fun testShouldCheckForOtp_regex() {
278         var shouldCheck = NotificationOtpDetectionHelper
279                 .shouldCheckForOtp(createNotification(text = "45454", category = ""))
280         assertWithMessage("Regex matches should be checked").that(shouldCheck).isTrue()
281     }
282 
283     @Test
284     fun testShouldCheckForOtp_publicVersion() {
285         var publicVersion = createNotification(category = CATEGORY_MESSAGE)
286         var shouldCheck = NotificationOtpDetectionHelper
287                 .shouldCheckForOtp(createNotification(publicVersion = publicVersion))
288 
289         addResult(expected = true, shouldCheck, "notifications with a checked category in their public version should " +
290                 "be checked")
291         publicVersion = createNotification(style = Notification.InboxStyle())
292         shouldCheck = NotificationOtpDetectionHelper
293             .shouldCheckForOtp(createNotification(publicVersion = publicVersion))
294         addResult(expected = true, shouldCheck, "notifications with a checked style in their public version should " +
295                 "be checked")
296     }
297 
298 
299     @Test
300     fun testContainsOtp_length() {
301         val tooShortAlphaNum = "123G"
302         val tooShortNumOnly = "123"
303         val minLenAlphaNum = "123G5"
304         val minLenNumOnly = "1235"
305         val twoTriplets = "123 456"
306         val tooShortTriplets = "12 345"
307         val maxLen = "123456F8"
308         val tooLong = "123T56789"
309 
310         addMatcherTestResult(expected = true, minLenAlphaNum)
311         addMatcherTestResult(expected = true, minLenNumOnly)
312         addMatcherTestResult(expected = true, maxLen)
313         addMatcherTestResult(expected = false, tooShortAlphaNum, customFailureMessage = "is too short")
314         addMatcherTestResult(expected = false, tooShortNumOnly, customFailureMessage = "is too short")
315         addMatcherTestResult(expected = false, tooLong, customFailureMessage = "is too long")
316         addMatcherTestResult(expected = true, twoTriplets)
317         addMatcherTestResult(expected = false, tooShortTriplets, customFailureMessage = "is too short")
318     }
319 
320     @Test
321     fun testContainsOtp_acceptsNonRomanAlphabeticalChars() {
322         val lowercase = "123ķ4"
323         val uppercase = "123Ŀ4"
324         val ideographicInMiddle = "123码456"
325         addMatcherTestResult(expected = true, lowercase)
326         addMatcherTestResult(expected = true, uppercase)
327         addMatcherTestResult(expected = false, ideographicInMiddle)
328     }
329 
330     @Test
331     fun testContainsOtp_mustHaveNumber() {
332         val noNums = "TEFHXES"
333         addMatcherTestResult(expected = false, noNums)
334     }
335 
336     @Test
337     fun testContainsOtp_dateExclusion() {
338         val date = "01-01-2001"
339         val singleDigitDate = "1-1-2001"
340         val twoDigitYear = "1-1-01"
341         val dateWithOtpAfter = "1-1-01 is the date of your code T3425"
342         val dateWithOtpBefore = "your code 54-234-3 was sent on 1-1-01"
343         val otpWithDashesButInvalidDate = "34-58-30"
344         val otpWithDashesButInvalidYear = "12-1-3089"
345 
346         addMatcherTestResult(expected =
347             true,
348             date,
349             checkForFalsePositives = false,
350             customFailureMessage = "should match if checkForFalsePositives is false"
351         )
352         addMatcherTestResult(expected =
353             false,
354             date,
355             customFailureMessage = "should not match if checkForFalsePositives is true"
356         )
357         addMatcherTestResult(expected = false, singleDigitDate)
358         addMatcherTestResult(expected = false, twoDigitYear)
359         addMatcherTestResult(expected = true, dateWithOtpAfter)
360         addMatcherTestResult(expected = true, dateWithOtpBefore)
361         addMatcherTestResult(expected = true, otpWithDashesButInvalidDate)
362         addMatcherTestResult(expected = true, otpWithDashesButInvalidYear)
363     }
364 
365     @Test
366     fun testContainsOtp_dashes() {
367         val oneDash = "G-3d523"
368         val manyDashes = "G-FD-745"
369         val tooManyDashes = "6--7893"
370         val oopsAllDashes = "------"
371         addMatcherTestResult(expected = true, oneDash)
372         addMatcherTestResult(expected = true, manyDashes)
373         addMatcherTestResult(expected = false, tooManyDashes)
374         addMatcherTestResult(expected = false, oopsAllDashes)
375     }
376 
377     @Test
378     fun testContainsOtp_startAndEnd() {
379         val noSpaceStart = "your code isG-345821"
380         val noSpaceEnd = "your code is G-345821for real"
381         val colonStart = "your code is:G-345821"
382         val parenStart = "your code is (G-345821"
383         val newLineStart = "your code is \nG-345821"
384         val quoteStart = "your code is 'G-345821"
385         val doubleQuoteStart = "your code is \"G-345821"
386         val bracketStart = "your code is [G-345821"
387         val ideographicStart = "your code is码G-345821"
388         val colonStartNumberPreceding = "your code is4:G-345821"
389         val periodEnd = "you code is G-345821."
390         val parenEnd = "you code is (G-345821)"
391         val quoteEnd = "you code is 'G-345821'"
392         val ideographicEnd = "your code is码G-345821码"
393         addMatcherTestResult(expected = false, noSpaceStart)
394         addMatcherTestResult(expected = false, noSpaceEnd)
395         addMatcherTestResult(expected = false, colonStartNumberPreceding)
396         addMatcherTestResult(expected = true, colonStart)
397         addMatcherTestResult(expected = true, parenStart)
398         addMatcherTestResult(expected = true, newLineStart)
399         addMatcherTestResult(expected = true, quoteStart)
400         addMatcherTestResult(expected = true, doubleQuoteStart)
401         addMatcherTestResult(expected = true, bracketStart)
402         addMatcherTestResult(expected = true, ideographicStart)
403         addMatcherTestResult(expected = true, periodEnd)
404         addMatcherTestResult(expected = true, parenEnd)
405         addMatcherTestResult(expected = true, quoteEnd)
406         addMatcherTestResult(expected = true, ideographicEnd)
407     }
408 
409     @Test
410     fun testContainsOtp_lookaheadMustBeOtpChar() {
411         val validLookahead = "g4zy75"
412         val spaceLookahead = "GVRXY 2"
413         addMatcherTestResult(expected = true, validLookahead)
414         addMatcherTestResult(expected = false, spaceLookahead)
415     }
416 
417     @Test
418     fun testContainsOtp_threeDontMatch_withoutLanguageSpecificRegex() {
419         val tc = getTestTextClassifier(invalidLocale)
420         val threeLowercase = "34agb"
421         addMatcherTestResult(expected = false, threeLowercase, textClassifier = tc)
422     }
423 
424     @Test
425     fun testContainsOtp_commonYearsDontMatch_withoutLanguageSpecificRegex() {
426         val tc = getTestTextClassifier(invalidLocale)
427         val twentyXX = "2009"
428         val twentyOneXX = "2109"
429         val thirtyXX = "3035"
430         val nineteenXX = "1945"
431         val eighteenXX = "1899"
432         addMatcherTestResult(expected = false, twentyXX, textClassifier = tc)
433         // Behavior should be the same for an invalid language, and null TextClassifier
434         addMatcherTestResult(expected = false, twentyXX, textClassifier = null)
435         addMatcherTestResult(expected = true, twentyOneXX, textClassifier = tc)
436         addMatcherTestResult(expected = true, thirtyXX, textClassifier = tc)
437         addMatcherTestResult(expected = false, nineteenXX, textClassifier = tc)
438         addMatcherTestResult(expected = true, eighteenXX, textClassifier = tc)
439     }
440 
441     @Test
442     fun testContainsOtp_engishSpecificRegex() {
443         val tc = getTestTextClassifier(ULocale.ENGLISH)
444         val englishFalsePositive = "This is a false positive 4543"
445         val englishContextWords = listOf("login", "log in", "2fa", "authenticate", "auth",
446             "authentication", "tan", "password", "passcode", "two factor", "two-factor", "2factor",
447             "2 factor", "pin")
448         val englishContextWordsCase = listOf("LOGIN", "logIn", "LoGiN")
449         // Strings with a context word somewhere in the substring
450         val englishContextSubstrings = listOf("pins", "gaping", "backspin")
451 
452         addMatcherTestResult(expected = false, englishFalsePositive, textClassifier = tc)
453         for (context in englishContextWords) {
454             val englishTruePositive = "$englishFalsePositive $context"
455             addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc)
456         }
457         for (context in englishContextWordsCase) {
458             val englishTruePositive = "$englishFalsePositive $context"
459             addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc)
460         }
461         for (falseContext in englishContextSubstrings) {
462             val anotherFalsePositive = "$englishFalsePositive $falseContext"
463             addMatcherTestResult(expected = false, anotherFalsePositive, textClassifier = tc)
464         }
465     }
466 
467     @Test
468     fun testContainsOtpCode_usesTcForFalsePositivesIfNoLanguageSpecificRegex() {
469         var tc = getTestTextClassifier(invalidLocale, listOf(TextClassifier.TYPE_ADDRESS))
470         val address = "this text doesn't actually matter, but meet me at 6353 Juan Tabo, Apt. 6"
471         addMatcherTestResult(expected = false, address, textClassifier = tc)
472         tc = getTestTextClassifier(invalidLocale, listOf(TextClassifier.TYPE_FLIGHT_NUMBER))
473         val flight = "your flight number is UA1234"
474         addMatcherTestResult(expected = false, flight, textClassifier = tc)
475     }
476 
477     @Test
478     fun testContainsOtpCode_languageSpecificOverridesFalsePositivesExceptDate() {
479         // TC will detect an address, but the language-specific regex will be preferred
480         val tc = getTestTextClassifier(localeWithRegex, listOf(TextClassifier.TYPE_ADDRESS))
481         val date = "1-1-01"
482         // Dates should still be checked
483         addMatcherTestResult(expected = false, date, textClassifier = tc)
484         // A string with a code with three lowercase letters, and an excluded year
485         val withOtherFalsePositives = "your login code is abd3 1985"
486         // Other false positive regular expressions should not be checked
487         addMatcherTestResult(expected = true, withOtherFalsePositives, textClassifier = tc)
488     }
489 
490     private fun createNotification(
491         text: String? = "",
492         title: String? = "",
493         subtext: String? = "",
494         category: String? = "",
495         style: Notification.Style? = null,
496         publicVersion: Notification? = null
497     ): Notification {
498         val intent = Intent(Intent.ACTION_MAIN)
499         intent.setFlags(
500             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
501                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
502         )
503         intent.setAction(Intent.ACTION_MAIN)
504         intent.setPackage(context.packageName)
505 
506         val nb = Notification.Builder(context, "")
507         nb.setContentText(text)
508         nb.setContentTitle(title)
509         nb.setSubText(subtext)
510         nb.setCategory(category)
511         nb.setContentIntent(createTestPendingIntent())
512         if (style != null) {
513             nb.setStyle(style)
514         }
515         if (publicVersion != null) {
516             nb.setPublicVersion(publicVersion)
517         }
518         return nb.build()
519     }
520 
521     private fun addMatcherTestResult(
522         expected: Boolean,
523         text: String,
524         checkForFalsePositives: Boolean = true,
525         textClassifier: TextClassifier? = null,
526         customFailureMessage: String? = null
527     ) {
528         val failureMessage = if (customFailureMessage != null) {
529             "$text $customFailureMessage"
530         } else if (expected) {
531             "$text should match"
532         } else {
533             "$text should not match"
534         }
535         addResult(expected = expected, NotificationOtpDetectionHelper.containsOtp(
536             createNotification(text), checkForFalsePositives, textClassifier), failureMessage)
537     }
538 
539     private fun createTestPendingIntent(): PendingIntent {
540         val intent = Intent(Intent.ACTION_MAIN)
541         intent.setFlags(
542             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
543                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
544         )
545         intent.setAction(Intent.ACTION_MAIN)
546         intent.setPackage(context.packageName)
547 
548         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE)
549     }
550 
551     // Creates a mock TextClassifier that will report back that text provided to it matches the
552     // given language codes (for language requests) and textClassifier entities (for links request)
553     private fun getTestTextClassifier(
554         locale: ULocale?,
555         tcEntities: List<String>? = null
556     ): TextClassifier {
557         val tc = Mockito.mock(TextClassifier::class.java)
558         if (locale != null) {
559             Mockito.doReturn(
560                 TextLanguage.Builder().putLocale(locale, 0.9f).build()
561             ).`when`(tc).detectLanguage(any(TextLanguage.Request::class.java))
562         }
563 
564         val entityMap = mutableMapOf<String, Float>()
565         // to build the TextLinks, the entity map must have at least one item
566         entityMap[TextClassifier.TYPE_URL] = 0.01f
567         for (entity in tcEntities ?: emptyList()) {
568             entityMap[entity] = 0.9f
569         }
570         Mockito.doReturn(
571             TextLinks.Builder("").addLink(0, 1, entityMap)
572                 .build()
573         ).`when`(tc).generateLinks(any(TextLinks.Request::class.java))
574         return tc
575     }
576 }