feat(mmp): 领取人ID/模块版本检测,并修正状态条误报红
展示 receiverId 与 activityPoolId;检测 Hook 是否最新;按真实时间排序;有心跳与定时拉取时不再因久无新数据爆红。
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) + "…";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/meta,claims 为空)
|
||||
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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user