- 删除 VerificationCodeParser 及相关测试,短信捕获和推送不再解析验证码 - 飞书推送改为只发送短信原文,时间戳格式化为可读日期 - 移除主界面"只推送验证码"开关和"调试时上传完整短信正文"选项 - 移除"保存轮询间隔"按钮,开启轮询时自动保存间隔(未输入默认1秒) - 按钮文字从"开始1秒轮询验证码"改为"开始短信轮询" - 删除"打印最近30条短信"功能及相关 SmsInboxReader.logRecentMessages - SmsInboxReader 用 RecentSmsResult 替换 RecentCodeResult - FeishuWebhookConfigStore.Config 移除 filterVerificationCode/sendFullBodyDebug - 修复代码缩进不一致问题
237 lines
9.6 KiB
Java
237 lines
9.6 KiB
Java
package com.smsreceive.app.sms;
|
|
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.net.Uri;
|
|
import android.provider.Telephony;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Date;
|
|
import java.util.Locale;
|
|
|
|
public final class SmsInboxReader {
|
|
private static final String TAG = "[SMS]SmsReceive";
|
|
private static final Uri SMS_INBOX_URI = Uri.parse("content://sms/inbox");
|
|
|
|
private SmsInboxReader() {
|
|
}
|
|
|
|
public static InboxResult readLatest(Context context) {
|
|
String[] projection = {
|
|
Telephony.Sms._ID,
|
|
Telephony.Sms.ADDRESS,
|
|
Telephony.Sms.BODY,
|
|
Telephony.Sms.DATE
|
|
};
|
|
|
|
try (Cursor cursor = context.getContentResolver().query(
|
|
SMS_INBOX_URI,
|
|
projection,
|
|
null,
|
|
null,
|
|
Telephony.Sms.DATE + " DESC LIMIT 1")) {
|
|
if (cursor == null) {
|
|
Log.w(TAG, "SmsInboxReader.readLatest failed: cursor is null");
|
|
return InboxResult.failure("短信库查询 cursor 为空");
|
|
}
|
|
if (!cursor.moveToFirst()) {
|
|
Log.w(TAG, "SmsInboxReader.readLatest failed: inbox empty");
|
|
return InboxResult.failure("短信收件箱为空");
|
|
}
|
|
|
|
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));
|
|
if (TextUtils.isEmpty(body)) {
|
|
Log.w(TAG, "SmsInboxReader.readLatest failed: empty body id=" + id);
|
|
return InboxResult.failure("最新短信正文为空");
|
|
}
|
|
|
|
Log.d(TAG, "SmsInboxReader.readLatest success id=" + id
|
|
+ ", sender=" + maskSender(sender)
|
|
+ ", date=" + date
|
|
+ ", bodyLength=" + body.length());
|
|
return InboxResult.success(id, sender, body, date);
|
|
} catch (SecurityException e) {
|
|
Log.w(TAG, "SmsInboxReader.readLatest failed: READ_SMS denied", e);
|
|
return InboxResult.failure("READ_SMS 未授权");
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "SmsInboxReader.readLatest failed", e);
|
|
return InboxResult.failure("短信库查询失败:" + e.getClass().getSimpleName());
|
|
}
|
|
}
|
|
|
|
public static RecentSmsResult findLatestSms(Context context, int limit) {
|
|
return findLatestSms(context, limit, 0L);
|
|
}
|
|
|
|
public static RecentSmsResult findLatestSms(Context context, int limit, long minDateMillis) {
|
|
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));
|
|
String selection = minDateMillis > 0L ? Telephony.Sms.DATE + ">=?" : null;
|
|
String[] selectionArgs = minDateMillis > 0L ? new String[]{String.valueOf(minDateMillis)} : null;
|
|
Log.d(TAG, "SmsInboxReader.findLatestSms start limit=" + safeLimit
|
|
+ ", minDate=" + (minDateMillis > 0L ? formatDate(minDateMillis) : "none"));
|
|
try (Cursor cursor = context.getContentResolver().query(
|
|
uri,
|
|
projection,
|
|
selection,
|
|
selectionArgs,
|
|
Telephony.Sms.DATE + " DESC LIMIT " + safeLimit)) {
|
|
if (cursor == null) {
|
|
Log.w(TAG, "SmsInboxReader.findLatestSms cursor is null");
|
|
return RecentSmsResult.failure("短信库查询 cursor 为空");
|
|
}
|
|
|
|
int scanned = 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));
|
|
scanned++;
|
|
Log.d(TAG, "poll scan SMS[" + (scanned - 1) + "] id=" + id
|
|
+ ", type=" + smsTypeName(type)
|
|
+ ", date=" + formatDate(date)
|
|
+ ", sender=" + maskSender(sender)
|
|
+ ", bodyPreview=" + previewBody(body));
|
|
if (TextUtils.isEmpty(body)) {
|
|
continue;
|
|
}
|
|
Log.d(TAG, "SmsInboxReader.findLatestSms hit latest id=" + id
|
|
+ ", date=" + formatDate(date)
|
|
+ ", scanned=" + scanned);
|
|
return RecentSmsResult.success(id, sender, body, date, scanned);
|
|
}
|
|
Log.d(TAG, "SmsInboxReader.findLatestSms no sms scanned=" + scanned);
|
|
return RecentSmsResult.noSms(scanned);
|
|
} catch (SecurityException e) {
|
|
Log.w(TAG, "SmsInboxReader.findLatestSms failed: READ_SMS denied", e);
|
|
return RecentSmsResult.failure("READ_SMS 未授权");
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "SmsInboxReader.findLatestSms failed", e);
|
|
return RecentSmsResult.failure("短信库查询失败:" + e.getClass().getSimpleName());
|
|
}
|
|
}
|
|
|
|
private static String maskSender(String sender) {
|
|
if (sender == null || sender.length() <= 4) {
|
|
return sender == null ? "" : sender;
|
|
}
|
|
return "***" + sender.substring(sender.length() - 4);
|
|
}
|
|
|
|
private static String previewBody(String body) {
|
|
if (TextUtils.isEmpty(body)) {
|
|
return "";
|
|
}
|
|
String normalized = body.replace('\n', ' ').replace('\r', ' ').trim();
|
|
return normalized.length() <= 80 ? normalized : normalized.substring(0, 80) + "...";
|
|
}
|
|
|
|
private static String formatDate(long dateMillis) {
|
|
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date(dateMillis));
|
|
}
|
|
|
|
private static String smsTypeName(int type) {
|
|
switch (type) {
|
|
case Telephony.Sms.MESSAGE_TYPE_INBOX:
|
|
return "INBOX";
|
|
case Telephony.Sms.MESSAGE_TYPE_SENT:
|
|
return "SENT";
|
|
case Telephony.Sms.MESSAGE_TYPE_DRAFT:
|
|
return "DRAFT";
|
|
case Telephony.Sms.MESSAGE_TYPE_OUTBOX:
|
|
return "OUTBOX";
|
|
case Telephony.Sms.MESSAGE_TYPE_FAILED:
|
|
return "FAILED";
|
|
case Telephony.Sms.MESSAGE_TYPE_QUEUED:
|
|
return "QUEUED";
|
|
default:
|
|
return "UNKNOWN(" + type + ")";
|
|
}
|
|
}
|
|
|
|
public static final class InboxResult {
|
|
public final boolean success;
|
|
public final long id;
|
|
public final String sender;
|
|
public final String body;
|
|
public final long dateMillis;
|
|
public final String failureReason;
|
|
|
|
private InboxResult(boolean success, long id, String sender, String body, long dateMillis, String failureReason) {
|
|
this.success = success;
|
|
this.id = id;
|
|
this.sender = sender == null ? "" : sender;
|
|
this.body = body == null ? "" : body;
|
|
this.dateMillis = dateMillis;
|
|
this.failureReason = failureReason == null ? "" : failureReason;
|
|
}
|
|
|
|
static InboxResult success(long id, String sender, String body, long dateMillis) {
|
|
return new InboxResult(true, id, sender, body, dateMillis, "");
|
|
}
|
|
|
|
static InboxResult failure(String reason) {
|
|
return new InboxResult(false, -1L, "", "", System.currentTimeMillis(), reason);
|
|
}
|
|
}
|
|
|
|
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 int scannedCount;
|
|
public final String failureReason;
|
|
|
|
private RecentSmsResult(
|
|
boolean success,
|
|
long id,
|
|
String sender,
|
|
String body,
|
|
long dateMillis,
|
|
int scannedCount,
|
|
String failureReason) {
|
|
this.success = success;
|
|
this.id = id;
|
|
this.sender = sender == null ? "" : sender;
|
|
this.body = body == null ? "" : body;
|
|
this.dateMillis = dateMillis;
|
|
this.scannedCount = scannedCount;
|
|
this.failureReason = failureReason == null ? "" : failureReason;
|
|
}
|
|
|
|
static RecentSmsResult success(
|
|
long id,
|
|
String sender,
|
|
String body,
|
|
long dateMillis,
|
|
int scannedCount) {
|
|
return new RecentSmsResult(true, id, sender, body, dateMillis, scannedCount, "");
|
|
}
|
|
|
|
static RecentSmsResult noSms(int scannedCount) {
|
|
return new RecentSmsResult(false, -1L, "", "", System.currentTimeMillis(), scannedCount, "最近短信未找到新短信");
|
|
}
|
|
|
|
static RecentSmsResult failure(String reason) {
|
|
return new RecentSmsResult(false, -1L, "", "", System.currentTimeMillis(), 0, reason);
|
|
}
|
|
}
|
|
}
|