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; } /** 本地监听模式下仅累计抓取条数,不上传服务器 */ public void recordLocalCapture() { incrementTotalCount(); } 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(); } }