diff --git a/app/src/androidTest/java/com/smsreceive/app/SmsProviderInstrumentedTest.java b/app/src/androidTest/java/com/smsreceive/app/SmsProviderInstrumentedTest.java deleted file mode 100644 index 0d9761d..0000000 --- a/app/src/androidTest/java/com/smsreceive/app/SmsProviderInstrumentedTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.smsreceive.app; - -import android.content.Context; -import android.util.Log; - -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Test; - -import static org.junit.Assert.assertTrue; - -public final class SmsProviderInstrumentedTest { - private static final String TAG = "[SMS]SmsReceive"; - - @Test - public void testLogRecentThirtyMessages() { - 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); - } -} diff --git a/app/src/androidTest/java/com/smsreceive/app/sms/SmsProviderInstrumentedTest.java b/app/src/androidTest/java/com/smsreceive/app/sms/SmsProviderInstrumentedTest.java new file mode 100644 index 0000000..332a608 --- /dev/null +++ b/app/src/androidTest/java/com/smsreceive/app/sms/SmsProviderInstrumentedTest.java @@ -0,0 +1,23 @@ +package com.smsreceive.app.sms; + +import android.content.Context; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public final class SmsProviderInstrumentedTest { + private static final String TAG = "[SMS]SmsReceive"; + + @Test + public void testReadLatestInboxQueryCompletes() { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + 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); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3a4ad5..1300a6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> @@ -25,7 +25,7 @@ @@ -35,7 +35,7 @@ @@ -46,12 +46,12 @@ diff --git a/app/src/main/java/com/smsreceive/app/FeishuWebhookPushResult.java b/app/src/main/java/com/smsreceive/app/FeishuWebhookPushResult.java deleted file mode 100644 index 0d66fdf..0000000 --- a/app/src/main/java/com/smsreceive/app/FeishuWebhookPushResult.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.smsreceive.app; - -final class FeishuWebhookPushResult { - static final String STATUS_SUCCESS = "success"; - static final String STATUS_DISABLED = "disabled"; - static final String STATUS_MISSING_CONFIG = "missing_config"; - static final String STATUS_SIGN_ERROR = "sign_error"; - static final String STATUS_NETWORK_ERROR = "network_error"; - static final String STATUS_TIMEOUT = "timeout"; - static final String STATUS_HTTP_ERROR = "http_error"; - static final String STATUS_INVALID_JSON = "invalid_json"; - static final String STATUS_API_ERROR = "api_error"; - - final boolean success; - final String status; - final String message; - final int httpStatus; - final int apiCode; - final long timeMillis; - - private FeishuWebhookPushResult( - boolean success, - String status, - String message, - int httpStatus, - int apiCode, - long timeMillis) { - this.success = success; - this.status = status == null ? "" : status; - this.message = message == null ? "" : message; - this.httpStatus = httpStatus; - this.apiCode = apiCode; - this.timeMillis = timeMillis; - } - - static FeishuWebhookPushResult success(String message) { - return new FeishuWebhookPushResult(true, STATUS_SUCCESS, message, 200, 0, System.currentTimeMillis()); - } - - static FeishuWebhookPushResult failure(String status, String message) { - return failure(status, message, 0, 0); - } - - static FeishuWebhookPushResult failure(String status, String message, int httpStatus, int apiCode) { - return new FeishuWebhookPushResult(false, status, message, httpStatus, apiCode, System.currentTimeMillis()); - } -} diff --git a/app/src/main/java/com/smsreceive/app/VerificationCodeParser.java b/app/src/main/java/com/smsreceive/app/VerificationCodeParser.java deleted file mode 100644 index a3f5d55..0000000 --- a/app/src/main/java/com/smsreceive/app/VerificationCodeParser.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.smsreceive.app; - -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("(? 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); - } - } -} diff --git a/app/src/main/java/com/smsreceive/app/FeishuWebhookClient.java b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookClient.java similarity index 81% rename from app/src/main/java/com/smsreceive/app/FeishuWebhookClient.java rename to app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookClient.java index ab29327..164445d 100644 --- a/app/src/main/java/com/smsreceive/app/FeishuWebhookClient.java +++ b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookClient.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.feishu; import android.content.Context; import android.content.Intent; @@ -11,6 +11,8 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import com.smsreceive.app.sms.CaptureResult; + import java.io.IOException; import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; @@ -29,7 +31,7 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -final class FeishuWebhookClient { +public final class FeishuWebhookClient { private static final String TAG = "[SMS]SmsReceive"; private static final String WEBHOOK_URL_PREFIX = "https://open.feishu.cn/open-apis/bot/v2/hook/"; private static final char[] BASE64_TABLE = @@ -47,17 +49,16 @@ final class FeishuWebhookClient { private FeishuWebhookClient() { } - static void pushCaptureResultAsync(Context context, CaptureResult result) { + public static void pushCaptureResultAsync(Context context, CaptureResult result) { if (context == null || result == null) { return; } + 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 (config.filterVerificationCode && !result.parseResult.success) { - Log.d(TAG, "Feishu push skipped: filter code enabled and parse failed, reason=" - + result.parseResult.failureReason); - return; - } if (FeishuWebhookConfigStore.wasSmsPushed(appContext, result)) { Log.d(TAG, "Feishu push skipped: duplicate sms receivedSecond=" + (result.receivedAtMillis / 1000L) @@ -72,11 +73,11 @@ final class FeishuWebhookClient { "远端推送未开启")); return; } - String markdown = buildMarkdownFromCapture(result, config); + String markdown = buildMarkdownFromCapture(result); pushMarkdownAsync(appContext, markdown, result); } - static void pushMarkdownAsync(Context context, String markdownContent) { + public static void pushMarkdownAsync(Context context, String markdownContent) { pushMarkdownAsync(context, markdownContent, null); } @@ -98,7 +99,7 @@ final class FeishuWebhookClient { }); } - static FeishuWebhookPushResult pushMarkdown( + public static FeishuWebhookPushResult pushMarkdown( FeishuWebhookConfigStore.Config config, String markdownContent, long timestampSeconds) { @@ -157,14 +158,14 @@ final class FeishuWebhookClient { } } - static String generateSign(String secret, long timestampSeconds) throws GeneralSecurityException { + public static String generateSign(String secret, long timestampSeconds) throws GeneralSecurityException { String stringToSign = timestampSeconds + "\n" + (secret == null ? "" : secret); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); return base64EncodeNoWrap(mac.doFinal(new byte[0])); } - static String buildRequestJson(String markdownContent, long timestampSeconds, String sign) throws JSONException { + public static String buildRequestJson(String markdownContent, long timestampSeconds, String sign) throws JSONException { JSONObject markdown = new JSONObject() .put("tag", "markdown") .put("content", markdownContent == null ? "" : markdownContent); @@ -178,7 +179,7 @@ final class FeishuWebhookClient { .toString(); } - static FeishuWebhookPushResult parseResponse(String responseBody) { + public static FeishuWebhookPushResult parseResponse(String responseBody) { try { JSONObject json = new JSONObject(responseBody == null ? "" : responseBody); int code = json.optInt("code", Integer.MIN_VALUE); @@ -198,43 +199,27 @@ final class FeishuWebhookClient { } } - static String buildWebhookUrl(String webhookId) { + public static String buildWebhookUrl(String webhookId) { return WEBHOOK_URL_PREFIX + (webhookId == null ? "" : webhookId.trim()); } - static String buildMarkdownFromCapture(CaptureResult result) { - return buildMarkdownFromCapture(result, new FeishuWebhookConfigStore.Config(false, "", "", false, false)); - } - - static String buildMarkdownFromCapture(CaptureResult result, FeishuWebhookConfigStore.Config config) { - boolean filterCode = config != null && config.filterVerificationCode; - boolean includeFullBody = config == null || !filterCode || config.sendFullBodyDebug; + public static String buildMarkdownFromCapture(CaptureResult result) { 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", diff --git a/app/src/main/java/com/smsreceive/app/FeishuWebhookConfigStore.java b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookConfigStore.java similarity index 81% rename from app/src/main/java/com/smsreceive/app/FeishuWebhookConfigStore.java rename to app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookConfigStore.java index 2774e1f..34d9c13 100644 --- a/app/src/main/java/com/smsreceive/app/FeishuWebhookConfigStore.java +++ b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookConfigStore.java @@ -1,9 +1,11 @@ -package com.smsreceive.app; +package com.smsreceive.app.feishu; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; +import com.smsreceive.app.sms.CaptureResult; + import org.json.JSONException; import org.json.JSONObject; @@ -16,8 +18,8 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -final class FeishuWebhookConfigStore { - static final String ACTION_PUSH_UPDATED = "com.smsreceive.app.ACTION_FEISHU_PUSH_UPDATED"; +public final class FeishuWebhookConfigStore { + public static final String ACTION_PUSH_UPDATED = "com.smsreceive.app.ACTION_FEISHU_PUSH_UPDATED"; private static final String TAG = "[SMS]SmsReceive"; private static final String PREFS = "feishu_webhook"; @@ -37,13 +39,11 @@ 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() { } - static Config loadConfig(Context context) { + public static Config loadConfig(Context context) { ensureDefaultConfigFile(context); File file = configFile(context); if (!file.exists()) { @@ -59,30 +59,26 @@ final class FeishuWebhookConfigStore { } } - static void saveConfig( + public static void saveConfig( 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); } } - static void saveLastResult(Context context, FeishuWebhookPushResult result) { + public static void saveLastResult(Context context, FeishuWebhookPushResult result) { preferences(context).edit() .putLong(KEY_LAST_TIME, result.timeMillis) .putBoolean(KEY_LAST_SUCCESS, result.success) @@ -136,7 +132,7 @@ final class FeishuWebhookConfigStore { + ", smsKey=" + smsKey); } - static LastResult loadLastResult(Context context) { + public static LastResult loadLastResult(Context context) { SharedPreferences prefs = preferences(context); return new LastResult( prefs.getLong(KEY_LAST_TIME, 0L), @@ -147,14 +143,14 @@ final class FeishuWebhookConfigStore { prefs.getInt(KEY_LAST_API_CODE, 0)); } - static LastPushedSms loadLastPushedSms(Context context) { + public static LastPushedSms loadLastPushedSms(Context context) { SharedPreferences prefs = preferences(context); return new LastPushedSms( prefs.getLong(KEY_LAST_PUSHED_SMS_RECEIVED_SECOND, 0L), prefs.getString(KEY_LAST_PUSHED_SMS_KEY, "")); } - static String maskSecret(String secret) { + public static String maskSecret(String secret) { if (isEmpty(secret)) { return ""; } @@ -164,19 +160,19 @@ final class FeishuWebhookConfigStore { return secret.substring(0, 3) + "***" + secret.substring(secret.length() - 3); } - static String configPath(Context context) { + public static String configPath(Context context) { return configFile(context).getAbsolutePath(); } - static String defaultConfigPath(Context context) { + public static String defaultConfigPath(Context context) { return defaultConfigFile(context).getAbsolutePath(); } - static String configTemplate() { + public static String configTemplate() { 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\":\"\"}"; } } @@ -215,25 +211,21 @@ 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 { @@ -289,42 +281,36 @@ final class FeishuWebhookConfigStore { + Integer.toHexString((result.body == null ? "" : result.body).hashCode()); } - static final class Config { - final boolean enabled; - final String webhookId; - final String secret; - final boolean sendFullBodyDebug; - final boolean filterVerificationCode; + public static final class Config { + public final boolean enabled; + public final String webhookId; + public final String secret; - Config( + 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; } - boolean hasWebhookId() { + public boolean hasWebhookId() { return !isEmpty(webhookId); } - boolean hasSecret() { + public boolean hasSecret() { return !isEmpty(secret); } } - static final class LastResult { - final long timeMillis; - final boolean success; - final String status; - final String message; - final int httpStatus; - final int apiCode; + public static final class LastResult { + public final long timeMillis; + public final boolean success; + public final String status; + public final String message; + public final int httpStatus; + public final int apiCode; LastResult(long timeMillis, boolean success, String status, String message, int httpStatus, int apiCode) { this.timeMillis = timeMillis; @@ -336,9 +322,9 @@ final class FeishuWebhookConfigStore { } } - static final class LastPushedSms { - final long receivedSecond; - final String smsKey; + public static final class LastPushedSms { + public final long receivedSecond; + public final String smsKey; LastPushedSms(long receivedSecond, String smsKey) { this.receivedSecond = receivedSecond; diff --git a/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookPushResult.java b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookPushResult.java new file mode 100644 index 0000000..232685a --- /dev/null +++ b/app/src/main/java/com/smsreceive/app/feishu/FeishuWebhookPushResult.java @@ -0,0 +1,47 @@ +package com.smsreceive.app.feishu; + +public final class FeishuWebhookPushResult { + public static final String STATUS_SUCCESS = "success"; + public static final String STATUS_DISABLED = "disabled"; + public static final String STATUS_MISSING_CONFIG = "missing_config"; + public static final String STATUS_SIGN_ERROR = "sign_error"; + public static final String STATUS_NETWORK_ERROR = "network_error"; + public static final String STATUS_TIMEOUT = "timeout"; + public static final String STATUS_HTTP_ERROR = "http_error"; + public static final String STATUS_INVALID_JSON = "invalid_json"; + public static final String STATUS_API_ERROR = "api_error"; + + public final boolean success; + public final String status; + public final String message; + public final int httpStatus; + public final int apiCode; + public final long timeMillis; + + private FeishuWebhookPushResult( + boolean success, + String status, + String message, + int httpStatus, + int apiCode, + long timeMillis) { + this.success = success; + this.status = status == null ? "" : status; + this.message = message == null ? "" : message; + this.httpStatus = httpStatus; + this.apiCode = apiCode; + this.timeMillis = timeMillis; + } + + public static FeishuWebhookPushResult success(String message) { + return new FeishuWebhookPushResult(true, STATUS_SUCCESS, message, 200, 0, System.currentTimeMillis()); + } + + public static FeishuWebhookPushResult failure(String status, String message) { + return failure(status, message, 0, 0); + } + + public static FeishuWebhookPushResult failure(String status, String message, int httpStatus, int apiCode) { + return new FeishuWebhookPushResult(false, status, message, httpStatus, apiCode, System.currentTimeMillis()); + } +} diff --git a/app/src/main/java/com/smsreceive/app/BootReceiver.java b/app/src/main/java/com/smsreceive/app/keepalive/BootReceiver.java similarity index 98% rename from app/src/main/java/com/smsreceive/app/BootReceiver.java rename to app/src/main/java/com/smsreceive/app/keepalive/BootReceiver.java index 39108b7..db9d459 100644 --- a/app/src/main/java/com/smsreceive/app/BootReceiver.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/BootReceiver.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/app/src/main/java/com/smsreceive/app/KeepAliveDatabase.java b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveDatabase.java similarity index 94% rename from app/src/main/java/com/smsreceive/app/KeepAliveDatabase.java rename to app/src/main/java/com/smsreceive/app/keepalive/KeepAliveDatabase.java index d4fab6e..3ed49a2 100644 --- a/app/src/main/java/com/smsreceive/app/KeepAliveDatabase.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveDatabase.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.content.ContentValues; import android.content.Context; @@ -11,7 +11,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -final class KeepAliveDatabase { +public final class KeepAliveDatabase { private static final String TAG = "[SMS]SmsReceive"; private static final String DATABASE_NAME = "sms_keep_alive.db"; private static final int DATABASE_VERSION = 1; @@ -23,7 +23,7 @@ final class KeepAliveDatabase { private KeepAliveDatabase() { } - static long writeLastActiveTime(Context context) { + public static long writeLastActiveTime(Context context) { long now = System.currentTimeMillis(); SQLiteDatabase database = helper(context).getWritableDatabase(); ContentValues values = new ContentValues(); @@ -35,7 +35,7 @@ final class KeepAliveDatabase { return now; } - static long readLastActiveTime(Context context) { + public static long readLastActiveTime(Context context) { SQLiteDatabase database = helper(context).getReadableDatabase(); try (Cursor cursor = database.query( TABLE_META, diff --git a/app/src/main/java/com/smsreceive/app/KeepAliveNotification.java b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveNotification.java similarity index 94% rename from app/src/main/java/com/smsreceive/app/KeepAliveNotification.java rename to app/src/main/java/com/smsreceive/app/keepalive/KeepAliveNotification.java index 4d44b08..99faadf 100644 --- a/app/src/main/java/com/smsreceive/app/KeepAliveNotification.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveNotification.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.app.Notification; import android.app.NotificationChannel; @@ -9,6 +9,8 @@ import android.content.Intent; import android.os.Build; import android.util.Log; +import com.smsreceive.app.ui.MainActivity; + final class KeepAliveNotification { static final int NOTIFICATION_ID = 2101; private static final String TAG = "[SMS]SmsReceive"; @@ -34,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) diff --git a/app/src/main/java/com/smsreceive/app/KeepAliveStateStore.java b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveStateStore.java similarity index 79% rename from app/src/main/java/com/smsreceive/app/KeepAliveStateStore.java rename to app/src/main/java/com/smsreceive/app/keepalive/KeepAliveStateStore.java index a1d75c0..74de526 100644 --- a/app/src/main/java/com/smsreceive/app/KeepAliveStateStore.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/KeepAliveStateStore.java @@ -1,11 +1,11 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; -final class KeepAliveStateStore { +public final class KeepAliveStateStore { private static final String TAG = "[SMS]SmsReceive"; private static final String PREFS = "sms_keep_alive"; @@ -23,14 +23,14 @@ final class KeepAliveStateStore { private KeepAliveStateStore() { } - static void setEnabledByUser(Context context, boolean enabled) { + public static void setEnabledByUser(Context context, boolean enabled) { Log.d(TAG, "KeepAliveStateStore.setEnabledByUser enabled=" + enabled); preferences(context).edit() .putBoolean(KEY_ENABLED_BY_USER, enabled) .apply(); } - static void recordServiceStarted(Context context) { + public static void recordServiceStarted(Context context) { long now = System.currentTimeMillis(); Log.d(TAG, "KeepAliveStateStore.recordServiceStarted time=" + now); preferences(context).edit() @@ -40,7 +40,7 @@ final class KeepAliveStateStore { .apply(); } - static void recordServiceStopped(Context context, String reason) { + public static void recordServiceStopped(Context context, String reason) { Log.d(TAG, "KeepAliveStateStore.recordServiceStopped reason=" + reason); preferences(context).edit() .putBoolean(KEY_SERVICE_RUNNING, false) @@ -48,7 +48,7 @@ final class KeepAliveStateStore { .apply(); } - static void recordHeartbeat(Context context) { + public static void recordHeartbeat(Context context) { Log.d(TAG, "KeepAliveStateStore.recordHeartbeat"); preferences(context).edit() .putBoolean(KEY_SERVICE_RUNNING, true) @@ -56,7 +56,7 @@ final class KeepAliveStateStore { .apply(); } - static void recordBootEvent(Context context, String action) { + public static void recordBootEvent(Context context, String action) { long now = System.currentTimeMillis(); Log.d(TAG, "KeepAliveStateStore.recordBootEvent action=" + action + ", time=" + now); preferences(context).edit() @@ -65,7 +65,7 @@ final class KeepAliveStateStore { .apply(); } - static void recordServiceStartFailure(Context context, String reason) { + public static void recordServiceStartFailure(Context context, String reason) { Log.w(TAG, "KeepAliveStateStore.recordServiceStartFailure reason=" + reason); preferences(context).edit() .putBoolean(KEY_SERVICE_RUNNING, false) @@ -73,39 +73,39 @@ final class KeepAliveStateStore { .apply(); } - static void setManualAutostartConfirmed(Context context, boolean confirmed) { + public static void setManualAutostartConfirmed(Context context, boolean confirmed) { Log.d(TAG, "KeepAliveStateStore.setManualAutostartConfirmed confirmed=" + confirmed); preferences(context).edit() .putBoolean(KEY_MANUAL_AUTOSTART_CONFIRMED, confirmed) .apply(); } - static void setManualBatteryUnrestrictedConfirmed(Context context, boolean confirmed) { + public static void setManualBatteryUnrestrictedConfirmed(Context context, boolean confirmed) { Log.d(TAG, "KeepAliveStateStore.setManualBatteryUnrestrictedConfirmed confirmed=" + confirmed); preferences(context).edit() .putBoolean(KEY_MANUAL_BATTERY_UNRESTRICTED_CONFIRMED, confirmed) .apply(); } - static void setBatteryOptimizationIgnored(Context context, boolean ignored) { + public static void setBatteryOptimizationIgnored(Context context, boolean ignored) { Log.d(TAG, "KeepAliveStateStore.setBatteryOptimizationIgnored ignored=" + ignored); preferences(context).edit() .putBoolean(KEY_BATTERY_OPTIMIZATION_IGNORED, ignored) .apply(); } - static void setToastOnDatabaseWrite(Context context, boolean enabled) { + public static void setToastOnDatabaseWrite(Context context, boolean enabled) { Log.d(TAG, "KeepAliveStateStore.setToastOnDatabaseWrite enabled=" + enabled); preferences(context).edit() .putBoolean(KEY_TOAST_ON_DATABASE_WRITE, enabled) .apply(); } - static boolean isToastOnDatabaseWriteEnabled(Context context) { + public static boolean isToastOnDatabaseWriteEnabled(Context context) { return preferences(context).getBoolean(KEY_TOAST_ON_DATABASE_WRITE, false); } - static State load(Context context) { + public static State load(Context context) { SharedPreferences prefs = preferences(context); return new State( prefs.getBoolean(KEY_ENABLED_BY_USER, false), @@ -128,17 +128,17 @@ final class KeepAliveStateStore { return TextUtils.isEmpty(value) ? "" : value; } - static final class State { - final boolean enabledByUser; - final boolean serviceRunning; - final long lastHeartbeatMillis; - final String lastBootEvent; - final long lastBootTimeMillis; - final String lastServiceStartFailure; - final boolean manualAutostartConfirmed; - final boolean manualBatteryUnrestrictedConfirmed; - final boolean batteryOptimizationIgnored; - final boolean toastOnDatabaseWrite; + public static final class State { + public final boolean enabledByUser; + public final boolean serviceRunning; + public final long lastHeartbeatMillis; + public final String lastBootEvent; + public final long lastBootTimeMillis; + public final String lastServiceStartFailure; + public final boolean manualAutostartConfirmed; + public final boolean manualBatteryUnrestrictedConfirmed; + public final boolean batteryOptimizationIgnored; + public final boolean toastOnDatabaseWrite; State( boolean enabledByUser, diff --git a/app/src/main/java/com/smsreceive/app/SmsKeepAliveService.java b/app/src/main/java/com/smsreceive/app/keepalive/SmsKeepAliveService.java similarity index 95% rename from app/src/main/java/com/smsreceive/app/SmsKeepAliveService.java rename to app/src/main/java/com/smsreceive/app/keepalive/SmsKeepAliveService.java index 8ec1138..0ebaa68 100644 --- a/app/src/main/java/com/smsreceive/app/SmsKeepAliveService.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/SmsKeepAliveService.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.app.Service; import android.content.Context; @@ -10,6 +10,8 @@ import android.os.Looper; import android.util.Log; import android.widget.Toast; +import com.smsreceive.app.sms.SmsCaptureStore; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -41,7 +43,7 @@ public final class SmsKeepAliveService extends Service { } }; - static void start(Context context) { + public static void start(Context context) { Log.d(TAG, "SmsKeepAliveService.start requested sdk=" + Build.VERSION.SDK_INT); Intent intent = new Intent(context, SmsKeepAliveService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -51,7 +53,7 @@ public final class SmsKeepAliveService extends Service { } } - static void stop(Context context) { + public static void stop(Context context) { Log.d(TAG, "SmsKeepAliveService.stop requested"); context.stopService(new Intent(context, SmsKeepAliveService.class)); } diff --git a/app/src/main/java/com/smsreceive/app/SmsPollingService.java b/app/src/main/java/com/smsreceive/app/keepalive/SmsPollingService.java similarity index 86% rename from app/src/main/java/com/smsreceive/app/SmsPollingService.java rename to app/src/main/java/com/smsreceive/app/keepalive/SmsPollingService.java index 0ec0aa9..e283a7f 100644 --- a/app/src/main/java/com/smsreceive/app/SmsPollingService.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/SmsPollingService.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.Manifest; import android.app.Service; @@ -12,6 +12,11 @@ import android.os.Looper; import android.util.Log; import android.widget.Toast; +import com.smsreceive.app.feishu.FeishuWebhookClient; +import com.smsreceive.app.sms.CaptureResult; +import com.smsreceive.app.sms.SmsCaptureStore; +import com.smsreceive.app.sms.SmsInboxReader; + public final class SmsPollingService extends Service { private static final String TAG = "[SMS]SmsReceive"; private static final String SOURCE_INBOX_POLLING = "sms_inbox_polling"; @@ -22,14 +27,14 @@ 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); } }; - static void start(Context context) { + public static void start(Context context) { Log.d(TAG, "SmsPollingService.start requested sdk=" + Build.VERSION.SDK_INT); long startTimeMillis = System.currentTimeMillis() - 2_000L; SmsPollingStateStore.recordStarted(context, startTimeMillis); @@ -42,7 +47,7 @@ public final class SmsPollingService extends Service { } } - static void stop(Context context) { + public static void stop(Context context) { Log.d(TAG, "SmsPollingService.stop requested"); SmsPollingStateStore.recordStopped(context, "用户停止轮询"); context.stopService(new Intent(context, SmsPollingService.class)); @@ -87,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(); @@ -96,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; } @@ -109,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); @@ -125,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() { diff --git a/app/src/main/java/com/smsreceive/app/SmsPollingStateStore.java b/app/src/main/java/com/smsreceive/app/keepalive/SmsPollingStateStore.java similarity index 81% rename from app/src/main/java/com/smsreceive/app/SmsPollingStateStore.java rename to app/src/main/java/com/smsreceive/app/keepalive/SmsPollingStateStore.java index 6cc23db..daa5521 100644 --- a/app/src/main/java/com/smsreceive/app/SmsPollingStateStore.java +++ b/app/src/main/java/com/smsreceive/app/keepalive/SmsPollingStateStore.java @@ -1,11 +1,11 @@ -package com.smsreceive.app; +package com.smsreceive.app.keepalive; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; -final class SmsPollingStateStore { +public final class SmsPollingStateStore { private static final String TAG = "[SMS]SmsReceive"; private static final String PREFS = "sms_polling"; private static final String KEY_ENABLED_BY_USER = "enabled_by_user"; @@ -22,7 +22,7 @@ final class SmsPollingStateStore { private SmsPollingStateStore() { } - static void recordStarted(Context context, long startTimeMillis) { + public static void recordStarted(Context context, long startTimeMillis) { Log.d(TAG, "SmsPollingStateStore.recordStarted startTime=" + startTimeMillis); preferences(context).edit() .putBoolean(KEY_ENABLED_BY_USER, true) @@ -32,7 +32,7 @@ final class SmsPollingStateStore { .apply(); } - static void recordStopped(Context context, String reason) { + public static void recordStopped(Context context, String reason) { Log.d(TAG, "SmsPollingStateStore.recordStopped reason=" + reason); preferences(context).edit() .putBoolean(KEY_ENABLED_BY_USER, false) @@ -41,7 +41,7 @@ final class SmsPollingStateStore { .apply(); } - static void recordServiceStopped(Context context, String reason) { + public static void recordServiceStopped(Context context, String reason) { Log.d(TAG, "SmsPollingStateStore.recordServiceStopped reason=" + reason); preferences(context).edit() .putBoolean(KEY_RUNNING, false) @@ -49,13 +49,13 @@ final class SmsPollingStateStore { .apply(); } - static void recordServiceRunning(Context context) { + public static void recordServiceRunning(Context context) { preferences(context).edit() .putBoolean(KEY_RUNNING, true) .apply(); } - static void recordHit(Context context, long smsId, long hitTimeMillis) { + public static void recordHit(Context context, long smsId, long hitTimeMillis) { Log.d(TAG, "SmsPollingStateStore.recordHit id=" + smsId + ", time=" + hitTimeMillis); preferences(context).edit() .putLong(KEY_LAST_HIT_ID, smsId) @@ -64,7 +64,7 @@ final class SmsPollingStateStore { .apply(); } - static void setIntervalSeconds(Context context, int seconds) { + public static void setIntervalSeconds(Context context, int seconds) { int safeSeconds = clampIntervalSeconds(seconds); Log.d(TAG, "SmsPollingStateStore.setIntervalSeconds seconds=" + safeSeconds); preferences(context).edit() @@ -72,11 +72,11 @@ final class SmsPollingStateStore { .apply(); } - static int getIntervalSeconds(Context context) { + public static int getIntervalSeconds(Context context) { return clampIntervalSeconds(preferences(context).getInt(KEY_INTERVAL_SECONDS, DEFAULT_INTERVAL_SECONDS)); } - static State load(Context context) { + public static State load(Context context) { SharedPreferences prefs = preferences(context); return new State( prefs.getBoolean(KEY_ENABLED_BY_USER, false), @@ -100,14 +100,14 @@ final class SmsPollingStateStore { return Math.max(MIN_INTERVAL_SECONDS, Math.min(seconds, MAX_INTERVAL_SECONDS)); } - static final class State { - final boolean enabledByUser; - final boolean running; - final long startTimeMillis; - final long lastHitId; - final long lastHitTimeMillis; - final String lastFailure; - final int intervalSeconds; + public static final class State { + public final boolean enabledByUser; + public final boolean running; + public final long startTimeMillis; + public final long lastHitId; + public final long lastHitTimeMillis; + public final String lastFailure; + public final int intervalSeconds; State( boolean enabledByUser, diff --git a/app/src/main/java/com/smsreceive/app/CaptureResult.java b/app/src/main/java/com/smsreceive/app/sms/CaptureResult.java similarity index 60% rename from app/src/main/java/com/smsreceive/app/CaptureResult.java rename to app/src/main/java/com/smsreceive/app/sms/CaptureResult.java index 39eb183..8e5d2e5 100644 --- a/app/src/main/java/com/smsreceive/app/CaptureResult.java +++ b/app/src/main/java/com/smsreceive/app/sms/CaptureResult.java @@ -1,53 +1,50 @@ -package com.smsreceive.app; +package com.smsreceive.app.sms; -final class CaptureResult { - static final long UNKNOWN_SMS_PROVIDER_ID = -1L; +public final class CaptureResult { + public static final long UNKNOWN_SMS_PROVIDER_ID = -1L; - final long receivedAtMillis; - final long smsProviderId; - final String sender; - final String body; - final VerificationCodeParser.ParseResult parseResult; - final String source; - final String failureReason; + public final boolean success; + public final long receivedAtMillis; + public final long smsProviderId; + public final String sender; + public final String body; + public final String source; + public final String failureReason; private CaptureResult( long receivedAtMillis, 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; } - static CaptureResult success( + public static CaptureResult success( 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); } - static CaptureResult success( + public static CaptureResult success( long receivedAtMillis, 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, ""); } - static CaptureResult failure( + public static CaptureResult failure( long receivedAtMillis, String sender, String body, @@ -56,14 +53,13 @@ final class CaptureResult { return failure(receivedAtMillis, UNKNOWN_SMS_PROVIDER_ID, sender, body, source, failureReason); } - static CaptureResult failure( + public static CaptureResult failure( long receivedAtMillis, long smsProviderId, String sender, 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); } } diff --git a/app/src/main/java/com/smsreceive/app/SmsCaptureStore.java b/app/src/main/java/com/smsreceive/app/sms/SmsCaptureStore.java similarity index 67% rename from app/src/main/java/com/smsreceive/app/SmsCaptureStore.java rename to app/src/main/java/com/smsreceive/app/sms/SmsCaptureStore.java index 4b2c111..b8992cd 100644 --- a/app/src/main/java/com/smsreceive/app/SmsCaptureStore.java +++ b/app/src/main/java/com/smsreceive/app/sms/SmsCaptureStore.java @@ -1,22 +1,21 @@ -package com.smsreceive.app; +package com.smsreceive.app.sms; import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.Log; -final class SmsCaptureStore { - static final String ACTION_CAPTURE_UPDATED = "com.smsreceive.app.ACTION_CAPTURE_UPDATED"; +public final class SmsCaptureStore { + public static final String ACTION_CAPTURE_UPDATED = "com.smsreceive.app.ACTION_CAPTURE_UPDATED"; 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"; @@ -25,20 +24,17 @@ final class SmsCaptureStore { private SmsCaptureStore() { } - static void save(Context context, CaptureResult result) { - VerificationCodeParser.ParseResult parse = result.parseResult; + public static void save(Context context, CaptureResult result) { 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=" @@ -53,25 +49,26 @@ final class SmsCaptureStore { editor.apply(); } - static StoredCapture load(Context context) { + 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); } - static void clear(Context context) { + public static void clear(Context context) { Log.d(TAG, "SmsCaptureStore.clear"); preferences(context).edit().clear().apply(); } - static DeliveryDiagnostics loadDeliveryDiagnostics(Context context) { + public static DeliveryDiagnostics loadDeliveryDiagnostics(Context context) { SharedPreferences prefs = preferences(context); return new DeliveryDiagnostics( prefs.getLong(KEY_LAST_BROADCAST_TIME, 0L), @@ -101,40 +98,37 @@ final class SmsCaptureStore { return normalized.length() <= 48 ? normalized : normalized.substring(0, 48) + "..."; } - static final class StoredCapture { - final long timeMillis; - final String sender; - final String code; - final String strategy; - final int confidence; - final String source; - final String failure; - final String bodyPreview; + public static final class StoredCapture { + public final boolean success; + public final long timeMillis; + public final String sender; + 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; } } - static final class DeliveryDiagnostics { - final long lastBroadcastTimeMillis; - final long lastInboxTimeMillis; - final String lastInboxSource; + public static final class DeliveryDiagnostics { + public final long lastBroadcastTimeMillis; + public final long lastInboxTimeMillis; + public final String lastInboxSource; DeliveryDiagnostics(long lastBroadcastTimeMillis, long lastInboxTimeMillis, String lastInboxSource) { this.lastBroadcastTimeMillis = lastBroadcastTimeMillis; @@ -142,7 +136,7 @@ final class SmsCaptureStore { this.lastInboxSource = lastInboxSource == null ? "" : lastInboxSource; } - boolean inboxNewerThanBroadcast() { + public boolean inboxNewerThanBroadcast() { return lastInboxTimeMillis > 0L && lastInboxTimeMillis > lastBroadcastTimeMillis; } } diff --git a/app/src/main/java/com/smsreceive/app/SmsInboxReader.java b/app/src/main/java/com/smsreceive/app/sms/SmsInboxReader.java similarity index 53% rename from app/src/main/java/com/smsreceive/app/SmsInboxReader.java rename to app/src/main/java/com/smsreceive/app/sms/SmsInboxReader.java index 9b12186..71f3c5b 100644 --- a/app/src/main/java/com/smsreceive/app/SmsInboxReader.java +++ b/app/src/main/java/com/smsreceive/app/sms/SmsInboxReader.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.sms; import android.content.Context; import android.database.Cursor; @@ -11,14 +11,14 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -final class SmsInboxReader { +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() { } - static InboxResult readLatest(Context context) { + public static InboxResult readLatest(Context context) { String[] projection = { Telephony.Sms._ID, Telephony.Sms.ADDRESS, @@ -64,63 +64,11 @@ final class SmsInboxReader { } } - 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); } - static RecentCodeResult findLatestVerificationCode(Context context, int limit) { - return findLatestVerificationCode(context, limit, 0L); - } - - 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 @@ 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 @@ 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()); } } @@ -231,13 +164,13 @@ final class SmsInboxReader { } } - static final class InboxResult { - final boolean success; - final long id; - final String sender; - final String body; - final long dateMillis; - final String failureReason; + 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; @@ -257,23 +190,21 @@ final class SmsInboxReader { } } - static final class RecentCodeResult { - final boolean success; - final long id; - final String sender; - final String body; - final long dateMillis; - final VerificationCodeParser.ParseResult parseResult; - final int scannedCount; - final String failureReason; + 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 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 @@ 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); } } } diff --git a/app/src/main/java/com/smsreceive/app/SmsMessageReader.java b/app/src/main/java/com/smsreceive/app/sms/SmsMessageReader.java similarity index 98% rename from app/src/main/java/com/smsreceive/app/SmsMessageReader.java rename to app/src/main/java/com/smsreceive/app/sms/SmsMessageReader.java index e28864a..dcac92e 100644 --- a/app/src/main/java/com/smsreceive/app/SmsMessageReader.java +++ b/app/src/main/java/com/smsreceive/app/sms/SmsMessageReader.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.sms; import android.content.Intent; import android.provider.Telephony; diff --git a/app/src/main/java/com/smsreceive/app/SmsReceiver.java b/app/src/main/java/com/smsreceive/app/sms/SmsReceiver.java similarity index 64% rename from app/src/main/java/com/smsreceive/app/SmsReceiver.java rename to app/src/main/java/com/smsreceive/app/sms/SmsReceiver.java index 78c3540..453fe45 100644 --- a/app/src/main/java/com/smsreceive/app/SmsReceiver.java +++ b/app/src/main/java/com/smsreceive/app/sms/SmsReceiver.java @@ -1,4 +1,4 @@ -package com.smsreceive.app; +package com.smsreceive.app.sms; import android.content.BroadcastReceiver; import android.content.Context; @@ -7,6 +7,8 @@ import android.provider.Telephony; import android.util.Log; import android.widget.Toast; +import com.smsreceive.app.feishu.FeishuWebhookClient; + public final class SmsReceiver extends BroadcastReceiver { private static final String TAG = "[SMS]SmsReceive"; private static final String SOURCE_SYSTEM_BROADCAST = "system_sms_broadcast"; @@ -23,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(); @@ -69,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) { diff --git a/app/src/main/java/com/smsreceive/app/MainActivity.java b/app/src/main/java/com/smsreceive/app/ui/MainActivity.java similarity index 63% rename from app/src/main/java/com/smsreceive/app/MainActivity.java rename to app/src/main/java/com/smsreceive/app/ui/MainActivity.java index 00c5f30..8d1f8e6 100644 --- a/app/src/main/java/com/smsreceive/app/MainActivity.java +++ b/app/src/main/java/com/smsreceive/app/ui/MainActivity.java @@ -1,7 +1,8 @@ -package com.smsreceive.app; +package com.smsreceive.app.ui; import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.app.NotificationManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -19,22 +20,30 @@ import android.os.Looper; import android.os.PowerManager; import android.provider.Settings; import android.provider.Telephony; -import android.text.InputType; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; 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; +import com.smsreceive.app.R; +import com.smsreceive.app.feishu.FeishuWebhookClient; +import com.smsreceive.app.feishu.FeishuWebhookConfigStore; +import com.smsreceive.app.feishu.FeishuWebhookPushResult; +import com.smsreceive.app.keepalive.KeepAliveDatabase; +import com.smsreceive.app.keepalive.KeepAliveStateStore; +import com.smsreceive.app.keepalive.SmsKeepAliveService; +import com.smsreceive.app.keepalive.SmsPollingService; +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 java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -48,24 +57,30 @@ public final class MainActivity extends Activity { private static final long DATABASE_HEARTBEAT_STALE_MILLIS = 30_000L; private TextView permissionText; + private TextView latestText; private TextView googlePlayText; private TextView keepAliveText; private TextView databaseHeartbeatText; private TextView deliveryDiagnosticsText; private TextView feishuPushText; - private TextView latestText; private Button keepAliveButton; - private Button autostartConfirmButton; - private Button batteryConfirmButton; private Button pollingButton; private RadioButton toastOnDatabaseWriteRadio; private CheckBox feishuPushEnabledCheckBox; - private CheckBox feishuDebugBodyCheckBox; - private Switch feishuFilterCodeSwitch; + private CheckBox autostartConfirmCheckBox; + private CheckBox batteryConfirmCheckBox; private EditText feishuWebhookIdEdit; private EditText feishuSecretEdit; private EditText pollingIntervalEdit; + private AlertDialog debugInfoDialog; private long lastInboxSmsId = -1L; + private String googlePlayTextValue = ""; + private String keepAliveTextValue = ""; + private String databaseHeartbeatTextValue = ""; + private String deliveryDiagnosticsTextValue = ""; + private String feishuPushTextValue = ""; + private int databaseHeartbeatBackgroundColor = 0xFFFFFFFF; + private int feishuPushBackgroundColor = 0xFFFFFFFF; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final BroadcastReceiver updateReceiver = new BroadcastReceiver() { @@ -82,11 +97,15 @@ public final class MainActivity extends Activity { readLatestInboxSms(SOURCE_INBOX_OBSERVER, true); } }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG, "MainActivity.onCreate"); - setContentView(createContentView()); + setContentView(R.layout.activity_main); + bindMainViews(); + bindMainActions(); + refreshUi(); } @Override @@ -130,214 +149,99 @@ public final class MainActivity extends Activity { } } - private View createContentView() { - ScrollView scrollView = new ScrollView(this); - scrollView.setFillViewport(true); + private void bindMainViews() { + permissionText = findViewById(R.id.permission_text); + latestText = findViewById(R.id.latest_text); + keepAliveButton = findViewById(R.id.keep_alive_button); + toastOnDatabaseWriteRadio = findViewById(R.id.toast_database_write_radio); + pollingButton = findViewById(R.id.polling_button); + feishuPushEnabledCheckBox = findViewById(R.id.feishu_push_enabled_checkbox); + autostartConfirmCheckBox = findViewById(R.id.autostart_confirm_checkbox); + batteryConfirmCheckBox = findViewById(R.id.battery_confirm_checkbox); + feishuWebhookIdEdit = findViewById(R.id.feishu_webhook_id_edit); + feishuSecretEdit = findViewById(R.id.feishu_secret_edit); + pollingIntervalEdit = findViewById(R.id.polling_interval_edit); + } - LinearLayout root = new LinearLayout(this); - root.setOrientation(LinearLayout.VERTICAL); - root.setPadding(dp(20), dp(24), dp(20), dp(24)); - scrollView.addView(root, new ScrollView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - - TextView title = new TextView(this); - 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.setTextSize(14); - subtitle.setTextColor(0xFF5F6B7A); - subtitle.setPadding(0, dp(6), 0, dp(16)); - root.addView(subtitle, matchWrap()); - - permissionText = section(root, "权限状态"); - googlePlayText = section(root, "Google API 诊断"); - keepAliveText = section(root, "后台保活状态"); - databaseHeartbeatText = section(root, "数据库心跳诊断"); - deliveryDiagnosticsText = section(root, "短信广播诊断"); - feishuPushText = section(root, "飞书推送状态"); - latestText = section(root, "最近结果"); - - LinearLayout actions = new LinearLayout(this); - actions.setOrientation(LinearLayout.VERTICAL); - actions.setPadding(0, dp(12), 0, 0); - root.addView(actions, matchWrap()); - - Button requestPermissionButton = button("申请短信权限"); - requestPermissionButton.setOnClickListener(v -> requestSmsPermission()); - actions.addView(requestPermissionButton, matchWrap()); - - keepAliveButton = button("开启常驻保活"); + private void bindMainActions() { + findViewById(R.id.request_permission_button).setOnClickListener(v -> requestSmsPermission()); + findViewById(R.id.debug_info_button).setOnClickListener(v -> showDebugInfoDialog()); keepAliveButton.setOnClickListener(v -> toggleKeepAlive()); - actions.addView(keepAliveButton, matchWrap()); - - toastOnDatabaseWriteRadio = new RadioButton(this); - toastOnDatabaseWriteRadio.setText("每次写入数据库时弹 Toast"); - toastOnDatabaseWriteRadio.setTextSize(14); - toastOnDatabaseWriteRadio.setTextColor(0xFF27313F); toastOnDatabaseWriteRadio.setOnClickListener(v -> toggleToastOnDatabaseWrite()); - actions.addView(toastOnDatabaseWriteRadio, matchWrap()); - - Button readInboxButton = button("读取最新短信"); - 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秒轮询验证码"); + findViewById(R.id.read_inbox_button).setOnClickListener(v -> readLatestInboxSms(SOURCE_INBOX_MANUAL, true)); pollingButton.setOnClickListener(v -> togglePolling()); - actions.addView(pollingButton, matchWrap()); - - pollingIntervalEdit = new EditText(this); - pollingIntervalEdit.setHint("轮询间隔秒数,默认 1"); - pollingIntervalEdit.setSingleLine(true); - 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); - feishuPushEnabledCheckBox.setTextColor(0xFF27313F); - actions.addView(feishuPushEnabledCheckBox, matchWrap()); - - feishuWebhookIdEdit = new EditText(this); - feishuWebhookIdEdit.setHint("飞书 webhook id"); - feishuWebhookIdEdit.setSingleLine(true); - actions.addView(feishuWebhookIdEdit, matchWrap()); - - feishuSecretEdit = new EditText(this); - feishuSecretEdit.setHint("飞书 webhook secret"); - feishuSecretEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - 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()); - - Button testFeishuButton = button("测试飞书推送"); - testFeishuButton.setOnClickListener(v -> testFeishuPush()); - actions.addView(testFeishuButton, matchWrap()); - - Button settingsButton = button("打开应用权限设置"); - settingsButton.setOnClickListener(v -> openAppSettings()); - actions.addView(settingsButton, matchWrap()); - - Button batterySettingsButton = button("打开电池优化设置"); - batterySettingsButton.setOnClickListener(v -> openBatteryOptimizationSettings()); - actions.addView(batterySettingsButton, matchWrap()); - - Button requestBatteryButton = button("请求忽略电池优化"); - requestBatteryButton.setOnClickListener(v -> requestIgnoreBatteryOptimizations()); - actions.addView(requestBatteryButton, matchWrap()); - - Button xiaomiAutostartButton = button("打开小米自启动设置"); - xiaomiAutostartButton.setOnClickListener(v -> openXiaomiAutostartSettings()); - actions.addView(xiaomiAutostartButton, matchWrap()); - - autostartConfirmButton = button("确认已开启小米自启动"); - autostartConfirmButton.setOnClickListener(v -> toggleManualAutostartConfirmed()); - actions.addView(autostartConfirmButton, matchWrap()); - - batteryConfirmButton = button("确认省电策略已设为无限制"); - batteryConfirmButton.setOnClickListener(v -> toggleManualBatteryConfirmed()); - actions.addView(batteryConfirmButton, matchWrap()); - - Button clearButton = button("清空最近结果"); - clearButton.setOnClickListener(v -> { + findViewById(R.id.save_feishu_button).setOnClickListener(v -> saveFeishuConfigFromUi()); + findViewById(R.id.test_feishu_button).setOnClickListener(v -> testFeishuPush()); + findViewById(R.id.settings_button).setOnClickListener(v -> openAppSettings()); + findViewById(R.id.battery_settings_button).setOnClickListener(v -> openBatteryOptimizationSettings()); + findViewById(R.id.request_battery_button).setOnClickListener(v -> requestIgnoreBatteryOptimizations()); + findViewById(R.id.xiaomi_autostart_button).setOnClickListener(v -> openXiaomiAutostartSettings()); + autostartConfirmCheckBox.setOnClickListener(v -> toggleManualAutostartConfirmed()); + batteryConfirmCheckBox.setOnClickListener(v -> toggleManualBatteryConfirmed()); + findViewById(R.id.clear_button).setOnClickListener(v -> { SmsCaptureStore.clear(this); Toast.makeText(this, "已清空最近结果", Toast.LENGTH_SHORT).show(); refreshUi(); }); - actions.addView(clearButton, matchWrap()); - - Button refreshButton = button("刷新状态"); - refreshButton.setOnClickListener(v -> refreshUi()); - actions.addView(refreshButton, matchWrap()); - - return scrollView; + findViewById(R.id.refresh_button).setOnClickListener(v -> refreshUi()); } - private TextView section(LinearLayout root, String label) { - TextView title = new TextView(this); - title.setText(label); - title.setTextSize(16); - title.setTextColor(0xFF17202A); - title.setPadding(0, dp(12), 0, dp(4)); - root.addView(title, matchWrap()); - - TextView value = new TextView(this); - value.setTextSize(14); - value.setTextColor(0xFF27313F); - value.setLineSpacing(dp(2), 1.0f); - value.setPadding(dp(12), dp(10), dp(12), dp(10)); - value.setBackgroundColor(0xFFFFFFFF); - root.addView(value, matchWrap()); - return value; + private void showDebugInfoDialog() { + if (debugInfoDialog == null) { + View debugInfoView = LayoutInflater.from(this).inflate(R.layout.dialog_debug_info, null); + googlePlayText = debugInfoView.findViewById(R.id.google_play_text); + keepAliveText = debugInfoView.findViewById(R.id.keep_alive_text); + databaseHeartbeatText = debugInfoView.findViewById(R.id.database_heartbeat_text); + deliveryDiagnosticsText = debugInfoView.findViewById(R.id.delivery_diagnostics_text); + feishuPushText = debugInfoView.findViewById(R.id.feishu_push_text); + debugInfoDialog = new AlertDialog.Builder(this) + .setTitle("调试信息") + .setView(debugInfoView) + .setPositiveButton("关闭", null) + .create(); + } + applyDebugInfoState(); + debugInfoDialog.show(); } - private Button button(String text) { - Button button = new Button(this); - button.setText(text); - button.setAllCaps(false); - return button; - } - - private LinearLayout.LayoutParams matchWrap() { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(0, dp(4), 0, dp(4)); - return params; + private void applyDebugInfoState() { + if (googlePlayText == null) { + return; + } + googlePlayText.setText(googlePlayTextValue); + keepAliveText.setText(keepAliveTextValue); + databaseHeartbeatText.setText(databaseHeartbeatTextValue); + databaseHeartbeatText.setBackgroundColor(databaseHeartbeatBackgroundColor); + deliveryDiagnosticsText.setText(deliveryDiagnosticsTextValue); + feishuPushText.setText(feishuPushTextValue); + feishuPushText.setBackgroundColor(feishuPushBackgroundColor); } private void refreshUi() { boolean receiveGranted = hasReceiveSmsPermission(); boolean readGranted = hasReadSmsPermission(); + boolean googlePlayInstalled = isGooglePlayServicesInstalled(); Log.d(TAG, "refreshUi receiveSmsPermissionGranted=" + receiveGranted + ", readSmsPermissionGranted=" + readGranted - + ", googlePlayInstalled=" + isGooglePlayServicesInstalled()); + + ", googlePlayInstalled=" + googlePlayInstalled); permissionText.setText("RECEIVE_SMS:" + (receiveGranted ? "已授权" : "未授权") + "\nREAD_SMS:" + (readGranted ? "已授权" : "未授权") + "\n说明:如果 receiver 收不到广播,前台会用 READ_SMS 读取最新收件箱作为兜底。"); - googlePlayText.setText(isGooglePlayServicesInstalled() + googlePlayTextValue = googlePlayInstalled ? "已检测到 com.google.android.gms。SMS User Consent / Retriever 可作为后续备选路径验证。" - : "未检测到 com.google.android.gms。当前实现不依赖 Google API,主路径仍是系统短信广播。"); + : "未检测到 com.google.android.gms。当前实现不依赖 Google API,主路径仍是系统短信广播。"; refreshKeepAliveUi(); refreshDatabaseHeartbeatUi(); refreshDeliveryDiagnosticsUi(); refreshPollingUi(); refreshFeishuPushUi(); + applyDebugInfoState(); SmsCaptureStore.StoredCapture capture = SmsCaptureStore.load(this); if (capture.timeMillis <= 0L) { - latestText.setText("暂无短信接收记录。可以先授权,再从另一台手机发送:验证码 123456,5 分钟内有效。"); + latestText.setText("暂无短信接收记录。可以先授权,再从另一台手机发送一条测试短信。"); return; } @@ -345,14 +249,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()); } @@ -368,15 +268,9 @@ public final class MainActivity extends Activity { + ", notificationsEnabled=" + areNotificationsEnabled() + ", manualAutostart=" + state.manualAutostartConfirmed + ", manualBatteryUnrestricted=" + state.manualBatteryUnrestrictedConfirmed); - if (keepAliveButton != null) { - keepAliveButton.setText(state.enabledByUser ? "关闭常驻保活" : "开启常驻保活"); - } - if (autostartConfirmButton != null) { - autostartConfirmButton.setText(state.manualAutostartConfirmed ? "取消自启动确认" : "确认已开启小米自启动"); - } - if (batteryConfirmButton != null) { - batteryConfirmButton.setText(state.manualBatteryUnrestrictedConfirmed ? "取消省电无限制确认" : "确认省电策略已设为无限制"); - } + keepAliveButton.setText(state.enabledByUser ? "关闭常驻保活" : "开启常驻保活"); + autostartConfirmCheckBox.setChecked(state.manualAutostartConfirmed); + batteryConfirmCheckBox.setChecked(state.manualBatteryUnrestrictedConfirmed); StringBuilder builder = new StringBuilder(); builder.append("用户开关:").append(state.enabledByUser ? "已开启" : "未开启").append('\n'); @@ -392,14 +286,12 @@ public final class MainActivity extends Activity { builder.append("通知可见性:").append(areNotificationsEnabled() ? "系统允许通知" : "通知可能被关闭").append('\n'); builder.append("小米自启动:").append(state.manualAutostartConfirmed ? "已人工确认" : "未确认").append('\n'); builder.append("省电无限制:").append(state.manualBatteryUnrestrictedConfirmed ? "已人工确认" : "未确认"); - keepAliveText.setText(builder.toString()); + keepAliveTextValue = builder.toString(); } private void refreshDatabaseHeartbeatUi() { KeepAliveStateStore.State state = KeepAliveStateStore.load(this); - if (toastOnDatabaseWriteRadio != null) { - toastOnDatabaseWriteRadio.setChecked(state.toastOnDatabaseWrite); - } + toastOnDatabaseWriteRadio.setChecked(state.toastOnDatabaseWrite); long now = System.currentTimeMillis(); long lastActiveTime = KeepAliveDatabase.readLastActiveTime(this); @@ -418,7 +310,7 @@ public final class MainActivity extends Activity { if (lastActiveTime <= 0L) { builder.append("最后写入:-").append('\n'); builder.append("判断:数据库还没有 lastActiveTime。开启常驻保活后会开始写入。"); - databaseHeartbeatText.setBackgroundColor(0xFFFFFFFF); + databaseHeartbeatBackgroundColor = 0xFFFFFFFF; } else { builder.append("最后写入:").append(formatTimeWithMillis(lastActiveTime)).append('\n'); builder.append("距离现在:").append(gapMillis).append(" ms").append('\n'); @@ -428,13 +320,13 @@ public final class MainActivity extends Activity { .append(",大约在此后 ") .append(DATABASE_HEARTBEAT_INTERVAL_MILLIS / 1000L) .append(" 秒内停止写入。"); - databaseHeartbeatText.setBackgroundColor(0xFFFFE0E0); + databaseHeartbeatBackgroundColor = 0xFFFFE0E0; } else { builder.append("判断:数据库心跳仍在正常窗口内。"); - databaseHeartbeatText.setBackgroundColor(0xFFE8F5E9); + databaseHeartbeatBackgroundColor = 0xFFE8F5E9; } } - databaseHeartbeatText.setText(builder.toString()); + databaseHeartbeatTextValue = builder.toString(); } private void refreshDeliveryDiagnosticsUi() { @@ -452,15 +344,13 @@ public final class MainActivity extends Activity { } else { builder.append("判断:暂无收件箱新于广播的异常记录。"); } - deliveryDiagnosticsText.setText(builder.toString()); + deliveryDiagnosticsTextValue = builder.toString(); } private void refreshPollingUi() { SmsPollingStateStore.State state = SmsPollingStateStore.load(this); - if (pollingButton != null) { - pollingButton.setText(state.enabledByUser ? "停止1秒轮询验证码" : "开始1秒轮询验证码"); - } - if (pollingIntervalEdit != null && !pollingIntervalEdit.hasFocus()) { + pollingButton.setText(state.enabledByUser ? "停止短信轮询" : "开始短信轮询"); + if (!pollingIntervalEdit.hasFocus()) { pollingIntervalEdit.setText(String.valueOf(state.intervalSeconds)); } Log.d(TAG, "refreshPollingUi enabled=" + state.enabledByUser @@ -476,19 +366,11 @@ public final class MainActivity extends Activity { FeishuWebhookConfigStore.Config config = FeishuWebhookConfigStore.loadConfig(this); FeishuWebhookConfigStore.LastResult lastResult = FeishuWebhookConfigStore.loadLastResult(this); FeishuWebhookConfigStore.LastPushedSms lastPushedSms = FeishuWebhookConfigStore.loadLastPushedSms(this); - 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()) { + feishuPushEnabledCheckBox.setChecked(config.enabled); + if (!feishuWebhookIdEdit.hasFocus()) { feishuWebhookIdEdit.setText(config.webhookId); } - if (feishuSecretEdit != null && !feishuSecretEdit.hasFocus()) { + if (!feishuSecretEdit.hasFocus()) { feishuSecretEdit.setText(config.secret); } @@ -496,12 +378,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'); @@ -522,8 +403,8 @@ public final class MainActivity extends Activity { boolean configIssue = !config.enabled || !config.hasWebhookId() || !config.hasSecret() || FeishuWebhookPushResult.STATUS_DISABLED.equals(lastResult.status) || FeishuWebhookPushResult.STATUS_MISSING_CONFIG.equals(lastResult.status); - feishuPushText.setBackgroundColor(configIssue ? 0xFFFFE0E0 : 0xFFFFFFFF); - feishuPushText.setText(builder.toString()); + feishuPushBackgroundColor = configIssue ? 0xFFFFE0E0 : 0xFFFFFFFF; + feishuPushTextValue = builder.toString(); } private void requestSmsPermission() { @@ -585,24 +466,18 @@ public final class MainActivity extends Activity { boolean enabled = !state.toastOnDatabaseWrite; Log.d(TAG, "toggleToastOnDatabaseWrite enabled=" + enabled); KeepAliveStateStore.setToastOnDatabaseWrite(this, enabled); - if (toastOnDatabaseWriteRadio != null) { - toastOnDatabaseWriteRadio.setChecked(enabled); - } + toastOnDatabaseWriteRadio.setChecked(enabled); refreshUi(); } 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(); + boolean enabled = feishuPushEnabledCheckBox.isChecked(); + String webhookId = feishuWebhookIdEdit.getText().toString(); + String secret = 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(); } @@ -627,21 +502,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(); + return "**SmsReceive 最近短信测试推送**" + '\n' + + "时间:" + formatTime(inboxResult.dateMillis) + '\n' + + "发送方:" + maskSender(inboxResult.sender) + '\n' + + "短信ID:" + inboxResult.id + '\n' + + "正文:" + emptyAsDash(inboxResult.body); } private boolean hasReceiveSmsPermission() { @@ -679,70 +544,40 @@ 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(); + pollingIntervalEdit.setText(String.valueOf(intervalSeconds)); + return intervalSeconds; } private int parsePollingIntervalSeconds() { - String raw = pollingIntervalEdit == null ? "" : pollingIntervalEdit.getText().toString().trim(); + String raw = 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; } } @@ -761,11 +596,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(); } @@ -855,9 +690,6 @@ public final class MainActivity extends Activity { } private boolean isIgnoringBatteryOptimizations() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return true; - } PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); return powerManager != null && powerManager.isIgnoringBatteryOptimizations(getPackageName()); } @@ -895,8 +727,4 @@ public final class MainActivity extends Activity { } return "***" + sender.substring(sender.length() - 4); } - - private int dp(int value) { - return (int) (value * getResources().getDisplayMetrics().density + 0.5f); - } } diff --git a/app/src/main/res/drawable/bg_button_primary.xml b/app/src/main/res/drawable/bg_button_primary.xml new file mode 100644 index 0000000..645c939 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_primary.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_secondary.xml b/app/src/main/res/drawable/bg_button_secondary.xml new file mode 100644 index 0000000..a6c8bf9 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_secondary.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_tonal.xml b/app/src/main/res/drawable/bg_button_tonal.xml new file mode 100644 index 0000000..7dfb352 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_tonal.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_surface.xml b/app/src/main/res/drawable/bg_card_surface.xml new file mode 100644 index 0000000..bccf573 --- /dev/null +++ b/app/src/main/res/drawable/bg_card_surface.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_hero_panel.xml b/app/src/main/res/drawable/bg_hero_panel.xml new file mode 100644 index 0000000..53b58e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_hero_panel.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_info_panel.xml b/app/src/main/res/drawable/bg_info_panel.xml new file mode 100644 index 0000000..16538ec --- /dev/null +++ b/app/src/main/res/drawable/bg_info_panel.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_input.xml b/app/src/main/res/drawable/bg_input.xml new file mode 100644 index 0000000..6947465 --- /dev/null +++ b/app/src/main/res/drawable/bg_input.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_screen_gradient.xml b/app/src/main/res/drawable/bg_screen_gradient.xml new file mode 100644 index 0000000..42434b1 --- /dev/null +++ b/app/src/main/res/drawable/bg_screen_gradient.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ae39ef6 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +