• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  }