SMS-Receive/app/src/main/java/com/smsreceive/app/sms/SmsInboxReader.java
灌糖包子 c5ef726134
移除验证码解析功能,简化为纯短信转发
- 删除 VerificationCodeParser 及相关测试,短信捕获和推送不再解析验证码
- 飞书推送改为只发送短信原文,时间戳格式化为可读日期
- 移除主界面"只推送验证码"开关和"调试时上传完整短信正文"选项
- 移除"保存轮询间隔"按钮,开启轮询时自动保存间隔(未输入默认1秒)
- 按钮文字从"开始1秒轮询验证码"改为"开始短信轮询"
- 删除"打印最近30条短信"功能及相关 SmsInboxReader.logRecentMessages
- SmsInboxReader 用 RecentSmsResult 替换 RecentCodeResult
- FeishuWebhookConfigStore.Config 移除 filterVerificationCode/sendFullBodyDebug
- 修复代码缩进不一致问题
2026-05-18 22:38:06 +08:00

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);
}
}
}