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 }