移除验证码解析功能,简化为纯短信转发

- 删除 VerificationCodeParser 及相关测试,短信捕获和推送不再解析验证码
- 飞书推送改为只发送短信原文,时间戳格式化为可读日期
- 移除主界面"只推送验证码"开关和"调试时上传完整短信正文"选项
- 移除"保存轮询间隔"按钮,开启轮询时自动保存间隔(未输入默认1秒)
- 按钮文字从"开始1秒轮询验证码"改为"开始短信轮询"
- 删除"打印最近30条短信"功能及相关 SmsInboxReader.logRecentMessages
- SmsInboxReader 用 RecentSmsResult 替换 RecentCodeResult
- FeishuWebhookConfigStore.Config 移除 filterVerificationCode/sendFullBodyDebug
- 修复代码缩进不一致问题
This commit is contained in:
2026-05-18 22:38:06 +08:00
parent 95a3c6d8c4
commit c5ef726134
27 changed files with 273 additions and 697 deletions
@@ -13,10 +13,11 @@ public final class SmsProviderInstrumentedTest {
private static final String TAG = "[SMS]SmsReceive";
@Test
public void testLogRecentThirtyMessages() {
public void testReadLatestInboxQueryCompletes() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
int count = SmsInboxReader.logRecentMessages(context, 30);
Log.d(TAG, "SmsProviderInstrumentedTest.testLogRecentThirtyMessages count=" + count);
assertTrue("Expected query to complete. Count can be 0 if SMS provider is empty.", count >= 0);
SmsInboxReader.InboxResult result = SmsInboxReader.readLatest(context);
Log.d(TAG, "SmsProviderInstrumentedTest.testReadLatestInboxQueryCompletes success=" + result.success
+ ", reason=" + result.failureReason);
assertTrue("Expected inbox query to return a structured result.", result.success || result.failureReason.length() > 0);
}
}
@@ -53,13 +53,12 @@ public final class FeishuWebhookClient {
if (context == null || result == null) {
return;
}
Context appContext = context.getApplicationContext();
FeishuWebhookConfigStore.Config config = FeishuWebhookConfigStore.loadConfig(appContext);
if (config.filterVerificationCode && !result.parseResult.success) {
Log.d(TAG, "Feishu push skipped: filter code enabled and parse failed, reason="
+ result.parseResult.failureReason);
if (isEmpty(result.body)) {
Log.d(TAG, "Feishu push skipped: empty body source=" + result.source);
return;
}
Context appContext = context.getApplicationContext();
FeishuWebhookConfigStore.Config config = FeishuWebhookConfigStore.loadConfig(appContext);
if (FeishuWebhookConfigStore.wasSmsPushed(appContext, result)) {
Log.d(TAG, "Feishu push skipped: duplicate sms receivedSecond="
+ (result.receivedAtMillis / 1000L)
@@ -74,7 +73,7 @@ public final class FeishuWebhookClient {
"远端推送未开启"));
return;
}
String markdown = buildMarkdownFromCapture(result, config);
String markdown = buildMarkdownFromCapture(result);
pushMarkdownAsync(appContext, markdown, result);
}
@@ -205,38 +204,22 @@ public final class FeishuWebhookClient {
}
public static String buildMarkdownFromCapture(CaptureResult result) {
return buildMarkdownFromCapture(result, new FeishuWebhookConfigStore.Config(false, "", "", false, false));
}
public static String buildMarkdownFromCapture(CaptureResult result, FeishuWebhookConfigStore.Config config) {
boolean filterCode = config != null && config.filterVerificationCode;
boolean includeFullBody = config == null || !filterCode || config.sendFullBodyDebug;
StringBuilder builder = new StringBuilder();
if (filterCode) {
builder.append("**短信验证码**").append(emptyAsDash(result.parseResult.code)).append('\n');
} else {
builder.append("**短信内容**").append(emptyAsDash(result.body)).append('\n');
if (result.parseResult.success) {
builder.append("**识别验证码**").append(result.parseResult.code).append('\n');
}
}
builder.append("**短信内容**").append(emptyAsDash(result.body)).append('\n');
builder.append("**来源**").append(emptyAsDash(result.source)).append('\n');
builder.append("**发送方**").append(maskSender(result.sender)).append('\n');
builder.append("**时间**").append(result.receivedAtMillis).append('\n');
if (result.parseResult.success) {
builder.append("**解析**")
.append(emptyAsDash(result.parseResult.strategy))
.append(" / ")
.append(result.parseResult.confidence);
} else {
builder.append("**解析失败**").append(emptyAsDash(result.parseResult.failureReason));
}
if (includeFullBody && filterCode) {
builder.append('\n').append("**原文**").append(emptyAsDash(result.body));
builder.append("**时间**").append(formatTime(result.receivedAtMillis));
if (!isEmpty(result.failureReason)) {
builder.append('\n').append("**读取异常**").append(emptyAsDash(result.failureReason));
}
return builder.toString();
}
private static String formatTime(long timeMillis) {
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.CHINA)
.format(new java.util.Date(timeMillis));
}
private static void saveAndNotify(Context context, FeishuWebhookPushResult result) {
Log.d(TAG, String.format(Locale.US,
"Feishu push result success=%s status=%s http=%d api=%d message=%s",
@@ -39,8 +39,6 @@ public final class FeishuWebhookConfigStore {
private static final String JSON_ENABLED = "enabled";
private static final String JSON_WEBHOOK_ID = "webhook_id";
private static final String JSON_SECRET = "secret";
private static final String JSON_SEND_FULL_BODY_DEBUG = "send_full_body_debug";
private static final String JSON_FILTER_VERIFICATION_CODE = "filter_verification_code";
private FeishuWebhookConfigStore() {
}
@@ -65,19 +63,15 @@ public final class FeishuWebhookConfigStore {
Context context,
boolean enabled,
String webhookId,
String secret,
boolean sendFullBodyDebug,
boolean filterVerificationCode) {
Config config = new Config(enabled, webhookId, secret, sendFullBodyDebug, filterVerificationCode);
String secret) {
Config config = new Config(enabled, webhookId, secret);
File file = configFile(context);
try {
writeFile(file, configToJson(config).toString(2));
Log.d(TAG, "save feishu config path=" + file.getAbsolutePath()
+ ", enabled=" + enabled
+ ", webhookConfigured=" + config.hasWebhookId()
+ ", secretConfigured=" + config.hasSecret()
+ ", debugBody=" + sendFullBodyDebug
+ ", filterCode=" + filterVerificationCode);
+ ", secretConfigured=" + config.hasSecret());
} catch (IOException | JSONException e) {
Log.w(TAG, "save feishu config failed path=" + file.getAbsolutePath()
+ ", reason=" + e.getClass().getSimpleName(), e);
@@ -178,7 +172,7 @@ public final class FeishuWebhookConfigStore {
try {
return configToJson(defaultConfig()).toString(2);
} catch (JSONException e) {
return "{\"enabled\":false,\"webhook_id\":\"\",\"secret\":\"\",\"send_full_body_debug\":false,\"filter_verification_code\":false}";
return "{\"enabled\":false,\"webhook_id\":\"\",\"secret\":\"\"}";
}
}
@@ -217,25 +211,21 @@ public final class FeishuWebhookConfigStore {
}
private static Config defaultConfig() {
return new Config(false, "", "", false, false);
return new Config(false, "", "");
}
private static Config configFromJson(JSONObject json) {
return new Config(
json.optBoolean(JSON_ENABLED, false),
json.optString(JSON_WEBHOOK_ID, ""),
json.optString(JSON_SECRET, ""),
json.optBoolean(JSON_SEND_FULL_BODY_DEBUG, false),
json.optBoolean(JSON_FILTER_VERIFICATION_CODE, false));
json.optString(JSON_SECRET, ""));
}
private static JSONObject configToJson(Config config) throws JSONException {
return new JSONObject()
.put(JSON_ENABLED, config.enabled)
.put(JSON_WEBHOOK_ID, config.webhookId)
.put(JSON_SECRET, config.secret)
.put(JSON_SEND_FULL_BODY_DEBUG, config.sendFullBodyDebug)
.put(JSON_FILTER_VERIFICATION_CODE, config.filterVerificationCode);
.put(JSON_SECRET, config.secret);
}
private static String readFile(File file) throws IOException {
@@ -295,20 +285,14 @@ public final class FeishuWebhookConfigStore {
public final boolean enabled;
public final String webhookId;
public final String secret;
public final boolean sendFullBodyDebug;
public final boolean filterVerificationCode;
public Config(
boolean enabled,
String webhookId,
String secret,
boolean sendFullBodyDebug,
boolean filterVerificationCode) {
String secret) {
this.enabled = enabled;
this.webhookId = normalize(webhookId);
this.secret = normalize(secret);
this.sendFullBodyDebug = sendFullBodyDebug;
this.filterVerificationCode = filterVerificationCode;
}
public boolean hasWebhookId() {
@@ -36,7 +36,7 @@ final class KeepAliveNotification {
: new Notification.Builder(context);
return builder
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("短信验证码监听运行中")
.setContentTitle("短信监听运行中")
.setContentText(contentText)
.setContentIntent(pendingIntent)
.setOngoing(true)
@@ -27,7 +27,7 @@ public final class SmsPollingService extends Service {
private final Runnable pollingRunnable = new Runnable() {
@Override
public void run() {
pollRecentSmsForCode();
pollRecentSms();
long intervalMillis = SmsPollingStateStore.getIntervalSeconds(SmsPollingService.this) * 1000L;
Log.d(TAG, "SmsPollingService.schedule next intervalMs=" + intervalMillis);
handler.postDelayed(this, intervalMillis);
@@ -92,7 +92,7 @@ public final class SmsPollingService extends Service {
return null;
}
private void pollRecentSmsForCode() {
private void pollRecentSms() {
if (!hasReadSmsPermission()) {
Log.w(TAG, "SmsPollingService.poll stop: READ_SMS not granted");
Toast.makeText(this, "READ_SMS 未授权,已停止短信轮询", Toast.LENGTH_LONG).show();
@@ -101,9 +101,9 @@ public final class SmsPollingService extends Service {
return;
}
SmsInboxReader.RecentCodeResult result = SmsInboxReader.findLatestVerificationCode(this, 3, pollingStartMillis);
SmsInboxReader.RecentSmsResult result = SmsInboxReader.findLatestSms(this, 3, pollingStartMillis);
if (!result.success) {
Log.d(TAG, "SmsPollingService.poll no code scanned=" + result.scannedCount
Log.d(TAG, "SmsPollingService.poll no sms scanned=" + result.scannedCount
+ ", reason=" + result.failureReason);
return;
}
@@ -114,15 +114,13 @@ public final class SmsPollingService extends Service {
lastHitSmsId = result.id;
SmsPollingStateStore.recordHit(this, result.id, result.dateMillis);
Log.d(TAG, "SmsPollingService.poll hit id=" + result.id
+ ", code=" + result.parseResult.code
+ ", strategy=" + result.parseResult.strategy
+ ", confidence=" + result.parseResult.confidence);
+ ", sender=" + result.sender
+ ", bodyLength=" + result.body.length());
CaptureResult captureResult = CaptureResult.success(
result.dateMillis,
result.id,
result.sender,
result.body,
result.parseResult,
SOURCE_INBOX_POLLING);
SmsCaptureStore.save(this, captureResult);
FeishuWebhookClient.pushCaptureResultAsync(this, captureResult);
@@ -130,7 +128,7 @@ public final class SmsPollingService extends Service {
Intent updateIntent = new Intent(SmsCaptureStore.ACTION_CAPTURE_UPDATED);
updateIntent.setPackage(getPackageName());
sendBroadcast(updateIntent);
Toast.makeText(this, "轮询提取验证码:" + result.parseResult.code, Toast.LENGTH_LONG).show();
Toast.makeText(this, "轮询读取到新短信", Toast.LENGTH_LONG).show();
}
private boolean hasReadSmsPermission() {
@@ -3,11 +3,11 @@ package com.smsreceive.app.sms;
public final class CaptureResult {
public static final long UNKNOWN_SMS_PROVIDER_ID = -1L;
public final boolean success;
public final long receivedAtMillis;
public final long smsProviderId;
public final String sender;
public final String body;
public final VerificationCodeParser.ParseResult parseResult;
public final String source;
public final String failureReason;
@@ -16,14 +16,13 @@ public final class CaptureResult {
long smsProviderId,
String sender,
String body,
VerificationCodeParser.ParseResult parseResult,
String source,
String failureReason) {
this.success = failureReason == null || failureReason.length() == 0;
this.receivedAtMillis = receivedAtMillis;
this.smsProviderId = smsProviderId;
this.sender = sender == null ? "" : sender;
this.body = body == null ? "" : body;
this.parseResult = parseResult;
this.source = source == null ? "unknown" : source;
this.failureReason = failureReason == null ? "" : failureReason;
}
@@ -32,9 +31,8 @@ public final class CaptureResult {
long receivedAtMillis,
String sender,
String body,
VerificationCodeParser.ParseResult parseResult,
String source) {
return success(receivedAtMillis, UNKNOWN_SMS_PROVIDER_ID, sender, body, parseResult, source);
return success(receivedAtMillis, UNKNOWN_SMS_PROVIDER_ID, sender, body, source);
}
public static CaptureResult success(
@@ -42,9 +40,8 @@ public final class CaptureResult {
long smsProviderId,
String sender,
String body,
VerificationCodeParser.ParseResult parseResult,
String source) {
return new CaptureResult(receivedAtMillis, smsProviderId, sender, body, parseResult, source, "");
return new CaptureResult(receivedAtMillis, smsProviderId, sender, body, source, "");
}
public static CaptureResult failure(
@@ -63,7 +60,6 @@ public final class CaptureResult {
String body,
String source,
String failureReason) {
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.ParseResult.failure(failureReason);
return new CaptureResult(receivedAtMillis, smsProviderId, sender, body, parseResult, source, failureReason);
return new CaptureResult(receivedAtMillis, smsProviderId, sender, body, source, failureReason);
}
}
@@ -10,13 +10,12 @@ public final class SmsCaptureStore {
private static final String TAG = "[SMS]SmsReceive";
private static final String PREFS = "sms_capture";
private static final String KEY_SUCCESS = "success";
private static final String KEY_TIME = "time";
private static final String KEY_SENDER = "sender";
private static final String KEY_CODE = "code";
private static final String KEY_STRATEGY = "strategy";
private static final String KEY_CONFIDENCE = "confidence";
private static final String KEY_SOURCE = "source";
private static final String KEY_FAILURE = "failure";
private static final String KEY_BODY = "body";
private static final String KEY_BODY_PREVIEW = "body_preview";
private static final String KEY_LAST_BROADCAST_TIME = "last_broadcast_time";
private static final String KEY_LAST_INBOX_TIME = "last_inbox_time";
@@ -26,19 +25,16 @@ public final class SmsCaptureStore {
}
public static void save(Context context, CaptureResult result) {
VerificationCodeParser.ParseResult parse = result.parseResult;
Log.d(TAG, "SmsCaptureStore.save source=" + result.source
+ ", success=" + parse.success
+ ", code=" + parse.code
+ ", failure=" + (TextUtils.isEmpty(result.failureReason) ? parse.failureReason : result.failureReason));
+ ", success=" + result.success
+ ", failure=" + result.failureReason);
SharedPreferences.Editor editor = preferences(context).edit()
.putBoolean(KEY_SUCCESS, result.success)
.putLong(KEY_TIME, result.receivedAtMillis)
.putString(KEY_SENDER, summarizeSender(result.sender))
.putString(KEY_CODE, parse.code)
.putString(KEY_STRATEGY, parse.strategy)
.putInt(KEY_CONFIDENCE, parse.confidence)
.putString(KEY_SOURCE, result.source)
.putString(KEY_FAILURE, TextUtils.isEmpty(result.failureReason) ? parse.failureReason : result.failureReason)
.putString(KEY_FAILURE, result.failureReason)
.putString(KEY_BODY, result.body)
.putString(KEY_BODY_PREVIEW, previewBody(result.body));
if ("system_sms_broadcast".equals(result.source)) {
Log.d(TAG, "SmsCaptureStore.save delivery source=system_sms_broadcast time="
@@ -55,15 +51,16 @@ public final class SmsCaptureStore {
public static StoredCapture load(Context context) {
SharedPreferences prefs = preferences(context);
String bodyPreview = prefs.getString(KEY_BODY_PREVIEW, "");
String body = prefs.getString(KEY_BODY, "");
return new StoredCapture(
prefs.getBoolean(KEY_SUCCESS, TextUtils.isEmpty(prefs.getString(KEY_FAILURE, ""))),
prefs.getLong(KEY_TIME, 0L),
prefs.getString(KEY_SENDER, ""),
prefs.getString(KEY_CODE, ""),
prefs.getString(KEY_STRATEGY, ""),
prefs.getInt(KEY_CONFIDENCE, 0),
prefs.getString(KEY_SOURCE, ""),
prefs.getString(KEY_FAILURE, ""),
prefs.getString(KEY_BODY_PREVIEW, ""));
TextUtils.isEmpty(body) ? bodyPreview : body,
bodyPreview);
}
public static void clear(Context context) {
@@ -102,31 +99,28 @@ public final class SmsCaptureStore {
}
public static final class StoredCapture {
public final boolean success;
public final long timeMillis;
public final String sender;
public final String code;
public final String strategy;
public final int confidence;
public final String source;
public final String failure;
public final String body;
public final String bodyPreview;
StoredCapture(
boolean success,
long timeMillis,
String sender,
String code,
String strategy,
int confidence,
String source,
String failure,
String body,
String bodyPreview) {
this.success = success;
this.timeMillis = timeMillis;
this.sender = sender;
this.code = code;
this.strategy = strategy;
this.confidence = confidence;
this.source = source;
this.failure = failure;
this.body = body;
this.bodyPreview = bodyPreview;
}
}
@@ -64,63 +64,11 @@ public final class SmsInboxReader {
}
}
public static int logRecentMessages(Context context, int limit) {
Uri uri = Telephony.Sms.CONTENT_URI;
String[] projection = {
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE,
Telephony.Sms.TYPE
};
int safeLimit = Math.max(1, Math.min(limit, 100));
Log.d(TAG, "SmsInboxReader.logRecentMessages start uri=" + uri + ", limit=" + safeLimit);
try (Cursor cursor = context.getContentResolver().query(
uri,
projection,
null,
null,
Telephony.Sms.DATE + " DESC LIMIT " + safeLimit)) {
if (cursor == null) {
Log.w(TAG, "SmsInboxReader.logRecentMessages cursor is null");
return 0;
}
int count = 0;
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms._ID));
String sender = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.ADDRESS));
String body = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.BODY));
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms.DATE));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Sms.TYPE));
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.parse(body);
Log.d(TAG, "SMS[" + count + "] id=" + id
+ ", type=" + smsTypeName(type)
+ ", date=" + formatDate(date)
+ ", sender=" + maskSender(sender)
+ ", parseSuccess=" + parseResult.success
+ ", code=" + parseResult.code
+ ", strategy=" + parseResult.strategy
+ ", bodyPreview=" + previewBody(body));
count++;
}
Log.d(TAG, "SmsInboxReader.logRecentMessages end count=" + count);
return count;
} catch (SecurityException e) {
Log.w(TAG, "SmsInboxReader.logRecentMessages failed: READ_SMS denied", e);
return 0;
} catch (Exception e) {
Log.w(TAG, "SmsInboxReader.logRecentMessages failed", e);
return 0;
}
public static RecentSmsResult findLatestSms(Context context, int limit) {
return findLatestSms(context, limit, 0L);
}
public static RecentCodeResult findLatestVerificationCode(Context context, int limit) {
return findLatestVerificationCode(context, limit, 0L);
}
public static RecentCodeResult findLatestVerificationCode(Context context, int limit, long minDateMillis) {
public static RecentSmsResult findLatestSms(Context context, int limit, long minDateMillis) {
Uri uri = Telephony.Sms.CONTENT_URI;
String[] projection = {
Telephony.Sms._ID,
@@ -133,7 +81,7 @@ public final class SmsInboxReader {
int safeLimit = Math.max(1, Math.min(limit, 100));
String selection = minDateMillis > 0L ? Telephony.Sms.DATE + ">=?" : null;
String[] selectionArgs = minDateMillis > 0L ? new String[]{String.valueOf(minDateMillis)} : null;
Log.d(TAG, "SmsInboxReader.findLatestVerificationCode start limit=" + safeLimit
Log.d(TAG, "SmsInboxReader.findLatestSms start limit=" + safeLimit
+ ", minDate=" + (minDateMillis > 0L ? formatDate(minDateMillis) : "none"));
try (Cursor cursor = context.getContentResolver().query(
uri,
@@ -142,54 +90,39 @@ public final class SmsInboxReader {
selectionArgs,
Telephony.Sms.DATE + " DESC LIMIT " + safeLimit)) {
if (cursor == null) {
Log.w(TAG, "SmsInboxReader.findLatestVerificationCode cursor is null");
return RecentCodeResult.failure("短信库查询 cursor 为空");
Log.w(TAG, "SmsInboxReader.findLatestSms cursor is null");
return RecentSmsResult.failure("短信库查询 cursor 为空");
}
int scanned = 0;
RecentCodeResult latest = null;
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms._ID));
String sender = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.ADDRESS));
String body = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.BODY));
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms.DATE));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Sms.TYPE));
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.parse(body);
Log.d(TAG, "poll scan SMS[" + scanned + "] id=" + id
scanned++;
Log.d(TAG, "poll scan SMS[" + (scanned - 1) + "] id=" + id
+ ", type=" + smsTypeName(type)
+ ", date=" + formatDate(date)
+ ", sender=" + maskSender(sender)
+ ", parseSuccess=" + parseResult.success
+ ", code=" + parseResult.code
+ ", strategy=" + parseResult.strategy
+ ", bodyPreview=" + previewBody(body));
scanned++;
if (parseResult.success) {
RecentCodeResult candidate = RecentCodeResult.success(id, sender, body, date, parseResult, scanned);
if (latest == null || candidate.dateMillis > latest.dateMillis) {
latest = candidate;
}
Log.d(TAG, "SmsInboxReader.findLatestVerificationCode candidate id=" + id
+ ", code=" + parseResult.code
+ ", date=" + formatDate(date)
+ ", scanned=" + scanned);
if (TextUtils.isEmpty(body)) {
continue;
}
}
if (latest != null) {
Log.d(TAG, "SmsInboxReader.findLatestVerificationCode hit latest id=" + latest.id
+ ", code=" + latest.parseResult.code
+ ", date=" + formatDate(latest.dateMillis)
Log.d(TAG, "SmsInboxReader.findLatestSms hit latest id=" + id
+ ", date=" + formatDate(date)
+ ", scanned=" + scanned);
return latest.withScannedCount(scanned);
return RecentSmsResult.success(id, sender, body, date, scanned);
}
Log.d(TAG, "SmsInboxReader.findLatestVerificationCode no code scanned=" + scanned);
return RecentCodeResult.noCode(scanned);
Log.d(TAG, "SmsInboxReader.findLatestSms no sms scanned=" + scanned);
return RecentSmsResult.noSms(scanned);
} catch (SecurityException e) {
Log.w(TAG, "SmsInboxReader.findLatestVerificationCode failed: READ_SMS denied", e);
return RecentCodeResult.failure("READ_SMS 未授权");
Log.w(TAG, "SmsInboxReader.findLatestSms failed: READ_SMS denied", e);
return RecentSmsResult.failure("READ_SMS 未授权");
} catch (Exception e) {
Log.w(TAG, "SmsInboxReader.findLatestVerificationCode failed", e);
return RecentCodeResult.failure("短信库查询失败:" + e.getClass().getSimpleName());
Log.w(TAG, "SmsInboxReader.findLatestSms failed", e);
return RecentSmsResult.failure("短信库查询失败:" + e.getClass().getSimpleName());
}
}
@@ -257,23 +190,21 @@ public final class SmsInboxReader {
}
}
public static final class RecentCodeResult {
public static final class RecentSmsResult {
public final boolean success;
public final long id;
public final String sender;
public final String body;
public final long dateMillis;
public final VerificationCodeParser.ParseResult parseResult;
public final int scannedCount;
public final String failureReason;
private RecentCodeResult(
private RecentSmsResult(
boolean success,
long id,
String sender,
String body,
long dateMillis,
VerificationCodeParser.ParseResult parseResult,
int scannedCount,
String failureReason) {
this.success = success;
@@ -281,33 +212,25 @@ public final class SmsInboxReader {
this.sender = sender == null ? "" : sender;
this.body = body == null ? "" : body;
this.dateMillis = dateMillis;
this.parseResult = parseResult;
this.scannedCount = scannedCount;
this.failureReason = failureReason == null ? "" : failureReason;
}
static RecentCodeResult success(
static RecentSmsResult success(
long id,
String sender,
String body,
long dateMillis,
VerificationCodeParser.ParseResult parseResult,
int scannedCount) {
return new RecentCodeResult(true, id, sender, body, dateMillis, parseResult, scannedCount, "");
return new RecentSmsResult(true, id, sender, body, dateMillis, scannedCount, "");
}
static RecentCodeResult noCode(int scannedCount) {
return new RecentCodeResult(false, -1L, "", "", System.currentTimeMillis(),
VerificationCodeParser.ParseResult.failure("最近短信未找到验证码"), scannedCount, "最近短信未找到验证码");
static RecentSmsResult noSms(int scannedCount) {
return new RecentSmsResult(false, -1L, "", "", System.currentTimeMillis(), scannedCount, "最近短信未找到新短信");
}
static RecentCodeResult failure(String reason) {
return new RecentCodeResult(false, -1L, "", "", System.currentTimeMillis(),
VerificationCodeParser.ParseResult.failure(reason), 0, reason);
}
RecentCodeResult withScannedCount(int scannedCount) {
return new RecentCodeResult(success, id, sender, body, dateMillis, parseResult, scannedCount, failureReason);
static RecentSmsResult failure(String reason) {
return new RecentSmsResult(false, -1L, "", "", System.currentTimeMillis(), 0, reason);
}
}
}
@@ -25,35 +25,19 @@ public final class SmsReceiver extends BroadcastReceiver {
Log.d(TAG, "SmsReceiver.onReceive ignore non SMS action");
return;
}
Toast.makeText(context, "收到短信广播,开始解析", Toast.LENGTH_SHORT).show();
Toast.makeText(context, "收到短信广播,开始处理", Toast.LENGTH_SHORT).show();
SmsMessageReader.ReadResult readResult = SmsMessageReader.read(intent);
CaptureResult captureResult;
if (readResult.success) {
Log.d(TAG, "SMS read success sender=" + maskSender(readResult.sender)
+ ", bodyPreview=" + preview(readResult.body));
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.parse(readResult.body);
if (parseResult.success) {
Log.d(TAG, "verification code parse success code=" + parseResult.code
+ ", strategy=" + parseResult.strategy
+ ", confidence=" + parseResult.confidence);
Toast.makeText(context, "验证码:" + parseResult.code, Toast.LENGTH_LONG).show();
captureResult = CaptureResult.success(
readResult.timestampMillis,
readResult.sender,
readResult.body,
parseResult,
SOURCE_SYSTEM_BROADCAST);
} else {
Log.w(TAG, "verification code parse failed reason=" + parseResult.failureReason);
Toast.makeText(context, "短信已收到,未解析到验证码", Toast.LENGTH_LONG).show();
captureResult = CaptureResult.failure(
readResult.timestampMillis,
readResult.sender,
readResult.body,
SOURCE_SYSTEM_BROADCAST,
parseResult.failureReason);
}
Toast.makeText(context, "短信已收到", Toast.LENGTH_LONG).show();
captureResult = CaptureResult.success(
readResult.timestampMillis,
readResult.sender,
readResult.body,
SOURCE_SYSTEM_BROADCAST);
} else {
Log.w(TAG, "SMS read failed reason=" + readResult.failureReason);
Toast.makeText(context, "短信读取失败:" + readResult.failureReason, Toast.LENGTH_LONG).show();
@@ -71,7 +55,7 @@ public final class SmsReceiver extends BroadcastReceiver {
updateIntent.setPackage(context.getPackageName());
context.sendBroadcast(updateIntent);
Log.d(TAG, "SmsReceiver.onReceive end source=" + SOURCE_SYSTEM_BROADCAST
+ ", success=" + captureResult.parseResult.success);
+ ", success=" + captureResult.success);
}
private static String maskSender(String sender) {
@@ -1,175 +0,0 @@
package com.smsreceive.app.sms;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class VerificationCodeParser {
private static final Pattern KEYWORD_PATTERN = Pattern.compile("(?i)(验证码|校验码|动态码|验证代码|verification|otp|code)");
private static final Pattern CANDIDATE_PATTERN = Pattern.compile("(?<![0-9A-Za-z])([A-Za-z0-9]{4,8}|[0-9](?:[\\s-]?[0-9]){3,7})(?![0-9A-Za-z])");
private static final Pattern PHONE_PATTERN = Pattern.compile("(?<!\\d)1[3-9]\\d{9}(?!\\d)");
private static final Pattern MONEY_PATTERN = Pattern.compile("\\d+(?:\\.\\d{1,2})?\\s*(元|RMB|CNY|¥|¥)", Pattern.CASE_INSENSITIVE);
private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}[-/.年]\\d{1,2}[-/.月]\\d{1,2}");
private VerificationCodeParser() {
}
public static ParseResult parse(String body) {
if (body == null || body.trim().isEmpty()) {
return ParseResult.failure("短信正文为空");
}
ParseResult keywordNearby = findKeywordNearbyCode(body);
if (keywordNearby.success) {
return keywordNearby;
}
List<String> candidates = new ArrayList<>();
Matcher matcher = CANDIDATE_PATTERN.matcher(body);
while (matcher.find()) {
String raw = matcher.group(1);
String normalized = normalizeCode(raw);
if (isPlausibleStandalone(body, matcher.start(1), matcher.end(1), normalized)) {
candidates.add(normalized);
}
}
if (!candidates.isEmpty()) {
String best = candidates.get(0);
for (String candidate : candidates) {
if (scoreStandalone(candidate) > scoreStandalone(best)) {
best = candidate;
}
}
return ParseResult.success(best, "standalone_numeric", 62);
}
return ParseResult.failure("未找到可靠验证码");
}
private static ParseResult findKeywordNearbyCode(String body) {
Matcher keywordMatcher = KEYWORD_PATTERN.matcher(body);
while (keywordMatcher.find()) {
if (isNegatedKeyword(body, keywordMatcher.start())) {
continue;
}
int forwardStart = keywordMatcher.end();
int forwardEnd = Math.min(body.length(), keywordMatcher.end() + 32);
ParseResult forward = findCandidateInWindow(body, forwardStart, forwardEnd, "keyword_before_code", 95);
if (forward.success) {
return forward;
}
int backwardStart = Math.max(0, keywordMatcher.start() - 24);
int backwardEnd = keywordMatcher.start();
ParseResult backward = findCandidateInWindow(body, backwardStart, backwardEnd, "code_before_keyword", 88);
if (backward.success) {
return backward;
}
}
return ParseResult.failure("未找到可靠验证码");
}
private static ParseResult findCandidateInWindow(String body, int start, int end, String strategy, int confidence) {
Matcher matcher = CANDIDATE_PATTERN.matcher(body.substring(start, end));
while (matcher.find()) {
int absoluteStart = start + matcher.start(1);
int absoluteEnd = start + matcher.end(1);
String normalized = normalizeCode(matcher.group(1));
if (isPlausibleStandalone(body, absoluteStart, absoluteEnd, normalized)) {
return ParseResult.success(normalized, strategy, confidence);
}
}
return ParseResult.failure("未找到可靠验证码");
}
private static boolean isNegatedKeyword(String body, int keywordStart) {
int prefixStart = Math.max(0, keywordStart - 2);
String prefix = body.substring(prefixStart, keywordStart);
return prefix.contains("") || prefix.contains("");
}
private static String normalizeCode(String raw) {
if (raw == null) {
return "";
}
return raw.replaceAll("[\\s-]", "").toUpperCase(Locale.US);
}
private static boolean isValidCode(String code) {
if (code == null || code.length() < 4 || code.length() > 8) {
return false;
}
boolean hasDigit = false;
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
if (!Character.isLetterOrDigit(c)) {
return false;
}
if (Character.isDigit(c)) {
hasDigit = true;
}
}
return hasDigit;
}
private static boolean isPlausibleStandalone(String body, int start, int end, String normalized) {
if (!isValidCode(normalized)) {
return false;
}
String window = body.substring(Math.max(0, start - 8), Math.min(body.length(), end + 8));
if (PHONE_PATTERN.matcher(window).find()) {
return false;
}
if (MONEY_PATTERN.matcher(window).find()) {
return false;
}
if (DATE_PATTERN.matcher(window).find()) {
return false;
}
if (normalized.length() == 8 && body.substring(Math.max(0, start - 2), Math.min(body.length(), end + 2)).contains("-")) {
return false;
}
return true;
}
private static int scoreStandalone(String code) {
int score = 0;
if (code.length() == 6) {
score += 10;
} else if (code.length() == 4) {
score += 6;
}
if (code.matches("\\d+")) {
score += 4;
}
return score;
}
public static final class ParseResult {
public final boolean success;
public final String code;
public final String strategy;
public final int confidence;
public final String failureReason;
private ParseResult(boolean success, String code, String strategy, int confidence, String failureReason) {
this.success = success;
this.code = code == null ? "" : code;
this.strategy = strategy == null ? "" : strategy;
this.confidence = confidence;
this.failureReason = failureReason == null ? "" : failureReason;
}
public static ParseResult success(String code, String strategy, int confidence) {
return new ParseResult(true, code, strategy, confidence, "");
}
public static ParseResult failure(String reason) {
return new ParseResult(false, "", "", 0, reason);
}
}
}
@@ -31,7 +31,6 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.ScrollView;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
@@ -46,7 +45,6 @@ import com.smsreceive.app.keepalive.SmsPollingStateStore;
import com.smsreceive.app.sms.CaptureResult;
import com.smsreceive.app.sms.SmsCaptureStore;
import com.smsreceive.app.sms.SmsInboxReader;
import com.smsreceive.app.sms.VerificationCodeParser;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -73,8 +71,6 @@ public final class MainActivity extends Activity {
private Button pollingButton;
private RadioButton toastOnDatabaseWriteRadio;
private CheckBox feishuPushEnabledCheckBox;
private CheckBox feishuDebugBodyCheckBox;
private Switch feishuFilterCodeSwitch;
private EditText feishuWebhookIdEdit;
private EditText feishuSecretEdit;
private EditText pollingIntervalEdit;
@@ -155,14 +151,14 @@ public final class MainActivity extends Activity {
ViewGroup.LayoutParams.WRAP_CONTENT));
TextView title = new TextView(this);
title.setText("短信验证码接收");
title.setText("短信接收");
title.setTextSize(24);
title.setTextColor(0xFF17202A);
title.setGravity(Gravity.START);
root.addView(title, matchWrap());
TextView subtitle = new TextView(this);
subtitle.setText("主路径:RECEIVE_SMS + SMS_RECEIVED_ACTION。收到短信后保存验证码和诊断摘要。");
subtitle.setText("主路径:RECEIVE_SMS + SMS_RECEIVED_ACTION。收到短信后保存短信原文和诊断摘要。");
subtitle.setTextSize(14);
subtitle.setTextColor(0xFF5F6B7A);
subtitle.setPadding(0, dp(6), 0, dp(16));
@@ -200,11 +196,7 @@ public final class MainActivity extends Activity {
readInboxButton.setOnClickListener(v -> readLatestInboxSms(SOURCE_INBOX_MANUAL, true));
actions.addView(readInboxButton, matchWrap());
Button dumpRecentButton = button("打印最近30条短信");
dumpRecentButton.setOnClickListener(v -> dumpRecentMessages());
actions.addView(dumpRecentButton, matchWrap());
pollingButton = button("开始1秒轮询验证码");
pollingButton = button("开始短信轮询");
pollingButton.setOnClickListener(v -> togglePolling());
actions.addView(pollingButton, matchWrap());
@@ -214,10 +206,6 @@ public final class MainActivity extends Activity {
pollingIntervalEdit.setInputType(InputType.TYPE_CLASS_NUMBER);
actions.addView(pollingIntervalEdit, matchWrap());
Button savePollingIntervalButton = button("保存轮询间隔");
savePollingIntervalButton.setOnClickListener(v -> savePollingIntervalFromUi());
actions.addView(savePollingIntervalButton, matchWrap());
feishuPushEnabledCheckBox = new CheckBox(this);
feishuPushEnabledCheckBox.setText("开启飞书远端推送");
feishuPushEnabledCheckBox.setTextSize(14);
@@ -235,19 +223,6 @@ public final class MainActivity extends Activity {
feishuSecretEdit.setSingleLine(true);
actions.addView(feishuSecretEdit, matchWrap());
feishuDebugBodyCheckBox = new CheckBox(this);
feishuDebugBodyCheckBox.setText("调试时上传完整短信正文");
feishuDebugBodyCheckBox.setTextSize(14);
feishuDebugBodyCheckBox.setTextColor(0xFF27313F);
actions.addView(feishuDebugBodyCheckBox, matchWrap());
feishuFilterCodeSwitch = new Switch(this);
feishuFilterCodeSwitch.setText("只推送验证码(过滤非验证码短信)");
feishuFilterCodeSwitch.setTextSize(14);
feishuFilterCodeSwitch.setTextColor(0xFF27313F);
feishuFilterCodeSwitch.setChecked(false);
actions.addView(feishuFilterCodeSwitch, matchWrap());
Button saveFeishuButton = button("保存飞书配置");
saveFeishuButton.setOnClickListener(v -> saveFeishuConfigFromUi());
actions.addView(saveFeishuButton, matchWrap());
@@ -350,7 +325,7 @@ public final class MainActivity extends Activity {
SmsCaptureStore.StoredCapture capture = SmsCaptureStore.load(this);
if (capture.timeMillis <= 0L) {
latestText.setText("暂无短信接收记录。可以先授权,再从另一台手机发送:验证码 1234565 分钟内有效");
latestText.setText("暂无短信接收记录。可以先授权,再从另一台手机发送一条测试短信");
return;
}
@@ -358,14 +333,10 @@ public final class MainActivity extends Activity {
builder.append("时间:").append(formatTime(capture.timeMillis)).append('\n');
builder.append("来源:").append(emptyAsDash(capture.source)).append('\n');
builder.append("发送方:").append(emptyAsDash(capture.sender)).append('\n');
if (!TextUtils.isEmpty(capture.code)) {
builder.append("验证码:").append(capture.code).append('\n');
builder.append("策略:").append(capture.strategy).append(" / ").append(capture.confidence).append('\n');
} else {
builder.append("验证码:-").append('\n');
if (!capture.success) {
builder.append("失败原因:").append(emptyAsDash(capture.failure)).append('\n');
}
builder.append("正文摘要").append(emptyAsDash(capture.bodyPreview));
builder.append("短信原文").append(emptyAsDash(capture.body));
latestText.setText(builder.toString());
}
@@ -471,7 +442,7 @@ public final class MainActivity extends Activity {
private void refreshPollingUi() {
SmsPollingStateStore.State state = SmsPollingStateStore.load(this);
if (pollingButton != null) {
pollingButton.setText(state.enabledByUser ? "停止1秒轮询验证码" : "开始1秒轮询验证码");
pollingButton.setText(state.enabledByUser ? "停止短信轮询" : "开始短信轮询");
}
if (pollingIntervalEdit != null && !pollingIntervalEdit.hasFocus()) {
pollingIntervalEdit.setText(String.valueOf(state.intervalSeconds));
@@ -492,12 +463,6 @@ public final class MainActivity extends Activity {
if (feishuPushEnabledCheckBox != null) {
feishuPushEnabledCheckBox.setChecked(config.enabled);
}
if (feishuDebugBodyCheckBox != null) {
feishuDebugBodyCheckBox.setChecked(config.sendFullBodyDebug);
}
if (feishuFilterCodeSwitch != null) {
feishuFilterCodeSwitch.setChecked(config.filterVerificationCode);
}
if (feishuWebhookIdEdit != null && !feishuWebhookIdEdit.hasFocus()) {
feishuWebhookIdEdit.setText(config.webhookId);
}
@@ -509,12 +474,11 @@ public final class MainActivity extends Activity {
builder.append("配置文件:").append(FeishuWebhookConfigStore.configPath(this)).append('\n');
builder.append("默认模板:").append(FeishuWebhookConfigStore.defaultConfigPath(this)).append('\n');
builder.append("推送开关:").append(config.enabled ? "已开启" : "未开启").append('\n');
builder.append("验证码过滤:").append(config.filterVerificationCode ? "已开启" : "未开启").append('\n');
builder.append("推送内容:短信原文").append('\n');
builder.append("Webhook ID").append(config.hasWebhookId() ? "已配置" : "未配置").append('\n');
builder.append("Secret").append(config.hasSecret()
? FeishuWebhookConfigStore.maskSecret(config.secret)
: "未配置").append('\n');
builder.append("完整正文上传:").append(config.sendFullBodyDebug ? "已开启" : "未开启").append('\n');
builder.append("已推送短信时间秒:")
.append(lastPushedSms.receivedSecond > 0L ? String.valueOf(lastPushedSms.receivedSecond) : "-")
.append('\n');
@@ -606,16 +570,12 @@ public final class MainActivity extends Activity {
private void saveFeishuConfigFromUi() {
boolean enabled = feishuPushEnabledCheckBox != null && feishuPushEnabledCheckBox.isChecked();
boolean debugBody = feishuDebugBodyCheckBox != null && feishuDebugBodyCheckBox.isChecked();
boolean filterCode = feishuFilterCodeSwitch != null && feishuFilterCodeSwitch.isChecked();
String webhookId = feishuWebhookIdEdit == null ? "" : feishuWebhookIdEdit.getText().toString();
String secret = feishuSecretEdit == null ? "" : feishuSecretEdit.getText().toString();
Log.d(TAG, "saveFeishuConfigFromUi enabled=" + enabled
+ ", webhookConfigured=" + !TextUtils.isEmpty(webhookId)
+ ", secretConfigured=" + !TextUtils.isEmpty(secret)
+ ", debugBody=" + debugBody
+ ", filterCode=" + filterCode);
FeishuWebhookConfigStore.saveConfig(this, enabled, webhookId, secret, debugBody, filterCode);
+ ", secretConfigured=" + !TextUtils.isEmpty(secret));
FeishuWebhookConfigStore.saveConfig(this, enabled, webhookId, secret);
Toast.makeText(this, "已保存飞书推送配置", Toast.LENGTH_SHORT).show();
refreshUi();
}
@@ -640,19 +600,11 @@ public final class MainActivity extends Activity {
}
private String buildLatestSmsTestMarkdown(SmsInboxReader.InboxResult inboxResult) {
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.parse(inboxResult.body);
StringBuilder builder = new StringBuilder();
builder.append("**SmsReceive 最近短信测试推送**").append('\n');
builder.append("时间:").append(formatTime(inboxResult.dateMillis)).append('\n');
builder.append("发送方:").append(maskSender(inboxResult.sender)).append('\n');
builder.append("短信ID").append(inboxResult.id).append('\n');
if (parseResult.success) {
builder.append("验证码:").append(parseResult.code).append('\n');
builder.append("解析:").append(parseResult.strategy).append(" / ").append(parseResult.confidence).append('\n');
} else {
builder.append("验证码:-").append('\n');
builder.append("解析失败:").append(emptyAsDash(parseResult.failureReason)).append('\n');
}
builder.append("正文:").append(emptyAsDash(inboxResult.body));
return builder.toString();
}
@@ -692,70 +644,42 @@ public final class MainActivity extends Activity {
}
lastInboxSmsId = inboxResult.id;
VerificationCodeParser.ParseResult parseResult = VerificationCodeParser.parse(inboxResult.body);
CaptureResult captureResult;
if (parseResult.success) {
Log.d(TAG, "readLatestInboxSms parse success source=" + source
+ ", id=" + inboxResult.id
+ ", code=" + parseResult.code
+ ", strategy=" + parseResult.strategy);
captureResult = CaptureResult.success(
inboxResult.dateMillis,
inboxResult.id,
inboxResult.sender,
inboxResult.body,
parseResult,
source);
if (showToast) {
Toast.makeText(this, "最新短信验证码:" + parseResult.code, Toast.LENGTH_LONG).show();
}
} else {
Log.w(TAG, "readLatestInboxSms parse failed source=" + source
+ ", id=" + inboxResult.id
+ ", reason=" + parseResult.failureReason);
captureResult = CaptureResult.failure(
inboxResult.dateMillis,
inboxResult.id,
inboxResult.sender,
inboxResult.body,
source,
parseResult.failureReason);
if (showToast) {
Toast.makeText(this, "最新短信未解析到验证码", Toast.LENGTH_LONG).show();
}
Log.d(TAG, "readLatestInboxSms success source=" + source
+ ", id=" + inboxResult.id
+ ", sender=" + inboxResult.sender);
CaptureResult captureResult = CaptureResult.success(
inboxResult.dateMillis,
inboxResult.id,
inboxResult.sender,
inboxResult.body,
source);
if (showToast) {
Toast.makeText(this, "已读取最新短信", Toast.LENGTH_LONG).show();
}
SmsCaptureStore.save(this, captureResult);
FeishuWebhookClient.pushCaptureResultAsync(this, captureResult);
refreshUi();
}
private void dumpRecentMessages() {
if (!hasReadSmsPermission()) {
Log.w(TAG, "dumpRecentMessages skip: READ_SMS not granted");
Toast.makeText(this, "READ_SMS 未授权,无法打印短信库", Toast.LENGTH_LONG).show();
return;
}
int count = SmsInboxReader.logRecentMessages(this, 30);
Toast.makeText(this, "已打印最近 " + count + " 条短信到 logcat", Toast.LENGTH_LONG).show();
}
private void savePollingIntervalFromUi() {
private int savePollingIntervalFromUi() {
int intervalSeconds = parsePollingIntervalSeconds();
SmsPollingStateStore.setIntervalSeconds(this, intervalSeconds);
Toast.makeText(this, "已保存轮询间隔:" + intervalSeconds + "", Toast.LENGTH_SHORT).show();
refreshUi();
if (pollingIntervalEdit != null && !pollingIntervalEdit.hasFocus()) {
pollingIntervalEdit.setText(String.valueOf(intervalSeconds));
}
return intervalSeconds;
}
private int parsePollingIntervalSeconds() {
String raw = pollingIntervalEdit == null ? "" : pollingIntervalEdit.getText().toString().trim();
if (TextUtils.isEmpty(raw)) {
return SmsPollingStateStore.getIntervalSeconds(this);
return 1;
}
try {
return Integer.parseInt(raw);
} catch (NumberFormatException e) {
Log.w(TAG, "parsePollingIntervalSeconds invalid raw=" + raw, e);
return SmsPollingStateStore.getIntervalSeconds(this);
return 1;
}
}
@@ -774,11 +698,11 @@ public final class MainActivity extends Activity {
Toast.makeText(this, "READ_SMS 未授权,无法轮询短信库", Toast.LENGTH_LONG).show();
return;
}
savePollingIntervalFromUi();
int intervalSeconds = savePollingIntervalFromUi();
Log.d(TAG, "startPolling via SmsPollingService");
SmsPollingService.start(this);
Toast.makeText(this,
"已启动后台轮询验证码,间隔 " + SmsPollingStateStore.getIntervalSeconds(this) + "",
"已启动后台短信轮询,间隔 " + intervalSeconds + "",
Toast.LENGTH_SHORT).show();
refreshUi();
}
@@ -1,5 +1,7 @@
package com.smsreceive.app.feishu;
import com.smsreceive.app.sms.CaptureResult;
import org.json.JSONObject;
import org.junit.Test;
@@ -56,11 +58,28 @@ public final class FeishuWebhookClientTest {
@Test
public void pushMarkdownRejectsMissingConfigBeforeNetwork() {
FeishuWebhookConfigStore.Config config = new FeishuWebhookConfigStore.Config(true, "", "", false, false);
FeishuWebhookConfigStore.Config config = new FeishuWebhookConfigStore.Config(true, "", "");
FeishuWebhookPushResult result = FeishuWebhookClient.pushMarkdown(config, "test", 1717020800L);
assertFalse(result.success);
assertEquals(FeishuWebhookPushResult.STATUS_MISSING_CONFIG, result.status);
}
@Test
public void buildMarkdownFromCaptureUsesRawSmsBody() {
CaptureResult result = CaptureResult.success(
1717020800000L,
"10690001",
"测试短信原文",
"system_sms_broadcast");
String markdown = FeishuWebhookClient.buildMarkdownFromCapture(result);
assertTrue(markdown.contains("测试短信原文"));
assertTrue(markdown.contains("短信内容"));
assertFalse(markdown.contains("验证码"));
assertFalse(markdown.contains("解析"));
assertTrue(markdown.contains("2024"));
}
}
@@ -1,47 +0,0 @@
package com.smsreceive.app.sms;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class VerificationCodeParserTest {
@Test
public void parsesChineseKeywordCode() {
VerificationCodeParser.ParseResult result = VerificationCodeParser.parse("【测试】验证码 123456,5 分钟内有效。");
assertTrue(result.success);
assertEquals("123456", result.code);
assertEquals("keyword_before_code", result.strategy);
}
@Test
public void parsesEnglishOtpCode() {
VerificationCodeParser.ParseResult result = VerificationCodeParser.parse("Your OTP code is 839204. Do not share it.");
assertTrue(result.success);
assertEquals("839204", result.code);
}
@Test
public void normalizesSpacesAndHyphens() {
assertEquals("123456", VerificationCodeParser.parse("验证码:12 34 56").code);
assertEquals("123456", VerificationCodeParser.parse("验证码:123-456").code);
}
@Test
public void prefersKeywordCandidate() {
VerificationCodeParser.ParseResult result = VerificationCodeParser.parse("订单 998877,验证码 246810,请勿泄露。");
assertTrue(result.success);
assertEquals("246810", result.code);
}
@Test
public void rejectsCommonFalsePositives() {
assertFalse(VerificationCodeParser.parse("订单金额 1234 元,手机号 13800138000。").success);
assertFalse(VerificationCodeParser.parse("会议日期 2026-05-16,无验证码。").success);
assertFalse(VerificationCodeParser.parse("这是一条普通通知。").success);
}
}