feat(mmp): 独立红包领取台(手机+PC)、同步、筛选与功能说明

隔离通用消息台;支持设置/手气摘要/电脑同步;忽略临时逆向脚本与运行时 JSON。
This commit is contained in:
mars
2026-08-04 17:39:29 +08:00
parent 1517ff4671
commit c8cbfdff82
23 changed files with 4388 additions and 240 deletions

View File

@@ -11,9 +11,14 @@ import com.miraclegarden.smsmessage.xposed.HookForwarder;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
@@ -43,21 +48,41 @@ public final class TngMoneyPacketHook {
private static final java.util.HashMap<String, Long> RECENT_PACKET_AT = new java.util.HashMap<>();
/** activityId → 领取指纹 */
private static final java.util.HashMap<String, String> RECENT_PACKET_CLAIMS = new java.util.HashMap<>();
/** 同一 activityId + 相同领取名单20 秒内不重复推(防双 Hook / 连点);超时可刷新 */
private static final long PACKET_DEDUP_MS = 20_000L;
/** 同一 activityId + 相同领取名单短时不重复推(可由领取台设置覆盖) */
private static final long DEFAULT_PACKET_DEDUP_MS = 3_000L;
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
/** 历史列表触发的自动拉详情:已排队 / 上次成功时间 */
private static final java.util.HashSet<String> AUTO_DETAIL_PENDING = new java.util.HashSet<>();
private static final java.util.HashMap<String, Long> AUTO_DETAIL_AT = new java.util.HashMap<>();
private static final long AUTO_DETAIL_COOLDOWN_MS = 60_000L;
private static final long DEFAULT_DETAIL_COOLDOWN_MS = 8_000L;
/** activityId → 历史列表里的发放时间 createTime */
private static final java.util.HashMap<String, String> ACTIVITY_ISSUE_TIME =
new java.util.HashMap<>();
/** 自动拉历史列表冷却(无需打开历史页) */
private static final long AUTO_HISTORY_COOLDOWN_MS = 45_000L;
/** 已成功拿到领取名单的 activityId */
private static final java.util.HashSet<String> PACKETS_WITH_DATA = new java.util.HashSet<>();
/** 已领完且已有数据 → 不再自动刷详情 */
private static final java.util.HashSet<String> COMPLETED_PACKETS = new java.util.HashSet<>();
/** 自动拉历史列表冷却(无需打开历史页;可由领取台设置覆盖) */
private static final long DEFAULT_HISTORY_COOLDOWN_MS = 8_000L;
private static final long DEFAULT_DETAIL_GAP_MS = 80L;
private static final long SETTINGS_POLL_MS = 10_000L;
/** 与 AppConfig.DEBUG_SERVER_URLS 对齐 */
private static final String[] SETTINGS_BASE_URLS = {
"http://127.0.0.1:8765",
"http://10.151.104.25:8765",
};
private static volatile long sHistoryCooldownMs = DEFAULT_HISTORY_COOLDOWN_MS;
private static volatile long sDetailCooldownMs = DEFAULT_DETAIL_COOLDOWN_MS;
private static volatile long sPacketDedupMs = DEFAULT_PACKET_DEDUP_MS;
private static volatile long sDetailGapMs = DEFAULT_DETAIL_GAP_MS;
private static volatile boolean sOpenHistoryIfNoTemplate = true;
private static volatile long sLastSettingsPollAt;
private static volatile long sLastAutoHistoryAt;
private static volatile boolean sAutoHistoryRunning;
/** 曾拿到过 session之后变空 / RPC 鉴权失败 → 视为断线,恢复后强制重拉 */
private static volatile boolean sHadSession;
private static volatile boolean sSessionDisconnected;
/** 本线程正在主动拉历史,避免 Quake afterHook 再 schedule 一遍 */
private static final ThreadLocal<Boolean> AUTO_HISTORY_SELF = new ThreadLocal<>();
private static volatile Object sMoneyPacketRepository;
@@ -93,9 +118,100 @@ public final class TngMoneyPacketHook {
hookQuakeRpc(lpparam);
hookLoginStorage(lpparam);
hookActivityResumeForAutoHistory(lpparam);
startSettingsPoller();
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
}
/** 后台轮询领取台 /api/mmp/settings约 10 秒内生效 */
private static void startSettingsPoller() {
Thread t = new Thread(() -> {
while (true) {
try {
pollMmpSettings(true);
Thread.sleep(SETTINGS_POLL_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
} catch (Throwable ignored) {
try {
Thread.sleep(SETTINGS_POLL_MS);
} catch (InterruptedException ie2) {
Thread.currentThread().interrupt();
return;
}
}
}
}, "TngMmp-settings");
t.setDaemon(true);
t.start();
}
private static void pollMmpSettings(boolean force) {
long now = System.currentTimeMillis();
if (!force && now - sLastSettingsPollAt < SETTINGS_POLL_MS) {
return;
}
sLastSettingsPollAt = now;
for (String base : SETTINGS_BASE_URLS) {
if (tryApplySettingsFromUrl(base + "/api/mmp/settings")) {
return;
}
}
}
private static boolean tryApplySettingsFromUrl(String urlStr) {
HttpURLConnection conn = null;
try {
URL url = new URL(urlStr);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(2500);
conn.setReadTimeout(2500);
conn.setRequestMethod("GET");
conn.setUseCaches(false);
int code = conn.getResponseCode();
if (code != 200) {
return false;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
}
JSONObject json = new JSONObject(sb.toString());
long historyMs = Math.max(1, json.optInt("historyCooldownSec", 8)) * 1000L;
long detailMs = Math.max(1, json.optInt("detailCooldownSec", 8)) * 1000L;
long dedupMs = Math.max(0, json.optInt("dedupSec", 3)) * 1000L;
long gapMs = Math.max(0, json.optInt("detailGapMs", 80));
boolean openHistory = json.optBoolean("openHistoryIfNoTemplate", true);
boolean changed = historyMs != sHistoryCooldownMs
|| detailMs != sDetailCooldownMs
|| dedupMs != sPacketDedupMs
|| gapMs != sDetailGapMs
|| openHistory != sOpenHistoryIfNoTemplate;
sHistoryCooldownMs = historyMs;
sDetailCooldownMs = detailMs;
sPacketDedupMs = dedupMs;
sDetailGapMs = gapMs;
sOpenHistoryIfNoTemplate = openHistory;
if (changed) {
XposedBridge.log(TAG + " settings applied hist=" + (historyMs / 1000)
+ "s detail=" + (detailMs / 1000) + "s dedup=" + (dedupMs / 1000)
+ "s gap=" + gapMs + "ms openHist=" + openHistory
+ " from " + urlStr);
}
return true;
} catch (Throwable ignored) {
return false;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static void hookOkHttpUrl(XC_LoadPackage.LoadPackageParam lpparam) {
try {
XposedHelpers.findAndHookMethod(
@@ -557,6 +673,26 @@ public final class TngMoneyPacketHook {
} catch (Throwable ignored) {
trySet(req, "setSenderUserId", senderUserId);
}
// 尽量一次拉全领取名单(避免默认 20 条截断)
bumpDetailPageSize(req);
}
private static void bumpDetailPageSize(Object req) {
if (req == null) {
return;
}
try {
XposedHelpers.setIntField(req, "page", 0);
} catch (Throwable ignored) {
}
try {
XposedHelpers.setIntField(req, "maxResult", 200);
} catch (Throwable ignored) {
try {
XposedHelpers.setObjectField(req, "maxResult", 200);
} catch (Throwable ignored2) {
}
}
}
@@ -594,6 +730,7 @@ public final class TngMoneyPacketHook {
if (historyResult == null) {
return;
}
pollMmpSettings(false);
// 解析放调用线程;真正 RPC 必须丢到主线程Quake/登录态常绑主线程)
final List<String[]> jobs;
try {
@@ -617,9 +754,23 @@ public final class TngMoneyPacketHook {
}
}
long now = System.currentTimeMillis();
long detailCooldown = sDetailCooldownMs;
if (COMPLETED_PACKETS.contains(activityId)) {
continue;
}
// job[3]=remainingCount, job[4]=claimedCount/totalCount hint, job[5]=status
String remainHint = job.length > 3 ? job[3] : null;
String countHint = job.length > 4 ? job[4] : null;
String statusHint = job.length > 5 ? job[5] : null;
if (isFinishedFlags(remainHint, countHint, statusHint)
&& PACKETS_WITH_DATA.contains(activityId)) {
COMPLETED_PACKETS.add(activityId);
XposedBridge.log(TAG + " skip completed(history) id=" + activityId);
continue;
}
synchronized (AUTO_DETAIL_AT) {
Long last = AUTO_DETAIL_AT.get(activityId);
if (last != null && now - last < AUTO_DETAIL_COOLDOWN_MS) {
if (last != null && now - last < detailCooldown) {
continue;
}
if (!AUTO_DETAIL_PENDING.add(activityId)) {
@@ -627,7 +778,7 @@ public final class TngMoneyPacketHook {
}
}
final long thisDelay = delayMs;
delayMs += 250L;
delayMs += Math.max(0L, sDetailGapMs);
AUTO_DETAIL_EXEC.execute(() -> {
try {
if (thisDelay > 0) {
@@ -675,8 +826,28 @@ public final class TngMoneyPacketHook {
String senderUserId = stringField(info, "senderUserId", "getSenderUserId");
String createTime = stringField(info, "createTime", "getCreateTime");
if (looksLikeActivityId(activityId) && !TextUtils.isEmpty(senderUserId)) {
jobs.add(new String[]{activityId, senderUserId,
TextUtils.isEmpty(createTime) ? "" : createTime});
String remain = firstNonEmpty(
stringField(info, "remainingCount", "getRemainingCount"),
stringField(info, "remainCount", "getRemainCount"));
String claimed = stringField(info, "claimedCount", "getClaimedCount");
String totalCnt = stringField(info, "totalCount", "getTotalCount");
String countHint = "";
if (!TextUtils.isEmpty(claimed) || !TextUtils.isEmpty(totalCnt)) {
countHint = nullToEmpty(claimed) + "/" + nullToEmpty(totalCnt);
}
String status = firstNonEmpty(
stringField(info, "activityStatus", "getActivityStatus"),
stringField(info, "participantStatus", "getParticipantStatus"),
stringField(info, "status", "getStatus"),
boolFieldText(info, "isFinished", "isFinished"));
jobs.add(new String[]{
activityId,
senderUserId,
TextUtils.isEmpty(createTime) ? "" : createTime,
nullToEmpty(remain),
countHint,
nullToEmpty(status)
});
}
}
} catch (Throwable t) {
@@ -703,6 +874,210 @@ public final class TngMoneyPacketHook {
return null;
}
private static String boolFieldText(Object obj, String field, String getter) {
try {
Object v = XposedHelpers.callMethod(obj, getter);
if (v instanceof Boolean) {
return Boolean.TRUE.equals(v) ? "FINISHED" : null;
}
} catch (Throwable ignored) {
}
try {
Object v = XposedHelpers.getObjectField(obj, field);
if (v instanceof Boolean && Boolean.TRUE.equals(v)) {
return "FINISHED";
}
} catch (Throwable ignored) {
}
return null;
}
private static String nullToEmpty(String s) {
return s == null ? "" : s;
}
/** remaining / claimed/total / status 任一表明已领完 */
private static boolean isFinishedFlags(String remain, String claimedSlashTotal, String status) {
if (!TextUtils.isEmpty(remain)) {
try {
if (Integer.parseInt(remain.trim()) <= 0) {
return true;
}
} catch (NumberFormatException ignored) {
}
}
if (!TextUtils.isEmpty(claimedSlashTotal) && claimedSlashTotal.contains("/")) {
String[] parts = claimedSlashTotal.split("/", 2);
try {
int claimed = Integer.parseInt(parts[0].trim());
int total = Integer.parseInt(parts[1].trim());
if (total > 0 && claimed >= total) {
return true;
}
} catch (NumberFormatException ignored) {
}
}
if (!TextUtils.isEmpty(status)) {
String s = status.toUpperCase(Locale.US);
if (s.contains("FINISH") || s.contains("COMPLETE") || s.contains("EXPIRE")
|| s.contains("DONE") || "TRUE".equals(s)) {
return true;
}
}
return false;
}
private static boolean evaluateFinished(MmpSnapshot snapshot) {
if (snapshot == null) {
return false;
}
if (isFinishedFlags(snapshot.remainingCount,
buildCountHint(snapshot.claimedCount, snapshot.totalCount),
snapshot.activityStatus)) {
return true;
}
// 已领金额 ≈ 总额
Double claimedAmt = parseMoneyDouble(snapshot.claimedAmountText);
Double totalAmt = parseMoneyDouble(snapshot.totalAmount);
if (claimedAmt == null && snapshot.claims != null && !snapshot.claims.isEmpty()) {
double sum = 0;
for (ClaimLine line : snapshot.claims) {
Double a = parseMoneyDouble(line.amount);
if (a != null) {
sum += a;
}
}
claimedAmt = sum;
}
if (claimedAmt != null && totalAmt != null && totalAmt > 0
&& Math.abs(claimedAmt - totalAmt) < 0.02) {
return true;
}
return false;
}
private static String buildCountHint(String claimed, String total) {
if (TextUtils.isEmpty(claimed) && TextUtils.isEmpty(total)) {
return null;
}
return nullToEmpty(claimed) + "/" + nullToEmpty(total);
}
private static Double parseMoneyDouble(String raw) {
if (TextUtils.isEmpty(raw)) {
return null;
}
try {
String n = normalizeMoneyText(raw);
if (TextUtils.isEmpty(n)) {
return null;
}
return Double.parseDouble(n);
} catch (Throwable ignored) {
return null;
}
}
private static void rememberPacketData(String packetId, MmpSnapshot snapshot, Object result) {
if (snapshot == null || snapshot.claims == null || snapshot.claims.isEmpty()) {
return;
}
String id = !TextUtils.isEmpty(packetId) ? packetId : snapshot.packetId;
if (TextUtils.isEmpty(id) || !looksLikeActivityId(id)) {
// 指纹 id 也记,避免反复刷
if (!TextUtils.isEmpty(id)) {
PACKETS_WITH_DATA.add(id);
}
} else {
PACKETS_WITH_DATA.add(id);
}
if (result != null
&& TextUtils.isEmpty(snapshot.remainingCount)
&& TextUtils.isEmpty(snapshot.claimedCount)
&& TextUtils.isEmpty(snapshot.activityStatus)) {
enrichSnapshotFromJavaResult(result, snapshot);
}
if (!snapshot.finished) {
snapshot.finished = evaluateFinished(snapshot);
}
if (snapshot.finished && !TextUtils.isEmpty(id)) {
if (COMPLETED_PACKETS.add(id)) {
XposedBridge.log(TAG + " packet DONE stop-refresh id=" + id
+ " claims=" + snapshot.claims.size()
+ " remain=" + snapshot.remainingCount
+ " status=" + snapshot.activityStatus);
}
}
}
private static void onSessionObserved(String sid) {
boolean has = !TextUtils.isEmpty(sid) && !"null".equalsIgnoreCase(sid);
if (has) {
sCachedSessionId = sid;
if (sSessionDisconnected) {
sSessionDisconnected = false;
XposedBridge.log(TAG + " session recovered → force re-fetch");
scheduleAutoHistoryForced("session-recovered");
}
sHadSession = true;
} else if (sHadSession) {
if (!sSessionDisconnected) {
sSessionDisconnected = true;
XposedBridge.log(TAG + " session lost / disconnected");
}
}
}
private static void noteSessionAlive() {
if (!TextUtils.isEmpty(sCachedSessionId)) {
sHadSession = true;
if (sSessionDisconnected) {
sSessionDisconnected = false;
XposedBridge.log(TAG + " connection alive again");
}
}
}
private static void tryRefreshLogin(ClassLoader cl) {
try {
Object ls = obtainLoginStorage(cl);
if (ls != null) {
applyLoginStorage(ls);
}
} catch (Throwable ignored) {
}
}
private static void markMaybeDisconnected(Throwable t, String where) {
String msg = t == null ? "" : String.valueOf(t.getMessage());
if (TextUtils.isEmpty(msg) && t != null) {
msg = t.getClass().getSimpleName();
}
String low = msg.toLowerCase(Locale.US);
if (low.contains("session") || low.contains("login") || low.contains("auth")
|| low.contains("unauthorized") || low.contains("timeout")
|| low.contains("unable to resolve") || low.contains("failed to connect")
|| low.contains("network") || low.contains("disconnect")
|| low.contains("socket") || low.contains("ssl")
|| msg.contains("未登录") || msg.contains("非法") || msg.contains("登录")
|| msg.contains("过期")) {
sSessionDisconnected = true;
XposedBridge.log(TAG + " disconnect suspected @" + where + ": " + msg);
tryRefreshLogin(sAppClassLoader);
}
}
private static void notePossibleDisconnectFromEmpty(Object result, String activityId) {
// totalCount:0 + 无领取人 常见于缺登录态
String totalCnt = stringField(result, "totalCount", "getTotalCount");
if ("0".equals(totalCnt) && sHadSession) {
XposedBridge.log(TAG + " empty claims+totalCount0 id=" + activityId
+ " → treat as possible disconnect");
sSessionDisconnected = true;
tryRefreshLogin(sAppClassLoader);
}
}
private static boolean fetchDetailByActivityId(String activityId, String senderUserId) {
ClassLoader cl = sAppClassLoader;
ensureRpcTaskAndLogin(cl);
@@ -761,27 +1136,32 @@ public final class TngMoneyPacketHook {
XposedBridge.log(TAG + " auto-detail empty claims id=" + activityId
+ " body=" + snippet
+ " userId=" + stringField(req, "userId", "getUserId"));
notePossibleDisconnectFromEmpty(result, activityId);
return false;
}
rememberPacketData(probe.packetId != null ? probe.packetId : activityId, probe, result);
forwardSnapshot(probe, "auto:" + activityId, "rpc");
XposedBridge.log(TAG + " auto-detail ok activityId=" + activityId
+ " claims=" + probe.claims.size()
+ " issued=" + probe.issueTime);
+ " issued=" + probe.issueTime
+ (COMPLETED_PACKETS.contains(activityId) ? " DONE" : ""));
noteSessionAlive();
return true;
} catch (Throwable t) {
XposedBridge.log(TAG + " auto-detail fail id=" + activityId + ": " + t.getMessage());
markMaybeDisconnected(t, "auto-detail");
return false;
}
}
private static Object newDetailRequest(Class<?> reqCls, String activityId, String senderUserId) {
// 优先: (activityId, senderUserId, loadTime, page, maxResult)
// 优先: (activityId, senderUserId, loadTime, page, maxResult) — maxResult 拉大以尽量一次取全
try {
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 20);
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 200);
} catch (Throwable ignored) {
}
try {
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 20,
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 200,
31, null);
} catch (Throwable ignored) {
}
@@ -789,14 +1169,7 @@ public final class TngMoneyPacketHook {
Object req = reqCls.getDeclaredConstructor().newInstance();
XposedHelpers.setObjectField(req, "activityId", activityId);
XposedHelpers.setObjectField(req, "senderUserId", senderUserId);
try {
XposedHelpers.setIntField(req, "page", 0);
} catch (Throwable ignored) {
}
try {
XposedHelpers.setIntField(req, "maxResult", 20);
} catch (Throwable ignored) {
}
bumpDetailPageSize(req);
return req;
} catch (Throwable t) {
XposedBridge.log(TAG + " newDetailRequest error: " + t.getMessage());
@@ -915,12 +1288,11 @@ public final class TngMoneyPacketHook {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object v = param.getResult();
if (v == null || TextUtils.isEmpty(String.valueOf(v))) {
return;
}
String name = param.method.getName();
if ("getSessionId".equals(name)) {
sCachedSessionId = String.valueOf(v);
onSessionObserved(v == null ? "" : String.valueOf(v));
} else if (v == null || TextUtils.isEmpty(String.valueOf(v))) {
return;
} else if ("getAccountId".equals(name)) {
sCachedUserId = String.valueOf(v);
} else if ("getLoginId".equals(name)) {
@@ -957,7 +1329,12 @@ public final class TngMoneyPacketHook {
if (!PACKAGE.equals(pkg)) {
return;
}
scheduleAutoHistory("activity-resume");
if (sSessionDisconnected) {
tryRefreshLogin(sAppClassLoader);
scheduleAutoHistoryForced("reconnect-resume");
} else {
scheduleAutoHistory("activity-resume");
}
} catch (Throwable ignored) {
}
}
@@ -969,11 +1346,21 @@ public final class TngMoneyPacketHook {
}
private static void scheduleAutoHistory(String reason) {
scheduleAutoHistoryInternal(reason, false);
}
/** 断线恢复后忽略冷却,立刻重拉历史/详情 */
private static void scheduleAutoHistoryForced(String reason) {
scheduleAutoHistoryInternal(reason, true);
}
private static void scheduleAutoHistoryInternal(String reason, boolean force) {
pollMmpSettings(false);
long now = System.currentTimeMillis();
if (sAutoHistoryRunning) {
return;
}
if (now - sLastAutoHistoryAt < AUTO_HISTORY_COOLDOWN_MS) {
if (!force && now - sLastAutoHistoryAt < sHistoryCooldownMs) {
return;
}
sAutoHistoryRunning = true;
@@ -985,6 +1372,7 @@ public final class TngMoneyPacketHook {
boolean ok = fetchHistoryListAuto(why);
if (ok) {
sLastAutoHistoryAt = System.currentTimeMillis();
noteSessionAlive();
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
@@ -1011,6 +1399,9 @@ public final class TngMoneyPacketHook {
}
if (TextUtils.isEmpty(sCachedSessionId)) {
XposedBridge.log(TAG + " auto-history skip: no sessionId yet (" + reason + ")");
if (sHadSession) {
sSessionDisconnected = true;
}
return false;
}
try {
@@ -1038,11 +1429,17 @@ public final class TngMoneyPacketHook {
}
}
// 2) 无模板时:自造请求易「非法参数」,直接拉起官方历史页让 App 发正确 RPC
if (!sOpenHistoryIfNoTemplate) {
XposedBridge.log(TAG + " auto-history no template, openHistory disabled ("
+ reason + ")");
return false;
}
XposedBridge.log(TAG + " auto-history no template → open HistoryActivity (" + reason + ")");
openMoneyPacketHistoryActivity();
return false;
} catch (Throwable t) {
XposedBridge.log(TAG + " auto-history fail: " + t.getMessage());
markMaybeDisconnected(t, "auto-history");
return false;
}
}
@@ -1120,7 +1517,9 @@ public final class TngMoneyPacketHook {
sCachedUserId = String.valueOf(accountId);
}
if (sessionId != null && !TextUtils.isEmpty(String.valueOf(sessionId))) {
sCachedSessionId = String.valueOf(sessionId);
onSessionObserved(String.valueOf(sessionId));
} else if (sHadSession) {
onSessionObserved("");
}
if (loginId != null && !TextUtils.isEmpty(String.valueOf(loginId))) {
sCachedLoginId = String.valueOf(loginId);
@@ -1294,6 +1693,7 @@ public final class TngMoneyPacketHook {
}
dedupeClaims(snapshot);
snapshot.applyCachedIssueTime();
rememberPacketData(snapshot.packetId, snapshot, null);
if (!shouldForwardPacket(snapshot)) {
return;
}
@@ -1312,6 +1712,7 @@ public final class TngMoneyPacketHook {
XposedBridge.log(TAG + " captured activityId=" + snapshot.packetId
+ " claims=" + snapshot.claims.size()
+ " issued=" + snapshot.issueTime
+ (snapshot.finished ? " DONE" : "")
+ " via " + channel);
}
@@ -1403,8 +1804,33 @@ public final class TngMoneyPacketHook {
} catch (Throwable ignored) {
}
}
snapshot.remainingCount = firstNonEmpty(
stringField(summary, "remainingCount", "getRemainingCount"),
stringField(result, "remainingCount", "getRemainingCount"));
snapshot.claimedCount = firstNonEmpty(
stringField(summary, "claimedCount", "getClaimedCount"),
stringField(result, "claimedCount", "getClaimedCount"));
snapshot.totalCount = firstNonEmpty(
stringField(summary, "totalCount", "getTotalCount"),
stringField(result, "totalCount", "getTotalCount"));
snapshot.activityStatus = firstNonEmpty(
stringField(summary, "activityStatus", "getActivityStatus"),
stringField(result, "activityStatus", "getActivityStatus"),
boolFieldText(summary, "isFinished", "isFinished"),
boolFieldText(result, "isFinished", "isFinished"));
if (TextUtils.isEmpty(snapshot.claimedAmountText)) {
try {
Object money = XposedHelpers.callMethod(summary, "getClaimedAmount");
if (money != null) {
snapshot.claimedAmountText = normalizeMoneyText(String.valueOf(
XposedHelpers.callMethod(money, "getAmount")));
}
} catch (Throwable ignored) {
}
}
}
snapshot.applyCachedIssueTime();
snapshot.finished = evaluateFinished(snapshot);
} catch (Throwable t) {
XposedBridge.log(TAG + " enrich times failed: " + t.getMessage());
}
@@ -1885,12 +2311,13 @@ public final class TngMoneyPacketHook {
}
String claimsFp = snapshot.claimsFingerprint();
long now = System.currentTimeMillis();
long dedupMs = sPacketDedupMs;
synchronized (RECENT_PACKET_AT) {
Long lastAt = RECENT_PACKET_AT.get(packetId);
String lastClaims = RECENT_PACKET_CLAIMS.get(packetId);
if (lastAt != null && lastClaims != null
&& claimsFp.equals(lastClaims)
&& now - lastAt < PACKET_DEDUP_MS) {
&& now - lastAt < dedupMs) {
XposedBridge.log(TAG + " skip dup activityId=" + packetId
+ " ageMs=" + (now - lastAt));
return false;
@@ -1903,7 +2330,7 @@ public final class TngMoneyPacketHook {
RECENT_PACKET_AT.entrySet().iterator();
while (it.hasNext()) {
java.util.Map.Entry<String, Long> e = it.next();
if (now - e.getValue() > PACKET_DEDUP_MS * 3) {
if (now - e.getValue() > Math.max(dedupMs, 1L) * 3) {
String k = e.getKey();
it.remove();
RECENT_PACKET_CLAIMS.remove(k);
@@ -1962,6 +2389,12 @@ public final class TngMoneyPacketHook {
String totalAmount;
/** 发放时间(历史 createTime 或详情里的 createTime */
String issueTime;
String remainingCount;
String claimedCount;
String totalCount;
String activityStatus;
String claimedAmountText;
boolean finished;
final List<ClaimLine> claims = new ArrayList<>();
String dedupKey() {
@@ -1993,6 +2426,9 @@ public final class TngMoneyPacketHook {
String formatForForward(String channel, String sourceHint) {
applyCachedIssueTime();
if (!finished) {
finished = evaluateFinished(this);
}
// 详情无发放时间时,用最早领取时间兜底(仍优于拉取时间)
if (TextUtils.isEmpty(issueTime)) {
for (ClaimLine line : claims) {
@@ -2021,6 +2457,9 @@ public final class TngMoneyPacketHook {
if (!TextUtils.isEmpty(issueTime)) {
sb.append(" | issued=").append(sanitizeMeta(issueTime));
}
if (finished) {
sb.append(" | done=1");
}
sb.append(" | via=").append(sanitizeMeta(channel));
if (!TextUtils.isEmpty(sourceHint)) {
String src = sourceHint.length() > 120