From 8d1ed7746b8626ce4c80a9dcc239437c14c2dfb9 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:35:30 +0800 Subject: [PATCH 1/9] Update OtpDetector.kt --- .../prauga/messages/app/utils/OtpDetector.kt | 155 ++++++++++++------ 1 file changed, 105 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt index f62fa5a7..80842df0 100644 --- a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt +++ b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt @@ -33,7 +33,23 @@ class OtpDetector { "transaction code", "confirm code", "confirmation code", - "code" + "code", + "验证码", + "登录码", + "安全码", + "校验码", + "密码", + "动态码", + "一次性密码", + "授权码", + "访问码", + "重置码", + "交易码", + "确认码", + "认证码", + "OTP", + "2FA", + "MFA" ).map { it.lowercase() } private val safetyKeywords = listOf( @@ -46,12 +62,25 @@ class OtpDetector { "valid for", "expires in", "expires within", - "expires after" + "expires after", + "请勿分享", + "不要分享", + "切勿分享", + "请勿透露", + "请勿转发", + "保密", + "有效时间", + "有效期为", + "将在", + "内过期", + "后过期" ).map { it.lowercase() } private val moneyIndicators = listOf( "rs", "inr", "usd", "eur", "gbp", "₹", "$", "€", "£", "balance", - "amount", "debited", "credited", "txn", "transaction id", "order id" + "amount", "debited", "credited", "txn", "transaction id", "order id", + "人民币", "元", "¥", "金额", "余额", "转入", "转出", "交易", "订单", "交易号", "订单号", + "已扣除", "已入账", "支付", "收款" ).map { it.lowercase() } fun detect(rawMessage: String): OtpDetectionResult { @@ -127,54 +156,79 @@ class OtpDetector { input.replace(Regex("\\s+"), " ").trim() private fun extractCandidates(message: String): List { - val candidates = mutableListOf() - - // 1) Pure numeric chunks 3–10 digits - val numericRegex = Regex("\\b\\d{3,10}\\b") - numericRegex.findAll(message).forEach { match -> - val code = match.value - candidates += Candidate( - code = code, - startIndex = match.range.first, - endIndex = match.range.last, - isNumeric = true + val candidates = mutableMapOf() + + val patterns = listOf( + PatternInfo( + Regex("(\\p{IsHan}+)(\\d{3,10})"), + 2, + true, + 1.5 + ), + PatternInfo( + Regex("(^|\\s|\\p{P}|\\p{IsHan})(\\d{3,10})(\\p{P}|\\s|$|\\p{IsHan})"), + 2, + true, + 1.0 + ), + PatternInfo( + Regex("(^|\\s|\\p{P}|\\p{IsHan})(\\d{2,4}[\\s-]\\d{2,4})([\\s-]\\d{2,4})*(\\p{P}|\\s|$|\\p{IsHan})"), + 2, + true, + 2.0 + ), + PatternInfo( + Regex("(^|\\s|\\p{P}|\\p{IsHan})([0-9A-Za-z]{4,10})(\\p{P}|\\s|$|\\p{IsHan})"), + 2, + false, + 0.8 ) - } - - // 2) Numeric with a single space or dash (e.g., "123 456", "12-34-56") - val spacedRegex = Regex("\\b\\d{2,4}([\\s-]\\d{2,4})+\\b") - spacedRegex.findAll(message).forEach { match -> - val raw = match.value - val normalizedCode = raw.replace("[\\s-]".toRegex(), "") - // Avoid duplicating codes we already saw as a plain numeric chunk - if (normalizedCode.length in 4..8 && candidates.none { it.code == normalizedCode }) { - candidates += Candidate( - code = normalizedCode, - startIndex = match.range.first, - endIndex = match.range.last, - isNumeric = true - ) - } - } + ) - // 3) Alphanumeric tokens 4–10 chars, at least 2 digits - val alnumRegex = Regex("\\b[0-9A-Za-z]{4,10}\\b") - alnumRegex.findAll(message).forEach { match -> - val token = match.value - if (token.any { it.isDigit() } && token.count { it.isDigit() } >= 2) { - // Skip if it's purely numeric; we already captured those - if (!token.all { it.isDigit() }) { - candidates += Candidate( - code = token, - startIndex = match.range.first, - endIndex = match.range.last, - isNumeric = false - ) + for (patternInfo in patterns) { + patternInfo.regex.findAll(message).forEach { match -> + val code = match.groupValues[patternInfo.groupIndex] + val normalizedCode = code.replace("[\\s-]".toRegex(), "") + + if (isValidCandidate(normalizedCode, patternInfo.isNumeric)) { + val startIndex = match.range.first + match.groupValues[1].length + val endIndex = startIndex + code.length - 1 + + val existing = candidates[normalizedCode] + if (existing == null || patternInfo.priority > existing.score) { + candidates[normalizedCode] = Candidate( + code = normalizedCode, + startIndex = startIndex, + endIndex = endIndex, + isNumeric = patternInfo.isNumeric, + score = patternInfo.priority + ) + } } } } - return candidates + return candidates.values.toList() + } + + private data class PatternInfo( + val regex: Regex, + val groupIndex: Int, + val isNumeric: Boolean, + val priority: Double + ) + + private fun isValidCandidate(code: String, isNumeric: Boolean): Boolean { + val length = code.length + + if (isNumeric) { + return length in 3..10 + } else { + return length in 4..10 && + code.any { it.isDigit() } && + code.count { it.isDigit() } >= 2 && + !code.all { it.isDigit() } + } } private fun scoreCandidate( @@ -206,9 +260,8 @@ class OtpDetector { score -= 1.5 } - // Local context: the line containing the candidate + // Local context: line containing the candidate val lineInfo = extractLineContext(original, candidate.startIndex, candidate.endIndex) - val lineLower = lineInfo.line.lowercase() // If the line is mostly just the code -> strong hint val trimmedLine = lineInfo.line.trim() @@ -216,16 +269,17 @@ class OtpDetector { score += 2.5 } - // Typical OTP line patterns + // Typical OTP line patterns - support both English and Chinese if (Regex( - "(otp|code|password|passcode)", + "(otp|code|password|passcode|验证码|登录码|安全码|校验码|动态码|密码|一次性密码|授权码|认证码)", RegexOption.IGNORE_CASE ).containsMatchIn(lineInfo.line) ) { score += 2.0 } - if (Regex("(:|is|=)\\s*${Regex.escape(candidate.code)}").containsMatchIn(lineInfo.line)) { + // Support both English and Chinese separators + if (Regex("(:|is|=|是|为|:)\\s*${Regex.escape(candidate.code)}").containsMatchIn(lineInfo.line)) { score += 1.5 } @@ -348,3 +402,4 @@ class OtpDetector { return confidence.coerceIn(0.0, 1.0) } } + From e4799cc802b7fc678b5978236f488d056e769de6 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:35:53 +0800 Subject: [PATCH 2/9] Add files via upload --- app/src/main/res/values-zh/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/src/main/res/values-zh/strings.xml diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml new file mode 100644 index 00000000..f2cdca6e --- /dev/null +++ b/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,5 @@ + + + 复制验证码 %s + OTP %s 已复制到剪贴板 + From ffd55c8517ae65662f62e8189735e4f333db545b Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:37:41 +0800 Subject: [PATCH 3/9] Update conversation_list_item.xml --- .../res/layout/conversation_list_item.xml | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/res/layout/conversation_list_item.xml b/presentation/src/main/res/layout/conversation_list_item.xml index 6a805424..38719413 100644 --- a/presentation/src/main/res/layout/conversation_list_item.xml +++ b/presentation/src/main/res/layout/conversation_list_item.xml @@ -71,11 +71,34 @@ android:maxLines="2" android:minLines="2" app:layout_constraintBottom_toTopOf="@id/divider" - app:layout_constraintEnd_toStartOf="@id/scheduled" + app:layout_constraintEnd_toStartOf="@id/otpTag" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" tools:text="@tools:sample/lorem/random" /> + + + + Date: Tue, 27 Jan 2026 21:38:32 +0800 Subject: [PATCH 4/9] Update ConversationsAdapter.kt --- .../conversations/ConversationsAdapter.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index 07b16c13..664d946f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -31,6 +31,7 @@ import org.prauga.messages.common.base.QkRealmAdapter import org.prauga.messages.common.base.QkViewHolder import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.DateFormatter +import org.prauga.messages.common.util.OtpDetector import org.prauga.messages.common.util.extensions.resolveThemeColor import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.databinding.ConversationListItemBinding @@ -133,6 +134,32 @@ class ConversationsAdapter @Inject constructor( disposables.add(disposable) binding.pinned.isVisible = conversation.pinned + + // Check if the conversation contains OTP (One Time Password) + // 1. Initialize OTP detector + val otpDetector = OtpDetector() + // 2. Get message snippet, handle possible null value + val snippet = conversation.snippet ?: "" + // 3. Perform OTP detection + val otpResult = otpDetector.detect(snippet) + // 4. Show or hide OTP tag based on detection result + binding.otpTag.isVisible = otpResult.isOtp + + if (otpResult.isOtp) { + // Choose appropriate tag text based on language + val locale = context.resources.configuration.locales[0] + val otpText = if (locale.language == "zh") { + "验证码" // Show "验证码" for Chinese locale + } else { + "OTP" // Show "OTP" for other locales + } + binding.otpTag.text = otpText + + // Set OTP tag background and text color to match theme + val theme = colors.theme(recipient).theme + binding.otpTag.background.setTint(theme) + binding.otpTag.setTextColor(colors.theme(recipient).textPrimary) + } } override fun getItemId(position: Int): Long { @@ -160,3 +187,4 @@ class ConversationsAdapter @Inject constructor( } } + From b84b1096052b1a398a37d321ecc6b1719533fadb Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:39:15 +0800 Subject: [PATCH 5/9] Add files via upload --- presentation/src/main/res/drawable/otp_tag_background.xml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 presentation/src/main/res/drawable/otp_tag_background.xml diff --git a/presentation/src/main/res/drawable/otp_tag_background.xml b/presentation/src/main/res/drawable/otp_tag_background.xml new file mode 100644 index 00000000..e7ab3e35 --- /dev/null +++ b/presentation/src/main/res/drawable/otp_tag_background.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file From 21bb5aa8976aba5ca0397bff7b8e7249399e0eb7 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:54:16 +0800 Subject: [PATCH 6/9] Update OtpDetector.kt --- .../prauga/messages/app/utils/OtpDetector.kt | 159 +++++++----------- 1 file changed, 61 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt index 80842df0..a0c0943a 100644 --- a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt +++ b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt @@ -37,19 +37,10 @@ class OtpDetector { "验证码", "登录码", "安全码", - "校验码", - "密码", "动态码", "一次性密码", - "授权码", - "访问码", - "重置码", - "交易码", - "确认码", - "认证码", - "OTP", - "2FA", - "MFA" + "二次验证码", + "两步验证" ).map { it.lowercase() } private val safetyKeywords = listOf( @@ -64,23 +55,17 @@ class OtpDetector { "expires within", "expires after", "请勿分享", - "不要分享", "切勿分享", - "请勿透露", - "请勿转发", + "不要分享", "保密", - "有效时间", - "有效期为", + "有效期", "将在", - "内过期", - "后过期" + "分钟内过期" ).map { it.lowercase() } private val moneyIndicators = listOf( "rs", "inr", "usd", "eur", "gbp", "₹", "$", "€", "£", "balance", - "amount", "debited", "credited", "txn", "transaction id", "order id", - "人民币", "元", "¥", "金额", "余额", "转入", "转出", "交易", "订单", "交易号", "订单号", - "已扣除", "已入账", "支付", "收款" + "amount", "debited", "credited", "txn", "transaction id", "order id" ).map { it.lowercase() } fun detect(rawMessage: String): OtpDetectionResult { @@ -94,11 +79,14 @@ class OtpDetector { val hasOtpKeyword = otpKeywords.any { lower.contains(it) } val hasSafetyKeyword = safetyKeywords.any { lower.contains(it) } + + // Check if it contains characters related to Chinese CAPTCHAs + val hasChineseOtpChars = lower.contains("验证码") || lower.contains("登录") || lower.contains("码") val candidates = extractCandidates(normalized) if (candidates.isEmpty()) { - val reason = if (hasOtpKeyword) { + val reason = if (hasOtpKeyword || hasChineseOtpChars) { "Contains OTP-like keywords but no numeric/alphanumeric candidate code found" } else { "No OTP-like keywords and no candidate code found" @@ -115,7 +103,7 @@ class OtpDetector { val best = scored.maxByOrNull { it.score }!! - val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword) + val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword, hasChineseOtpChars) val isOtp = globalConfidence >= 0.6 @@ -156,79 +144,54 @@ class OtpDetector { input.replace(Regex("\\s+"), " ").trim() private fun extractCandidates(message: String): List { - val candidates = mutableMapOf() - - val patterns = listOf( - PatternInfo( - Regex("(\\p{IsHan}+)(\\d{3,10})"), - 2, - true, - 1.5 - ), - PatternInfo( - Regex("(^|\\s|\\p{P}|\\p{IsHan})(\\d{3,10})(\\p{P}|\\s|$|\\p{IsHan})"), - 2, - true, - 1.0 - ), - PatternInfo( - Regex("(^|\\s|\\p{P}|\\p{IsHan})(\\d{2,4}[\\s-]\\d{2,4})([\\s-]\\d{2,4})*(\\p{P}|\\s|$|\\p{IsHan})"), - 2, - true, - 2.0 - ), - PatternInfo( - Regex("(^|\\s|\\p{P}|\\p{IsHan})([0-9A-Za-z]{4,10})(\\p{P}|\\s|$|\\p{IsHan})"), - 2, - false, - 0.8 + val candidates = mutableListOf() + + // 1) Pure numeric chunks 3–10 digits (with word boundary support for Chinese) + val numericRegex = Regex("(?:\\b|^|(?<=[^0-9]))\\d{3,10}(?:\\b|$|(?=[^0-9]))") + numericRegex.findAll(message).forEach { match -> + val code = match.value + candidates += Candidate( + code = code, + startIndex = match.range.first, + endIndex = match.range.last, + isNumeric = true ) - ) + } - for (patternInfo in patterns) { - patternInfo.regex.findAll(message).forEach { match -> - val code = match.groupValues[patternInfo.groupIndex] - val normalizedCode = code.replace("[\\s-]".toRegex(), "") - - if (isValidCandidate(normalizedCode, patternInfo.isNumeric)) { - val startIndex = match.range.first + match.groupValues[1].length - val endIndex = startIndex + code.length - 1 - - val existing = candidates[normalizedCode] - if (existing == null || patternInfo.priority > existing.score) { - candidates[normalizedCode] = Candidate( - code = normalizedCode, - startIndex = startIndex, - endIndex = endIndex, - isNumeric = patternInfo.isNumeric, - score = patternInfo.priority - ) - } - } + // 2) Numeric with a single space or dash (e.g., "123 456", "12-34-56") + val spacedRegex = Regex("\\b\\d{2,4}([\\s-]\\d{2,4})+\\b") + spacedRegex.findAll(message).forEach { match -> + val raw = match.value + val normalizedCode = raw.replace("[\\s-]".toRegex(), "") + // Avoid duplicating codes we already saw as a plain numeric chunk + if (normalizedCode.length in 4..8 && candidates.none { it.code == normalizedCode }) { + candidates += Candidate( + code = normalizedCode, + startIndex = match.range.first, + endIndex = match.range.last, + isNumeric = true + ) } } - return candidates.values.toList() - } - - private data class PatternInfo( - val regex: Regex, - val groupIndex: Int, - val isNumeric: Boolean, - val priority: Double - ) - - private fun isValidCandidate(code: String, isNumeric: Boolean): Boolean { - val length = code.length - - if (isNumeric) { - return length in 3..10 - } else { - return length in 4..10 && - code.any { it.isDigit() } && - code.count { it.isDigit() } >= 2 && - !code.all { it.isDigit() } + // 3) Alphanumeric tokens 4–10 chars, at least 2 digits + val alnumRegex = Regex("\\b[0-9A-Za-z]{4,10}\\b") + alnumRegex.findAll(message).forEach { match -> + val token = match.value + if (token.any { it.isDigit() } && token.count { it.isDigit() } >= 2) { + // Skip if it's purely numeric; we already captured those + if (!token.all { it.isDigit() }) { + candidates += Candidate( + code = token, + startIndex = match.range.first, + endIndex = match.range.last, + isNumeric = false + ) + } + } } + + return candidates } private fun scoreCandidate( @@ -260,8 +223,9 @@ class OtpDetector { score -= 1.5 } - // Local context: line containing the candidate + // Local context: the line containing the candidate val lineInfo = extractLineContext(original, candidate.startIndex, candidate.endIndex) + val lineLower = lineInfo.line.lowercase() // If the line is mostly just the code -> strong hint val trimmedLine = lineInfo.line.trim() @@ -269,17 +233,16 @@ class OtpDetector { score += 2.5 } - // Typical OTP line patterns - support both English and Chinese + // Typical OTP line patterns if (Regex( - "(otp|code|password|passcode|验证码|登录码|安全码|校验码|动态码|密码|一次性密码|授权码|认证码)", + "(otp|code|password|passcode)", RegexOption.IGNORE_CASE ).containsMatchIn(lineInfo.line) ) { score += 2.0 } - // Support both English and Chinese separators - if (Regex("(:|is|=|是|为|:)\\s*${Regex.escape(candidate.code)}").containsMatchIn(lineInfo.line)) { + if (Regex("(:|is|=)\\s*${Regex.escape(candidate.code)}").containsMatchIn(lineInfo.line)) { score += 1.5 } @@ -389,17 +352,17 @@ class OtpDetector { private fun computeGlobalConfidence( best: Candidate, hasOtpKeyword: Boolean, - hasSafetyKeyword: Boolean + hasSafetyKeyword: Boolean, + hasChineseOtpChars: Boolean ): Double { var confidence = 0.0 // Base on score; tuned experimentally confidence += (best.score / 8.0).coerceIn(0.0, 1.0) - if (hasOtpKeyword) confidence += 0.15 + if (hasOtpKeyword || hasChineseOtpChars) confidence += 0.15 if (hasSafetyKeyword) confidence += 0.15 return confidence.coerceIn(0.0, 1.0) } } - From 9d2037d2999a4b50ec24cc20d1484a0b02f976fe Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:29:08 +0800 Subject: [PATCH 7/9] Update OtpDetector.kt --- .../prauga/messages/app/utils/OtpDetector.kt | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt index a0c0943a..f62fa5a7 100644 --- a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt +++ b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt @@ -33,14 +33,7 @@ class OtpDetector { "transaction code", "confirm code", "confirmation code", - "code", - "验证码", - "登录码", - "安全码", - "动态码", - "一次性密码", - "二次验证码", - "两步验证" + "code" ).map { it.lowercase() } private val safetyKeywords = listOf( @@ -53,14 +46,7 @@ class OtpDetector { "valid for", "expires in", "expires within", - "expires after", - "请勿分享", - "切勿分享", - "不要分享", - "保密", - "有效期", - "将在", - "分钟内过期" + "expires after" ).map { it.lowercase() } private val moneyIndicators = listOf( @@ -79,14 +65,11 @@ class OtpDetector { val hasOtpKeyword = otpKeywords.any { lower.contains(it) } val hasSafetyKeyword = safetyKeywords.any { lower.contains(it) } - - // Check if it contains characters related to Chinese CAPTCHAs - val hasChineseOtpChars = lower.contains("验证码") || lower.contains("登录") || lower.contains("码") val candidates = extractCandidates(normalized) if (candidates.isEmpty()) { - val reason = if (hasOtpKeyword || hasChineseOtpChars) { + val reason = if (hasOtpKeyword) { "Contains OTP-like keywords but no numeric/alphanumeric candidate code found" } else { "No OTP-like keywords and no candidate code found" @@ -103,7 +86,7 @@ class OtpDetector { val best = scored.maxByOrNull { it.score }!! - val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword, hasChineseOtpChars) + val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword) val isOtp = globalConfidence >= 0.6 @@ -146,8 +129,8 @@ class OtpDetector { private fun extractCandidates(message: String): List { val candidates = mutableListOf() - // 1) Pure numeric chunks 3–10 digits (with word boundary support for Chinese) - val numericRegex = Regex("(?:\\b|^|(?<=[^0-9]))\\d{3,10}(?:\\b|$|(?=[^0-9]))") + // 1) Pure numeric chunks 3–10 digits + val numericRegex = Regex("\\b\\d{3,10}\\b") numericRegex.findAll(message).forEach { match -> val code = match.value candidates += Candidate( @@ -352,15 +335,14 @@ class OtpDetector { private fun computeGlobalConfidence( best: Candidate, hasOtpKeyword: Boolean, - hasSafetyKeyword: Boolean, - hasChineseOtpChars: Boolean + hasSafetyKeyword: Boolean ): Double { var confidence = 0.0 // Base on score; tuned experimentally confidence += (best.score / 8.0).coerceIn(0.0, 1.0) - if (hasOtpKeyword || hasChineseOtpChars) confidence += 0.15 + if (hasOtpKeyword) confidence += 0.15 if (hasSafetyKeyword) confidence += 0.15 return confidence.coerceIn(0.0, 1.0) From 98fe06df015ddbc3e5fe715c5e4e09c101b15486 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:37:54 +0800 Subject: [PATCH 8/9] Update OtpDetector.kt --- .../prauga/messages/app/utils/OtpDetector.kt | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt index f62fa5a7..a0c0943a 100644 --- a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt +++ b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt @@ -33,7 +33,14 @@ class OtpDetector { "transaction code", "confirm code", "confirmation code", - "code" + "code", + "验证码", + "登录码", + "安全码", + "动态码", + "一次性密码", + "二次验证码", + "两步验证" ).map { it.lowercase() } private val safetyKeywords = listOf( @@ -46,7 +53,14 @@ class OtpDetector { "valid for", "expires in", "expires within", - "expires after" + "expires after", + "请勿分享", + "切勿分享", + "不要分享", + "保密", + "有效期", + "将在", + "分钟内过期" ).map { it.lowercase() } private val moneyIndicators = listOf( @@ -65,11 +79,14 @@ class OtpDetector { val hasOtpKeyword = otpKeywords.any { lower.contains(it) } val hasSafetyKeyword = safetyKeywords.any { lower.contains(it) } + + // Check if it contains characters related to Chinese CAPTCHAs + val hasChineseOtpChars = lower.contains("验证码") || lower.contains("登录") || lower.contains("码") val candidates = extractCandidates(normalized) if (candidates.isEmpty()) { - val reason = if (hasOtpKeyword) { + val reason = if (hasOtpKeyword || hasChineseOtpChars) { "Contains OTP-like keywords but no numeric/alphanumeric candidate code found" } else { "No OTP-like keywords and no candidate code found" @@ -86,7 +103,7 @@ class OtpDetector { val best = scored.maxByOrNull { it.score }!! - val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword) + val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword, hasChineseOtpChars) val isOtp = globalConfidence >= 0.6 @@ -129,8 +146,8 @@ class OtpDetector { private fun extractCandidates(message: String): List { val candidates = mutableListOf() - // 1) Pure numeric chunks 3–10 digits - val numericRegex = Regex("\\b\\d{3,10}\\b") + // 1) Pure numeric chunks 3–10 digits (with word boundary support for Chinese) + val numericRegex = Regex("(?:\\b|^|(?<=[^0-9]))\\d{3,10}(?:\\b|$|(?=[^0-9]))") numericRegex.findAll(message).forEach { match -> val code = match.value candidates += Candidate( @@ -335,14 +352,15 @@ class OtpDetector { private fun computeGlobalConfidence( best: Candidate, hasOtpKeyword: Boolean, - hasSafetyKeyword: Boolean + hasSafetyKeyword: Boolean, + hasChineseOtpChars: Boolean ): Double { var confidence = 0.0 // Base on score; tuned experimentally confidence += (best.score / 8.0).coerceIn(0.0, 1.0) - if (hasOtpKeyword) confidence += 0.15 + if (hasOtpKeyword || hasChineseOtpChars) confidence += 0.15 if (hasSafetyKeyword) confidence += 0.15 return confidence.coerceIn(0.0, 1.0) From 3b97a52cc7f4539a6f1d73bd70da0fd8f6cebb40 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Wed, 28 Jan 2026 01:29:24 +0800 Subject: [PATCH 9/9] Update OtpDetector.kt --- .../prauga/messages/app/utils/OtpDetector.kt | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt index a0c0943a..77778952 100644 --- a/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt +++ b/app/src/main/java/org/prauga/messages/app/utils/OtpDetector.kt @@ -8,7 +8,8 @@ data class OtpDetectionResult( val isOtp: Boolean, val code: String?, val confidence: Double, - val reason: String + val reason: String, + val isParcel: Boolean = false ) class OtpDetector { @@ -42,6 +43,18 @@ class OtpDetector { "二次验证码", "两步验证" ).map { it.lowercase() } + + private val parcelKeywords = listOf( + "collection code", + "pickup code", + "collect code", + "use code", + "code", + "取件码", + "提取码", + "凭码", + "收集码" + ).map { it.lowercase() } private val safetyKeywords = listOf( "do not share", @@ -82,14 +95,18 @@ class OtpDetector { // Check if it contains characters related to Chinese CAPTCHAs val hasChineseOtpChars = lower.contains("验证码") || lower.contains("登录") || lower.contains("码") + + // Check if it contains parcel-related keywords or characters + val hasParcelKeyword = parcelKeywords.any { lower.contains(it) } + val hasChineseParcelChars = lower.contains("取件码") || lower.contains("提取码") || lower.contains("凭码") val candidates = extractCandidates(normalized) if (candidates.isEmpty()) { - val reason = if (hasOtpKeyword || hasChineseOtpChars) { - "Contains OTP-like keywords but no numeric/alphanumeric candidate code found" - } else { - "No OTP-like keywords and no candidate code found" + val reason = when { + hasOtpKeyword || hasChineseOtpChars -> "Contains OTP-like keywords but no numeric/alphanumeric candidate code found" + hasParcelKeyword || hasChineseParcelChars -> "Contains parcel-like keywords but no numeric/alphanumeric candidate code found" + else -> "No OTP-like keywords and no candidate code found" } return OtpDetectionResult(false, null, 0.1, reason) } @@ -103,7 +120,8 @@ class OtpDetector { val best = scored.maxByOrNull { it.score }!! - val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword, hasChineseOtpChars) + val isParcel = hasParcelKeyword || hasChineseParcelChars + val globalConfidence = computeGlobalConfidence(best, hasOtpKeyword, hasSafetyKeyword, hasChineseOtpChars, isParcel) val isOtp = globalConfidence >= 0.6 @@ -116,7 +134,7 @@ class OtpDetector { }. " ) append( - "HasOtpKeyword=$hasOtpKeyword, HasSafetyKeyword=$hasSafetyKeyword, GlobalConfidence=${ + "HasOtpKeyword=$hasOtpKeyword, HasSafetyKeyword=$hasSafetyKeyword, IsParcel=$isParcel, GlobalConfidence=${ "%.2f".format( globalConfidence ) @@ -126,9 +144,10 @@ class OtpDetector { return OtpDetectionResult( isOtp = isOtp, - code = if (isOtp) best.code else null, + code = if (isOtp || isParcel) best.code else null, confidence = globalConfidence, - reason = reason + reason = reason, + isParcel = isParcel ) } @@ -353,7 +372,8 @@ class OtpDetector { best: Candidate, hasOtpKeyword: Boolean, hasSafetyKeyword: Boolean, - hasChineseOtpChars: Boolean + hasChineseOtpChars: Boolean, + isParcel: Boolean ): Double { var confidence = 0.0 @@ -362,6 +382,7 @@ class OtpDetector { if (hasOtpKeyword || hasChineseOtpChars) confidence += 0.15 if (hasSafetyKeyword) confidence += 0.15 + if (isParcel) confidence += 0.15 return confidence.coerceIn(0.0, 1.0) }