新增服务组件和工具类

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
wchino
2026-04-05 22:29:42 +08:00
parent e11841f19d
commit f0fe8de4e2
5 changed files with 600 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -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 持久化)。
* <p>
* 主要职责:
* 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<QueueItem> 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<QueueItem> 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<QueueItem> 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();
}
}

View File

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