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