feat(tng): 自动拉红包历史/详情,并展示发放与领取时间
从 ILoginStorage 与官方历史页缓存请求模板,级联拉详情;领取台列表用发放时间而非拉取时间。
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
@@ -49,13 +51,27 @@ public final class TngMoneyPacketHook {
|
||||
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;
|
||||
/** 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;
|
||||
private static volatile long sLastAutoHistoryAt;
|
||||
private static volatile boolean sAutoHistoryRunning;
|
||||
/** 本线程正在主动拉历史,避免 Quake afterHook 再 schedule 一遍 */
|
||||
private static final ThreadLocal<Boolean> AUTO_HISTORY_SELF = new ThreadLocal<>();
|
||||
private static volatile Object sMoneyPacketRepository;
|
||||
private static volatile Object sMoneyPacketRpcTask;
|
||||
private static volatile ClassLoader sAppClassLoader;
|
||||
private static volatile String sCachedUserId;
|
||||
private static volatile String sCachedSessionId;
|
||||
private static volatile String sCachedLoginId;
|
||||
/** 手动打开详情时抓到的真实请求,自动拉详情时克隆它 */
|
||||
/** 真实详情/历史请求模板(有则克隆;无则用 ILoginStorage 自造) */
|
||||
private static volatile Object sTemplateDetailRequest;
|
||||
private static volatile Object sTemplateHistoryRequest;
|
||||
/** 最近一次带登录态的请求(历史/详情),用于给自造详情请求补 session */
|
||||
private static volatile Object sLoginDonorRequest;
|
||||
private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private static final java.util.concurrent.ExecutorService AUTO_DETAIL_EXEC =
|
||||
java.util.concurrent.Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "TngMmp-auto-detail");
|
||||
@@ -75,6 +91,8 @@ public final class TngMoneyPacketHook {
|
||||
hookResponseBody(lpparam);
|
||||
hookGsonFromJson(lpparam);
|
||||
hookQuakeRpc(lpparam);
|
||||
hookLoginStorage(lpparam);
|
||||
hookActivityResumeForAutoHistory(lpparam);
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
}
|
||||
|
||||
@@ -209,9 +227,10 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
|
||||
private static void hookQuakeRpc(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// 只挂 TNG 封装层,避免与底层 RpcInvocationHandler 双触发导致重复
|
||||
// TNG 封装 + Quake 底层都挂,避免历史/详情只走其中一条链路时漏抓
|
||||
String[] rpcClasses = {
|
||||
"my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost",
|
||||
"com.alipay.imobile.network.quake.rpc.RpcInvocationHandler",
|
||||
};
|
||||
XC_MethodHook hook = new XC_MethodHook() {
|
||||
@Override
|
||||
@@ -225,9 +244,25 @@ public final class TngMoneyPacketHook {
|
||||
if (op == null) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " rpc op=" + op
|
||||
+ " via=" + param.thisObject.getClass().getSimpleName());
|
||||
Object result = param.getResult();
|
||||
// 历史列表:无领取排行,但可用 activityId 自动拉详情
|
||||
if (isHistoryRpcOp(op)) {
|
||||
Object historyReq = extractRpcRequestArg(param.args);
|
||||
if (historyReq != null) {
|
||||
sTemplateHistoryRequest = cloneRequestShallow(historyReq);
|
||||
sLoginDonorRequest = historyReq;
|
||||
cacheLoginFromRequestObject(historyReq);
|
||||
XposedBridge.log(TAG + " cached history request template"
|
||||
+ " session=" + abbreviate(stringField(historyReq,
|
||||
"sessionId", "getSessionId")));
|
||||
}
|
||||
// 无论是否自己触发的历史请求,都先缓存发放时间
|
||||
cacheIssueTimesFromHistory(result);
|
||||
if (Boolean.TRUE.equals(AUTO_HISTORY_SELF.get())) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " history hit → schedule auto detail");
|
||||
scheduleAutoDetailsFromHistory(result);
|
||||
return;
|
||||
@@ -248,9 +283,11 @@ public final class TngMoneyPacketHook {
|
||||
return;
|
||||
}
|
||||
MmpSnapshot probe = parseSnapshot(json);
|
||||
enrichSnapshotFromJavaResult(result, probe);
|
||||
if (probe != null && !probe.claims.isEmpty()) {
|
||||
if (detailReq != null) {
|
||||
sTemplateDetailRequest = cloneRequestShallow(detailReq);
|
||||
sLoginDonorRequest = detailReq;
|
||||
cacheLoginFromRequestObject(detailReq);
|
||||
XposedBridge.log(TAG + " cached detail request template"
|
||||
+ " userId=" + stringField(detailReq, "userId", "getUserId")
|
||||
@@ -259,7 +296,7 @@ public final class TngMoneyPacketHook {
|
||||
+ " page=" + stringField(detailReq, "page", "getPage")
|
||||
+ " max=" + stringField(detailReq, "maxResult", "getMaxResult"));
|
||||
}
|
||||
forwardParsedJson(json, op, "rpc");
|
||||
forwardSnapshot(probe, op, "rpc");
|
||||
} else {
|
||||
String snippet = json.length() > 500 ? json.substring(0, 500) + "..." : json;
|
||||
XposedBridge.log(TAG + " rpc no-claims op=" + op + " body=" + snippet);
|
||||
@@ -287,6 +324,105 @@ public final class TngMoneyPacketHook {
|
||||
XposedBridge.log(TAG + " skip RPC " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
hookMoneyPacketRepository(lpparam);
|
||||
}
|
||||
|
||||
/** DI 创建 Repository 时立刻缓存 task + login,不必等用户进红包页 */
|
||||
private static void hookMoneyPacketRepository(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> repoCls = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.funding.moneypacket.common.data.repo.MoneyPacketRepositoryImpl",
|
||||
lpparam.classLoader);
|
||||
XposedBridge.hookAllConstructors(repoCls, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
captureRepoInstance(param.thisObject, "ctor");
|
||||
}
|
||||
});
|
||||
// Dagger 可能先无参构造再注入字段;在业务方法入口再捕获一次
|
||||
for (String methodName : new String[]{
|
||||
"getMoneyPacketList", "getMoneyPacketDetail", "doClaim", "preClaim"}) {
|
||||
try {
|
||||
for (Method m : repoCls.getDeclaredMethods()) {
|
||||
if (!methodName.equals(m.getName())) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(m, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
captureRepoInstance(param.thisObject, "method:" + methodName);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked MoneyPacketRepositoryImpl");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " repo hook skip: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void captureRepoInstance(Object repo, String reason) {
|
||||
if (repo == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sMoneyPacketRepository = repo;
|
||||
Object task = null;
|
||||
try {
|
||||
task = XposedHelpers.getObjectField(repo, "task");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (task != null && sMoneyPacketRpcTask == null) {
|
||||
sMoneyPacketRpcTask = task;
|
||||
XposedBridge.log(TAG + " cached RPC task from repo (" + reason + ")");
|
||||
}
|
||||
Object loginStorage = null;
|
||||
try {
|
||||
loginStorage = XposedHelpers.getObjectField(repo, "loginStorage");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
applyLoginStorage(loginStorage);
|
||||
if (sMoneyPacketRpcTask != null && !TextUtils.isEmpty(sCachedSessionId)) {
|
||||
scheduleAutoHistory("repo-" + reason);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " captureRepo fail: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 App 一致:发 RPC 前走 Repository.addNetworkHandle,否则易 RpcApiException / 空结果 */
|
||||
private static void prepareRpcRequest(Object req) {
|
||||
if (req == null) {
|
||||
return;
|
||||
}
|
||||
Object repo = sMoneyPacketRepository;
|
||||
if (repo == null) {
|
||||
repo = obtainMoneyPacketRepository(sAppClassLoader);
|
||||
if (repo != null) {
|
||||
sMoneyPacketRepository = repo;
|
||||
}
|
||||
}
|
||||
if (repo == null) {
|
||||
XposedBridge.log(TAG + " prepareRpc skip: no repository");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// addNetworkHandle$default(repo, request, processor=null, onError=null, mask=3, marker=null)
|
||||
XposedHelpers.callStaticMethod(repo.getClass(), "addNetworkHandle$default",
|
||||
repo, req, null, null, 3, null);
|
||||
XposedBridge.log(TAG + " prepareRpc addNetworkHandle$default ok");
|
||||
return;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " addNetworkHandle$default fail: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.callMethod(repo, "addNetworkHandle", req, null, null);
|
||||
XposedBridge.log(TAG + " prepareRpc addNetworkHandle ok");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " addNetworkHandle fail: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHistoryRpcOp(String op) {
|
||||
@@ -331,6 +467,7 @@ public final class TngMoneyPacketHook {
|
||||
if (name.endsWith("MoneyPacketRpcTask") || name.contains("MoneyPacketRpc")) {
|
||||
sMoneyPacketRpcTask = proxy;
|
||||
XposedBridge.log(TAG + " cached RPC task " + name);
|
||||
scheduleAutoHistory("rpc-task-cached");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -341,6 +478,7 @@ public final class TngMoneyPacketHook {
|
||||
sAppClassLoader));
|
||||
sMoneyPacketRpcTask = proxy;
|
||||
XposedBridge.log(TAG + " cached RPC task via moneyPacketDetail method");
|
||||
scheduleAutoHistory("rpc-task-cached");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
@@ -456,46 +594,60 @@ public final class TngMoneyPacketHook {
|
||||
if (historyResult == null) {
|
||||
return;
|
||||
}
|
||||
AUTO_DETAIL_EXEC.execute(() -> {
|
||||
try {
|
||||
List<String[]> jobs = extractHistoryJobs(historyResult);
|
||||
XposedBridge.log(TAG + " auto-detail jobs=" + jobs.size());
|
||||
for (String[] job : jobs) {
|
||||
final String activityId = job[0];
|
||||
final String senderUserId = job[1];
|
||||
long now = System.currentTimeMillis();
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
Long last = AUTO_DETAIL_AT.get(activityId);
|
||||
if (last != null && now - last < AUTO_DETAIL_COOLDOWN_MS) {
|
||||
continue;
|
||||
}
|
||||
if (!AUTO_DETAIL_PENDING.add(activityId)) {
|
||||
continue;
|
||||
}
|
||||
// 解析放调用线程;真正 RPC 必须丢到主线程(Quake/登录态常绑主线程)
|
||||
final List<String[]> jobs;
|
||||
try {
|
||||
jobs = extractHistoryJobs(historyResult);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " extract history jobs failed: " + t.getMessage());
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(TAG + " auto-detail jobs=" + jobs.size());
|
||||
if (jobs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long delayMs = 0L;
|
||||
for (String[] job : jobs) {
|
||||
final String activityId = job[0];
|
||||
final String senderUserId = job[1];
|
||||
final String createTime = job.length > 2 ? job[2] : null;
|
||||
if (!TextUtils.isEmpty(createTime)) {
|
||||
synchronized (ACTIVITY_ISSUE_TIME) {
|
||||
ACTIVITY_ISSUE_TIME.put(activityId, createTime);
|
||||
}
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
Long last = AUTO_DETAIL_AT.get(activityId);
|
||||
if (last != null && now - last < AUTO_DETAIL_COOLDOWN_MS) {
|
||||
continue;
|
||||
}
|
||||
if (!AUTO_DETAIL_PENDING.add(activityId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
final long thisDelay = delayMs;
|
||||
delayMs += 250L;
|
||||
AUTO_DETAIL_EXEC.execute(() -> {
|
||||
try {
|
||||
if (thisDelay > 0) {
|
||||
Thread.sleep(thisDelay);
|
||||
}
|
||||
try {
|
||||
boolean ok = fetchDetailByActivityId(activityId, senderUserId);
|
||||
if (ok) {
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
AUTO_DETAIL_AT.put(activityId, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
boolean ok = fetchDetailByActivityId(activityId, senderUserId);
|
||||
if (ok) {
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
AUTO_DETAIL_PENDING.remove(activityId);
|
||||
AUTO_DETAIL_AT.put(activityId, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(400);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
AUTO_DETAIL_PENDING.remove(activityId);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " auto-detail schedule failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String[]> extractHistoryJobs(Object historyResult) {
|
||||
@@ -521,8 +673,10 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
String activityId = stringField(info, "activityId", "getActivityId");
|
||||
String senderUserId = stringField(info, "senderUserId", "getSenderUserId");
|
||||
String createTime = stringField(info, "createTime", "getCreateTime");
|
||||
if (looksLikeActivityId(activityId) && !TextUtils.isEmpty(senderUserId)) {
|
||||
jobs.add(new String[]{activityId, senderUserId});
|
||||
jobs.add(new String[]{activityId, senderUserId,
|
||||
TextUtils.isEmpty(createTime) ? "" : createTime});
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
@@ -551,6 +705,7 @@ public final class TngMoneyPacketHook {
|
||||
|
||||
private static boolean fetchDetailByActivityId(String activityId, String senderUserId) {
|
||||
ClassLoader cl = sAppClassLoader;
|
||||
ensureRpcTaskAndLogin(cl);
|
||||
Object task = sMoneyPacketRpcTask;
|
||||
if (cl == null || task == null) {
|
||||
XposedBridge.log(TAG + " auto-detail skip: no rpc task yet, id=" + activityId);
|
||||
@@ -572,25 +727,20 @@ public final class TngMoneyPacketHook {
|
||||
XposedBridge.log(TAG + " auto-detail newRequest failed id=" + activityId);
|
||||
return false;
|
||||
}
|
||||
fillLoginOnRequest(req, cl);
|
||||
}
|
||||
cacheLoginFromRequestObject(req);
|
||||
fillLoginOnRequest(req, cl);
|
||||
prepareRpcRequest(req);
|
||||
XposedBridge.log(TAG + " auto-detail req id=" + activityId
|
||||
+ " userId=" + stringField(req, "userId", "getUserId")
|
||||
+ " session=" + abbreviate(stringField(req, "sessionId", "getSessionId"))
|
||||
+ " loginId=" + abbreviate(stringField(req, "loginId", "getLoginId"))
|
||||
+ " page=" + stringField(req, "page", "getPage")
|
||||
+ " max=" + stringField(req, "maxResult", "getMaxResult"));
|
||||
if (TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))
|
||||
&& TextUtils.isEmpty(sCachedSessionId)) {
|
||||
if (TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))) {
|
||||
XposedBridge.log(TAG + " auto-detail skip: no sessionId id=" + activityId
|
||||
+ " (先手动点开任意一个红包详情一次以缓存模板)");
|
||||
+ " (ILoginStorage 尚未就绪,等登录后 Activity resume 会重试)");
|
||||
return false;
|
||||
}
|
||||
if (TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))
|
||||
&& !TextUtils.isEmpty(sCachedSessionId)) {
|
||||
fillLoginOnRequest(req, cl);
|
||||
}
|
||||
Object result = XposedHelpers.callMethod(task, "moneyPacketDetail", req);
|
||||
if (result == null) {
|
||||
XposedBridge.log(TAG + " auto-detail null result id=" + activityId);
|
||||
@@ -605,6 +755,7 @@ public final class TngMoneyPacketHook {
|
||||
return false;
|
||||
}
|
||||
MmpSnapshot probe = parseSnapshot(json);
|
||||
enrichSnapshotFromJavaResult(result, probe);
|
||||
if (probe == null || probe.claims.isEmpty()) {
|
||||
String snippet = json.length() > 240 ? json.substring(0, 240) + "..." : json;
|
||||
XposedBridge.log(TAG + " auto-detail empty claims id=" + activityId
|
||||
@@ -612,9 +763,10 @@ public final class TngMoneyPacketHook {
|
||||
+ " userId=" + stringField(req, "userId", "getUserId"));
|
||||
return false;
|
||||
}
|
||||
forwardParsedJson(json, "auto:" + activityId, "rpc");
|
||||
forwardSnapshot(probe, "auto:" + activityId, "rpc");
|
||||
XposedBridge.log(TAG + " auto-detail ok activityId=" + activityId
|
||||
+ " claims=" + probe.claims.size());
|
||||
+ " claims=" + probe.claims.size()
|
||||
+ " issued=" + probe.issueTime);
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " auto-detail fail id=" + activityId + ": " + t.getMessage());
|
||||
@@ -653,7 +805,20 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
|
||||
private static void fillLoginOnRequest(Object req, ClassLoader cl) {
|
||||
// 1) 优先用历史请求里缓存的登录态
|
||||
ensureRpcTaskAndLogin(cl);
|
||||
// 0) 从最近真实请求拷贝登录相关字段(历史页打开后即可,无需点详情)
|
||||
Object donor = sLoginDonorRequest;
|
||||
if (donor == null) {
|
||||
donor = sTemplateHistoryRequest;
|
||||
}
|
||||
if (donor == null) {
|
||||
donor = sTemplateDetailRequest;
|
||||
}
|
||||
if (donor != null && donor != req) {
|
||||
copyLoginFields(donor, req);
|
||||
cacheLoginFromRequestObject(donor);
|
||||
}
|
||||
// 1) 再用缓存的登录态覆盖
|
||||
trySet(req, "setUserId", sCachedUserId);
|
||||
trySet(req, "setSessionId", sCachedSessionId);
|
||||
trySet(req, "setLoginId", sCachedLoginId);
|
||||
@@ -673,37 +838,60 @@ public final class TngMoneyPacketHook {
|
||||
&& !TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))) {
|
||||
return;
|
||||
}
|
||||
// 2) Hilt EntryPoint 兜底
|
||||
// 2) 从 Repository.loginStorage / ILoginStorage 再读一遍
|
||||
Object loginStorage = obtainLoginStorage(cl);
|
||||
if (loginStorage == null) {
|
||||
return;
|
||||
}
|
||||
applyLoginStorage(loginStorage);
|
||||
trySet(req, "setUserId", sCachedUserId);
|
||||
trySet(req, "setSessionId", sCachedSessionId);
|
||||
trySet(req, "setLoginId", sCachedLoginId);
|
||||
try {
|
||||
Object repo = obtainMoneyPacketRepository(cl);
|
||||
if (repo == null) {
|
||||
return;
|
||||
if (!TextUtils.isEmpty(sCachedUserId)) {
|
||||
XposedHelpers.setObjectField(req, "userId", sCachedUserId);
|
||||
}
|
||||
Object loginStorage = null;
|
||||
if (!TextUtils.isEmpty(sCachedSessionId)) {
|
||||
XposedHelpers.setObjectField(req, "sessionId", sCachedSessionId);
|
||||
}
|
||||
if (!TextUtils.isEmpty(sCachedLoginId)) {
|
||||
XposedHelpers.setObjectField(req, "loginId", sCachedLoginId);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyLoginFields(Object src, Object dst) {
|
||||
if (src == null || dst == null) {
|
||||
return;
|
||||
}
|
||||
String[] keys = {"userId", "sessionId", "loginId", "securityId", "apiName"};
|
||||
for (String key : keys) {
|
||||
try {
|
||||
loginStorage = XposedHelpers.getObjectField(repo, "loginStorage");
|
||||
Object v = null;
|
||||
try {
|
||||
v = XposedHelpers.getObjectField(src, key);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (v == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
XposedHelpers.setObjectField(dst, key, v);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
String setter = "set" + Character.toUpperCase(key.charAt(0)) + key.substring(1);
|
||||
trySet(dst, setter, v);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (loginStorage == null) {
|
||||
return;
|
||||
}
|
||||
// extParams 等 Map 也尽量带上
|
||||
try {
|
||||
Object ext = XposedHelpers.getObjectField(src, "extParams");
|
||||
if (ext != null) {
|
||||
XposedHelpers.setObjectField(dst, "extParams", ext);
|
||||
}
|
||||
Object accountId = XposedHelpers.callMethod(loginStorage, "getAccountId");
|
||||
Object sessionId = XposedHelpers.callMethod(loginStorage, "getSessionId");
|
||||
Object loginId = XposedHelpers.callMethod(loginStorage, "getLoginId");
|
||||
trySet(req, "setUserId", accountId);
|
||||
trySet(req, "setSessionId", sessionId);
|
||||
trySet(req, "setLoginId", loginId);
|
||||
if (accountId != null) {
|
||||
sCachedUserId = String.valueOf(accountId);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
sCachedSessionId = String.valueOf(sessionId);
|
||||
}
|
||||
if (loginId != null) {
|
||||
sCachedLoginId = String.valueOf(loginId);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " fillLogin skip: " + t.getMessage());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,30 +905,344 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
}
|
||||
|
||||
private static Object obtainMoneyPacketRepository(ClassLoader cl) {
|
||||
/** 被动缓存:App 任何地方读 session 时同步到本 Hook */
|
||||
private static void hookLoginStorage(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// 不能 hook 接口抽象方法;只挂实现类
|
||||
try {
|
||||
Class<?> impl = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.user.model.CacheLoginStorageImpl", lpparam.classLoader);
|
||||
XC_MethodHook cacheHook = new XC_MethodHook() {
|
||||
@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);
|
||||
} else if ("getAccountId".equals(name)) {
|
||||
sCachedUserId = String.valueOf(v);
|
||||
} else if ("getLoginId".equals(name)) {
|
||||
sCachedLoginId = String.valueOf(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (String m : new String[]{"getSessionId", "getAccountId", "getLoginId"}) {
|
||||
XposedBridge.hookAllMethods(impl, m, cacheHook);
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked CacheLoginStorageImpl getters");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " CacheLoginStorageImpl hook skip: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 前台 Activity 恢复时自动拉历史列表(再级联详情),无需手动进红包历史页 */
|
||||
private static void hookActivityResumeForAutoHistory(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.app.Activity",
|
||||
lpparam.classLoader,
|
||||
"onResume",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object act = param.thisObject;
|
||||
if (act == null) {
|
||||
return;
|
||||
}
|
||||
String pkg = String.valueOf(
|
||||
XposedHelpers.callMethod(act, "getPackageName"));
|
||||
if (!PACKAGE.equals(pkg)) {
|
||||
return;
|
||||
}
|
||||
scheduleAutoHistory("activity-resume");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked Activity.onResume for auto history");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Activity.onResume hook skip: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleAutoHistory(String reason) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (sAutoHistoryRunning) {
|
||||
return;
|
||||
}
|
||||
if (now - sLastAutoHistoryAt < AUTO_HISTORY_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
sAutoHistoryRunning = true;
|
||||
final String why = reason;
|
||||
AUTO_DETAIL_EXEC.execute(() -> {
|
||||
try {
|
||||
// 等登录/Hilt/Network 就绪;不要在主线程阻塞等 RPC
|
||||
Thread.sleep(why != null && why.startsWith("repo") ? 1500L : 800L);
|
||||
boolean ok = fetchHistoryListAuto(why);
|
||||
if (ok) {
|
||||
sLastAutoHistoryAt = System.currentTimeMillis();
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
sAutoHistoryRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 主动调 moneyPacketHistoryList,结果走现有 scheduleAutoDetailsFromHistory */
|
||||
private static boolean fetchHistoryListAuto(String reason) {
|
||||
ClassLoader cl = sAppClassLoader;
|
||||
ensureRpcTaskAndLogin(cl);
|
||||
Object task = sMoneyPacketRpcTask;
|
||||
if (cl == null || task == null) {
|
||||
XposedBridge.log(TAG + " auto-history skip: no rpc task (" + reason + ")");
|
||||
return false;
|
||||
}
|
||||
if (TextUtils.isEmpty(sCachedSessionId)) {
|
||||
Object ls = obtainLoginStorage(cl);
|
||||
if (ls != null) {
|
||||
applyLoginStorage(ls);
|
||||
}
|
||||
}
|
||||
if (TextUtils.isEmpty(sCachedSessionId)) {
|
||||
XposedBridge.log(TAG + " auto-history skip: no sessionId yet (" + reason + ")");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Class<?> reqCls = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.funding.moneypacket.common.data.rpc.MoneyPacketHistoryListRequest",
|
||||
cl);
|
||||
// 1) 已有官方请求模板(首次打开历史页后会缓存)→ 静默复用
|
||||
if (sTemplateHistoryRequest != null
|
||||
&& reqCls.isInstance(sTemplateHistoryRequest)) {
|
||||
Object req = cloneRequestShallow(sTemplateHistoryRequest);
|
||||
XposedBridge.log(TAG + " auto-history using template (" + reason + ")");
|
||||
fillLoginOnRequest(req, cl);
|
||||
prepareRpcRequest(req);
|
||||
AUTO_HISTORY_SELF.set(Boolean.TRUE);
|
||||
Object result;
|
||||
try {
|
||||
result = callHistoryListRpc(task, req);
|
||||
} finally {
|
||||
AUTO_HISTORY_SELF.remove();
|
||||
}
|
||||
if (result != null && historyHasJobs(result)) {
|
||||
XposedBridge.log(TAG + " auto-history ok(template) → schedule details");
|
||||
scheduleAutoDetailsFromHistory(result);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 2) 无模板时:自造请求易「非法参数」,直接拉起官方历史页让 App 发正确 RPC
|
||||
XposedBridge.log(TAG + " auto-history no template → open HistoryActivity (" + reason + ")");
|
||||
openMoneyPacketHistoryActivity();
|
||||
return false;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " auto-history fail: " + t.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean historyHasJobs(Object historyResult) {
|
||||
return !extractHistoryJobs(historyResult).isEmpty();
|
||||
}
|
||||
|
||||
/** 拉起官方历史页;App 自己的 moneyPacketHistoryList 成功后 Hook 会缓存模板并级联拉详情 */
|
||||
private static void openMoneyPacketHistoryActivity() {
|
||||
try {
|
||||
Context ctx = getContext();
|
||||
if (ctx == null) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
Class<?> entryPoint = Class.forName(
|
||||
"my.com.tngdigital.funding.moneypacket.common.di.MoneyPacketEntryPoint",
|
||||
false, cl);
|
||||
Class<?> entryPoints = Class.forName("dagger.hilt.EntryPoints", false, cl);
|
||||
Object ep = XposedHelpers.callStaticMethod(
|
||||
entryPoints, "get", ctx.getApplicationContext(), entryPoint);
|
||||
try {
|
||||
return XposedHelpers.callMethod(ep, "repository");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
for (Method m : ep.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length == 0
|
||||
&& m.getReturnType().getName().contains("MoneyPacketRepository")) {
|
||||
return m.invoke(ep);
|
||||
android.content.Intent intent = new android.content.Intent();
|
||||
intent.setClassName(PACKAGE,
|
||||
"my.com.tngdigital.funding.moneypacket.create.ui.MoneyPacketHistoryActivity");
|
||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
ctx.startActivity(intent);
|
||||
XposedBridge.log(TAG + " started MoneyPacketHistoryActivity");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " start HistoryActivity fail: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Object callHistoryListRpc(Object task, Object req) {
|
||||
try {
|
||||
return XposedHelpers.callMethod(task, "moneyPacketHistoryList", req);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " history rpc fail: " + t.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureRpcTaskAndLogin(ClassLoader cl) {
|
||||
if (cl == null) {
|
||||
return;
|
||||
}
|
||||
if (sMoneyPacketRpcTask == null || TextUtils.isEmpty(sCachedSessionId)) {
|
||||
Object repo = obtainMoneyPacketRepository(cl);
|
||||
if (repo != null) {
|
||||
if (sMoneyPacketRpcTask == null) {
|
||||
try {
|
||||
Object task = XposedHelpers.getObjectField(repo, "task");
|
||||
if (task != null) {
|
||||
sMoneyPacketRpcTask = task;
|
||||
XposedBridge.log(TAG + " cached RPC task from repository");
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
Object loginStorage = XposedHelpers.getObjectField(repo, "loginStorage");
|
||||
applyLoginStorage(loginStorage);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TextUtils.isEmpty(sCachedSessionId)) {
|
||||
applyLoginStorage(obtainLoginStorage(cl));
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyLoginStorage(Object loginStorage) {
|
||||
if (loginStorage == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object accountId = XposedHelpers.callMethod(loginStorage, "getAccountId");
|
||||
Object sessionId = XposedHelpers.callMethod(loginStorage, "getSessionId");
|
||||
Object loginId = XposedHelpers.callMethod(loginStorage, "getLoginId");
|
||||
if (accountId != null && !TextUtils.isEmpty(String.valueOf(accountId))) {
|
||||
sCachedUserId = String.valueOf(accountId);
|
||||
}
|
||||
if (sessionId != null && !TextUtils.isEmpty(String.valueOf(sessionId))) {
|
||||
sCachedSessionId = String.valueOf(sessionId);
|
||||
}
|
||||
if (loginId != null && !TextUtils.isEmpty(String.valueOf(loginId))) {
|
||||
sCachedLoginId = String.valueOf(loginId);
|
||||
}
|
||||
if (!TextUtils.isEmpty(sCachedSessionId)) {
|
||||
XposedBridge.log(TAG + " login from storage session="
|
||||
+ abbreviate(sCachedSessionId)
|
||||
+ " userId=" + sCachedUserId);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " obtain repo fail: " + t.getMessage());
|
||||
XposedBridge.log(TAG + " applyLoginStorage skip: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Object obtainLoginStorage(ClassLoader cl) {
|
||||
try {
|
||||
Object repo = obtainMoneyPacketRepository(cl);
|
||||
if (repo != null) {
|
||||
try {
|
||||
Object ls = XposedHelpers.getObjectField(repo, "loginStorage");
|
||||
if (ls != null) {
|
||||
return ls;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
// Hilt:尝试常见 EntryPoint 上返回 ILoginStorage 的无参方法
|
||||
try {
|
||||
Context ctx = getContext();
|
||||
if (ctx == null || cl == null) {
|
||||
return null;
|
||||
}
|
||||
String[] entryPoints = {
|
||||
"my.com.tngdigital.member.di.MemberEntryPoint",
|
||||
"my.com.tngdigital.user.di.UserEntryPoint",
|
||||
"my.com.tngdigital.common.di.CommonEntryPoint",
|
||||
};
|
||||
Class<?> entryPointsCls = Class.forName("dagger.hilt.EntryPoints", false, cl);
|
||||
for (String epName : entryPoints) {
|
||||
try {
|
||||
Class<?> epCls = Class.forName(epName, false, cl);
|
||||
Object ep = XposedHelpers.callStaticMethod(
|
||||
entryPointsCls, "get", ctx.getApplicationContext(), epCls);
|
||||
for (Method m : ep.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) {
|
||||
continue;
|
||||
}
|
||||
if (m.getReturnType().getName().contains("ILoginStorage")) {
|
||||
Object ls = m.invoke(ep);
|
||||
if (ls != null) {
|
||||
return ls;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object obtainMoneyPacketRepository(ClassLoader cl) {
|
||||
Context ctx = getContext();
|
||||
if (ctx == null || cl == null) {
|
||||
return null;
|
||||
}
|
||||
Context appCtx = ctx.getApplicationContext() != null ? ctx.getApplicationContext() : ctx;
|
||||
Class<?> entryPoint;
|
||||
try {
|
||||
entryPoint = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.funding.moneypacket.common.di.MoneyPacketEntryPoint", cl);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " MoneyPacketEntryPoint missing: " + t.getMessage());
|
||||
return null;
|
||||
}
|
||||
// 1) EntryPointAccessors.fromApplication(Hilt Android 推荐)
|
||||
try {
|
||||
Class<?> accessors = XposedHelpers.findClass(
|
||||
"dagger.hilt.android.EntryPointAccessors", cl);
|
||||
Object ep = XposedHelpers.callStaticMethod(
|
||||
accessors, "fromApplication", appCtx, entryPoint);
|
||||
Object repo = invokeRepository(ep);
|
||||
if (repo != null) {
|
||||
return repo;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " EntryPointAccessors fail: " + t.getMessage());
|
||||
}
|
||||
// 2) EntryPoints.get
|
||||
try {
|
||||
Class<?> entryPoints = XposedHelpers.findClass("dagger.hilt.EntryPoints", cl);
|
||||
Object ep = XposedHelpers.callStaticMethod(
|
||||
entryPoints, "get", appCtx, entryPoint);
|
||||
Object repo = invokeRepository(ep);
|
||||
if (repo != null) {
|
||||
return repo;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " EntryPoints.get fail: " + t.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object invokeRepository(Object ep) {
|
||||
if (ep == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return XposedHelpers.callMethod(ep, "repository");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
for (Method m : ep.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length == 0
|
||||
&& m.getReturnType().getName().contains("MoneyPacketRepository")) {
|
||||
try {
|
||||
return m.invoke(ep);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -783,6 +1285,15 @@ public final class TngMoneyPacketHook {
|
||||
if (snapshot == null || snapshot.claims.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
forwardSnapshot(snapshot, sourceHint, channel);
|
||||
}
|
||||
|
||||
private static void forwardSnapshot(MmpSnapshot snapshot, String sourceHint, String channel) {
|
||||
if (snapshot == null || snapshot.claims.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
dedupeClaims(snapshot);
|
||||
snapshot.applyCachedIssueTime();
|
||||
if (!shouldForwardPacket(snapshot)) {
|
||||
return;
|
||||
}
|
||||
@@ -799,7 +1310,158 @@ public final class TngMoneyPacketHook {
|
||||
String content = snapshot.formatForForward(channel, sourceHint);
|
||||
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
|
||||
XposedBridge.log(TAG + " captured activityId=" + snapshot.packetId
|
||||
+ " claims=" + snapshot.claims.size() + " via " + channel);
|
||||
+ " claims=" + snapshot.claims.size()
|
||||
+ " issued=" + snapshot.issueTime
|
||||
+ " via " + channel);
|
||||
}
|
||||
|
||||
/** 直接从 Java 结果对象读 gmtModified / createTime,不依赖 JSON 字段是否序列化成功 */
|
||||
private static void enrichSnapshotFromJavaResult(Object result, MmpSnapshot snapshot) {
|
||||
if (result == null || snapshot == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object pools = null;
|
||||
try {
|
||||
pools = XposedHelpers.callMethod(result, "getActivityPoolInfos");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (pools == null) {
|
||||
try {
|
||||
pools = XposedHelpers.getObjectField(result, "activityPoolInfos");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (pools instanceof Iterable) {
|
||||
for (Object info : (Iterable<?>) pools) {
|
||||
if (info == null) {
|
||||
continue;
|
||||
}
|
||||
String name = stringField(info, "receiverName", "getReceiverName");
|
||||
String claimTime = firstNonEmpty(
|
||||
stringField(info, "gmtModified", "getGmtModified"),
|
||||
stringField(info, "gmtCreate", "getGmtCreate"),
|
||||
stringField(info, "createTime", "getCreateTime"));
|
||||
String amount = null;
|
||||
try {
|
||||
Object money = XposedHelpers.callMethod(info, "getAmount");
|
||||
if (money != null) {
|
||||
amount = normalizeMoneyText(String.valueOf(
|
||||
XposedHelpers.callMethod(money, "getAmount")));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (TextUtils.isEmpty(amount)) {
|
||||
amount = normalizeMoneyText(stringField(info, "amount", "getAmount"));
|
||||
}
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
continue;
|
||||
}
|
||||
boolean matched = false;
|
||||
for (ClaimLine line : snapshot.claims) {
|
||||
if (name.equals(line.nickname)) {
|
||||
if (TextUtils.isEmpty(line.claimTime) && !TextUtils.isEmpty(claimTime)) {
|
||||
line.claimTime = claimTime;
|
||||
}
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (!matched && !TextUtils.isEmpty(amount)) {
|
||||
ClaimLine line = new ClaimLine();
|
||||
line.nickname = name;
|
||||
line.amount = amount;
|
||||
line.claimTime = claimTime;
|
||||
snapshot.claims.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
Object summary = null;
|
||||
try {
|
||||
summary = XposedHelpers.callMethod(result, "getSummaryInfo");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (summary == null) {
|
||||
try {
|
||||
summary = XposedHelpers.getObjectField(result, "summaryInfo");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (summary != null) {
|
||||
if (TextUtils.isEmpty(snapshot.senderName)) {
|
||||
snapshot.senderName = stringField(summary, "senderName", "getSenderName");
|
||||
}
|
||||
if (TextUtils.isEmpty(snapshot.packetId)) {
|
||||
snapshot.packetId = stringField(summary, "activityId", "getActivityId");
|
||||
}
|
||||
if (TextUtils.isEmpty(snapshot.totalAmount)) {
|
||||
try {
|
||||
Object money = XposedHelpers.callMethod(summary, "getTotalAmount");
|
||||
if (money != null) {
|
||||
snapshot.totalAmount = normalizeMoneyText(String.valueOf(
|
||||
XposedHelpers.callMethod(money, "getAmount")));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.applyCachedIssueTime();
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " enrich times failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void cacheIssueTimesFromHistory(Object historyResult) {
|
||||
if (historyResult == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (String[] job : extractHistoryJobs(historyResult)) {
|
||||
if (job.length > 2 && looksLikeActivityId(job[0]) && !TextUtils.isEmpty(job[2])) {
|
||||
synchronized (ACTIVITY_ISSUE_TIME) {
|
||||
ACTIVITY_ISSUE_TIME.put(job[0], job[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
XposedBridge.log(TAG + " cached issue times size=" + ACTIVITY_ISSUE_TIME.size());
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " cache issue times failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void dedupeClaims(MmpSnapshot snapshot) {
|
||||
if (snapshot == null || snapshot.claims.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
java.util.LinkedHashMap<String, ClaimLine> map = new java.util.LinkedHashMap<>();
|
||||
for (ClaimLine line : snapshot.claims) {
|
||||
if (line == null || TextUtils.isEmpty(line.nickname)) {
|
||||
continue;
|
||||
}
|
||||
ClaimLine old = map.get(line.nickname);
|
||||
if (old == null) {
|
||||
map.put(line.nickname, line);
|
||||
continue;
|
||||
}
|
||||
// 同昵称保留金额更大的;时间有则补上
|
||||
try {
|
||||
double a = Double.parseDouble(old.amount);
|
||||
double b = Double.parseDouble(line.amount);
|
||||
if (b > a) {
|
||||
if (TextUtils.isEmpty(line.claimTime)) {
|
||||
line.claimTime = old.claimTime;
|
||||
}
|
||||
map.put(line.nickname, line);
|
||||
} else if (TextUtils.isEmpty(old.claimTime) && !TextUtils.isEmpty(line.claimTime)) {
|
||||
old.claimTime = line.claimTime;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
if (TextUtils.isEmpty(old.claimTime) && !TextUtils.isEmpty(line.claimTime)) {
|
||||
old.claimTime = line.claimTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.claims.clear();
|
||||
snapshot.claims.addAll(map.values());
|
||||
}
|
||||
|
||||
private static MmpSnapshot parseSnapshot(String json) {
|
||||
@@ -905,6 +1567,8 @@ public final class TngMoneyPacketHook {
|
||||
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName", "creatorName");
|
||||
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "packetTotalAmount",
|
||||
"totalPacketAmount", "originalAmount");
|
||||
putIfPresent(obj, snapshot, "createTime", "createdTime", "gmtCreate", "issueTime",
|
||||
"issuedTime", "sendTime", "packetCreateTime");
|
||||
if (TextUtils.isEmpty(snapshot.title)) {
|
||||
String merchant = firstNonEmpty(
|
||||
obj.optString("merchantName", null),
|
||||
@@ -925,7 +1589,13 @@ public final class TngMoneyPacketHook {
|
||||
continue;
|
||||
}
|
||||
String keyLower = key.toLowerCase(Locale.US);
|
||||
if (keyLower.equals("activityid") || keyLower.contains("packet")
|
||||
if (keyLower.contains("createtime") || keyLower.contains("created")
|
||||
|| keyLower.contains("gmtcreate") || keyLower.contains("issue")
|
||||
|| keyLower.contains("sendtime")) {
|
||||
if (TextUtils.isEmpty(snapshot.issueTime) && !looksLikeMoneyJson(val)) {
|
||||
snapshot.issueTime = val;
|
||||
}
|
||||
} else if (keyLower.equals("activityid") || keyLower.contains("packet")
|
||||
|| keyLower.contains("mmp") || keyLower.contains("order")
|
||||
|| keyLower.contains("biz") || keyLower.equals("packetcode")) {
|
||||
// 已有 UUID 形态的 activityId 时不覆盖
|
||||
@@ -1003,7 +1673,10 @@ public final class TngMoneyPacketHook {
|
||||
readFieldText(obj, "claimedTime"),
|
||||
readFieldText(obj, "receiveTime"),
|
||||
readFieldText(obj, "gmtClaim"),
|
||||
readFieldText(obj, "gmtModified"),
|
||||
readFieldText(obj, "gmtModify"),
|
||||
readFieldText(obj, "gmtCreate"),
|
||||
readFieldText(obj, "createTime"),
|
||||
readFieldText(obj, "time"));
|
||||
return line;
|
||||
}
|
||||
@@ -1287,6 +1960,8 @@ public final class TngMoneyPacketHook {
|
||||
String title;
|
||||
String senderName;
|
||||
String totalAmount;
|
||||
/** 发放时间(历史 createTime 或详情里的 createTime) */
|
||||
String issueTime;
|
||||
final List<ClaimLine> claims = new ArrayList<>();
|
||||
|
||||
String dedupKey() {
|
||||
@@ -1304,7 +1979,31 @@ public final class TngMoneyPacketHook {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
void applyCachedIssueTime() {
|
||||
if (!TextUtils.isEmpty(issueTime) || TextUtils.isEmpty(packetId)) {
|
||||
return;
|
||||
}
|
||||
synchronized (ACTIVITY_ISSUE_TIME) {
|
||||
String cached = ACTIVITY_ISSUE_TIME.get(packetId);
|
||||
if (!TextUtils.isEmpty(cached)) {
|
||||
issueTime = cached;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String formatForForward(String channel, String sourceHint) {
|
||||
applyCachedIssueTime();
|
||||
// 详情无发放时间时,用最早领取时间兜底(仍优于拉取时间)
|
||||
if (TextUtils.isEmpty(issueTime)) {
|
||||
for (ClaimLine line : claims) {
|
||||
if (!TextUtils.isEmpty(line.claimTime)) {
|
||||
if (TextUtils.isEmpty(issueTime)
|
||||
|| line.claimTime.compareTo(issueTime) < 0) {
|
||||
issueTime = line.claimTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[MMP统计]");
|
||||
if (!TextUtils.isEmpty(packetId)) {
|
||||
@@ -1319,6 +2018,9 @@ public final class TngMoneyPacketHook {
|
||||
if (!TextUtils.isEmpty(totalAmount)) {
|
||||
sb.append(" | total=").append(sanitizeMeta(totalAmount));
|
||||
}
|
||||
if (!TextUtils.isEmpty(issueTime)) {
|
||||
sb.append(" | issued=").append(sanitizeMeta(issueTime));
|
||||
}
|
||||
sb.append(" | via=").append(sanitizeMeta(channel));
|
||||
if (!TextUtils.isEmpty(sourceHint)) {
|
||||
String src = sourceHint.length() > 120
|
||||
|
||||
Reference in New Issue
Block a user