diff --git a/app/src/main/java/com/miraclegarden/smsmessage/service/BootReceiver.java b/app/src/main/java/com/miraclegarden/smsmessage/service/BootReceiver.java
new file mode 100644
index 0000000..894858d
--- /dev/null
+++ b/app/src/main/java/com/miraclegarden/smsmessage/service/BootReceiver.java
@@ -0,0 +1,24 @@
+package com.miraclegarden.smsmessage.service;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * 开机后重启监听服务。
+ */
+public class BootReceiver extends BroadcastReceiver {
+ private static final long BOOT_DELAY_MS = 5000;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ NotificationService.requestStartMonitoring(context);
+ }, BOOT_DELAY_MS);
+ }
+ }
+}
diff --git a/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationExtractor.java b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationExtractor.java
new file mode 100644
index 0000000..7fd78b5
--- /dev/null
+++ b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationExtractor.java
@@ -0,0 +1,122 @@
+package com.miraclegarden.smsmessage.service;
+
+import android.app.Notification;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+
+/**
+ * @Author wchino
+ * 创建时间 2026/03/30
+ * 用途:通知内容多层提取工具,兼容不同厂商字段差异
+ */
+public class NotificationExtractor {
+
+ private NotificationExtractor() {
+ }
+
+ /**
+ * @Author wchino
+ * 创建时间 2026/03/30
+ * 用途:提取通知标题、内容和提取时间戳
+ */
+ public static Result extract(StatusBarNotification sbn) {
+ long timestamp = System.currentTimeMillis();
+ if (sbn == null || sbn.getNotification() == null) {
+ return new Result("", "", timestamp);
+ }
+
+ Notification notification = sbn.getNotification();
+ Bundle extras = notification.extras;
+
+ String title = "";
+ String content = "";
+
+ if (extras != null) {
+ title = pickFirstNonEmpty(
+ extras.getCharSequence(Notification.EXTRA_TITLE),
+ extras.getCharSequence(Notification.EXTRA_TITLE_BIG)
+ );
+
+ content = pickFirstNonEmpty(
+ extras.getCharSequence(Notification.EXTRA_TEXT),
+ extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
+ extras.getCharSequence(Notification.EXTRA_INFO_TEXT),
+ extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
+ );
+ }
+
+ if (TextUtils.isEmpty(title)) {
+ title = firstSegment(notification.tickerText);
+ }
+
+ if (TextUtils.isEmpty(content)) {
+ content = normalized(notification.tickerText);
+ }
+
+ if (title == null) {
+ title = "";
+ }
+ if (content == null) {
+ content = "";
+ }
+
+ return new Result(title, content, timestamp);
+ }
+
+ private static String pickFirstNonEmpty(CharSequence... values) {
+ if (values == null || values.length == 0) {
+ return "";
+ }
+ for (CharSequence value : values) {
+ String normalized = normalized(value);
+ if (!TextUtils.isEmpty(normalized)) {
+ return normalized;
+ }
+ }
+ return "";
+ }
+
+ private static String normalized(CharSequence value) {
+ if (value == null) {
+ return "";
+ }
+ String result = value.toString().trim();
+ if (TextUtils.isEmpty(result)) {
+ return "";
+ }
+ if ("null".equalsIgnoreCase(result)) {
+ return "";
+ }
+ return result;
+ }
+
+ private static String firstSegment(CharSequence tickerText) {
+ String ticker = normalized(tickerText);
+ if (TextUtils.isEmpty(ticker)) {
+ return "";
+ }
+ int spaceIndex = ticker.indexOf(' ');
+ if (spaceIndex > 0) {
+ return ticker.substring(0, spaceIndex).trim();
+ }
+ return ticker;
+ }
+
+ /**
+ * @Author wchino
+ * 创建时间 2026/03/30
+ * 用途:通知提取结果对象
+ */
+ public static class Result {
+ public String title;
+ public String content;
+ public long timestamp;
+
+ public Result(String title, String content, long timestamp) {
+ this.title = title == null ? "" : title;
+ this.content = content == null ? "" : content;
+ this.timestamp = timestamp;
+ }
+ }
+}
diff --git a/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationHealthCheckWorker.java b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationHealthCheckWorker.java
new file mode 100644
index 0000000..d6e6947
--- /dev/null
+++ b/app/src/main/java/com/miraclegarden/smsmessage/service/NotificationHealthCheckWorker.java
@@ -0,0 +1,35 @@
+package com.miraclegarden.smsmessage.service;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+public class NotificationHealthCheckWorker extends Worker {
+
+ public NotificationHealthCheckWorker(@NonNull Context context, @NonNull WorkerParameters params) {
+ super(context, params);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ if (!isNotificationListenerEnabled()) {
+ NotificationService.toggleNotificationListenerService(getApplicationContext());
+ }
+ return Result.success();
+ }
+
+ private boolean isNotificationListenerEnabled() {
+ String flat = Settings.Secure.getString(
+ getApplicationContext().getContentResolver(),
+ "enabled_notification_listeners");
+ if (TextUtils.isEmpty(flat)) {
+ return false;
+ }
+ return flat.contains(getApplicationContext().getPackageName());
+ }
+}
diff --git a/app/src/main/java/com/miraclegarden/smsmessage/service/RetryManager.java b/app/src/main/java/com/miraclegarden/smsmessage/service/RetryManager.java
new file mode 100644
index 0000000..9ad771b
--- /dev/null
+++ b/app/src/main/java/com/miraclegarden/smsmessage/service/RetryManager.java
@@ -0,0 +1,272 @@
+package com.miraclegarden.smsmessage.service;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * 上传失败重试管理器(内存队列 + SharedPreferences 持久化)。
+ *
+ * 主要职责:
+ * 1. 入队失败上传任务并立即尝试上传;
+ * 2. 上传失败后按延迟策略重试;
+ * 3. 进程重启后从本地恢复重试队列;
+ * 4. 统计成功上传总数,供前台通知展示。
+ */
+public class RetryManager {
+ private static final String TAG = "RetryManager";
+
+ private static final String SP_NAME = "server";
+ private static final String KEY_RETRY_QUEUE = "retry_queue";
+ private static final String KEY_UPLOADED_COUNT = "uploaded_count";
+ private static final String KEY_TOTAL_COUNT = "total_count";
+ private static final String KEY_FAILED_COUNT = "failed_count";
+
+ private static final int MAX_QUEUE_SIZE = 100;
+ private static final long[] RETRY_DELAYS = new long[]{2000L, 5000L, 15000L};
+
+ private final SharedPreferences sharedPreferences;
+ private final Handler handler;
+ private final List retryQueue = new ArrayList<>();
+
+ private UploadCallback uploadCallback;
+ private int uploadedCount;
+ private int totalCount;
+ private int failedCount;
+
+ /**
+ * 上传抽象回调。
+ */
+ public interface UploadCallback {
+ void onUpload(String jsonPayload, UploadResultListener listener);
+ }
+
+ /**
+ * 上传结果监听。
+ */
+ public interface UploadResultListener {
+ void onSuccess();
+
+ void onFailure(String error);
+ }
+
+ /**
+ * 重试队列元素。
+ */
+ private static class QueueItem {
+ String jsonPayload;
+ int retryCount; // starts at 0, max 2 (3 attempts total: initial + 2 retries)
+ long nextRetryTime;
+ }
+
+ public RetryManager(Context context) {
+ Context appContext = context.getApplicationContext();
+ this.sharedPreferences = appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
+ this.handler = new Handler(Looper.getMainLooper());
+ this.uploadedCount = sharedPreferences.getInt(KEY_UPLOADED_COUNT, 0);
+ this.totalCount = sharedPreferences.getInt(KEY_TOTAL_COUNT, 0);
+ this.failedCount = sharedPreferences.getInt(KEY_FAILED_COUNT, 0);
+ }
+
+ /**
+ * 设置上传实现。
+ */
+ public void setUploadCallback(UploadCallback callback) {
+ this.uploadCallback = callback;
+ }
+
+ /**
+ * 添加任务到队列并立即尝试上传。
+ */
+ public void enqueue(String jsonPayload) {
+ if (jsonPayload == null || jsonPayload.trim().isEmpty()) {
+ Log.w(TAG, "enqueue payload 为空,忽略");
+ return;
+ }
+
+ incrementTotalCount();
+
+ QueueItem item = new QueueItem();
+ item.jsonPayload = jsonPayload;
+ item.retryCount = 0;
+ item.nextRetryTime = SystemClock.elapsedRealtime();
+
+ synchronized (retryQueue) {
+ if (retryQueue.size() >= MAX_QUEUE_SIZE) {
+ QueueItem removed = retryQueue.remove(0);
+ Log.w(TAG, "重试队列已满,丢弃最旧任务: " + (removed == null ? "null" : removed.jsonPayload));
+ }
+ retryQueue.add(item);
+ persistQueue();
+ }
+
+ attemptUpload(item);
+ }
+
+ /**
+ * 从持久化恢复重试队列并重新触发上传。
+ */
+ public void restoreFromPersistence() {
+ String queueJson = sharedPreferences.getString(KEY_RETRY_QUEUE, "");
+ if (queueJson == null || queueJson.trim().isEmpty()) {
+ return;
+ }
+
+ List restoredItems = new ArrayList<>();
+ try {
+ JSONArray jsonArray = new JSONArray(queueJson);
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject object = jsonArray.optJSONObject(i);
+ if (object == null) {
+ continue;
+ }
+
+ QueueItem item = new QueueItem();
+ item.jsonPayload = object.optString("jsonPayload", "");
+ item.retryCount = object.optInt("retryCount", 0);
+ item.nextRetryTime = object.optLong("nextRetryTime", SystemClock.elapsedRealtime());
+
+ if (item.jsonPayload == null || item.jsonPayload.trim().isEmpty()) {
+ continue;
+ }
+
+ restoredItems.add(item);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "恢复重试队列失败", e);
+ return;
+ }
+
+ synchronized (retryQueue) {
+ retryQueue.clear();
+ retryQueue.addAll(restoredItems);
+ }
+
+ for (QueueItem item : new ArrayList<>(restoredItems)) {
+ long delay = item.nextRetryTime - SystemClock.elapsedRealtime();
+ if (delay > 0) {
+ handler.postDelayed(() -> attemptUpload(item), delay);
+ } else {
+ attemptUpload(item);
+ }
+ }
+ }
+
+ /**
+ * 获取累计成功上传数量。
+ */
+ public int getUploadedCount() {
+ return uploadedCount;
+ }
+
+ public int getTotalCount() {
+ return totalCount;
+ }
+
+ public int getFailedCount() {
+ return failedCount;
+ }
+
+ private void incrementTotalCount() {
+ totalCount++;
+ sharedPreferences.edit()
+ .putInt(KEY_TOTAL_COUNT, totalCount)
+ .apply();
+ }
+
+ private void incrementFailedCount() {
+ failedCount++;
+ sharedPreferences.edit()
+ .putInt(KEY_FAILED_COUNT, failedCount)
+ .apply();
+ }
+
+ /**
+ * 清理回调,通常在 Service 销毁时调用。
+ */
+ public void destroy() {
+ handler.removeCallbacksAndMessages(null);
+ }
+
+ private void attemptUpload(final QueueItem item) {
+ if (item == null) {
+ return;
+ }
+
+ if (uploadCallback == null) {
+ Log.w(TAG, "uploadCallback 未设置,无法上传");
+ return;
+ }
+
+ uploadCallback.onUpload(item.jsonPayload, new UploadResultListener() {
+ @Override
+ public void onSuccess() {
+ synchronized (retryQueue) {
+ retryQueue.remove(item);
+ persistQueue();
+ }
+ uploadedCount++;
+ sharedPreferences.edit()
+ .putInt(KEY_UPLOADED_COUNT, uploadedCount)
+ .apply();
+ }
+
+ @Override
+ public void onFailure(String error) {
+ synchronized (retryQueue) {
+ if (!retryQueue.contains(item)) {
+ return;
+ }
+
+ if (item.retryCount < 3) {
+ long delay = RETRY_DELAYS[Math.min(item.retryCount, RETRY_DELAYS.length - 1)];
+ item.retryCount++;
+ item.nextRetryTime = SystemClock.elapsedRealtime() + delay;
+ handler.postDelayed(() -> attemptUpload(item), delay);
+ persistQueue();
+ Log.w(TAG, "上传失败,准备重试。retryCount=" + item.retryCount + ", error=" + error);
+ } else {
+ retryQueue.remove(item);
+ persistQueue();
+ incrementFailedCount();
+ Log.e(TAG, "上传失败达到上限,丢弃任务: " + error);
+ }
+ }
+ }
+ });
+ }
+
+ private void persistQueue() {
+ JSONArray jsonArray = new JSONArray();
+ synchronized (retryQueue) {
+ Iterator iterator = retryQueue.iterator();
+ while (iterator.hasNext()) {
+ QueueItem item = iterator.next();
+ JSONObject object = new JSONObject();
+ try {
+ object.put("jsonPayload", item.jsonPayload);
+ object.put("retryCount", item.retryCount);
+ object.put("nextRetryTime", item.nextRetryTime);
+ jsonArray.put(object);
+ } catch (JSONException e) {
+ Log.e(TAG, "序列化重试项失败", e);
+ }
+ }
+ }
+
+ sharedPreferences.edit()
+ .putString(KEY_RETRY_QUEUE, jsonArray.toString())
+ .apply();
+ }
+}
diff --git a/app/src/main/java/com/miraclegarden/smsmessage/util/OEMBackgroundHelper.java b/app/src/main/java/com/miraclegarden/smsmessage/util/OEMBackgroundHelper.java
new file mode 100644
index 0000000..a2dafb8
--- /dev/null
+++ b/app/src/main/java/com/miraclegarden/smsmessage/util/OEMBackgroundHelper.java
@@ -0,0 +1,147 @@
+package com.miraclegarden.smsmessage.util;
+
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.PowerManager;
+import android.provider.Settings;
+
+public class OEMBackgroundHelper {
+
+ private static final String MIUI_SECURITY = "com.miui.securitycenter";
+ private static final String MIUI_AUTOSTART = "com.miui.permcenter.autostart.AutoStartManagementActivity";
+
+ private static final String COLOROS_SAFECENTER = "com.coloros.safecenter";
+ private static final String COLOROS_STARTUP = "com.coloros.safecenter.permission.startup.StartupAppListActivity";
+
+ private static final String OPPO_SAFE = "com.oppo.safe";
+ private static final String OPPO_STARTUP = "com.oppo.safe.permission.startup.StartupAppListActivity";
+
+ private static final String VIVO_PERMISSION = "com.vivo.permissionmanager";
+ private static final String VIVO_BGSTARTUP = "com.vivo.permissionmanager.activity.BgStartUpManagerActivity";
+
+ private static final String IQOO_SECURE = "com.iqoo.secure";
+ private static final String IQOO_BGSTARTUP = "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager";
+
+ private static final String HUAWEI_SYSTEM_MANAGER = "com.huawei.systemmanager";
+ private static final String HUAWEI_STARTUP_CTRL = "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity";
+ private static final String HUAWEI_POWER_MANAGER = "com.huawei.systemmanager.power.ui.HwPowerManagerActivity";
+
+ public static void openAutoStartSettings(Context context) {
+ String manufacturer = Build.MANUFACTURER.toLowerCase();
+
+ Intent intent = new Intent();
+ boolean success = false;
+
+ try {
+ if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi")) {
+ intent.setComponent(new ComponentName(MIUI_SECURITY, MIUI_AUTOSTART));
+ success = true;
+ } else if (manufacturer.contains("oppo")) {
+ if (tryStartActivity(context, intent, COLOROS_SAFECENTER, COLOROS_STARTUP)) return;
+ if (tryStartActivity(context, intent, OPPO_SAFE, OPPO_STARTUP)) return;
+ } else if (manufacturer.contains("realme")) {
+ if (tryStartActivity(context, intent, COLOROS_SAFECENTER, COLOROS_STARTUP)) return;
+ } else if (manufacturer.contains("vivo")) {
+ if (tryStartActivity(context, intent, VIVO_PERMISSION, VIVO_BGSTARTUP)) return;
+ if (tryStartActivity(context, intent, IQOO_SECURE, IQOO_BGSTARTUP)) return;
+ } else if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
+ if (tryStartActivity(context, intent, HUAWEI_SYSTEM_MANAGER, HUAWEI_STARTUP_CTRL)) return;
+ if (tryStartActivity(context, intent, HUAWEI_SYSTEM_MANAGER, HUAWEI_POWER_MANAGER)) return;
+ } else if (manufacturer.contains("oneplus")) {
+ openBatteryOptimizationSettings(context);
+ return;
+ } else if (manufacturer.contains("samsung")) {
+ openAppBatterySettings(context);
+ return;
+ }
+
+ if (success) {
+ context.startActivity(intent);
+ } else {
+ openBatteryOptimizationSettings(context);
+ }
+ } catch (ActivityNotFoundException e) {
+ openBatteryOptimizationSettings(context);
+ }
+ }
+
+ public static void openBatteryOptimizationSettings(Context context) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Intent intent = new Intent(Settings.ACTION_SETTINGS);
+ context.startActivity(intent);
+ }
+ }
+
+ public static void requestBatteryOptimizationExemption(Context context) {
+ if (!isIgnoringBatteryOptimizations(context)) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ intent.setData(Uri.parse("package:" + context.getPackageName()));
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ openBatteryOptimizationSettings(context);
+ }
+ }
+ }
+
+ public static boolean isIgnoringBatteryOptimizations(Context context) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ return pm.isIgnoringBatteryOptimizations(context.getPackageName());
+ }
+
+ public static String getManufacturer() {
+ return Build.MANUFACTURER;
+ }
+
+ public static boolean needsAutoStartPermission() {
+ String manufacturer = Build.MANUFACTURER.toLowerCase();
+ return manufacturer.contains("xiaomi") || manufacturer.contains("redmi")
+ || manufacturer.contains("oppo") || manufacturer.contains("realme")
+ || manufacturer.contains("vivo") || manufacturer.contains("huawei")
+ || manufacturer.contains("honor") || manufacturer.contains("oneplus");
+ }
+
+ public static String getAutoStartPermissionName() {
+ String manufacturer = Build.MANUFACTURER.toLowerCase();
+ if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi")) {
+ return "自启动";
+ } else if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
+ return "自动启动";
+ } else if (manufacturer.contains("vivo")) {
+ return "后台启动";
+ } else if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
+ return "自动启动管理";
+ } else if (manufacturer.contains("oneplus")) {
+ return "电池优化";
+ }
+ return "电池/自启动";
+ }
+
+ private static boolean tryStartActivity(Context context, Intent intent, String pkg, String cls) {
+ try {
+ intent.setComponent(new ComponentName(pkg, cls));
+ context.startActivity(intent);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static void openAppBatterySettings(Context context) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.parse("package:" + context.getPackageName()));
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Intent intent = new Intent(Settings.ACTION_SETTINGS);
+ context.startActivity(intent);
+ }
+ }
+}
\ No newline at end of file