feat(mmp): 领取人ID/模块版本检测,并修正状态条误报红

展示 receiverId 与 activityPoolId;检测 Hook 是否最新;按真实时间排序;有心跳与定时拉取时不再因久无新数据爆红。
This commit is contained in:
mars
2026-08-05 15:09:51 +08:00
parent a4aed01b29
commit 293979122d
24 changed files with 1641 additions and 186 deletions

View File

@@ -124,6 +124,7 @@
android:exported="true">
<intent-filter>
<action android:name="com.miraclegarden.smsmessage.action.HOOK_MESSAGE" />
<action android:name="com.miraclegarden.smsmessage.action.HOOK_STATUS" />
</intent-filter>
</receiver>

View File

@@ -21,10 +21,14 @@ import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.comm.CommonAdapter;
import com.miraclegarden.smsmessage.comm.ViewHolder;
import com.miraclegarden.smsmessage.databinding.ActivityMmpClaimBinding;
import com.miraclegarden.smsmessage.mmp.MmpHookStatus;
import com.miraclegarden.smsmessage.mmp.MmpLocalSettings;
import com.miraclegarden.smsmessage.mmp.MmpModuleInfo;
import com.miraclegarden.smsmessage.mmp.MmpPacket;
import com.miraclegarden.smsmessage.mmp.MmpPacketStore;
import com.miraclegarden.smsmessage.mmp.MmpSettingsClient;
import com.miraclegarden.smsmessage.mmp.MmpSyncClient;
import com.miraclegarden.smsmessage.mmp.TngProcessHelper;
import org.json.JSONObject;
@@ -52,10 +56,20 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
private long customToMs;
private String keyword = "";
private boolean settingsOpen;
private boolean autoWatchWhileOpen = true;
private boolean autoLaunchTngIfKilled = true;
private long lastTngLaunchAt;
private final Runnable watchRunnable = this::watchTngOnce;
/** null=检测中 true=通 false=不通 */
private Boolean debugServerOk;
private long lastDebugPingAt;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JSONObject local = MmpLocalSettings.load(this);
autoWatchWhileOpen = local.optBoolean("autoWatchWhileOpen", true);
autoLaunchTngIfKilled = local.optBoolean("autoLaunchTngIfKilled", true);
initView();
reload();
loadSettingsQuiet();
@@ -69,6 +83,9 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
MmpSyncClient.syncIfStale(this, 5000L);
handler.removeCallbacks(refreshRunnable);
handler.postDelayed(refreshRunnable, 2000);
scheduleWatch();
updateConnStatus();
pingDebugServer();
}
@Override
@@ -76,11 +93,15 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
super.onPause();
MmpPacketStore.removeListener(this);
handler.removeCallbacks(refreshRunnable);
handler.removeCallbacks(watchRunnable);
}
@Override
public void onMmpPacketsChanged() {
runOnUiThread(this::reload);
runOnUiThread(() -> {
reload();
updateConnStatus();
});
}
private void initView() {
@@ -94,6 +115,7 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
binding.settingsPanel.setVisibility(View.GONE);
});
binding.btnSync.setOnClickListener(v -> syncNow());
binding.btnRefresh.setOnClickListener(v -> manualRefresh());
binding.chipAll.setOnClickListener(v -> setStatusFilter(0));
binding.chipOpen.setOnClickListener(v -> setStatusFilter(1));
@@ -183,6 +205,7 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
allPackets.clear();
allPackets.addAll(MmpPacketStore.getPackets(this));
applyFilter();
updateConnStatus();
handler.removeCallbacks(refreshRunnable);
handler.postDelayed(refreshRunnable, 2000);
}
@@ -274,6 +297,145 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
Toast.LENGTH_SHORT).show()));
}
private void manualRefresh() {
reload();
MmpSyncClient.syncIfStale(this, 0L);
boolean alive = TngProcessHelper.isRunning(this);
String tip = "已刷新,当前 " + packets.size() + "/" + allPackets.size() + "";
if (!alive) {
tip += "TNG 未运行";
if (autoLaunchTngIfKilled) {
if (TngProcessHelper.launch(this)) {
lastTngLaunchAt = System.currentTimeMillis();
tip += ",已尝试打开";
} else {
tip += ",打开失败";
}
}
} else {
tip += "TNG 在跑";
}
Toast.makeText(this, tip, Toast.LENGTH_SHORT).show();
updateConnStatus();
pingDebugServer();
scheduleWatch();
}
private void scheduleWatch() {
handler.removeCallbacks(watchRunnable);
handler.postDelayed(watchRunnable, 3_000L);
}
private void watchTngOnce() {
if (isFinishing()) {
return;
}
boolean alive = TngProcessHelper.isRunning(this);
if (autoWatchWhileOpen && !alive && autoLaunchTngIfKilled) {
long now = System.currentTimeMillis();
if (now - lastTngLaunchAt > 60_000L) {
if (TngProcessHelper.launch(this)) {
lastTngLaunchAt = now;
Toast.makeText(this, "检测到 TNG 已退出,正在重新打开…", Toast.LENGTH_SHORT).show();
}
}
}
updateConnStatus();
if (System.currentTimeMillis() - lastDebugPingAt > 20_000L) {
pingDebugServer();
}
handler.removeCallbacks(watchRunnable);
handler.postDelayed(watchRunnable, 12_000L);
}
private void pingDebugServer() {
lastDebugPingAt = System.currentTimeMillis();
MmpSettingsClient.pingHealth(ok -> runOnUiThread(() -> {
debugServerOk = ok;
updateConnStatus();
}));
}
private void updateConnStatus() {
if (binding.tvConnStatus == null) {
return;
}
boolean tngAlive = TngProcessHelper.isRunning(this);
long lastIngest = MmpPacketStore.lastIngestAt(this);
long ageMs = lastIngest > 0 ? System.currentTimeMillis() - lastIngest : -1L;
long hookAge = MmpHookStatus.lastAliveAt(this) > 0
? System.currentTimeMillis() - MmpHookStatus.lastAliveAt(this) : -1L;
// 有近期心跳 = Hook 活着(定时拉取会报活);久无入库不等于故障
boolean hookAlive = hookAge >= 0 && hookAge <= 3 * 60_000L;
boolean modLatest = MmpHookStatus.liveVersionCode(this) >= 0
&& MmpHookStatus.isLiveLatest(this);
boolean modKnownOld = hookAlive
&& MmpHookStatus.liveVersionCode(this) > 0
&& MmpHookStatus.liveVersionCode(this) < MmpModuleInfo.EXPECTED_VERSION_CODE;
String tngPart = tngAlive ? "TNG运行中" : "TNG未运行";
String hookPart;
if (!tngAlive) {
hookPart = "Hook未连接TNG 未开)";
} else if (hookAlive && ageMs >= 0 && ageMs <= 120_000L) {
hookPart = "Hook正常" + formatAge(ageMs) + "前有数据)";
} else if (hookAlive) {
if (ageMs >= 0) {
hookPart = "Hook正常定时拉取中上次数据 " + formatAge(ageMs) + "前)";
} else {
hookPart = "Hook正常定时拉取中尚无入库";
}
} else if (ageMs >= 0 && ageMs <= 10 * 60_000L) {
hookPart = "Hook待确认" + formatAge(ageMs) + "前有数据,心跳偏旧)";
} else if (ageMs < 0) {
hookPart = "Hook待确认无心跳尚无入库";
} else {
hookPart = "Hook异常无心跳" + formatAge(ageMs) + "前曾有数据)";
}
String watchPart = autoWatchWhileOpen
? (autoLaunchTngIfKilled ? "检测:开(杀掉会拉起)" : "检测:开(不自动拉起)")
: "检测:关";
String debugPart;
if (debugServerOk == null) {
debugPart = "调试台:检测中";
} else if (debugServerOk) {
debugPart = "调试台:已连接";
} else {
debugPart = "调试台:未连接";
}
String text = tngPart + " · " + hookPart + "\n"
+ watchPart + " · " + debugPart + "\n"
+ MmpHookStatus.describe(this);
binding.tvConnStatus.setText(text);
// 红TNG 挂了 / 无心跳 / 模块明确是旧版;久无新红包数据不爆红
boolean okAll = tngAlive && hookAlive && modLatest;
boolean bad = !tngAlive || !hookAlive || modKnownOld;
if (okAll) {
binding.tvConnStatus.setBackgroundColor(Color.parseColor("#064E3B"));
binding.tvConnStatus.setTextColor(Color.parseColor("#D1FAE5"));
} else if (bad) {
binding.tvConnStatus.setBackgroundColor(Color.parseColor("#7F1D1D"));
binding.tvConnStatus.setTextColor(Color.parseColor("#FEE2E2"));
} else {
binding.tvConnStatus.setBackgroundColor(Color.parseColor("#78350F"));
binding.tvConnStatus.setTextColor(Color.parseColor("#FEF3C7"));
}
}
private static String formatAge(long ageMs) {
if (ageMs < 60_000L) {
return Math.max(1, ageMs / 1000) + "";
}
if (ageMs < 3600_000L) {
return (ageMs / 60_000L) + "分钟";
}
return (ageMs / 3600_000L) + "小时";
}
private void toggleSettings() {
settingsOpen = !settingsOpen;
binding.settingsPanel.setVisibility(settingsOpen ? View.VISIBLE : View.GONE);
@@ -283,12 +445,17 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
}
private void loadSettingsQuiet() {
MmpSettingsClient.load((json, error) -> {
MmpSettingsClient.load(this, (json, error) -> {
if (json != null) {
fillSettingsForm(json);
binding.tvSettingsStatus.setText("已同步调试台设置");
} else if (error != null) {
binding.tvSettingsStatus.setText("设置未连上:" + error);
if (error != null && !error.isEmpty()) {
binding.tvSettingsStatus.setText(error);
} else {
binding.tvSettingsStatus.setText("设置已加载(本地优先,可离线保存)");
}
} else {
fillSettingsForm(MmpLocalSettings.load(this));
binding.tvSettingsStatus.setText(error != null ? error : "已用本地默认设置");
}
});
}
@@ -299,33 +466,33 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
binding.cfgDedup.setText(String.valueOf(json.optInt("dedupSec", 3)));
binding.cfgGap.setText(String.valueOf(json.optInt("detailGapMs", 80)));
binding.cfgOpenHistory.setChecked(json.optBoolean("openHistoryIfNoTemplate", true));
binding.cfgBounceHistory.setChecked(json.optBoolean("autoBounceHistoryOnDisconnect", true));
binding.cfgAutoWatch.setChecked(json.optBoolean("autoWatchWhileOpen", true));
binding.cfgAutoLaunch.setChecked(json.optBoolean("autoLaunchTngIfKilled", true));
autoWatchWhileOpen = binding.cfgAutoWatch.isChecked();
autoLaunchTngIfKilled = binding.cfgAutoLaunch.isChecked();
}
private void applyPreset(String name) {
try {
JSONObject o = new JSONObject();
JSONObject o = MmpLocalSettings.defaults();
if ("fast".equals(name)) {
o.put("historyCooldownSec", 5);
o.put("detailCooldownSec", 5);
o.put("dedupSec", 2);
o.put("detailGapMs", 50);
o.put("pagePollMs", 800);
o.put("openHistoryIfNoTemplate", true);
} else if ("slow".equals(name)) {
o.put("historyCooldownSec", 30);
o.put("detailCooldownSec", 45);
o.put("dedupSec", 15);
o.put("detailGapMs", 200);
o.put("pagePollMs", 3000);
o.put("openHistoryIfNoTemplate", true);
} else {
o.put("historyCooldownSec", 8);
o.put("detailCooldownSec", 8);
o.put("dedupSec", 3);
o.put("detailGapMs", 80);
o.put("pagePollMs", 1000);
o.put("openHistoryIfNoTemplate", true);
}
o.put("openHistoryIfNoTemplate", true);
o.put("autoBounceHistoryOnDisconnect", true);
o.put("autoWatchWhileOpen", true);
o.put("autoLaunchTngIfKilled", true);
fillSettingsForm(o);
binding.tvSettingsStatus.setText("已套用预设,点保存生效");
} catch (Exception e) {
@@ -342,15 +509,22 @@ public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBind
body.put("detailGapMs", parseIntSafe(binding.cfgGap.getText().toString(), 80));
body.put("pagePollMs", 1000);
body.put("openHistoryIfNoTemplate", binding.cfgOpenHistory.isChecked());
body.put("autoBounceHistoryOnDisconnect", binding.cfgBounceHistory.isChecked());
body.put("autoWatchWhileOpen", binding.cfgAutoWatch.isChecked());
body.put("autoLaunchTngIfKilled", binding.cfgAutoLaunch.isChecked());
autoWatchWhileOpen = binding.cfgAutoWatch.isChecked();
autoLaunchTngIfKilled = binding.cfgAutoLaunch.isChecked();
binding.tvSettingsStatus.setText("保存中…");
MmpSettingsClient.save(body, (json, error) -> {
MmpSettingsClient.save(this, body, (json, error) -> {
if (json != null) {
fillSettingsForm(json);
binding.tvSettingsStatus.setText("已保存Hook 约 10 秒内生效");
String msg = error != null ? error : "已保存Hook 参数约 10 秒内生效(需调试台)";
binding.tvSettingsStatus.setText(msg);
Toast.makeText(this, "设置已保存", Toast.LENGTH_SHORT).show();
scheduleWatch();
} else {
binding.tvSettingsStatus.setText("保存失败:" + error);
Toast.makeText(this, "保存失败,请确认调试台已开", Toast.LENGTH_SHORT).show();
Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {

View File

@@ -79,6 +79,22 @@ public class MmpDetailActivity extends MiracleGardenActivity<ActivityMmpDetailBi
public void convert(ViewHolder holder, MmpClaim claim, int index) {
holder.setText(R.id.tv_rank, String.valueOf(claim.rank > 0 ? claim.rank : index + 1));
holder.setText(R.id.tv_nickname, claim.nickname != null ? claim.nickname : "?");
TextView userIdView = holder.getView(R.id.tv_user_id);
if (!TextUtils.isEmpty(claim.userId)) {
userIdView.setVisibility(View.VISIBLE);
userIdView.setText("用户ID " + claim.userId);
} else {
userIdView.setVisibility(View.GONE);
userIdView.setText("");
}
TextView poolIdView = holder.getView(R.id.tv_pool_id);
if (!TextUtils.isEmpty(claim.poolId)) {
poolIdView.setVisibility(View.VISIBLE);
poolIdView.setText("领取ID " + claim.poolId);
} else {
poolIdView.setVisibility(View.GONE);
poolIdView.setText("");
}
holder.setText(R.id.tv_amount, claim.amountText != null
? claim.amountText
: String.format(Locale.US, "%.2f", claim.amount));

View File

@@ -5,6 +5,10 @@ package com.miraclegarden.smsmessage.mmp;
*/
public class MmpClaim {
public String nickname;
/** TNG receiverId重名时用来区分 */
public String userId;
/** activityPoolId单次领取池条目 */
public String poolId;
public String amountText;
public double amount;
public String claimTime;
@@ -19,4 +23,11 @@ public class MmpClaim {
this.amountText = String.format(java.util.Locale.US, "%.2f", amount);
this.claimTime = claimTime != null ? claimTime : "";
}
public String shortUserId() {
if (userId == null || userId.isEmpty()) {
return "";
}
return userId.length() <= 10 ? userId : userId.substring(0, 8) + "";
}
}

View File

@@ -0,0 +1,136 @@
package com.miraclegarden.smsmessage.mmp;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
/**
* 记录 TNG 进程内 Hook 心跳,用于判断是否挂上最新模块。
*/
public final class MmpHookStatus {
private static final String PREF = "mmp_hook_status";
private static volatile int sLiveCode;
private static volatile String sLiveName = "";
private static volatile long sLiveAt;
private static volatile String sHostPkg = "";
private MmpHookStatus() {
}
public static void noteAlive(Context context, int versionCode, String versionName,
String hostPackage) {
sLiveCode = versionCode;
sLiveName = versionName != null ? versionName : "";
sLiveAt = System.currentTimeMillis();
sHostPkg = hostPackage != null ? hostPackage : "";
if (context == null) {
return;
}
prefs(context).edit()
.putInt("live_code", sLiveCode)
.putString("live_name", sLiveName)
.putLong("live_at", sLiveAt)
.putString("host_pkg", sHostPkg)
.apply();
}
public static void ensureLoaded(Context context) {
if (sLiveAt > 0 || context == null) {
return;
}
SharedPreferences sp = prefs(context);
sLiveCode = sp.getInt("live_code", 0);
sLiveName = sp.getString("live_name", "");
sLiveAt = sp.getLong("live_at", 0L);
sHostPkg = sp.getString("host_pkg", "");
}
public static long lastAliveAt(Context context) {
ensureLoaded(context);
return sLiveAt;
}
public static int liveVersionCode(Context context) {
ensureLoaded(context);
return sLiveCode;
}
public static String liveVersionName(Context context) {
ensureLoaded(context);
return sLiveName != null ? sLiveName : "";
}
public static int installedVersionCode(Context context) {
if (context == null) {
return 0;
}
try {
PackageInfo pi = context.getPackageManager()
.getPackageInfo(MmpModuleInfo.XPOSED_PACKAGE, 0);
return pi != null ? pi.versionCode : 0;
} catch (PackageManager.NameNotFoundException e) {
return 0;
}
}
public static String installedVersionName(Context context) {
if (context == null) {
return "";
}
try {
PackageInfo pi = context.getPackageManager()
.getPackageInfo(MmpModuleInfo.XPOSED_PACKAGE, 0);
return pi != null && pi.versionName != null ? pi.versionName : "";
} catch (PackageManager.NameNotFoundException e) {
return "";
}
}
/**
* @return 人类可读模块状态
*/
public static String describe(Context context) {
ensureLoaded(context);
int installed = installedVersionCode(context);
String installedName = installedVersionName(context);
if (installed <= 0) {
return "模块APK未安装";
}
String apkPart = "模块APK" + (TextUtils.isEmpty(installedName)
? String.valueOf(installed) : installedName);
boolean apkLatest = installed >= MmpModuleInfo.EXPECTED_VERSION_CODE;
if (!apkLatest) {
apkPart += "(旧,期望" + MmpModuleInfo.EXPECTED_VERSION_NAME + ")";
}
long age = sLiveAt > 0 ? System.currentTimeMillis() - sLiveAt : -1L;
String livePart;
if (age < 0) {
livePart = "运行中:未检测到(请强停再开 TNG";
} else if (age > 3 * 60_000L) {
livePart = "运行中:心跳偏旧(" + (age / 60_000L) + "分钟前 "
+ (TextUtils.isEmpty(sLiveName) ? sLiveCode : sLiveName) + ")";
} else if (sLiveCode >= MmpModuleInfo.EXPECTED_VERSION_CODE) {
livePart = "运行中:最新(" + (TextUtils.isEmpty(sLiveName) ? sLiveCode : sLiveName) + ")";
} else if (sLiveCode > 0) {
livePart = "运行中:旧版(" + (TextUtils.isEmpty(sLiveName) ? sLiveCode : sLiveName)
+ ",期望" + MmpModuleInfo.EXPECTED_VERSION_NAME + ")";
} else {
livePart = "运行中:未知";
}
return apkPart + " · " + livePart;
}
public static boolean isLiveLatest(Context context) {
ensureLoaded(context);
long age = sLiveAt > 0 ? System.currentTimeMillis() - sLiveAt : -1L;
return age >= 0 && age <= 3 * 60_000L && sLiveCode >= MmpModuleInfo.EXPECTED_VERSION_CODE;
}
private static SharedPreferences prefs(Context context) {
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
}
}

View File

@@ -0,0 +1,85 @@
package com.miraclegarden.smsmessage.mmp;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import org.json.JSONObject;
/**
* 领取台本地设置(不依赖电脑调试台也能读写)。
*/
public final class MmpLocalSettings {
private static final String PREF = "mmp_settings_local";
private MmpLocalSettings() {
}
public static JSONObject defaults() {
JSONObject o = new JSONObject();
try {
o.put("historyCooldownSec", 8);
o.put("detailCooldownSec", 8);
o.put("dedupSec", 3);
o.put("detailGapMs", 80);
o.put("pagePollMs", 1000);
o.put("openHistoryIfNoTemplate", true);
o.put("autoBounceHistoryOnDisconnect", true);
o.put("autoLaunchTngIfKilled", true);
o.put("autoWatchWhileOpen", true);
} catch (Exception ignored) {
}
return o;
}
public static JSONObject load(Context context) {
JSONObject out = defaults();
if (context == null) {
return out;
}
SharedPreferences sp = prefs(context);
try {
String raw = sp.getString("json", null);
if (!TextUtils.isEmpty(raw)) {
JSONObject saved = new JSONObject(raw);
mergeInto(out, saved);
}
} catch (Exception ignored) {
}
return out;
}
public static void save(Context context, JSONObject json) {
if (context == null || json == null) {
return;
}
JSONObject merged = defaults();
mergeInto(merged, json);
prefs(context).edit().putString("json", merged.toString()).apply();
}
public static void mergeInto(JSONObject target, JSONObject src) {
if (target == null || src == null) {
return;
}
String[] keys = {
"historyCooldownSec", "detailCooldownSec", "dedupSec", "detailGapMs", "pagePollMs",
"openHistoryIfNoTemplate", "autoBounceHistoryOnDisconnect",
"autoLaunchTngIfKilled", "autoWatchWhileOpen"
};
for (String k : keys) {
if (!src.has(k)) {
continue;
}
try {
target.put(k, src.get(k));
} catch (Exception ignored) {
}
}
}
private static SharedPreferences prefs(Context context) {
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
}
}

View File

@@ -0,0 +1,13 @@
package com.miraclegarden.smsmessage.mmp;
/**
* Xposed 模块版本约定(与 xposed-module/build.gradle 同步)。
*/
public final class MmpModuleInfo {
public static final String XPOSED_PACKAGE = "com.miraclegarden.smsmessage.xposed";
public static final int EXPECTED_VERSION_CODE = 3;
public static final String EXPECTED_VERSION_NAME = "1.2.0";
private MmpModuleInfo() {
}
}

View File

@@ -123,7 +123,9 @@ public class MmpPacket {
}
if (leaderboard != null) {
for (MmpClaim c : leaderboard) {
if (c != null && containsIgnoreCase(c.nickname, q)) {
if (c != null && (containsIgnoreCase(c.nickname, q)
|| containsIgnoreCase(c.userId, q)
|| containsIgnoreCase(c.poolId, q))) {
return true;
}
}

View File

@@ -28,6 +28,7 @@ public final class MmpPacketStore {
private static final String TAG = "MmpPacketStore";
private static final String PREF = "mmp_packets";
private static final String KEY_JSON = "packets_json";
private static final String KEY_LAST_INGEST = "last_ingest_at";
private static final int MAX_PACKETS = 200;
private static final Pattern AMOUNT_JSON = Pattern.compile(
@@ -39,6 +40,7 @@ public final class MmpPacketStore {
}
private static final CopyOnWriteArrayList<Listener> LISTENERS = new CopyOnWriteArrayList<>();
private static volatile long sLastIngestAt;
private MmpPacketStore() {
}
@@ -66,15 +68,20 @@ public final class MmpPacketStore {
return false;
}
ParseResult parsed = parse(content);
if (parsed.claims.isEmpty()) {
return false;
}
noteModuleFromMeta(context, parsed.meta);
String packetId = parsed.meta.get("packet");
if (TextUtils.isEmpty(packetId)) {
packetId = parsed.meta.get("packetId");
}
if (TextUtils.isEmpty(packetId) || "unknown".equals(packetId)
|| "TNG 红包".equals(packetId) || "TNG Money Packet".equals(packetId)) {
boolean hasRealPacketId = !TextUtils.isEmpty(packetId)
&& !"unknown".equals(packetId)
&& !"TNG 红包".equals(packetId)
&& !"TNG Money Packet".equals(packetId);
// 允许无人领取的新红包入库(只有 packet/metaclaims 为空)
if (parsed.claims.isEmpty() && !hasRealPacketId) {
return false;
}
if (!hasRealPacketId) {
packetId = "红包-" + claimsFingerprint(parsed.claims);
if (packetId.length() > 56) {
packetId = packetId.substring(0, 56);
@@ -138,6 +145,11 @@ public final class MmpPacketStore {
saveMap(context, map);
}
notifyListeners();
sLastIngestAt = System.currentTimeMillis();
try {
prefs(context).edit().putLong(KEY_LAST_INGEST, sLastIngestAt).apply();
} catch (Exception ignored) {
}
Log.i(TAG, "ingest packet=" + packetId + " claims=" + ranked.size());
try {
MmpSyncClient.syncIfStale(context.getApplicationContext(), 3000L);
@@ -147,21 +159,55 @@ public final class MmpPacketStore {
return true;
}
/** Hook 最近一次成功写入本地的时间戳0 表示尚无 */
public static long lastIngestAt(Context context) {
if (sLastIngestAt > 0) {
return sLastIngestAt;
}
if (context == null) {
return 0L;
}
try {
long v = prefs(context).getLong(KEY_LAST_INGEST, 0L);
if (v > 0) {
sLastIngestAt = v;
}
return v;
} catch (Exception e) {
return 0L;
}
}
public static List<MmpPacket> getPackets(Context context) {
synchronized (MmpPacketStore.class) {
List<MmpPacket> list = new ArrayList<>(loadMap(context).values());
Collections.sort(list, new Comparator<MmpPacket>() {
@Override
public int compare(MmpPacket a, MmpPacket b) {
String ta = !TextUtils.isEmpty(a.issuedAt) ? a.issuedAt : nullToEmpty(a.updatedAt);
String tb = !TextUtils.isEmpty(b.issuedAt) ? b.issuedAt : nullToEmpty(b.updatedAt);
return tb.compareTo(ta);
// DD/MM/YYYY 不能按字符串比;按真实时间降序,最新在前
long ta = sortTimeMs(a);
long tb = sortTimeMs(b);
if (tb != ta) {
return Long.compare(tb, ta);
}
return nullToEmpty(b.packetId).compareTo(nullToEmpty(a.packetId));
}
});
return list;
}
}
private static long sortTimeMs(MmpPacket p) {
if (p == null) {
return 0L;
}
long ms = MmpPacket.parseIssueMs(p.issuedAt);
if (ms <= 0) {
ms = MmpPacket.parseIssueMs(p.updatedAt);
}
return ms;
}
public static MmpPacket getPacket(Context context, String packetId) {
if (TextUtils.isEmpty(packetId)) {
return null;
@@ -195,6 +241,30 @@ public final class MmpPacketStore {
// --- parse ---
/** 从 header 的 mod=1.2.0/3 更新 Hook 运行版本(兜底) */
private static void noteModuleFromMeta(Context context, Map<String, String> meta) {
if (context == null || meta == null) {
return;
}
String mod = meta.get("mod");
if (TextUtils.isEmpty(mod)) {
return;
}
int code = 0;
String name = mod.trim();
int slash = name.lastIndexOf('/');
if (slash >= 0 && slash + 1 < name.length()) {
String codePart = name.substring(slash + 1).trim();
name = name.substring(0, slash).trim();
try {
code = Integer.parseInt(codePart);
} catch (NumberFormatException ignored) {
code = 0;
}
}
MmpHookStatus.noteAlive(context.getApplicationContext(), code, name, "my.com.tngdigital.ewallet");
}
private static final class ParseResult {
final Map<String, String> meta = new LinkedHashMap<>();
final List<RawClaim> claims = new ArrayList<>();
@@ -202,6 +272,8 @@ public final class MmpPacketStore {
private static final class RawClaim {
String nickname;
String userId;
String poolId;
double amount;
String claimTime;
}
@@ -258,6 +330,34 @@ public final class MmpPacketStore {
int arrow = line.indexOf("->");
String nick = line.substring(0, arrow).trim();
String right = line.substring(arrow + 2).trim();
String userId = "";
String poolId = "";
int ridAt = right.indexOf(" #rid=");
if (ridAt >= 0) {
String rest = right.substring(ridAt + 6).trim();
right = right.substring(0, ridAt).trim();
int sp = rest.indexOf(' ');
if (sp > 0) {
userId = rest.substring(0, sp).trim();
String more = rest.substring(sp + 1).trim();
if (more.startsWith("#pool=")) {
poolId = more.substring(6).trim();
}
} else {
userId = rest;
}
}
int poolAt = right.indexOf(" #pool=");
if (poolAt >= 0 && TextUtils.isEmpty(poolId)) {
poolId = right.substring(poolAt + 7).trim();
right = right.substring(0, poolAt).trim();
}
// 兼容 #rid= 后仍有 #pool=
int poolInId = userId.indexOf("#pool=");
if (poolInId >= 0) {
poolId = userId.substring(poolInId + 6).trim();
userId = userId.substring(0, poolInId).trim();
}
String claimTime = "";
String amountRaw = right;
if (right.endsWith(")") && right.contains("(") && !right.startsWith("{")) {
@@ -266,9 +366,11 @@ public final class MmpPacketStore {
claimTime = right.substring(lp + 1, right.length() - 1).trim();
}
Double amt = normalizeMoney(amountRaw);
if (!TextUtils.isEmpty(nick)) {
if (!TextUtils.isEmpty(nick) || !TextUtils.isEmpty(userId)) {
RawClaim c = new RawClaim();
c.nickname = nick;
c.nickname = TextUtils.isEmpty(nick) ? "?" : nick;
c.userId = userId;
c.poolId = poolId;
c.amount = amt != null ? amt : 0;
c.claimTime = claimTime;
out.claims.add(c);
@@ -329,9 +431,13 @@ public final class MmpPacketStore {
LinkedHashMap<String, MmpClaim> buckets = new LinkedHashMap<>();
for (RawClaim c : claims) {
String nick = c.nickname != null ? c.nickname : "?";
MmpClaim b = buckets.get(nick);
String key = !TextUtils.isEmpty(c.userId) ? ("id:" + c.userId) : ("n:" + nick);
MmpClaim b = buckets.get(key);
if (b == null) {
buckets.put(nick, new MmpClaim(nick, c.amount, c.claimTime));
MmpClaim created = new MmpClaim(nick, c.amount, c.claimTime);
created.userId = c.userId;
created.poolId = c.poolId;
buckets.put(key, created);
continue;
}
if (c.amount >= b.amount) {
@@ -340,8 +446,25 @@ public final class MmpPacketStore {
if (!TextUtils.isEmpty(c.claimTime)) {
b.claimTime = c.claimTime;
}
} else if (TextUtils.isEmpty(b.claimTime) && !TextUtils.isEmpty(c.claimTime)) {
b.claimTime = c.claimTime;
if (!TextUtils.isEmpty(c.userId)) {
b.userId = c.userId;
}
if (!TextUtils.isEmpty(c.poolId)) {
b.poolId = c.poolId;
}
if (!TextUtils.isEmpty(nick) && !"?".equals(nick)) {
b.nickname = nick;
}
} else {
if (TextUtils.isEmpty(b.claimTime) && !TextUtils.isEmpty(c.claimTime)) {
b.claimTime = c.claimTime;
}
if (TextUtils.isEmpty(b.userId) && !TextUtils.isEmpty(c.userId)) {
b.userId = c.userId;
}
if (TextUtils.isEmpty(b.poolId) && !TextUtils.isEmpty(c.poolId)) {
b.poolId = c.poolId;
}
}
}
List<MmpClaim> result = new ArrayList<>(buckets.values());
@@ -360,8 +483,8 @@ public final class MmpPacketStore {
private static String claimsFingerprint(List<RawClaim> claims) {
List<String> rows = new ArrayList<>();
for (RawClaim c : claims) {
rows.add((c.nickname != null ? c.nickname : "?") + "="
+ String.format(Locale.US, "%.2f", c.amount));
String id = !TextUtils.isEmpty(c.userId) ? c.userId : (c.nickname != null ? c.nickname : "?");
rows.add(id + "=" + String.format(Locale.US, "%.2f", c.amount));
}
Collections.sort(rows);
StringBuilder sb = new StringBuilder();
@@ -488,6 +611,8 @@ public final class MmpPacketStore {
for (MmpClaim c : p.leaderboard) {
JSONObject row = new JSONObject();
row.put("nickname", nullToEmpty(c.nickname));
row.put("userId", nullToEmpty(c.userId));
row.put("poolId", nullToEmpty(c.poolId));
row.put("amountText", nullToEmpty(c.amountText));
row.put("amount", c.amount);
row.put("claimTime", nullToEmpty(c.claimTime));
@@ -519,6 +644,8 @@ public final class MmpPacketStore {
JSONObject row = board.getJSONObject(i);
MmpClaim c = new MmpClaim();
c.nickname = row.optString("nickname", "");
c.userId = row.optString("userId", "");
c.poolId = row.optString("poolId", "");
c.amount = row.optDouble("amount", 0);
c.amountText = row.optString("amountText",
String.format(Locale.US, "%.2f", c.amount));

View File

@@ -1,5 +1,6 @@
package com.miraclegarden.smsmessage.mmp;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@@ -19,7 +20,7 @@ import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 从 PC 调试台读写红包拉取设置(与 /mmp 设置同源)
* 领取台设置:本地 SharedPreferences 优先,能连上调试台时再同步 Hook 侧参数
*/
public final class MmpSettingsClient {
@@ -40,8 +41,11 @@ public final class MmpSettingsClient {
private MmpSettingsClient() {
}
public static void load(CallbackJson cb) {
/** 先本地再尝试合并调试台Hook 冷却等) */
public static void load(Context context, CallbackJson cb) {
final Context app = context != null ? context.getApplicationContext() : null;
EXEC.execute(() -> {
JSONObject local = MmpLocalSettings.load(app);
Exception last = null;
for (String base : AppConfig.DEBUG_SERVER_URLS) {
if (base == null || base.isEmpty()) {
@@ -54,8 +58,16 @@ public final class MmpSettingsClient {
.build();
try (Response resp = CLIENT.newCall(req).execute()) {
if (resp.isSuccessful() && resp.body() != null) {
JSONObject obj = new JSONObject(resp.body().string());
MAIN.post(() -> cb.onResult(obj, null));
JSONObject remote = new JSONObject(resp.body().string());
// 远程覆盖 Hook 相关;手机专属开关保留本地
boolean launch = local.optBoolean("autoLaunchTngIfKilled", true);
boolean watch = local.optBoolean("autoWatchWhileOpen", true);
MmpLocalSettings.mergeInto(local, remote);
local.put("autoLaunchTngIfKilled", launch);
local.put("autoWatchWhileOpen", watch);
MmpLocalSettings.save(app, local);
JSONObject out = local;
MAIN.post(() -> cb.onResult(out, null));
return;
}
}
@@ -63,17 +75,22 @@ public final class MmpSettingsClient {
last = e;
}
}
Exception err = last;
MAIN.post(() -> cb.onResult(null,
err != null ? err.getMessage() : "调试台不可达"));
JSONObject out = local;
String tip = last != null
? ("已用本地设置(调试台未连上:" + last.getMessage() + "")
: null;
MAIN.post(() -> cb.onResult(out, tip));
});
}
public static void save(JSONObject body, CallbackJson cb) {
/** 始终写本地;能连调试台则同步 Hook 参数 */
public static void save(Context context, JSONObject body, CallbackJson cb) {
final Context app = context != null ? context.getApplicationContext() : null;
EXEC.execute(() -> {
MmpLocalSettings.save(app, body);
JSONObject saved = MmpLocalSettings.load(app);
Exception last = null;
boolean anyOk = false;
JSONObject lastOk = null;
boolean remoteOk = false;
for (String base : AppConfig.DEBUG_SERVER_URLS) {
if (base == null || base.isEmpty()) {
continue;
@@ -81,37 +98,81 @@ public final class MmpSettingsClient {
try {
Request req = new Request.Builder()
.url(base.replaceAll("/$", "") + "/api/mmp/settings")
.post(RequestBody.create(body.toString(), JSON))
.post(RequestBody.create(saved.toString(), JSON))
.build();
try (Response resp = CLIENT.newCall(req).execute()) {
if (resp.isSuccessful() && resp.body() != null) {
lastOk = new JSONObject(resp.body().string());
anyOk = true;
JSONObject remote = new JSONObject(resp.body().string());
boolean launch = saved.optBoolean("autoLaunchTngIfKilled", true);
boolean watch = saved.optBoolean("autoWatchWhileOpen", true);
MmpLocalSettings.mergeInto(saved, remote);
saved.put("autoLaunchTngIfKilled", launch);
saved.put("autoWatchWhileOpen", watch);
MmpLocalSettings.save(app, saved);
remoteOk = true;
break;
}
}
} catch (Exception e) {
last = e;
}
}
final boolean ok = anyOk;
final JSONObject result = lastOk;
final boolean okRemote = remoteOk;
final JSONObject out = saved;
final Exception err = last;
MAIN.post(() -> {
if (ok) {
cb.onResult(result, null);
if (okRemote) {
cb.onResult(out, null);
} else {
cb.onResult(null, err != null ? err.getMessage() : "保存失败");
// 本地已保存成功
cb.onResult(out, err != null
? ("已保存到手机;调试台未同步:" + err.getMessage())
: "已保存到手机");
}
});
});
}
/** 异步探测,不关心结果时用。 */
public static void pingAsync() {
load((json, error) -> {
load(null, (json, error) -> {
if (error != null) {
Log.w(TAG, "settings ping: " + error);
}
});
}
public interface HealthCallback {
void onResult(boolean ok);
}
/** 探测调试台 /health 是否可达 */
public static void pingHealth(HealthCallback cb) {
EXEC.execute(() -> {
boolean ok = false;
for (String base : AppConfig.DEBUG_SERVER_URLS) {
if (base == null || base.isEmpty()) {
continue;
}
try {
Request req = new Request.Builder()
.url(base.replaceAll("/$", "") + "/health")
.get()
.build();
try (Response resp = CLIENT.newCall(req).execute()) {
if (resp.isSuccessful()) {
ok = true;
break;
}
}
} catch (Exception ignored) {
}
}
boolean result = ok;
MAIN.post(() -> {
if (cb != null) {
cb.onResult(result);
}
});
});
}
}

View File

@@ -68,6 +68,14 @@ public final class MmpSyncClient {
body.put("packets", packets);
body.put("device", "android");
body.put("syncedAt", System.currentTimeMillis());
JSONObject hook = new JSONObject();
MmpHookStatus.ensureLoaded(app);
hook.put("liveCode", MmpHookStatus.liveVersionCode(app));
hook.put("liveName", MmpHookStatus.liveVersionName(app));
hook.put("liveAt", MmpHookStatus.lastAliveAt(app));
hook.put("installedCode", MmpHookStatus.installedVersionCode(app));
hook.put("installedName", MmpHookStatus.installedVersionName(app));
body.put("hookStatus", hook);
String payload = body.toString();
boolean anyOk = false;

View File

@@ -0,0 +1,114 @@
package com.miraclegarden.smsmessage.mmp;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 检测 / 拉起 TNG供领取台「自动检测」「杀掉后自动打开」使用。
*/
public final class TngProcessHelper {
private static final String TAG = "TngProcessHelper";
public static final String TNG_PACKAGE = "my.com.tngdigital.ewallet";
private TngProcessHelper() {
}
public static boolean isRunning(Context context) {
if (context == null) {
return false;
}
if (pidofAlive()) {
return true;
}
try {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return false;
}
List<ActivityManager.RunningAppProcessInfo> list = am.getRunningAppProcesses();
if (list == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info : list) {
if (info == null || TextUtils.isEmpty(info.processName)) {
continue;
}
if (info.processName.equals(TNG_PACKAGE)
|| info.processName.startsWith(TNG_PACKAGE + ":")) {
return true;
}
}
} catch (Exception e) {
Log.w(TAG, "running check fail", e);
}
return false;
}
private static boolean pidofAlive() {
String out = shell("pidof " + TNG_PACKAGE);
if (TextUtils.isEmpty(out)) {
out = shell("su -c pidof " + TNG_PACKAGE);
}
return !TextUtils.isEmpty(out) && out.trim().matches(".*\\d.*");
}
private static String shell(String cmd) {
Process p = null;
try {
p = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
if (!p.waitFor(2, TimeUnit.SECONDS)) {
p.destroy();
return "";
}
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append('\n');
}
}
return sb.toString().trim();
} catch (Exception e) {
return "";
} finally {
if (p != null) {
p.destroy();
}
}
}
/** @return true 已发出打开 Intent */
public static boolean launch(Context context) {
if (context == null) {
return false;
}
try {
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(TNG_PACKAGE);
if (intent == null) {
Log.w(TAG, "no launch intent for TNG");
return false;
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
context.startActivity(intent);
Log.i(TAG, "launched TNG");
return true;
} catch (Exception e) {
Log.e(TAG, "launch TNG fail", e);
return false;
}
}
}

View File

@@ -10,6 +10,7 @@ import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppConfig;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.mmp.MmpHookStatus;
import com.miraclegarden.smsmessage.mmp.MmpPacketStore;
import com.miraclegarden.smsmessage.network.DebugForwarder;
@@ -22,15 +23,30 @@ public class HookMessageReceiver extends BroadcastReceiver {
private static final String TAG = "HookMessageReceiver";
public static final String ACTION_HOOK_MESSAGE = "com.miraclegarden.smsmessage.action.HOOK_MESSAGE";
public static final String ACTION_HOOK_STATUS = "com.miraclegarden.smsmessage.action.HOOK_STATUS";
public static final String EXTRA_PACKAGE_NAME = "packageName";
public static final String EXTRA_TITLE = "title";
public static final String EXTRA_CONTENT = "content";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_SOURCE = "source";
public static final String EXTRA_MODULE_VERSION_CODE = "moduleVersionCode";
public static final String EXTRA_MODULE_VERSION_NAME = "moduleVersionName";
public static final String EXTRA_HOST_PACKAGE = "hostPackage";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
if (intent == null || intent.getAction() == null) {
return;
}
if (ACTION_HOOK_STATUS.equals(intent.getAction())) {
int code = intent.getIntExtra(EXTRA_MODULE_VERSION_CODE, 0);
String name = intent.getStringExtra(EXTRA_MODULE_VERSION_NAME);
String host = intent.getStringExtra(EXTRA_HOST_PACKAGE);
MmpHookStatus.noteAlive(context.getApplicationContext(), code, name, host);
Log.i(TAG, "hook status host=" + host + " mod=" + name + "/" + code);
return;
}
if (!ACTION_HOOK_MESSAGE.equals(intent.getAction())) {
return;
}

View File

@@ -11,76 +11,123 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_height="wrap_content"
android:background="#111827"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="8dp">
android:orientation="vertical"
android:paddingBottom="4dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="返回"
android:src="@drawable/ic_action_back" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:text="红包领取台"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="返回"
android:src="@drawable/ic_action_back" />
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="0 个"
android:textColor="#FDBA74"
android:textSize="13sp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="红包领取台"
android:textColor="#FFFFFF"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btn_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="同步电脑"
android:textColor="#FFFFFF"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:maxLines="1"
android:text="0 个"
android:textColor="#FDBA74"
android:textSize="13sp" />
</LinearLayout>
<TextView
android:id="@+id/btn_help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="说明"
android:textColor="#FDBA74"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/btn_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="设置"
android:textColor="#FFFFFF"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_refresh"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="刷新"
android:textColor="#FFFFFF"
android:textSize="13sp" />
<TextView
android:id="@+id/btn_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="清空"
android:textColor="#FECACA"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_sync"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="同步"
android:textColor="#FFFFFF"
android:textSize="13sp" />
<TextView
android:id="@+id/btn_help"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="说明"
android:textColor="#FDBA74"
android:textSize="13sp" />
<TextView
android:id="@+id/btn_settings"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="设置"
android:textColor="#FFFFFF"
android:textSize="13sp" />
<TextView
android:id="@+id/btn_clear"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="清空"
android:textColor="#FECACA"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_conn_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#1F2937"
android:lineSpacingExtra="2dp"
android:paddingStart="12dp"
android:paddingTop="8dp"
android:paddingEnd="12dp"
android:paddingBottom="8dp"
android:text="连接检测中…"
android:textColor="#E5E7EB"
android:textSize="12sp" />
<!-- 筛选 -->
<LinearLayout
android:layout_width="match_parent"
@@ -296,7 +343,7 @@
android:id="@+id/settings_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="56dp"
android:layout_marginTop="84dp"
android:background="#FFF8F1"
android:clickable="true"
android:elevation="12dp"
@@ -474,6 +521,39 @@
android:textColor="#222222"
android:textSize="13sp" />
<CheckBox
android:id="@+id/cfg_bounce_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:buttonTint="#C45C26"
android:checked="true"
android:text="检测不到 Hook 连接时:自动进一下红包历史页再返回(约 45 秒最多一次)"
android:textColor="#222222"
android:textSize="13sp" />
<CheckBox
android:id="@+id/cfg_auto_watch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:buttonTint="#C45C26"
android:checked="true"
android:text="领取台打开时自动检测 TNG 是否在跑"
android:textColor="#222222"
android:textSize="13sp" />
<CheckBox
android:id="@+id/cfg_auto_launch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:buttonTint="#C45C26"
android:checked="true"
android:text="检测到 TNG 被完全杀掉时,自动重新打开 TNG"
android:textColor="#222222"
android:textSize="13sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -151,6 +151,24 @@
android:textColor="#4E342E"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:text="断线自动进历史页"
android:textColor="#1C1410"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:lineSpacingExtra="4dp"
android:text="设置里可勾选:\n• 检测不到 Hook 连接 → 自动进历史页再返回\n• 领取台打开时自动检测 TNG 是否在跑\n• TNG 被完全杀掉时自动重新打开\n\n这些设置保存在手机本地不依赖电脑调试台。顶栏「刷新」可立刻重载列表并检测 TNG。\n\n注意自动打开 TNG 需要领取台在前台(或刚打开);完全退出本 App 后不会后台常驻拉起。"
android:textColor="#4E342E"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -35,6 +35,28 @@
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxLines="1"
android:text=""
android:textColor="#9CA3AF"
android:textSize="11sp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_pool_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxLines="1"
android:text=""
android:textColor="#9CA3AF"
android:textSize="11sp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_tag"
android:layout_width="wrap_content"