diff --git a/app/src/main/java/com/smsreceive/app/ui/MainActivity.java b/app/src/main/java/com/smsreceive/app/ui/MainActivity.java index f6c83e0..12b5b9c 100644 --- a/app/src/main/java/com/smsreceive/app/ui/MainActivity.java +++ b/app/src/main/java/com/smsreceive/app/ui/MainActivity.java @@ -2,6 +2,7 @@ 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,21 +20,18 @@ 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.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; @@ -59,22 +57,31 @@ 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 autostartConfirmCheckBox; + private CheckBox batteryConfirmCheckBox; private EditText feishuWebhookIdEdit; private EditText feishuSecretEdit; private EditText pollingIntervalEdit; + private AlertDialog debugInfoDialog; + private View debugInfoView; 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() { @@ -91,11 +98,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 @@ -139,189 +150,95 @@ 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()); - - pollingButton = button("开始短信轮询"); + 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()); - - 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()); - - 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) { + 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) { @@ -352,15 +269,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'); @@ -376,14 +287,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); @@ -402,7 +311,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'); @@ -412,13 +321,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() { @@ -436,15 +345,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 ? "停止短信轮询" : "开始短信轮询"); - } - 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 @@ -460,13 +367,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 (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); } @@ -499,8 +404,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() { @@ -562,16 +467,14 @@ 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(); - 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)); @@ -664,14 +567,12 @@ public final class MainActivity extends Activity { private int savePollingIntervalFromUi() { int intervalSeconds = parsePollingIntervalSeconds(); SmsPollingStateStore.setIntervalSeconds(this, intervalSeconds); - if (pollingIntervalEdit != null && !pollingIntervalEdit.hasFocus()) { - pollingIntervalEdit.setText(String.valueOf(intervalSeconds)); - } + 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 1; } @@ -702,7 +603,7 @@ public final class MainActivity extends Activity { Log.d(TAG, "startPolling via SmsPollingService"); SmsPollingService.start(this); Toast.makeText(this, - "已启动后台短信轮询,间隔 " + intervalSeconds + " 秒", + "已启动后台短信轮询,间隔 " + intervalSeconds + " 秒", Toast.LENGTH_SHORT).show(); refreshUi(); } @@ -832,8 +733,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/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..eba8ffa --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + +