Skip to content

Commit d702f99

Browse files
committed
fix(i18n): name unsafe links by kind, not by the threat feed's code
The phishing and screened-message notifications joined each flagged link's threat detail with a hard-coded ", " and an English "malware" fallback, so users read feed codes like verified_phish, feed_domain_match or known_spam_domain (found by the refuting review). urlThreatLabels() now names each link by kind in the app language (phishing, malware, spam, otherwise dangerous), once per kind, joined with the language's separator. The phishing notification takes the kind as its "contains a %s link" word; the screened-message notification wraps it as "phishing link". The local spam-domain threat gets a named constant, and the zh-rCN phishing text no longer credits URLhaus for feeds that aren't URLhaus. zh-rCN floor ratchets to 78.9. Tests: LocalizedSystemTextTest adds the zh-rCN labels for all four kinds from five feed codes. SmsReceiverRobolectricTest asserted the phishing notification contained the raw "known_spam_domain" code, which is the defect itself; it now expects the "spam" label and rejects the raw code.
1 parent 09ed569 commit d702f99

11 files changed

Lines changed: 81 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ The rest of this section takes effect when the report Worker is next deployed.
494494
phrase in the app language ("Possible neighbor spoofing", "Risky link
495495
pattern"), listed once even when several links trip it, and each check
496496
goes by the name the block log uses.
497+
- Warnings about an unsafe link say what kind it is (phishing, malware,
498+
spam) in the app language. They used to print the threat feed's own code,
499+
such as verified_phish or feed_domain_match.
497500
- The caller-identity evidence an ML block can carry ("PASSporT attestation
498501
A metadata; DNO-listed origin") is translated as well, so a Chinese block
499502
log no longer switches to English halfway through an ML entry.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ language in [issue #7](https://github.com/SysAdminDoc/CallShield/issues/7).
510510
| URL Safety | Local spam-domain data; optional URLhaus (abuse.ch) |
511511
| Verification | Local Gradle, lint, and release-artifact checks |
512512
| Tests | 1100 JVM unit tests (JUnit) |
513-
| Strings | 1562 string resources and 37 plural groups (translation-ready) |
513+
| Strings | 1567 string resources and 37 plural groups (translation-ready) |
514514
| Accessibility | 100+ content descriptions, 48dp touch targets |
515515
| Min SDK | 29 (Android 10) |
516516
| Target SDK | 36 |

‎app/src/main/java/com/sysadmindoc/callshield/data/remote/UrlSafetyChecker.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1515
* service can never add latency to call or SMS interception.
1616
*/
1717
object UrlSafetyChecker {
18+
/** The threat a link on the downloaded spam-domain list carries. */
19+
internal const val KNOWN_SPAM_DOMAIN_THREAT = "known_spam_domain"
20+
1821
data class UrlCheckResult(
1922
val url: String,
2023
val isMalicious: Boolean,
@@ -155,7 +158,7 @@ object UrlSafetyChecker {
155158
return UrlCheckResult(
156159
url = normalizeLocalResultUrl(url, stripQuery),
157160
isMalicious = true,
158-
threat = "known_spam_domain",
161+
threat = KNOWN_SPAM_DOMAIN_THREAT,
159162
tags = listOf("local_spam_domain"),
160163
source = UrlThreatSource.LOCAL_SPAM_DOMAINS,
161164
sourceVersion = UrlThreatSource.LOCAL_SPAM_DOMAINS.defaultVersion,

‎app/src/main/java/com/sysadmindoc/callshield/service/RcsNotificationListener.kt‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.os.Bundle
99
import android.service.notification.NotificationListenerService
1010
import android.service.notification.StatusBarNotification
1111
import androidx.datastore.preferences.core.Preferences
12+
import com.sysadmindoc.callshield.R
1213
import com.sysadmindoc.callshield.data.MeetingModeRegistry
1314
import com.sysadmindoc.callshield.data.MessageCapabilityDetector
1415
import com.sysadmindoc.callshield.data.NotificationScreeningCategory
@@ -24,6 +25,7 @@ import com.sysadmindoc.callshield.domain.usecase.CheckSpamSmsUseCase
2425
import com.sysadmindoc.callshield.ui.joinSignalLabels
2526
import com.sysadmindoc.callshield.ui.reasonCodeLabelRes
2627
import com.sysadmindoc.callshield.ui.signalLabel
28+
import com.sysadmindoc.callshield.ui.urlThreatLabels
2729
import com.sysadmindoc.callshield.util.filterAsciiDigits
2830
import kotlinx.coroutines.CoroutineScope
2931
import kotlinx.coroutines.Dispatchers
@@ -315,7 +317,7 @@ class RcsNotificationListener : NotificationListenerService() {
315317
allowRemoteLookup = allowRemoteLookup,
316318
)
317319
if (malicious.isNotEmpty()) {
318-
val threats = malicious.joinToString(", ") { it.threat.ifEmpty { "malware" } }
320+
val threats = urlThreatLabels(applicationContext, malicious)
319321
if (source.category == NotificationScreeningCategory.RCS && filterAsciiDigits(sender).length >= 7) {
320322
NotificationHelper.notifyPhishingUrl(applicationContext, sender, threats)
321323
} else {
@@ -324,7 +326,7 @@ class RcsNotificationListener : NotificationListenerService() {
324326
sourceName = source.stableName,
325327
sender = sender,
326328
confidence = 100,
327-
reason = threats,
329+
reason = applicationContext.getString(R.string.url_threat_signal, threats),
328330
)
329331
}
330332
}

‎app/src/main/java/com/sysadmindoc/callshield/service/SmsReceiver.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.sysadmindoc.callshield.data.remote.UrlSafetyChecker
1212
import com.sysadmindoc.callshield.data.repository.SpamRepositoryAdapter
1313
import com.sysadmindoc.callshield.di.ApplicationScope
1414
import com.sysadmindoc.callshield.domain.usecase.CheckSpamSmsUseCase
15+
import com.sysadmindoc.callshield.ui.urlThreatLabels
1516
import dagger.hilt.android.AndroidEntryPoint
1617
import kotlinx.coroutines.CoroutineScope
1718
import kotlinx.coroutines.launch
@@ -184,8 +185,7 @@ class SmsReceiver : BroadcastReceiver() {
184185
allowRemoteLookup = remoteUrlLookupEnabled,
185186
)
186187
if (maliciousUrls.isNotEmpty()) {
187-
val threats = maliciousUrls.joinToString(", ") { it.threat.ifEmpty { "malware" } }
188-
NotificationHelper.notifyPhishingUrl(appContext, sender, threats)
188+
NotificationHelper.notifyPhishingUrl(appContext, sender, urlThreatLabels(appContext, maliciousUrls))
189189
}
190190
} catch (_: Exception) {
191191
// URL check is best-effort

‎app/src/main/java/com/sysadmindoc/callshield/ui/MatchReasonLabels.kt‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.sysadmindoc.callshield.ui
33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.res.stringResource
55
import com.sysadmindoc.callshield.R
6+
import com.sysadmindoc.callshield.data.remote.UrlSafetyChecker
7+
import com.sysadmindoc.callshield.data.remote.UrlThreatCategory
68
import com.sysadmindoc.callshield.domain.model.BlockReasonCode
79
import com.sysadmindoc.callshield.domain.model.CallerIdentity
810
import com.sysadmindoc.callshield.domain.model.CallerIdentitySignals
@@ -82,6 +84,28 @@ fun signalLabel(
8284
*/
8385
fun List<String>.joinSignalLabels(context: android.content.Context): String = distinct().joinToString(context.getString(R.string.signal_list_separator))
8486

87+
/**
88+
* What the unsafe links in a message are, in the app language, each kind
89+
* once ("phishing, malware"). It fills the "contains a %s link" slot of the
90+
* phishing notification. The feeds' own detail codes ("verified_phish",
91+
* "feed_domain_match") never reach the user.
92+
*/
93+
fun urlThreatLabels(
94+
context: android.content.Context,
95+
results: List<UrlSafetyChecker.UrlCheckResult>,
96+
): String =
97+
results
98+
.map { result ->
99+
context.getString(
100+
when {
101+
result.threat == UrlSafetyChecker.KNOWN_SPAM_DOMAIN_THREAT -> R.string.url_threat_spam
102+
result.category == UrlThreatCategory.PHISHING -> R.string.url_threat_phishing
103+
result.category == UrlThreatCategory.MALWARE -> R.string.url_threat_malware
104+
else -> R.string.url_threat_dangerous
105+
},
106+
)
107+
}.joinSignalLabels(context)
108+
85109
/**
86110
* The carrier identity evidence behind an ML verdict, in the app language, or
87111
* null when the call carried none.

‎app/src/main/res/values-zh-rCN/strings.xml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,9 +936,14 @@
936936
<string name="notif_type_sms">短信</string>
937937
<string name="notif_action_block_forever">永久拦截</string>
938938
<string name="notif_action_report">举报</string>
939+
<string name="url_threat_phishing">钓鱼</string>
940+
<string name="url_threat_malware">恶意软件</string>
941+
<string name="url_threat_spam">垃圾信息</string>
942+
<string name="url_threat_dangerous">危险</string>
943+
<string name="url_threat_signal">%1$s链接</string>
939944
<string name="notif_phishing_title">检测到钓鱼链接</string>
940945
<string name="notif_phishing_text">来自 %1$s 的短信包含一条 %2$s 链接</string>
941-
<string name="notif_phishing_big_text">来自 %1$s 的短信包含一条被 URLhaus 标记为 %2$s 的链接。请勿点击此消息中的任何链接。</string>
946+
<string name="notif_phishing_big_text">来自 %1$s 的短信包含一条被威胁情报源标记为 %2$s 的链接。请勿点击此消息中的任何链接。</string>
942947
<string name="notif_status_title">CallShield 已启用</string>
943948
<string name="notif_status_text">正在防护垃圾通话与短信</string>
944949
<string name="notif_role_lost_title">来电筛查已关闭</string>

‎app/src/main/res/values/strings.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,12 @@
13701370
<string name="notif_type_sms">SMS</string>
13711371
<string name="notif_action_block_forever">Block forever</string>
13721372
<string name="notif_action_report">Report</string>
1373+
<!-- Kinds of unsafe link; each fills "contains a %s link" and "flagged as %s". -->
1374+
<string name="url_threat_phishing">phishing</string>
1375+
<string name="url_threat_malware">malware</string>
1376+
<string name="url_threat_spam">spam</string>
1377+
<string name="url_threat_dangerous">dangerous</string>
1378+
<string name="url_threat_signal">%1$s link</string>
13731379
<string name="notif_phishing_title">Phishing URL detected</string>
13741380
<string name="notif_phishing_text">SMS from %1$s contains a %2$s link</string>
13751381
<string name="notif_phishing_big_text">An SMS from %1$s contains a URL flagged by a threat feed as %2$s. Do not tap any links in this message.</string>

‎app/src/test/java/com/sysadmindoc/callshield/LocalizedSystemTextTest.kt‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import com.sysadmindoc.callshield.data.checker.SmsContentChecker
1717
import com.sysadmindoc.callshield.data.checker.StirShakenChecker
1818
import com.sysadmindoc.callshield.data.checker.TimeBlockChecker
1919
import com.sysadmindoc.callshield.data.checker.VerificationMessageFloorChecker
20+
import com.sysadmindoc.callshield.data.remote.UrlSafetyChecker
21+
import com.sysadmindoc.callshield.data.remote.UrlThreatCategory
2022
import com.sysadmindoc.callshield.domain.model.BlockReasonCode
2123
import com.sysadmindoc.callshield.domain.model.CallerIdentity
2224
import com.sysadmindoc.callshield.domain.model.DnoStatus
@@ -27,6 +29,7 @@ import com.sysadmindoc.callshield.domain.model.SpamCheckResult
2729
import com.sysadmindoc.callshield.service.CallShieldTileService
2830
import com.sysadmindoc.callshield.service.RcsNotificationListener
2931
import com.sysadmindoc.callshield.ui.describeIdentityEvidence
32+
import com.sysadmindoc.callshield.ui.urlThreatLabels
3033
import kotlinx.coroutines.runBlocking
3134
import org.junit.Assert.assertEquals
3235
import org.junit.Assert.assertFalse
@@ -195,6 +198,28 @@ class LocalizedSystemTextTest {
195198
)
196199
}
197200

201+
@Test
202+
fun `unsafe links are named by kind, never by a feed's code`() {
203+
fun link(
204+
threat: String,
205+
category: UrlThreatCategory = UrlThreatCategory.UNKNOWN,
206+
) = UrlSafetyChecker.UrlCheckResult(url = "https://$threat.example", isMalicious = true, threat = threat, category = category)
207+
208+
val labels =
209+
urlThreatLabels(
210+
context,
211+
listOf(
212+
link("verified_phish", UrlThreatCategory.PHISHING),
213+
link(UrlSafetyChecker.KNOWN_SPAM_DOMAIN_THREAT),
214+
link("malware_download", UrlThreatCategory.MALWARE),
215+
link("feed_domain_match"),
216+
link("unverified_phish", UrlThreatCategory.PHISHING),
217+
),
218+
)
219+
220+
assertEquals("钓鱼、垃圾信息、恶意软件、危险", labels)
221+
}
222+
198223
private fun passport(attestation: String) =
199224
ParsedPassport(
200225
typ = "passport",

‎app/src/test/java/com/sysadmindoc/callshield/service/SmsReceiverRobolectricTest.kt‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import kotlinx.coroutines.runBlocking
2222
import kotlinx.coroutines.withTimeout
2323
import org.junit.After
2424
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertFalse
2526
import org.junit.Assert.assertTrue
2627
import org.junit.Before
2728
import org.junit.Test
@@ -94,12 +95,10 @@ class SmsReceiverRobolectricTest {
9495
context.getString(com.sysadmindoc.callshield.R.string.notif_phishing_title),
9596
notification.extras.getString(Notification.EXTRA_TITLE),
9697
)
97-
assertTrue(
98-
notification.extras
99-
.getString(Notification.EXTRA_TEXT)
100-
.orEmpty()
101-
.contains("known_spam_domain"),
102-
)
98+
// Named by kind in the app language, never by the feed's own code.
99+
val text = notification.extras.getString(Notification.EXTRA_TEXT).orEmpty()
100+
assertTrue(text, text.contains(context.getString(com.sysadmindoc.callshield.R.string.url_threat_spam)))
101+
assertFalse(text, text.contains("known_spam_domain"))
103102
}
104103

105104
private fun awaitPhishingNotification(): Notification =

0 commit comments

Comments
 (0)