feat(mmp): 独立红包领取台(手机+PC)、同步、筛选与功能说明
隔离通用消息台;支持设置/手气摘要/电脑同步;忽略临时逆向脚本与运行时 JSON。
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user