feat(mmp): 独立红包领取台(手机+PC)、同步、筛选与功能说明
隔离通用消息台;支持设置/手气摘要/电脑同步;忽略临时逆向脚本与运行时 JSON。
This commit is contained in:
@@ -72,6 +72,10 @@ public class MainActivity extends MiracleGardenActivity<ActivityMainBinding> {
|
||||
startActivity(new Intent(this, BankListActivity.class));
|
||||
});
|
||||
|
||||
binding.btnMmpClaim.setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, MmpClaimActivity.class));
|
||||
});
|
||||
|
||||
binding.permissionNotificationAccess.setOnClickListener(v -> {
|
||||
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.miraclegarden.smsmessage.Activity;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.miraclegarden.library.app.MiracleGardenActivity;
|
||||
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.MmpPacket;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpPacketStore;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpSettingsClient;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpSyncClient;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 手机端红包领取台(独立于通用消息/情况监听)。
|
||||
*/
|
||||
public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBinding>
|
||||
implements MmpPacketStore.Listener {
|
||||
|
||||
private final List<MmpPacket> allPackets = new ArrayList<>();
|
||||
private final List<MmpPacket> packets = new ArrayList<>();
|
||||
private CommonAdapter<MmpPacket> adapter;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable refreshRunnable = this::reload;
|
||||
/** 0全部 1领取中 2已领完 */
|
||||
private int statusFilter = 0;
|
||||
/** 0全部时间 1近1天 2近3天 3近7天 4自定义 */
|
||||
private int timeMode = 0;
|
||||
private long customFromMs;
|
||||
private long customToMs;
|
||||
private String keyword = "";
|
||||
private boolean settingsOpen;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
initView();
|
||||
reload();
|
||||
loadSettingsQuiet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
MmpPacketStore.addListener(this);
|
||||
reload();
|
||||
MmpSyncClient.syncIfStale(this, 5000L);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
handler.postDelayed(refreshRunnable, 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MmpPacketStore.removeListener(this);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMmpPacketsChanged() {
|
||||
runOnUiThread(this::reload);
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
binding.backIv.setOnClickListener(v -> finish());
|
||||
binding.btnClear.setOnClickListener(v -> confirmClear());
|
||||
binding.btnHelp.setOnClickListener(v ->
|
||||
startActivity(new android.content.Intent(this, MmpHelpActivity.class)));
|
||||
binding.btnSettings.setOnClickListener(v -> toggleSettings());
|
||||
binding.btnCloseSettings.setOnClickListener(v -> {
|
||||
settingsOpen = false;
|
||||
binding.settingsPanel.setVisibility(View.GONE);
|
||||
});
|
||||
binding.btnSync.setOnClickListener(v -> syncNow());
|
||||
|
||||
binding.chipAll.setOnClickListener(v -> setStatusFilter(0));
|
||||
binding.chipOpen.setOnClickListener(v -> setStatusFilter(1));
|
||||
binding.chipDone.setOnClickListener(v -> setStatusFilter(2));
|
||||
binding.chipTimeAll.setOnClickListener(v -> setTimeMode(0));
|
||||
binding.chipTime1.setOnClickListener(v -> setTimeMode(1));
|
||||
binding.chipTime3.setOnClickListener(v -> setTimeMode(2));
|
||||
binding.chipTime7.setOnClickListener(v -> setTimeMode(3));
|
||||
binding.chipTimeCustom.setOnClickListener(v -> setTimeMode(4));
|
||||
binding.btnDateFrom.setOnClickListener(v -> pickDate(true));
|
||||
binding.btnDateTo.setOnClickListener(v -> pickDate(false));
|
||||
|
||||
binding.etFilter.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
keyword = s != null ? s.toString() : "";
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
binding.btnPresetFast.setOnClickListener(v -> applyPreset("fast"));
|
||||
binding.btnPresetNormal.setOnClickListener(v -> applyPreset("normal"));
|
||||
binding.btnPresetSlow.setOnClickListener(v -> applyPreset("slow"));
|
||||
binding.btnSaveSettings.setOnClickListener(v -> saveSettings());
|
||||
|
||||
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new CommonAdapter<MmpPacket>(this, R.layout.item_mmp_packet, packets) {
|
||||
@Override
|
||||
public void convert(ViewHolder holder, MmpPacket packet, int index) {
|
||||
holder.setText(R.id.tv_title, packet.displayTitle());
|
||||
holder.setText(R.id.tv_sum, formatMoney(packet.sumClaimed > 0
|
||||
? packet.sumClaimed : parseDouble(packet.total)));
|
||||
|
||||
android.widget.TextView status = holder.getView(R.id.tv_status);
|
||||
if (packet.finished) {
|
||||
status.setText("已领完");
|
||||
status.setTextColor(Color.parseColor("#047857"));
|
||||
status.setBackgroundColor(Color.parseColor("#ECFDF5"));
|
||||
} else {
|
||||
status.setText("领取中");
|
||||
status.setTextColor(Color.parseColor("#B45309"));
|
||||
status.setBackgroundColor(Color.parseColor("#FFFBEB"));
|
||||
}
|
||||
|
||||
String meta = packet.claimantCount + " 人";
|
||||
if (packet.issuedAt != null && !packet.issuedAt.isEmpty()) {
|
||||
meta += " · 发放 " + packet.issuedAt;
|
||||
}
|
||||
holder.setText(R.id.tv_meta, meta);
|
||||
|
||||
String hi = packet.finished ? "最佳" : "最高";
|
||||
String lo = packet.finished ? "最差" : "最低";
|
||||
com.miraclegarden.smsmessage.mmp.MmpClaim best = packet.bestClaim();
|
||||
com.miraclegarden.smsmessage.mmp.MmpClaim worst = packet.worstClaim();
|
||||
if (best != null) {
|
||||
String amt = best.amountText != null ? best.amountText : formatMoney(best.amount);
|
||||
String nick = best.nickname != null ? best.nickname : "?";
|
||||
holder.setText(R.id.tv_best, hi + " " + nick + " " + amt);
|
||||
} else {
|
||||
holder.setText(R.id.tv_best, hi + " -");
|
||||
}
|
||||
if (worst != null && packet.claimantCount > 1) {
|
||||
String amt = worst.amountText != null ? worst.amountText : formatMoney(worst.amount);
|
||||
String nick = worst.nickname != null ? worst.nickname : "?";
|
||||
holder.setText(R.id.tv_worst, lo + " " + nick + " " + amt);
|
||||
} else {
|
||||
holder.setText(R.id.tv_worst, lo + " -");
|
||||
}
|
||||
holder.setText(R.id.tv_id, packet.shortId());
|
||||
holder.itemView.setOnClickListener(v -> openDetail(packet.packetId));
|
||||
}
|
||||
};
|
||||
binding.recyclerview.setAdapter(adapter);
|
||||
updateChipUi();
|
||||
updateTimeChipUi();
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
allPackets.clear();
|
||||
allPackets.addAll(MmpPacketStore.getPackets(this));
|
||||
applyFilter();
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
handler.postDelayed(refreshRunnable, 2000);
|
||||
}
|
||||
|
||||
private void applyFilter() {
|
||||
packets.clear();
|
||||
for (MmpPacket p : allPackets) {
|
||||
if (p.matchesFilter(keyword, statusFilter, timeMode, customFromMs, customToMs)) {
|
||||
packets.add(p);
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
binding.tvCount.setText(packets.size() + "/" + allPackets.size() + " 个");
|
||||
binding.emptyLy.setVisibility(packets.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void setStatusFilter(int filter) {
|
||||
statusFilter = filter;
|
||||
updateChipUi();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
private void setTimeMode(int mode) {
|
||||
timeMode = mode;
|
||||
binding.customRangeLy.setVisibility(mode == 4 ? View.VISIBLE : View.GONE);
|
||||
updateTimeChipUi();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
private void pickDate(boolean from) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
long base = from ? customFromMs : customToMs;
|
||||
if (base > 0) {
|
||||
cal.setTimeInMillis(base);
|
||||
}
|
||||
new DatePickerDialog(this, (view, y, m, d) -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
if (from) {
|
||||
c.set(y, m, d, 0, 0, 0);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
customFromMs = c.getTimeInMillis();
|
||||
binding.btnDateFrom.setText(String.format(Locale.US, "%04d-%02d-%02d", y, m + 1, d));
|
||||
} else {
|
||||
c.set(y, m, d, 23, 59, 59);
|
||||
c.set(Calendar.MILLISECOND, 999);
|
||||
customToMs = c.getTimeInMillis();
|
||||
binding.btnDateTo.setText(String.format(Locale.US, "%04d-%02d-%02d", y, m + 1, d));
|
||||
}
|
||||
if (timeMode != 4) {
|
||||
setTimeMode(4);
|
||||
} else {
|
||||
applyFilter();
|
||||
}
|
||||
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show();
|
||||
}
|
||||
|
||||
private void updateChipUi() {
|
||||
styleChip(binding.chipAll, statusFilter == 0, true);
|
||||
styleChip(binding.chipOpen, statusFilter == 1, true);
|
||||
styleChip(binding.chipDone, statusFilter == 2, true);
|
||||
}
|
||||
|
||||
private void updateTimeChipUi() {
|
||||
styleChip(binding.chipTimeAll, timeMode == 0, false);
|
||||
styleChip(binding.chipTime1, timeMode == 1, false);
|
||||
styleChip(binding.chipTime3, timeMode == 2, false);
|
||||
styleChip(binding.chipTime7, timeMode == 3, false);
|
||||
styleChip(binding.chipTimeCustom, timeMode == 4, false);
|
||||
}
|
||||
|
||||
private static void styleChip(View chip, boolean active, boolean brand) {
|
||||
if (!(chip instanceof android.widget.TextView)) {
|
||||
return;
|
||||
}
|
||||
android.widget.TextView tv = (android.widget.TextView) chip;
|
||||
if (active) {
|
||||
tv.setBackgroundColor(Color.parseColor(brand ? "#C45C26" : "#1C1410"));
|
||||
tv.setTextColor(Color.WHITE);
|
||||
} else {
|
||||
tv.setBackgroundColor(Color.parseColor("#EEEEEE"));
|
||||
tv.setTextColor(Color.parseColor("#333333"));
|
||||
}
|
||||
}
|
||||
|
||||
private void syncNow() {
|
||||
Toast.makeText(this, "正在同步到电脑…", Toast.LENGTH_SHORT).show();
|
||||
MmpSyncClient.syncToPc(this, (ok, message) -> runOnUiThread(() ->
|
||||
Toast.makeText(this, message != null ? message : (ok ? "同步成功" : "同步失败"),
|
||||
Toast.LENGTH_SHORT).show()));
|
||||
}
|
||||
|
||||
private void toggleSettings() {
|
||||
settingsOpen = !settingsOpen;
|
||||
binding.settingsPanel.setVisibility(settingsOpen ? View.VISIBLE : View.GONE);
|
||||
if (settingsOpen) {
|
||||
loadSettingsQuiet();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettingsQuiet() {
|
||||
MmpSettingsClient.load((json, error) -> {
|
||||
if (json != null) {
|
||||
fillSettingsForm(json);
|
||||
binding.tvSettingsStatus.setText("已同步调试台设置");
|
||||
} else if (error != null) {
|
||||
binding.tvSettingsStatus.setText("设置未连上:" + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fillSettingsForm(JSONObject json) {
|
||||
binding.cfgHistory.setText(String.valueOf(json.optInt("historyCooldownSec", 8)));
|
||||
binding.cfgDetail.setText(String.valueOf(json.optInt("detailCooldownSec", 8)));
|
||||
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));
|
||||
}
|
||||
|
||||
private void applyPreset(String name) {
|
||||
try {
|
||||
JSONObject o = new JSONObject();
|
||||
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);
|
||||
}
|
||||
fillSettingsForm(o);
|
||||
binding.tvSettingsStatus.setText("已套用预设,点保存生效");
|
||||
} catch (Exception e) {
|
||||
binding.tvSettingsStatus.setText("预设失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSettings() {
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("historyCooldownSec", parseIntSafe(binding.cfgHistory.getText().toString(), 8));
|
||||
body.put("detailCooldownSec", parseIntSafe(binding.cfgDetail.getText().toString(), 8));
|
||||
body.put("dedupSec", parseIntSafe(binding.cfgDedup.getText().toString(), 3));
|
||||
body.put("detailGapMs", parseIntSafe(binding.cfgGap.getText().toString(), 80));
|
||||
body.put("pagePollMs", 1000);
|
||||
body.put("openHistoryIfNoTemplate", binding.cfgOpenHistory.isChecked());
|
||||
binding.tvSettingsStatus.setText("保存中…");
|
||||
MmpSettingsClient.save(body, (json, error) -> {
|
||||
if (json != null) {
|
||||
fillSettingsForm(json);
|
||||
binding.tvSettingsStatus.setText("已保存,Hook 约 10 秒内生效");
|
||||
Toast.makeText(this, "设置已保存", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
binding.tvSettingsStatus.setText("保存失败:" + error);
|
||||
Toast.makeText(this, "保存失败,请确认调试台已开", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
binding.tvSettingsStatus.setText("保存异常");
|
||||
}
|
||||
}
|
||||
|
||||
private void openDetail(String packetId) {
|
||||
Intent intent = new Intent(this, MmpDetailActivity.class);
|
||||
intent.putExtra(MmpDetailActivity.EXTRA_PACKET_ID, packetId);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void confirmClear() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清空红包台")
|
||||
.setMessage("仅清空本机红包领取记录,不影响通用消息台。")
|
||||
.setPositiveButton("清空", (d, w) -> {
|
||||
MmpPacketStore.clear(this);
|
||||
Toast.makeText(this, "已清空", Toast.LENGTH_SHORT).show();
|
||||
reload();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static String formatMoney(double v) {
|
||||
return String.format(Locale.US, "%.2f", v);
|
||||
}
|
||||
|
||||
private static double parseDouble(String s) {
|
||||
try {
|
||||
return s == null || s.isEmpty() ? 0 : Double.parseDouble(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseIntSafe(String s, int def) {
|
||||
try {
|
||||
return Integer.parseInt(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.miraclegarden.smsmessage.Activity;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.miraclegarden.library.app.MiracleGardenActivity;
|
||||
import com.miraclegarden.smsmessage.R;
|
||||
import com.miraclegarden.smsmessage.comm.CommonAdapter;
|
||||
import com.miraclegarden.smsmessage.comm.ViewHolder;
|
||||
import com.miraclegarden.smsmessage.databinding.ActivityMmpDetailBinding;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpClaim;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpPacket;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpPacketStore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 单个红包领取排行(数据优先展示)。
|
||||
*/
|
||||
public class MmpDetailActivity extends MiracleGardenActivity<ActivityMmpDetailBinding>
|
||||
implements MmpPacketStore.Listener {
|
||||
|
||||
public static final String EXTRA_PACKET_ID = "packetId";
|
||||
|
||||
private String packetId;
|
||||
private final List<MmpClaim> claims = new ArrayList<>();
|
||||
private CommonAdapter<MmpClaim> adapter;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable refreshRunnable = this::reload;
|
||||
private double bestAmount = -1;
|
||||
private double worstAmount = -1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
packetId = getIntent().getStringExtra(EXTRA_PACKET_ID);
|
||||
if (TextUtils.isEmpty(packetId)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
initView();
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
MmpPacketStore.addListener(this);
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MmpPacketStore.removeListener(this);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMmpPacketsChanged() {
|
||||
runOnUiThread(this::reload);
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
binding.backIv.setOnClickListener(v -> finish());
|
||||
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new CommonAdapter<MmpClaim>(this, R.layout.item_mmp_claim, claims) {
|
||||
@Override
|
||||
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 : "?");
|
||||
holder.setText(R.id.tv_amount, claim.amountText != null
|
||||
? claim.amountText
|
||||
: String.format(Locale.US, "%.2f", claim.amount));
|
||||
holder.setText(R.id.tv_time,
|
||||
TextUtils.isEmpty(claim.claimTime) ? "-" : claim.claimTime);
|
||||
|
||||
TextView tag = holder.getView(R.id.tv_tag);
|
||||
boolean isBest = claims.size() > 1 && bestAmount >= 0
|
||||
&& Math.abs(claim.amount - bestAmount) < 0.0001;
|
||||
boolean isWorst = claims.size() > 1 && worstAmount >= 0
|
||||
&& Math.abs(claim.amount - worstAmount) < 0.0001
|
||||
&& Math.abs(bestAmount - worstAmount) > 0.0001;
|
||||
if (isBest) {
|
||||
tag.setVisibility(View.VISIBLE);
|
||||
tag.setText("最高");
|
||||
tag.setTextColor(Color.parseColor("#047857"));
|
||||
holder.itemView.setBackgroundColor(Color.parseColor("#ECFDF5"));
|
||||
} else if (isWorst) {
|
||||
tag.setVisibility(View.VISIBLE);
|
||||
tag.setText("最低");
|
||||
tag.setTextColor(Color.parseColor("#B45309"));
|
||||
holder.itemView.setBackgroundColor(Color.parseColor("#FFFBEB"));
|
||||
} else {
|
||||
tag.setVisibility(View.GONE);
|
||||
holder.itemView.setBackgroundColor(Color.WHITE);
|
||||
}
|
||||
}
|
||||
};
|
||||
binding.recyclerview.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
MmpPacket packet = MmpPacketStore.getPacket(this, packetId);
|
||||
if (packet == null) {
|
||||
binding.tvTitle.setText("红包详情");
|
||||
binding.tvHeaderMoney.setText("-");
|
||||
binding.tvSub.setText("记录不存在或已清空");
|
||||
binding.tvBestName.setText("-");
|
||||
binding.tvBestAmt.setText("-");
|
||||
binding.tvWorstName.setText("-");
|
||||
binding.tvWorstAmt.setText("-");
|
||||
claims.clear();
|
||||
adapter.notifyDataSetChanged();
|
||||
binding.emptyLy.setVisibility(View.VISIBLE);
|
||||
binding.recyclerview.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
binding.tvTitle.setText(packet.displayTitle());
|
||||
binding.tvHeaderMoney.setText(String.format(Locale.US, "%.2f",
|
||||
packet.sumClaimed > 0 ? packet.sumClaimed : parseDouble(packet.total)));
|
||||
|
||||
StringBuilder sub = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(packet.packetId)) {
|
||||
sub.append("ID ").append(packet.packetId);
|
||||
}
|
||||
if (!TextUtils.isEmpty(packet.issuedAt)) {
|
||||
if (sub.length() > 0) {
|
||||
sub.append(" · ");
|
||||
}
|
||||
sub.append("发放 ").append(packet.issuedAt);
|
||||
}
|
||||
if (packet.finished) {
|
||||
if (sub.length() > 0) {
|
||||
sub.append(" · ");
|
||||
}
|
||||
sub.append("已领完");
|
||||
} else {
|
||||
if (sub.length() > 0) {
|
||||
sub.append(" · ");
|
||||
}
|
||||
sub.append("领取中");
|
||||
}
|
||||
binding.tvSub.setText(sub.toString());
|
||||
|
||||
binding.tvStatPeople.setText(String.valueOf(packet.claimantCount));
|
||||
binding.tvStatSum.setText(String.format(Locale.US, "%.2f", packet.sumClaimed));
|
||||
binding.tvStatTotal.setText(TextUtils.isEmpty(packet.total) ? "-" : packet.total);
|
||||
|
||||
String hi = packet.finished ? "手气最佳" : "目前最高";
|
||||
String lo = packet.finished ? "手气最差" : "目前最低";
|
||||
binding.tvBestLab.setText(hi);
|
||||
binding.tvWorstLab.setText(lo);
|
||||
MmpClaim best = packet.bestClaim();
|
||||
MmpClaim worst = packet.worstClaim();
|
||||
if (best != null) {
|
||||
bestAmount = best.amount;
|
||||
binding.tvBestName.setText(best.nickname != null ? best.nickname : "?");
|
||||
binding.tvBestAmt.setText(best.amountText != null ? best.amountText
|
||||
: String.format(Locale.US, "%.2f", best.amount));
|
||||
} else {
|
||||
bestAmount = -1;
|
||||
binding.tvBestName.setText("-");
|
||||
binding.tvBestAmt.setText("-");
|
||||
}
|
||||
if (worst != null && packet.claimantCount > 1) {
|
||||
worstAmount = worst.amount;
|
||||
binding.tvWorstName.setText(worst.nickname != null ? worst.nickname : "?");
|
||||
binding.tvWorstAmt.setText(worst.amountText != null ? worst.amountText
|
||||
: String.format(Locale.US, "%.2f", worst.amount));
|
||||
} else {
|
||||
worstAmount = -1;
|
||||
binding.tvWorstName.setText("-");
|
||||
binding.tvWorstAmt.setText("-");
|
||||
}
|
||||
|
||||
claims.clear();
|
||||
if (packet.leaderboard != null) {
|
||||
claims.addAll(packet.leaderboard);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
boolean empty = claims.isEmpty();
|
||||
binding.emptyLy.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||
binding.recyclerview.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
handler.postDelayed(refreshRunnable, 2000);
|
||||
}
|
||||
|
||||
private static double parseDouble(String s) {
|
||||
try {
|
||||
return s == null || s.isEmpty() ? 0 : Double.parseDouble(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.miraclegarden.smsmessage.Activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.miraclegarden.library.app.MiracleGardenActivity;
|
||||
import com.miraclegarden.smsmessage.databinding.ActivityMmpHelpBinding;
|
||||
|
||||
/**
|
||||
* 红包领取台功能说明页。
|
||||
*/
|
||||
public class MmpHelpActivity extends MiracleGardenActivity<ActivityMmpHelpBinding> {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding.backIv.setOnClickListener(v -> finish());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.miraclegarden.smsmessage.mmp;
|
||||
|
||||
/**
|
||||
* 单个红包领取人。
|
||||
*/
|
||||
public class MmpClaim {
|
||||
public String nickname;
|
||||
public String amountText;
|
||||
public double amount;
|
||||
public String claimTime;
|
||||
public int rank;
|
||||
|
||||
public MmpClaim() {
|
||||
}
|
||||
|
||||
public MmpClaim(String nickname, double amount, String claimTime) {
|
||||
this.nickname = nickname;
|
||||
this.amount = amount;
|
||||
this.amountText = String.format(java.util.Locale.US, "%.2f", amount);
|
||||
this.claimTime = claimTime != null ? claimTime : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.miraclegarden.smsmessage.mmp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 按 activityId 聚合的红包快照。
|
||||
*/
|
||||
public class MmpPacket {
|
||||
public String packetId;
|
||||
public String title;
|
||||
public String sender;
|
||||
public String group;
|
||||
public String total;
|
||||
public String via;
|
||||
public String issuedAt;
|
||||
public String updatedAt;
|
||||
public boolean finished;
|
||||
public int snapshots;
|
||||
public int claimantCount;
|
||||
public double sumClaimed;
|
||||
public List<MmpClaim> leaderboard = new ArrayList<>();
|
||||
|
||||
public MmpPacket() {
|
||||
}
|
||||
|
||||
public String displayTitle() {
|
||||
if (sender != null && !sender.isEmpty()) {
|
||||
return sender + " 的红包";
|
||||
}
|
||||
if (title != null && !title.isEmpty()) {
|
||||
return title;
|
||||
}
|
||||
return "红包";
|
||||
}
|
||||
|
||||
public String shortId() {
|
||||
if (packetId == null || packetId.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
if (packetId.length() >= 8 && packetId.indexOf('-') > 0) {
|
||||
return packetId.substring(0, 8);
|
||||
}
|
||||
return packetId.length() > 16 ? packetId.substring(0, 16) : packetId;
|
||||
}
|
||||
|
||||
/** 手气最佳(金额最大);排行已按金额降序时取首位。 */
|
||||
public MmpClaim bestClaim() {
|
||||
if (leaderboard == null || leaderboard.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MmpClaim best = leaderboard.get(0);
|
||||
for (MmpClaim c : leaderboard) {
|
||||
if (c != null && c.amount > best.amount) {
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 手气最差(金额最小)。 */
|
||||
public MmpClaim worstClaim() {
|
||||
if (leaderboard == null || leaderboard.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MmpClaim worst = leaderboard.get(0);
|
||||
for (MmpClaim c : leaderboard) {
|
||||
if (c != null && c.amount < worst.amount) {
|
||||
worst = c;
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
public String luckSummaryLine() {
|
||||
MmpClaim best = bestClaim();
|
||||
MmpClaim worst = worstClaim();
|
||||
String hi = finished ? "手气最佳" : "目前最高";
|
||||
String lo = finished ? "手气最差" : "目前最低";
|
||||
if (best == null) {
|
||||
return hi + ":- " + lo + ":-";
|
||||
}
|
||||
String bn = best.nickname != null ? best.nickname : "?";
|
||||
String ba = best.amountText != null ? best.amountText
|
||||
: String.format(Locale.US, "%.2f", best.amount);
|
||||
if (worst == null || leaderboard.size() == 1) {
|
||||
return hi + ":" + bn + "(" + ba + ") " + lo + ":-";
|
||||
}
|
||||
String wn = worst.nickname != null ? worst.nickname : "?";
|
||||
String wa = worst.amountText != null ? worst.amountText
|
||||
: String.format(Locale.US, "%.2f", worst.amount);
|
||||
return hi + ":" + bn + "(" + ba + ") " + lo + ":" + wn + "(" + wa + ")";
|
||||
}
|
||||
|
||||
public boolean matchesFilter(String keyword, int statusFilter) {
|
||||
return matchesFilter(keyword, statusFilter, 0, 0L, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeMode 0全部 1近1天 2近3天 3近7天 4自定义
|
||||
* @param customFromMs / customToMs 自定义起止(含当天),未用传 0
|
||||
*/
|
||||
public boolean matchesFilter(String keyword, int statusFilter,
|
||||
int timeMode, long customFromMs, long customToMs) {
|
||||
if (statusFilter == 1 && finished) {
|
||||
return false;
|
||||
}
|
||||
if (statusFilter == 2 && !finished) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesTime(timeMode, customFromMs, customToMs)) {
|
||||
return false;
|
||||
}
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String q = keyword.trim().toLowerCase(Locale.US);
|
||||
if (containsIgnoreCase(packetId, q) || containsIgnoreCase(sender, q)
|
||||
|| containsIgnoreCase(title, q) || containsIgnoreCase(group, q)
|
||||
|| containsIgnoreCase(total, q)) {
|
||||
return true;
|
||||
}
|
||||
if (leaderboard != null) {
|
||||
for (MmpClaim c : leaderboard) {
|
||||
if (c != null && containsIgnoreCase(c.nickname, q)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchesTime(int timeMode, long customFromMs, long customToMs) {
|
||||
if (timeMode <= 0) {
|
||||
return true;
|
||||
}
|
||||
long ms = parseIssueMs(issuedAt);
|
||||
if (ms <= 0) {
|
||||
ms = parseIssueMs(updatedAt);
|
||||
}
|
||||
if (ms <= 0) {
|
||||
return false;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (timeMode == 1) {
|
||||
return ms >= now - 1L * 24 * 3600 * 1000;
|
||||
}
|
||||
if (timeMode == 2) {
|
||||
return ms >= now - 3L * 24 * 3600 * 1000;
|
||||
}
|
||||
if (timeMode == 3) {
|
||||
return ms >= now - 7L * 24 * 3600 * 1000;
|
||||
}
|
||||
if (timeMode == 4) {
|
||||
if (customFromMs > 0 && ms < customFromMs) {
|
||||
return false;
|
||||
}
|
||||
if (customToMs > 0 && ms > customToMs) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 解析发放时间:13/07/2026 11:39:15 或 yyyy-MM-dd… */
|
||||
public static long parseIssueMs(String text) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
String s = text.trim();
|
||||
try {
|
||||
java.text.SimpleDateFormat[] formats = new java.text.SimpleDateFormat[]{
|
||||
new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.US),
|
||||
new java.text.SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US),
|
||||
new java.text.SimpleDateFormat("dd/MM/yyyy", Locale.US),
|
||||
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US),
|
||||
new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.US),
|
||||
};
|
||||
for (java.text.SimpleDateFormat f : formats) {
|
||||
f.setLenient(true);
|
||||
try {
|
||||
java.util.Date d = f.parse(s);
|
||||
if (d != null) {
|
||||
return d.getTime();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static boolean containsIgnoreCase(String hay, String needle) {
|
||||
return hay != null && hay.toLowerCase(Locale.US).contains(needle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
package com.miraclegarden.smsmessage.mmp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 本地红包领取台数据:解析 Hook 的 [MMP统计] 文本,按 activityId 持久化。
|
||||
*/
|
||||
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 int MAX_PACKETS = 200;
|
||||
|
||||
private static final Pattern AMOUNT_JSON = Pattern.compile(
|
||||
"\"amount\"\\s*:\\s*\"?([0-9]+(?:\\.[0-9]+)?)\"?");
|
||||
private static final Pattern AMOUNT_PLAIN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)");
|
||||
|
||||
public interface Listener {
|
||||
void onMmpPacketsChanged();
|
||||
}
|
||||
|
||||
private static final CopyOnWriteArrayList<Listener> LISTENERS = new CopyOnWriteArrayList<>();
|
||||
|
||||
private MmpPacketStore() {
|
||||
}
|
||||
|
||||
public static void addListener(Listener listener) {
|
||||
if (listener != null) {
|
||||
LISTENERS.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeListener(Listener listener) {
|
||||
LISTENERS.remove(listener);
|
||||
}
|
||||
|
||||
public static boolean isMmpContent(String content, String source) {
|
||||
if (source != null && source.toLowerCase(Locale.US).contains("tng_mmp")) {
|
||||
return true;
|
||||
}
|
||||
return content != null && content.contains("[MMP统计]");
|
||||
}
|
||||
|
||||
/** 解析并写入;非 MMP 或无领取人则忽略。 */
|
||||
public static boolean ingest(Context context, String title, String content, String source) {
|
||||
if (context == null || TextUtils.isEmpty(content) || !isMmpContent(content, source)) {
|
||||
return false;
|
||||
}
|
||||
ParseResult parsed = parse(content);
|
||||
if (parsed.claims.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
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)) {
|
||||
packetId = "红包-" + claimsFingerprint(parsed.claims);
|
||||
if (packetId.length() > 56) {
|
||||
packetId = packetId.substring(0, 56);
|
||||
}
|
||||
}
|
||||
|
||||
MmpPacket incoming = new MmpPacket();
|
||||
incoming.packetId = packetId;
|
||||
incoming.title = TextUtils.isEmpty(title) ? "TNG 红包" : title;
|
||||
incoming.sender = nullToEmpty(parsed.meta.get("sender"));
|
||||
incoming.group = nullToEmpty(parsed.meta.get("group"));
|
||||
incoming.total = nullToEmpty(parsed.meta.get("total"));
|
||||
incoming.via = nullToEmpty(parsed.meta.get("via"));
|
||||
incoming.issuedAt = firstNonEmpty(parsed.meta, "issued", "createTime", "issuedAt", "create");
|
||||
incoming.finished = "1".equals(parsed.meta.get("done"))
|
||||
|| "true".equalsIgnoreCase(parsed.meta.get("done"))
|
||||
|| "FINISHED".equalsIgnoreCase(parsed.meta.get("status"));
|
||||
incoming.updatedAt = nowText();
|
||||
incoming.snapshots = 1;
|
||||
List<MmpClaim> ranked = aggregateClaims(parsed.claims);
|
||||
incoming.leaderboard = ranked;
|
||||
incoming.claimantCount = ranked.size();
|
||||
double sum = 0;
|
||||
for (MmpClaim c : ranked) {
|
||||
sum += c.amount;
|
||||
}
|
||||
incoming.sumClaimed = Math.round(sum * 10000.0) / 10000.0;
|
||||
Double totalNum = normalizeMoney(incoming.total);
|
||||
if (totalNum == null || (incoming.sumClaimed > 0
|
||||
&& Math.abs(totalNum - incoming.sumClaimed) > 0.001
|
||||
&& totalNum <= maxClaimAmount(ranked) + 1e-9)) {
|
||||
incoming.total = String.format(Locale.US, "%.2f", incoming.sumClaimed);
|
||||
} else {
|
||||
incoming.total = String.format(Locale.US, "%.2f", totalNum);
|
||||
}
|
||||
|
||||
synchronized (MmpPacketStore.class) {
|
||||
Map<String, MmpPacket> map = loadMap(context);
|
||||
MmpPacket existing = map.get(packetId);
|
||||
if (existing != null) {
|
||||
if (score(incoming) >= score(existing)) {
|
||||
incoming.snapshots = existing.snapshots + 1;
|
||||
if (TextUtils.isEmpty(incoming.issuedAt) && !TextUtils.isEmpty(existing.issuedAt)) {
|
||||
incoming.issuedAt = existing.issuedAt;
|
||||
}
|
||||
if (!incoming.finished && existing.finished) {
|
||||
incoming.finished = true;
|
||||
}
|
||||
map.put(packetId, incoming);
|
||||
} else {
|
||||
existing.snapshots = existing.snapshots + 1;
|
||||
if (TextUtils.isEmpty(existing.issuedAt) && !TextUtils.isEmpty(incoming.issuedAt)) {
|
||||
existing.issuedAt = incoming.issuedAt;
|
||||
}
|
||||
existing.updatedAt = nowText();
|
||||
}
|
||||
} else {
|
||||
map.put(packetId, incoming);
|
||||
}
|
||||
trimMap(map);
|
||||
saveMap(context, map);
|
||||
}
|
||||
notifyListeners();
|
||||
Log.i(TAG, "ingest packet=" + packetId + " claims=" + ranked.size());
|
||||
try {
|
||||
MmpSyncClient.syncIfStale(context.getApplicationContext(), 3000L);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "auto sync skip", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public static MmpPacket getPacket(Context context, String packetId) {
|
||||
if (TextUtils.isEmpty(packetId)) {
|
||||
return null;
|
||||
}
|
||||
synchronized (MmpPacketStore.class) {
|
||||
return loadMap(context).get(packetId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
synchronized (MmpPacketStore.class) {
|
||||
prefs(context).edit().remove(KEY_JSON).apply();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/** 导出给 PC /api/mmp/sync 的全量 JSON。 */
|
||||
public static JSONArray exportJsonArray(Context context) {
|
||||
JSONArray arr = new JSONArray();
|
||||
synchronized (MmpPacketStore.class) {
|
||||
for (MmpPacket p : loadMap(context).values()) {
|
||||
try {
|
||||
arr.put(toJson(p));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "export packet failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// --- parse ---
|
||||
|
||||
private static final class ParseResult {
|
||||
final Map<String, String> meta = new LinkedHashMap<>();
|
||||
final List<RawClaim> claims = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static final class RawClaim {
|
||||
String nickname;
|
||||
double amount;
|
||||
String claimTime;
|
||||
}
|
||||
|
||||
private static ParseResult parse(String content) {
|
||||
ParseResult out = new ParseResult();
|
||||
String text = content.trim();
|
||||
String[] lines = text.split("\\r?\\n");
|
||||
if (lines.length == 0) {
|
||||
return out;
|
||||
}
|
||||
String head = lines[0].trim();
|
||||
if (head.startsWith("[MMP统计]")) {
|
||||
head = head.substring("[MMP统计]".length()).trim();
|
||||
}
|
||||
List<String> parts;
|
||||
if (head.contains(" | ")) {
|
||||
parts = splitPipe(head);
|
||||
} else {
|
||||
parts = splitLegacy(head);
|
||||
}
|
||||
for (String part : parts) {
|
||||
int eq = part.indexOf('=');
|
||||
if (eq <= 0) {
|
||||
continue;
|
||||
}
|
||||
out.meta.put(part.substring(0, eq).trim(), part.substring(eq + 1).trim());
|
||||
}
|
||||
// sender 被空格截断时尽量还原
|
||||
String sender = out.meta.get("sender");
|
||||
if (sender != null && head.contains("sender=")) {
|
||||
String raw = head.substring(head.indexOf("sender=") + "sender=".length());
|
||||
for (String stop : new String[]{" | ", " total=", " via=", " group=", " packet=", " src=", " issued="}) {
|
||||
int i = raw.indexOf(stop);
|
||||
if (i >= 0) {
|
||||
raw = raw.substring(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
raw = raw.trim();
|
||||
if (raw.length() > sender.length()) {
|
||||
out.meta.put("sender", raw);
|
||||
}
|
||||
}
|
||||
Double totalNum = normalizeMoney(out.meta.get("total"));
|
||||
if (totalNum != null) {
|
||||
out.meta.put("total", String.format(Locale.US, "%.2f", totalNum));
|
||||
}
|
||||
for (int i = 1; i < lines.length; i++) {
|
||||
String line = lines[i].trim();
|
||||
if (line.isEmpty() || !line.contains("->")) {
|
||||
continue;
|
||||
}
|
||||
int arrow = line.indexOf("->");
|
||||
String nick = line.substring(0, arrow).trim();
|
||||
String right = line.substring(arrow + 2).trim();
|
||||
String claimTime = "";
|
||||
String amountRaw = right;
|
||||
if (right.endsWith(")") && right.contains("(") && !right.startsWith("{")) {
|
||||
int lp = right.lastIndexOf('(');
|
||||
amountRaw = right.substring(0, lp).trim();
|
||||
claimTime = right.substring(lp + 1, right.length() - 1).trim();
|
||||
}
|
||||
Double amt = normalizeMoney(amountRaw);
|
||||
if (!TextUtils.isEmpty(nick)) {
|
||||
RawClaim c = new RawClaim();
|
||||
c.nickname = nick;
|
||||
c.amount = amt != null ? amt : 0;
|
||||
c.claimTime = claimTime;
|
||||
out.claims.add(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<String> splitPipe(String head) {
|
||||
String[] arr = head.split(" \\| ");
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (String p : arr) {
|
||||
if (!TextUtils.isEmpty(p.trim())) {
|
||||
parts.add(p.trim());
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static List<String> splitLegacy(String head) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
String buf = "";
|
||||
for (String part : head.split(" ")) {
|
||||
if (!buf.isEmpty()) {
|
||||
buf += " " + part;
|
||||
if (countChar(buf, '{') <= countChar(buf, '}')) {
|
||||
parts.add(buf);
|
||||
buf = "";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (part.contains("=")) {
|
||||
String v = part.substring(part.indexOf('=') + 1);
|
||||
if (v.startsWith("{") && countChar(part, '{') > countChar(part, '}')) {
|
||||
buf = part;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
parts.add(part);
|
||||
}
|
||||
if (!buf.isEmpty()) {
|
||||
parts.add(buf);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static int countChar(String s, char c) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
if (s.charAt(i) == c) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static List<MmpClaim> aggregateClaims(List<RawClaim> claims) {
|
||||
LinkedHashMap<String, MmpClaim> buckets = new LinkedHashMap<>();
|
||||
for (RawClaim c : claims) {
|
||||
String nick = c.nickname != null ? c.nickname : "?";
|
||||
MmpClaim b = buckets.get(nick);
|
||||
if (b == null) {
|
||||
buckets.put(nick, new MmpClaim(nick, c.amount, c.claimTime));
|
||||
continue;
|
||||
}
|
||||
if (c.amount >= b.amount) {
|
||||
b.amount = c.amount;
|
||||
b.amountText = String.format(Locale.US, "%.2f", c.amount);
|
||||
if (!TextUtils.isEmpty(c.claimTime)) {
|
||||
b.claimTime = c.claimTime;
|
||||
}
|
||||
} else if (TextUtils.isEmpty(b.claimTime) && !TextUtils.isEmpty(c.claimTime)) {
|
||||
b.claimTime = c.claimTime;
|
||||
}
|
||||
}
|
||||
List<MmpClaim> result = new ArrayList<>(buckets.values());
|
||||
Collections.sort(result, new Comparator<MmpClaim>() {
|
||||
@Override
|
||||
public int compare(MmpClaim a, MmpClaim b) {
|
||||
return Double.compare(b.amount, a.amount);
|
||||
}
|
||||
});
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result.get(i).rank = i + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Collections.sort(rows);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String r : rows) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append('|');
|
||||
}
|
||||
sb.append(r);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static Double normalizeMoney(String raw) {
|
||||
if (TextUtils.isEmpty(raw)) {
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim();
|
||||
if (s.startsWith("{")) {
|
||||
Matcher m = AMOUNT_JSON.matcher(s);
|
||||
if (m.find()) {
|
||||
try {
|
||||
return Double.parseDouble(m.group(1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Matcher m = AMOUNT_PLAIN.matcher(s.replace(",", ""));
|
||||
if (m.find()) {
|
||||
try {
|
||||
return Double.parseDouble(m.group(1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static double maxClaimAmount(List<MmpClaim> ranked) {
|
||||
double max = 0;
|
||||
for (MmpClaim c : ranked) {
|
||||
if (c.amount > max) {
|
||||
max = c.amount;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
private static int score(MmpPacket p) {
|
||||
int issued = TextUtils.isEmpty(p.issuedAt) ? 0 : 1;
|
||||
int claims = p.leaderboard != null ? p.leaderboard.size() : 0;
|
||||
return issued * 1000 + claims;
|
||||
}
|
||||
|
||||
private static void trimMap(Map<String, MmpPacket> map) {
|
||||
if (map.size() <= MAX_PACKETS) {
|
||||
return;
|
||||
}
|
||||
List<MmpPacket> list = new ArrayList<>(map.values());
|
||||
Collections.sort(list, new Comparator<MmpPacket>() {
|
||||
@Override
|
||||
public int compare(MmpPacket a, MmpPacket b) {
|
||||
return nullToEmpty(a.updatedAt).compareTo(nullToEmpty(b.updatedAt));
|
||||
}
|
||||
});
|
||||
int remove = map.size() - MAX_PACKETS;
|
||||
for (int i = 0; i < remove; i++) {
|
||||
map.remove(list.get(i).packetId);
|
||||
}
|
||||
}
|
||||
|
||||
// --- persistence ---
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getApplicationContext().getSharedPreferences(PREF, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static Map<String, MmpPacket> loadMap(Context context) {
|
||||
Map<String, MmpPacket> map = new HashMap<>();
|
||||
String raw = prefs(context).getString(KEY_JSON, null);
|
||||
if (TextUtils.isEmpty(raw)) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
JSONArray arr = new JSONArray(raw);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
MmpPacket p = fromJson(arr.getJSONObject(i));
|
||||
if (p != null && !TextUtils.isEmpty(p.packetId)) {
|
||||
map.put(p.packetId, p);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "load failed", e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void saveMap(Context context, Map<String, MmpPacket> map) {
|
||||
try {
|
||||
JSONArray arr = new JSONArray();
|
||||
for (MmpPacket p : map.values()) {
|
||||
arr.put(toJson(p));
|
||||
}
|
||||
prefs(context).edit().putString(KEY_JSON, arr.toString()).apply();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "save failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject toJson(MmpPacket p) throws Exception {
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("packetId", nullToEmpty(p.packetId));
|
||||
o.put("title", nullToEmpty(p.title));
|
||||
o.put("sender", nullToEmpty(p.sender));
|
||||
o.put("group", nullToEmpty(p.group));
|
||||
o.put("total", nullToEmpty(p.total));
|
||||
o.put("via", nullToEmpty(p.via));
|
||||
o.put("issuedAt", nullToEmpty(p.issuedAt));
|
||||
o.put("updatedAt", nullToEmpty(p.updatedAt));
|
||||
o.put("finished", p.finished);
|
||||
o.put("snapshots", p.snapshots);
|
||||
o.put("claimantCount", p.claimantCount);
|
||||
o.put("sumClaimed", p.sumClaimed);
|
||||
JSONArray board = new JSONArray();
|
||||
if (p.leaderboard != null) {
|
||||
for (MmpClaim c : p.leaderboard) {
|
||||
JSONObject row = new JSONObject();
|
||||
row.put("nickname", nullToEmpty(c.nickname));
|
||||
row.put("amountText", nullToEmpty(c.amountText));
|
||||
row.put("amount", c.amount);
|
||||
row.put("claimTime", nullToEmpty(c.claimTime));
|
||||
row.put("rank", c.rank);
|
||||
board.put(row);
|
||||
}
|
||||
}
|
||||
o.put("leaderboard", board);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static MmpPacket fromJson(JSONObject o) throws Exception {
|
||||
MmpPacket p = new MmpPacket();
|
||||
p.packetId = o.optString("packetId", "");
|
||||
p.title = o.optString("title", "");
|
||||
p.sender = o.optString("sender", "");
|
||||
p.group = o.optString("group", "");
|
||||
p.total = o.optString("total", "");
|
||||
p.via = o.optString("via", "");
|
||||
p.issuedAt = o.optString("issuedAt", "");
|
||||
p.updatedAt = o.optString("updatedAt", "");
|
||||
p.finished = o.optBoolean("finished", false);
|
||||
p.snapshots = o.optInt("snapshots", 1);
|
||||
p.claimantCount = o.optInt("claimantCount", 0);
|
||||
p.sumClaimed = o.optDouble("sumClaimed", 0);
|
||||
JSONArray board = o.optJSONArray("leaderboard");
|
||||
if (board != null) {
|
||||
for (int i = 0; i < board.length(); i++) {
|
||||
JSONObject row = board.getJSONObject(i);
|
||||
MmpClaim c = new MmpClaim();
|
||||
c.nickname = row.optString("nickname", "");
|
||||
c.amount = row.optDouble("amount", 0);
|
||||
c.amountText = row.optString("amountText",
|
||||
String.format(Locale.US, "%.2f", c.amount));
|
||||
c.claimTime = row.optString("claimTime", "");
|
||||
c.rank = row.optInt("rank", i + 1);
|
||||
p.leaderboard.add(c);
|
||||
}
|
||||
}
|
||||
if (p.claimantCount <= 0) {
|
||||
p.claimantCount = p.leaderboard.size();
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private static void notifyListeners() {
|
||||
for (Listener l : LISTENERS) {
|
||||
try {
|
||||
l.onMmpPacketsChanged();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "listener error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
private static String firstNonEmpty(Map<String, String> meta, String... keys) {
|
||||
for (String k : keys) {
|
||||
String v = meta.get(k);
|
||||
if (!TextUtils.isEmpty(v)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String nowText() {
|
||||
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
.format(new java.util.Date());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.miraclegarden.smsmessage.mmp;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.miraclegarden.smsmessage.AppConfig;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* 从 PC 调试台读写红包拉取设置(与 /mmp 设置同源)。
|
||||
*/
|
||||
public final class MmpSettingsClient {
|
||||
|
||||
private static final String TAG = "MmpSettingsClient";
|
||||
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(4, TimeUnit.SECONDS)
|
||||
.readTimeout(4, TimeUnit.SECONDS)
|
||||
.writeTimeout(4, TimeUnit.SECONDS)
|
||||
.build();
|
||||
private static final ExecutorService EXEC = Executors.newSingleThreadExecutor();
|
||||
private static final Handler MAIN = new Handler(Looper.getMainLooper());
|
||||
|
||||
public interface CallbackJson {
|
||||
void onResult(JSONObject json, String error);
|
||||
}
|
||||
|
||||
private MmpSettingsClient() {
|
||||
}
|
||||
|
||||
public static void load(CallbackJson cb) {
|
||||
EXEC.execute(() -> {
|
||||
Exception last = null;
|
||||
for (String base : AppConfig.DEBUG_SERVER_URLS) {
|
||||
if (base == null || base.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Request req = new Request.Builder()
|
||||
.url(base.replaceAll("/$", "") + "/api/mmp/settings")
|
||||
.get()
|
||||
.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));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
Exception err = last;
|
||||
MAIN.post(() -> cb.onResult(null,
|
||||
err != null ? err.getMessage() : "调试台不可达"));
|
||||
});
|
||||
}
|
||||
|
||||
public static void save(JSONObject body, CallbackJson cb) {
|
||||
EXEC.execute(() -> {
|
||||
Exception last = null;
|
||||
boolean anyOk = false;
|
||||
JSONObject lastOk = null;
|
||||
for (String base : AppConfig.DEBUG_SERVER_URLS) {
|
||||
if (base == null || base.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Request req = new Request.Builder()
|
||||
.url(base.replaceAll("/$", "") + "/api/mmp/settings")
|
||||
.post(RequestBody.create(body.toString(), JSON))
|
||||
.build();
|
||||
try (Response resp = CLIENT.newCall(req).execute()) {
|
||||
if (resp.isSuccessful() && resp.body() != null) {
|
||||
lastOk = new JSONObject(resp.body().string());
|
||||
anyOk = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
final boolean ok = anyOk;
|
||||
final JSONObject result = lastOk;
|
||||
final Exception err = last;
|
||||
MAIN.post(() -> {
|
||||
if (ok) {
|
||||
cb.onResult(result, null);
|
||||
} else {
|
||||
cb.onResult(null, err != null ? err.getMessage() : "保存失败");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 异步探测,不关心结果时用。 */
|
||||
public static void pingAsync() {
|
||||
load((json, error) -> {
|
||||
if (error != null) {
|
||||
Log.w(TAG, "settings ping: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.miraclegarden.smsmessage.mmp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.miraclegarden.smsmessage.AppConfig;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* 把手机本地红包台全量同步到 PC 调试台,方便电脑打开 /mmp 查看。
|
||||
*/
|
||||
public final class MmpSyncClient {
|
||||
|
||||
private static final String TAG = "MmpSyncClient";
|
||||
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.writeTimeout(8, TimeUnit.SECONDS)
|
||||
.readTimeout(8, TimeUnit.SECONDS)
|
||||
.build();
|
||||
private static final ExecutorService EXEC = Executors.newSingleThreadExecutor();
|
||||
private static final AtomicBoolean IN_FLIGHT = new AtomicBoolean(false);
|
||||
private static volatile long sLastSyncAt;
|
||||
|
||||
public interface Callback {
|
||||
void onDone(boolean ok, String message);
|
||||
}
|
||||
|
||||
private MmpSyncClient() {
|
||||
}
|
||||
|
||||
public static void syncToPc(Context context) {
|
||||
syncToPc(context, null);
|
||||
}
|
||||
|
||||
public static void syncToPc(Context context, Callback callback) {
|
||||
if (context == null || !AppConfig.ENABLE_DEBUG_FORWARD) {
|
||||
if (callback != null) {
|
||||
callback.onDone(false, "调试转发未开启");
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Context app = context.getApplicationContext();
|
||||
EXEC.execute(() -> {
|
||||
if (!IN_FLIGHT.compareAndSet(false, true)) {
|
||||
if (callback != null) {
|
||||
callback.onDone(false, "同步进行中");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONArray packets = MmpPacketStore.exportJsonArray(app);
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("packets", packets);
|
||||
body.put("device", "android");
|
||||
body.put("syncedAt", System.currentTimeMillis());
|
||||
String payload = body.toString();
|
||||
|
||||
boolean anyOk = false;
|
||||
String lastErr = "调试台不可达";
|
||||
String lastOkMsg = "";
|
||||
for (String base : AppConfig.DEBUG_SERVER_URLS) {
|
||||
if (TextUtils.isEmpty(base)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Request req = new Request.Builder()
|
||||
.url(base.replaceAll("/$", "") + "/api/mmp/sync")
|
||||
.post(RequestBody.create(payload, JSON))
|
||||
.build();
|
||||
try (Response resp = CLIENT.newCall(req).execute()) {
|
||||
String respBody = resp.body() != null ? resp.body().string() : "";
|
||||
if (resp.isSuccessful()) {
|
||||
anyOk = true;
|
||||
lastOkMsg = "已同步 " + packets.length() + " 个红包到电脑";
|
||||
Log.i(TAG, "sync ok -> " + base + " " + respBody);
|
||||
} else {
|
||||
lastErr = "HTTP " + resp.code();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastErr = e.getMessage();
|
||||
Log.w(TAG, "sync fail " + base + ": " + lastErr);
|
||||
}
|
||||
}
|
||||
sLastSyncAt = System.currentTimeMillis();
|
||||
if (callback != null) {
|
||||
if (anyOk) {
|
||||
callback.onDone(true, lastOkMsg);
|
||||
} else {
|
||||
callback.onDone(false, lastErr != null ? lastErr : "同步失败");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "sync error", e);
|
||||
if (callback != null) {
|
||||
callback.onDone(false, e.getMessage());
|
||||
}
|
||||
} finally {
|
||||
IN_FLIGHT.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 距上次同步超过 cooldownMs 才再同步。 */
|
||||
public static void syncIfStale(Context context, long cooldownMs) {
|
||||
if (System.currentTimeMillis() - sLastSyncAt < cooldownMs) {
|
||||
return;
|
||||
}
|
||||
syncToPc(context, null);
|
||||
}
|
||||
}
|
||||
@@ -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.MmpPacketStore;
|
||||
import com.miraclegarden.smsmessage.network.DebugForwarder;
|
||||
|
||||
/**
|
||||
@@ -58,6 +59,26 @@ public class HookMessageReceiver extends BroadcastReceiver {
|
||||
Log.i(TAG, "hook received: pkg=" + packageName + " source=" + source + " title=" + title);
|
||||
|
||||
final String finalTitle = title;
|
||||
final boolean isMmp = MmpPacketStore.isMmpContent(content, source);
|
||||
|
||||
// 红包统计:只进领取台,不混入「情况」监听台 / 正式上传
|
||||
if (isMmp) {
|
||||
try {
|
||||
MmpPacketStore.ingest(context.getApplicationContext(), finalTitle, content, source);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "mmp ingest failed", e);
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
PendingResult pendingResult = goAsync();
|
||||
Runnable finish = pendingResult::finish;
|
||||
if (AppConfig.ENABLE_DEBUG_FORWARD) {
|
||||
DebugForwarder.forward(appContext, messageInfo, finalTitle, content, timestamp, source, finish);
|
||||
} else {
|
||||
finish.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationActivity.sendMessage("[Hook/" + source + "] " + finalTitle + " " + content);
|
||||
|
||||
Context appContext = context.getApplicationContext();
|
||||
|
||||
Reference in New Issue
Block a user