• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
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.Context
27 import android.content.Intent
28 import android.icu.util.ULocale
29 import android.os.Build
30 import android.os.Build.VERSION.SDK_INT
31 import android.view.textclassifier.TextClassifier
32 import android.view.textclassifier.TextLanguage
33 import android.view.textclassifier.TextLinks
34 import androidx.test.core.app.ApplicationProvider
35 import androidx.test.ext.junit.runners.AndroidJUnit4
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.Test
41 import org.junit.runner.RunWith
42 import org.mockito.ArgumentMatchers.any
43 import org.mockito.Mockito
44 
45 @RunWith(AndroidJUnit4::class)
46 class NotificationOtpDetectionHelperTest {
47     private val context = ApplicationProvider.getApplicationContext<Context>()
48     private val localeWithRegex = ULocale.ENGLISH
49     private val invalidLocale = ULocale.ROOT
50 
51     private data class TestResult(
52         val expected: Boolean,
53         val actual: Boolean,
54         val failureMessage: String
55     )
56 
57     private val results = mutableListOf<TestResult>()
58 
59     @Before
enableFlagnull60     fun enableFlag() {
61         assumeTrue(SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM)
62         results.clear()
63     }
64 
65     @After
verifyResultsnull66     fun verifyResults() {
67         val allFailuresMessage = StringBuilder("")
68         var numFailures = 0;
69         for ((expected, actual, failureMessage) in results) {
70             if (expected != actual) {
71                 numFailures += 1
72                 allFailuresMessage.append("$failureMessage\n")
73             }
74         }
75         assertWithMessage("Found $numFailures failures:\n$allFailuresMessage")
76             .that(numFailures).isEqualTo(0)
77     }
78 
addResultnull79     private fun addResult(expected: Boolean, actual: Boolean, failureMessage: String) {
80         results.add(TestResult(expected, actual, failureMessage))
81     }
82 
83     @Test
testGetTextForDetection_textFieldsIncludednull84     fun testGetTextForDetection_textFieldsIncluded() {
85         val text = "text"
86         val title = "title"
87         val subtext = "subtext"
88         val sensitive = NotificationOtpDetectionHelper.getTextForDetection(
89             createNotification(text = text, title = title, subtext = subtext))
90         addResult(expected = true, sensitive.contains(text),"expected sensitive text to contain $text")
91         addResult(expected = true, sensitive.contains(title), "expected sensitive text to contain $title")
92         addResult(expected = true, sensitive.contains(subtext), "expected sensitive text to contain $subtext")
93     }
94 
95     @Test
testGetTextForDetection_nullTextFieldsnull96     fun testGetTextForDetection_nullTextFields() {
97         val text = "text"
98         val title = "title"
99         val subtext = "subtext"
100         var sensitive = NotificationOtpDetectionHelper.getTextForDetection(
101             createNotification(text = text, title = null, subtext = null))
102         addResult(expected = true, sensitive.contains(text), "expected sensitive text to contain $text")
103         addResult(expected = false, sensitive.contains(title), "expected sensitive text not to contain $title")
104         addResult(expected = false, sensitive.contains("subtext"), "expected sensitive text not to contain $subtext")
105         sensitive = NotificationOtpDetectionHelper.getTextForDetection(
106             createNotification(text = null, title = null, subtext = null))
107         addResult(expected = true, sensitive != null, "expected to get a nonnull string")
108         val nullExtras = createNotification(text = null, title = null, subtext = null).apply {
109             this.extras = null
110         }
111         sensitive = NotificationOtpDetectionHelper.getTextForDetection(nullExtras)
112         addResult(expected = true, sensitive != null, "expected to get a nonnull string")
113     }
114 
115     @Test
testGetTextForDetection_messagesIncludedSortednull116     fun testGetTextForDetection_messagesIncludedSorted() {
117         val empty = Person.Builder().setName("test name").build()
118         val messageText1 = "message text 1"
119         val messageText2 = "message text 2"
120         val messageText3 = "message text 3"
121         val timestamp1 = 0L
122         val timestamp2 = 1000L
123         val timestamp3 = 50L
124         val message1 =
125             Notification.MessagingStyle.Message(messageText1,
126                 timestamp1,
127                 empty)
128         val message2 =
129             Notification.MessagingStyle.Message(messageText2,
130                 timestamp2,
131                 empty)
132         val message3 =
133             Notification.MessagingStyle.Message(messageText3,
134                 timestamp3,
135                 empty)
136         val style = Notification.MessagingStyle(empty).apply {
137             addMessage(message1)
138             addMessage(message2)
139             addMessage(message3)
140         }
141         val notif = createNotification(style = style)
142         val sensitive = NotificationOtpDetectionHelper.getTextForDetection(notif)
143         addResult(expected = true, sensitive.contains(messageText1), "expected sensitive text to contain $messageText1")
144         addResult(expected = true, sensitive.contains(messageText2), "expected sensitive text to contain $messageText2")
145         addResult(expected = true, sensitive.contains(messageText3), "expected sensitive text to contain $messageText3")
146 
147         // MessagingStyle notifications get their main text set automatically to their first
148         // message, so we should skip to the end of that to find the message text
149         val notifText = notif.extras.getCharSequence(EXTRA_TEXT)?.toString() ?: ""
150         val messagesSensitiveStartIdx = sensitive.indexOf(notifText) + notifText.length
151         val sensitiveSub = sensitive.substring(messagesSensitiveStartIdx)
152         val text1Position = sensitiveSub.indexOf(messageText1)
153         val text2Position = sensitiveSub.indexOf(messageText2)
154         val text3Position = sensitiveSub.indexOf(messageText3)
155         // The messages should be sorted by timestamp, newest first, so 2 -> 3 -> 1
156         addResult(expected = true, text2Position < text1Position, "expected the newest message (2) to be first in \"$sensitiveSub\"")
157         addResult(expected = true, text2Position < text3Position, "expected the newest message (2) to be first in \"$sensitiveSub\"")
158         addResult(expected = true, text3Position < text1Position, "expected the middle message (3) to be center in \"$sensitiveSub\"")
159     }
160 
161     @Test
testGetTextForDetection_textLinesIncludednull162     fun testGetTextForDetection_textLinesIncluded() {
163         val style = Notification.InboxStyle()
164         val extraLine = "extra line"
165         style.addLine(extraLine)
166         val sensitive = NotificationOtpDetectionHelper
167                 .getTextForDetection(createNotification(style = style))
168         addResult(expected = true, sensitive.contains(extraLine), "expected sensitive text to contain $extraLine")
169     }
170 
171     @Test
testGetTextForDetection_bigTextStyleTextsIncludednull172     fun testGetTextForDetection_bigTextStyleTextsIncluded() {
173         val style = Notification.BigTextStyle()
174         val bigText = "BIG TEXT"
175         val bigTitleText = "BIG TITLE TEXT"
176         val summaryText = "summary text"
177         style.bigText(bigText)
178         style.setBigContentTitle(bigTitleText)
179         style.setSummaryText(summaryText)
180         val sensitive = NotificationOtpDetectionHelper
181             .getTextForDetection(createNotification(style = style))
182         addResult(expected = true, sensitive.contains(bigText), "expected sensitive text to contain $bigText")
183         addResult(expected =
184             true,
185             sensitive.contains(bigTitleText),
186             "expected sensitive text to contain $bigTitleText"
187         )
188         addResult(expected =
189             true,
190             sensitive.contains(summaryText),
191             "expected sensitive text to contain $summaryText"
192         )
193     }
194 
195     @Test
testGetTextForDetection_maxLennull196     fun testGetTextForDetection_maxLen() {
197         val text = "0123456789".repeat(70) // 700 chars
198         val sensitive =
199             NotificationOtpDetectionHelper.getTextForDetection(createNotification(text = text))
200         addResult(expected = true, sensitive.length <= 600, "Expected to be 600 chars or fewer")
201     }
202 
203     @Test
testShouldCheckForOtp_stylesnull204     fun testShouldCheckForOtp_styles() {
205         val style = Notification.InboxStyle()
206         var shouldCheck = NotificationOtpDetectionHelper
207                 .shouldCheckForOtp(createNotification(style = style))
208         addResult(expected = true, shouldCheck, "InboxStyle should be checked")
209         val empty = Person.Builder().setName("test").build()
210         val style2 = Notification.MessagingStyle(empty)
211         val style3 = Notification.BigPictureStyle()
212         val rejectedStyle = Notification.MediaStyle()
213         shouldCheck = NotificationOtpDetectionHelper
214                 .shouldCheckForOtp(createNotification(style = style2))
215         addResult(expected = true, shouldCheck, "MessagingStyle should be checked")
216         shouldCheck = NotificationOtpDetectionHelper
217                 .shouldCheckForOtp(createNotification())
218         addResult(expected = false, shouldCheck, "No style should not be checked")
219         shouldCheck = NotificationOtpDetectionHelper
220                 .shouldCheckForOtp(createNotification(style = style3))
221         addResult(expected = false, shouldCheck, "Valid non-messaging non-inbox style should not be checked")
222         shouldCheck = NotificationOtpDetectionHelper
223             .shouldCheckForOtp(createNotification(text = "your one time code is 4343434",
224                 style = rejectedStyle))
225         addResult(expected = false, shouldCheck, "MediaStyle should always be rejected")
226     }
227 
228     @Test
testShouldCheckForOtp_categoriesnull229     fun testShouldCheckForOtp_categories() {
230         var shouldCheck = NotificationOtpDetectionHelper
231                 .shouldCheckForOtp(createNotification(category = CATEGORY_MESSAGE))
232         addResult(expected = true, shouldCheck, "$CATEGORY_MESSAGE should be checked")
233         shouldCheck = NotificationOtpDetectionHelper
234             .shouldCheckForOtp(createNotification(category = CATEGORY_SOCIAL))
235         addResult(expected = true, shouldCheck, "$CATEGORY_SOCIAL should be checked")
236         shouldCheck = NotificationOtpDetectionHelper
237             .shouldCheckForOtp(createNotification(category = CATEGORY_EMAIL))
238         addResult(expected = true, shouldCheck, "$CATEGORY_EMAIL should be checked")
239         shouldCheck = NotificationOtpDetectionHelper
240             .shouldCheckForOtp(createNotification(category = ""))
241         addResult(expected = false, shouldCheck, "Empty string category should not be checked")
242     }
243 
244     @Test
testShouldCheckForOtp_regexnull245     fun testShouldCheckForOtp_regex() {
246         val shouldCheck = NotificationOtpDetectionHelper
247                 .shouldCheckForOtp(createNotification(text = "45454", category = ""))
248         assertWithMessage("Regex matches should be checked").that(shouldCheck).isTrue()
249     }
250 
251     @Test
testShouldCheckForOtp_publicVersionnull252     fun testShouldCheckForOtp_publicVersion() {
253         var publicVersion = createNotification(category = CATEGORY_MESSAGE)
254         var shouldCheck = NotificationOtpDetectionHelper
255                 .shouldCheckForOtp(createNotification(publicVersion = publicVersion))
256 
257         addResult(expected = true, shouldCheck, "notifications with a checked category in their public version should " +
258                 "be checked")
259         publicVersion = createNotification(style = Notification.InboxStyle())
260         shouldCheck = NotificationOtpDetectionHelper
261             .shouldCheckForOtp(createNotification(publicVersion = publicVersion))
262         addResult(expected = true, shouldCheck, "notifications with a checked style in their public version should " +
263                 "be checked")
264     }
265 
266 
267     @Test
testContainsOtp_lengthnull268     fun testContainsOtp_length() {
269         val tooShortAlphaNum = "123G"
270         val tooShortNumOnly = "123"
271         val minLenAlphaNum = "123G5"
272         val minLenNumOnly = "1235"
273         val twoTriplets = "123 456"
274         val tooShortTriplets = "12 345"
275         val maxLen = "123456F8"
276         val tooLong = "123T56789"
277 
278         addMatcherTestResult(expected = true, minLenAlphaNum)
279         addMatcherTestResult(expected = true, minLenNumOnly)
280         addMatcherTestResult(expected = true, maxLen)
281         addMatcherTestResult(expected = false, tooShortAlphaNum, customFailureMessage = "is too short")
282         addMatcherTestResult(expected = false, tooShortNumOnly, customFailureMessage = "is too short")
283         addMatcherTestResult(expected = false, tooLong, customFailureMessage = "is too long")
284         addMatcherTestResult(expected = true, twoTriplets)
285         addMatcherTestResult(expected = false, tooShortTriplets, customFailureMessage = "is too short")
286     }
287 
288     @Test
testContainsOtp_acceptsNonRomanAlphabeticalCharsnull289     fun testContainsOtp_acceptsNonRomanAlphabeticalChars() {
290         val lowercase = "123ķ4"
291         val uppercase = "123Ŀ4"
292         val ideographicInMiddle = "123码456"
293         addMatcherTestResult(expected = true, lowercase)
294         addMatcherTestResult(expected = true, uppercase)
295         addMatcherTestResult(expected = false, ideographicInMiddle)
296     }
297 
298     @Test
testContainsOtp_mustHaveNumbernull299     fun testContainsOtp_mustHaveNumber() {
300         val noNums = "TEFHXES"
301         addMatcherTestResult(expected = false, noNums)
302     }
303 
304     @Test
testContainsOtp_dateExclusionnull305     fun testContainsOtp_dateExclusion() {
306         val date = "01-01-2001"
307         val singleDigitDate = "1-1-2001"
308         val twoDigitYear = "1-1-01"
309         val dateWithOtpAfter = "1-1-01 is the date of your code T3425"
310         val dateWithOtpBefore = "your code 54-234-3 was sent on 1-1-01"
311         val otpWithDashesButInvalidDate = "34-58-30"
312         val otpWithDashesButInvalidYear = "12-1-3089"
313 
314         addMatcherTestResult(
315             expected = true,
316             date,
317             checkForFalsePositives = false,
318             customFailureMessage = "should match if checkForFalsePositives is false"
319         )
320         addMatcherTestResult(
321             expected = false,
322             date,
323             customFailureMessage = "should not match if checkForFalsePositives is true"
324         )
325         addMatcherTestResult(expected = false, singleDigitDate)
326         addMatcherTestResult(expected = false, twoDigitYear)
327         addMatcherTestResult(expected = true, dateWithOtpAfter)
328         addMatcherTestResult(expected = true, dateWithOtpBefore)
329         addMatcherTestResult(expected = true, otpWithDashesButInvalidDate)
330         addMatcherTestResult(expected = true, otpWithDashesButInvalidYear)
331     }
332 
333     @Test
testContainsOtp_phoneExclusionnull334     fun testContainsOtp_phoneExclusion() {
335         val parens = "(888) 8888888"
336         val allSpaces = "888 888 8888"
337         val withDash = "(888) 888-8888"
338         val allDashes = "888-888-8888"
339         val allDashesWithParen = "(888)-888-8888"
340         addMatcherTestResult(
341             expected = true,
342             parens,
343             checkForFalsePositives = false,
344             customFailureMessage = "should match if checkForFalsePositives is false"
345         )
346         addMatcherTestResult(expected = false, parens)
347         addMatcherTestResult(expected = false, allSpaces)
348         addMatcherTestResult(expected = false, withDash)
349         addMatcherTestResult(expected = false, allDashes)
350         addMatcherTestResult(expected = false, allDashesWithParen)
351     }
352 
353     @Test
testContainsOtp_dashesnull354     fun testContainsOtp_dashes() {
355         val oneDash = "G-3d523"
356         val manyDashes = "G-FD-745"
357         val tooManyDashes = "6--7893"
358         val oopsAllDashes = "------"
359         addMatcherTestResult(expected = true, oneDash)
360         addMatcherTestResult(expected = true, manyDashes)
361         addMatcherTestResult(expected = false, tooManyDashes)
362         addMatcherTestResult(expected = false, oopsAllDashes)
363     }
364 
365     @Test
testContainsOtp_startAndEndnull366     fun testContainsOtp_startAndEnd() {
367         val noSpaceStart = "your code isG-345821"
368         val noSpaceEnd = "your code is G-345821for real"
369         val numberSpaceStart = "your code is 4 G-345821"
370         val numberSpaceEnd = "your code is G-345821 3"
371         val colonStart = "your code is:G-345821"
372         val newLineStart = "your code is \nG-345821"
373         val quote = "your code is 'G-345821'"
374         val doubleQuote = "your code is \"G-345821\""
375         val bracketStart = "your code is [G-345821"
376         val ideographicStart = "your code is码G-345821"
377         val colonStartNumberPreceding = "your code is4:G-345821"
378         val periodEnd = "you code is G-345821."
379         val parens = "you code is (G-345821)"
380         val squareBrkt = "you code is [G-345821]"
381         val dashEnd = "you code is 'G-345821-'"
382         val randomSymbolEnd = "your code is G-345821$"
383         val underscoreEnd = "you code is 'G-345821_'"
384         val ideographicEnd = "your code is码G-345821码"
385         addMatcherTestResult(expected = false, noSpaceStart)
386         addMatcherTestResult(expected = false, noSpaceEnd)
387         addMatcherTestResult(expected = false, numberSpaceStart)
388         addMatcherTestResult(expected = false, numberSpaceEnd)
389         addMatcherTestResult(expected = false, colonStartNumberPreceding)
390         addMatcherTestResult(expected = false, dashEnd)
391         addMatcherTestResult(expected = false, underscoreEnd)
392         addMatcherTestResult(expected = false, randomSymbolEnd)
393         addMatcherTestResult(expected = true, colonStart)
394         addMatcherTestResult(expected = true, newLineStart)
395         addMatcherTestResult(expected = true, quote)
396         addMatcherTestResult(expected = true, doubleQuote)
397         addMatcherTestResult(expected = true, bracketStart)
398         addMatcherTestResult(expected = true, ideographicStart)
399         addMatcherTestResult(expected = true, periodEnd)
400         addMatcherTestResult(expected = true, parens)
401         addMatcherTestResult(expected = true, squareBrkt)
402         addMatcherTestResult(expected = true, ideographicEnd)
403     }
404 
405     @Test
testContainsOtp_lookaheadMustBeOtpCharnull406     fun testContainsOtp_lookaheadMustBeOtpChar() {
407         val validLookahead = "g4zy75"
408         val spaceLookahead = "GVRXY 2"
409         addMatcherTestResult(expected = true, validLookahead)
410         addMatcherTestResult(expected = false, spaceLookahead)
411     }
412 
413     @Test
testContainsOtp_threeDontMatch_withoutLanguageSpecificRegexnull414     fun testContainsOtp_threeDontMatch_withoutLanguageSpecificRegex() {
415         val tc = getTestTextClassifier(invalidLocale)
416         val threeLowercase = "34agb"
417         addMatcherTestResult(expected = false, threeLowercase, textClassifier = tc)
418     }
419 
420     @Test
testContainsOtp_englishSpecificRegexnull421     fun testContainsOtp_englishSpecificRegex() {
422         val tc = getTestTextClassifier(ULocale.ENGLISH)
423         val englishFalsePositive = "This is a false positive 4543"
424         val englishContextWords = listOf("login", "log in", "2fa", "authenticate", "auth",
425             "authentication", "tan", "password", "passcode", "two factor", "two-factor", "2factor",
426             "2 factor", "pin", "one time")
427         val englishContextWordsCase = listOf("LOGIN", "logIn", "LoGiN")
428         // Strings with a context word somewhere in the substring
429         val englishContextSubstrings = listOf("pins", "gaping", "backspin")
430         val codeInNextSentence = "context word: code. This sentence has the actual value of 434343"
431         val codeInNextSentenceTooFar =
432             "context word: code. ${"f".repeat(60)} This sentence has the actual value of 434343"
433         val codeTwoSentencesAfterContext = "context word: code. One sentence. actual value 34343"
434         val codeInSentenceBeforeContext = "34343 is a number. This number is a code"
435         val codeInSentenceAfterNewline = "your code is \n 34343"
436         val codeTooFarBeforeContext = "34343 ${"f".repeat(60)} code"
437 
438         addMatcherTestResult(expected = false, englishFalsePositive, textClassifier = tc)
439         for (context in englishContextWords) {
440             val englishTruePositive = "$context $englishFalsePositive"
441             addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc)
442         }
443         for (context in englishContextWordsCase) {
444             val englishTruePositive = "$context $englishFalsePositive"
445             addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc)
446         }
447         for (falseContext in englishContextSubstrings) {
448             val anotherFalsePositive = "$falseContext $englishFalsePositive"
449             addMatcherTestResult(expected = false, anotherFalsePositive, textClassifier = tc)
450         }
451         addMatcherTestResult(expected = true, codeInNextSentence, textClassifier = tc)
452         addMatcherTestResult(expected = true, codeInSentenceAfterNewline, textClassifier = tc)
453         addMatcherTestResult(expected = false, codeTwoSentencesAfterContext, textClassifier = tc)
454         addMatcherTestResult(expected = false, codeInSentenceBeforeContext, textClassifier = tc)
455         addMatcherTestResult(expected = false, codeInNextSentenceTooFar, textClassifier = tc)
456         addMatcherTestResult(expected = false, codeTooFarBeforeContext, textClassifier = tc)
457     }
458 
459     @Test
testContainsOtp_notificationFieldsCheckedIndividuallynull460     fun testContainsOtp_notificationFieldsCheckedIndividually() {
461         val tc = getTestTextClassifier(ULocale.ENGLISH)
462         // Together, the title and text will match the language-specific regex and the main regex,
463         // but apart, neither are enough
464         val notification = createNotification(text = "code", title = "434343")
465         addMatcherTestResult(expected = true, "code 434343")
466         addResult(expected = false, NotificationOtpDetectionHelper.containsOtp(notification, true,
467             tc), "Expected text of 'code' and title of '434343' not to match")
468     }
469 
470     @Test
testContainsOtp_multipleFalsePositivesnull471     fun testContainsOtp_multipleFalsePositives() {
472         val otp = "code 1543 code"
473         val longFp = "888-777-6666"
474         val shortFp = "34ess"
475         val multipleLongFp = "$longFp something something $longFp"
476         val multipleLongFpWithOtpBefore = "$otp $multipleLongFp"
477         val multipleLongFpWithOtpAfter = "$multipleLongFp $otp"
478         val multipleLongFpWithOtpBetween = "$longFp $otp $longFp"
479         val multipleShortFp = "$shortFp something something $shortFp"
480         val multipleShortFpWithOtpBefore = "$otp $multipleShortFp"
481         val multipleShortFpWithOtpAfter = "$otp $multipleShortFp"
482         val multipleShortFpWithOtpBetween = "$shortFp $otp $shortFp"
483         addMatcherTestResult(expected = false, multipleLongFp)
484         addMatcherTestResult(expected = false, multipleShortFp)
485         addMatcherTestResult(expected = true, multipleLongFpWithOtpBefore)
486         addMatcherTestResult(expected = true, multipleLongFpWithOtpAfter)
487         addMatcherTestResult(expected = true, multipleLongFpWithOtpBetween)
488         addMatcherTestResult(expected = true, multipleShortFpWithOtpBefore)
489         addMatcherTestResult(expected = true, multipleShortFpWithOtpAfter)
490         addMatcherTestResult(expected = true, multipleShortFpWithOtpBetween)
491     }
492 
493     @Test
testContainsOtpCode_falseIfNoLanguageSpecificRegexnull494     fun testContainsOtpCode_falseIfNoLanguageSpecificRegex() {
495         val tc = getTestTextClassifier(invalidLocale)
496         val text = "your one time code is 34343"
497         addMatcherTestResult(expected = false, text, textClassifier = tc)
498     }
499 
500     @Test
testContainsOtpCode_languageSpecificOverridesFalsePositivesExceptDatenull501     fun testContainsOtpCode_languageSpecificOverridesFalsePositivesExceptDate() {
502         // TC will detect an address, but the language-specific regex will be preferred
503         val tc = getTestTextClassifier(localeWithRegex, listOf(TextClassifier.TYPE_ADDRESS))
504         val date = "1-1-01"
505         // Dates should still be checked
506         addMatcherTestResult(expected = false, date, textClassifier = tc)
507         // A string with a code with three lowercase letters, and an excluded year
508         val withOtherFalsePositives = "your login code is abd4f 1985"
509         // Other false positive regular expressions should not be checked
510         addMatcherTestResult(expected = true, withOtherFalsePositives, textClassifier = tc)
511     }
512 
createNotificationnull513     private fun createNotification(
514         text: String? = "",
515         title: String? = "",
516         subtext: String? = "",
517         category: String? = "",
518         style: Notification.Style? = null,
519         publicVersion: Notification? = null
520     ): Notification {
521         val intent = Intent(Intent.ACTION_MAIN)
522         intent.setFlags(
523             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
524                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
525         )
526         intent.setAction(Intent.ACTION_MAIN)
527         intent.setPackage(context.packageName)
528 
529         val nb = Notification.Builder(context, "")
530         nb.setContentText(text)
531         nb.setContentTitle(title)
532         nb.setSubText(subtext)
533         nb.setCategory(category)
534         nb.setContentIntent(createTestPendingIntent())
535         if (style != null) {
536             nb.setStyle(style)
537         }
538         if (publicVersion != null) {
539             nb.setPublicVersion(publicVersion)
540         }
541         return nb.build()
542     }
543 
addMatcherTestResultnull544     private fun addMatcherTestResult(
545         expected: Boolean,
546         text: String,
547         checkForFalsePositives: Boolean = true,
548         textClassifier: TextClassifier? = null,
549         customFailureMessage: String? = null
550     ) {
551         val failureMessage = if (customFailureMessage != null) {
552             "$text $customFailureMessage"
553         } else if (expected) {
554             "$text should match"
555         } else {
556             "$text should not match"
557         }
558         addResult(expected = expected, NotificationOtpDetectionHelper.containsOtp(
559             createNotification(text), checkForFalsePositives, textClassifier), failureMessage)
560     }
561 
createTestPendingIntentnull562     private fun createTestPendingIntent(): PendingIntent {
563         val intent = Intent(Intent.ACTION_MAIN)
564         intent.setFlags(
565             Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
566                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
567         )
568         intent.setAction(Intent.ACTION_MAIN)
569         intent.setPackage(context.packageName)
570 
571         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE)
572     }
573 
574     // Creates a mock TextClassifier that will report back that text provided to it matches the
575     // given language codes (for language requests) and textClassifier entities (for links request)
getTestTextClassifiernull576     private fun getTestTextClassifier(
577         locale: ULocale?,
578         tcEntities: List<String>? = null
579     ): TextClassifier {
580         val tc = Mockito.mock(TextClassifier::class.java)
581         if (locale != null) {
582             Mockito.doReturn(
583                 TextLanguage.Builder().putLocale(locale, 0.9f).build()
584             ).`when`(tc).detectLanguage(any(TextLanguage.Request::class.java))
585         }
586 
587         val entityMap = mutableMapOf<String, Float>()
588         // to build the TextLinks, the entity map must have at least one item
589         entityMap[TextClassifier.TYPE_URL] = 0.01f
590         for (entity in tcEntities ?: emptyList()) {
591             entityMap[entity] = 0.9f
592         }
593         Mockito.doReturn(
594             TextLinks.Builder("").addLink(0, 1, entityMap)
595                 .build()
596         ).`when`(tc).generateLinks(any(TextLinks.Request::class.java))
597         return tc
598     }
599 }
600