feat: Telegram 双通道抓取、PC 调试台与 Hook 后台转发修复
v2.2.1 更新说明: - 新增 xposed-module(Telegram/微信/SQLite Hook),双 APK + LSPosed 作用域 - HookMessageReceiver 后台直接 DebugForwarder + goAsync,修复 notiMessage 退后台丢消息 - MessageLogStore 日志持久化;AppConfig 调试/上传开关;PC 调试台 debug-server - 健康检查去掉联网限制;通知/Hook 通道增加诊断日志 - 安装脚本 install-full/configure-lsposed/start-debug-server;文档 CHANGELOG + HOOK_GUIDE
This commit is contained in:
22
xposed-module/src/main/AndroidManifest.xml
Normal file
22
xposed-module/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="@string/module_name">
|
||||
|
||||
<meta-data
|
||||
android:name="xposedmodule"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="xposeddescription"
|
||||
android:value="@string/xposed_description" />
|
||||
<meta-data
|
||||
android:name="xposedminversion"
|
||||
android:value="82" />
|
||||
<meta-data
|
||||
android:name="xposedscope"
|
||||
android:resource="@array/xposed_scope" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
1
xposed-module/src/main/assets/xposed_init
Normal file
1
xposed-module/src/main/assets/xposed_init
Normal file
@@ -0,0 +1 @@
|
||||
com.miraclegarden.smsmessage.xposed.MainHook
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.miraclegarden.smsmessage.xposed;
|
||||
|
||||
public final class HookBridge {
|
||||
|
||||
public static final String TARGET_APP_PACKAGE = "com.miraclegarden.smsmessage";
|
||||
public static final String ACTION_HOOK_MESSAGE = "com.miraclegarden.smsmessage.action.HOOK_MESSAGE";
|
||||
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 SOURCE_XPOSED_SQLITE = "xposed_sqlite";
|
||||
public static final String SOURCE_XPOSED_WECHAT = "xposed_wechat";
|
||||
public static final String SOURCE_XPOSED_TELEGRAM = "xposed_telegram";
|
||||
|
||||
private HookBridge() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.miraclegarden.smsmessage.xposed;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
public final class HookForwarder {
|
||||
|
||||
private static final String TAG = "notiMessageHook";
|
||||
|
||||
private HookForwarder() {
|
||||
}
|
||||
|
||||
public static void forward(Context context, String packageName, String title,
|
||||
String content, String source) {
|
||||
if (context == null || TextUtils.isEmpty(packageName) || TextUtils.isEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(HookBridge.ACTION_HOOK_MESSAGE);
|
||||
intent.setPackage(HookBridge.TARGET_APP_PACKAGE);
|
||||
intent.putExtra(HookBridge.EXTRA_PACKAGE_NAME, packageName);
|
||||
intent.putExtra(HookBridge.EXTRA_TITLE,
|
||||
TextUtils.isEmpty(title) ? packageName : title);
|
||||
intent.putExtra(HookBridge.EXTRA_CONTENT, content);
|
||||
intent.putExtra(HookBridge.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
intent.putExtra(HookBridge.EXTRA_SOURCE, source);
|
||||
context.sendBroadcast(intent);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " sendBroadcast failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.miraclegarden.smsmessage.xposed;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.hook.SqliteMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.TelegramMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.WeChatMessageHook;
|
||||
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
public class MainHook implements IXposedHookLoadPackage {
|
||||
|
||||
private static final String WECHAT_PACKAGE = "com.tencent.mm";
|
||||
private static final String TELEGRAM_PACKAGE = "org.telegram.messenger";
|
||||
private static final String TELEGRAM_WEB_PACKAGE = "org.telegram.messenger.web";
|
||||
private static final String MAIN_APP_PACKAGE = "com.miraclegarden.smsmessage";
|
||||
|
||||
@Override
|
||||
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
|
||||
if (lpparam == null || lpparam.packageName == null) {
|
||||
return;
|
||||
}
|
||||
if (MAIN_APP_PACKAGE.equals(lpparam.packageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WECHAT_PACKAGE.equals(lpparam.packageName)) {
|
||||
WeChatMessageHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TELEGRAM_PACKAGE.equals(lpparam.packageName)
|
||||
|| TELEGRAM_WEB_PACKAGE.equals(lpparam.packageName)) {
|
||||
TelegramMessageHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
SqliteMessageHook.install(lpparam);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
public final class SqliteMessageHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/Sqlite";
|
||||
private static final String[] SQLITE_CLASSES = {
|
||||
"android.database.sqlite.SQLiteDatabase",
|
||||
"androidx.sqlite.db.framework.FrameworkSQLiteDatabase"
|
||||
};
|
||||
|
||||
private SqliteMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
for (String sqliteClass : SQLITE_CLASSES) {
|
||||
try {
|
||||
hookInsert(lpparam, sqliteClass, "insert");
|
||||
hookInsert(lpparam, sqliteClass, "insertWithOnConflict");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip " + sqliteClass + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookInsert(XC_LoadPackage.LoadPackageParam lpparam,
|
||||
String className, String methodName) {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
className,
|
||||
lpparam.classLoader,
|
||||
methodName,
|
||||
String.class,
|
||||
String.class,
|
||||
ContentValues.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
handleInsert(lpparam.packageName, param);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static void handleInsert(String packageName, XC_MethodHook.MethodHookParam param) {
|
||||
if (param.args == null || param.args.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object tableArg = param.args[0];
|
||||
Object valuesArg = param.args[2];
|
||||
if (!(tableArg instanceof String) || !(valuesArg instanceof ContentValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String table = ((String) tableArg).toLowerCase();
|
||||
if (!isMessageTable(table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues values = (ContentValues) valuesArg;
|
||||
String title = firstNonEmpty(values, "talker", "sender", "from", "title", "name");
|
||||
String content = firstNonEmpty(values, "content", "text", "body", "msg", "message");
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext(param.thisObject);
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HookForwarder.forward(context, packageName, title, content,
|
||||
HookBridge.SOURCE_XPOSED_SQLITE);
|
||||
}
|
||||
|
||||
private static boolean isMessageTable(String table) {
|
||||
return "message".equals(table)
|
||||
|| "messages".equals(table)
|
||||
|| table.endsWith("_message")
|
||||
|| table.contains("message");
|
||||
}
|
||||
|
||||
private static String firstNonEmpty(ContentValues values, String... keys) {
|
||||
for (String key : keys) {
|
||||
String value = values.getAsString(key);
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static Context getContext(Object database) {
|
||||
try {
|
||||
Object ctx = XposedHelpers.callMethod(database, "getContext");
|
||||
if (ctx instanceof Context) {
|
||||
return (Context) ctx;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* Telegram 专用 Hook:监听 NotificationCenter.didReceiveNewMessages,
|
||||
* 从 MessageObject 提取已解密的 messageText。
|
||||
*/
|
||||
public final class TelegramMessageHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/Telegram";
|
||||
private static final String NOTIFICATION_CENTER = "org.telegram.messenger.NotificationCenter";
|
||||
private static final String MESSAGE_OBJECT = "org.telegram.messenger.MessageObject";
|
||||
private static final int DEDUP_SIZE = 512;
|
||||
|
||||
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
|
||||
private static final HashSet<String> RECENT_SET = new HashSet<>();
|
||||
|
||||
private TelegramMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> notificationCenter = XposedHelpers.findClass(
|
||||
NOTIFICATION_CENTER, lpparam.classLoader);
|
||||
final int didReceiveNewMessages = XposedHelpers.getStaticIntField(
|
||||
notificationCenter, "didReceiveNewMessages");
|
||||
|
||||
XposedHelpers.findAndHookMethod(
|
||||
notificationCenter,
|
||||
"postNotificationName",
|
||||
int.class,
|
||||
Object[].class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
int id = (int) param.args[0];
|
||||
if (id != didReceiveNewMessages) {
|
||||
return;
|
||||
}
|
||||
Object[] args = (Object[]) param.args[1];
|
||||
if (args == null || args.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof List) {
|
||||
processMessageList(context, lpparam.packageName, (List<?>) arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName
|
||||
+ ", didReceiveNewMessages=" + didReceiveNewMessages);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " install failed: " + t.getMessage());
|
||||
installMessageObjectFallback(lpparam);
|
||||
}
|
||||
}
|
||||
|
||||
/** NotificationCenter Hook 失败时的兜底:Hook MessageObject 构造完成后的消息。 */
|
||||
private static void installMessageObjectFallback(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
Class<?> messageObjectClass = XposedHelpers.findClass(
|
||||
MESSAGE_OBJECT, lpparam.classLoader);
|
||||
XposedBridge.hookAllConstructors(messageObjectClass, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
forwardMessageObject(context, lpparam.packageName, param.thisObject);
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " fallback MessageObject hook installed for "
|
||||
+ lpparam.packageName);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " fallback install failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void processMessageList(Context context, String packageName, List<?> messages) {
|
||||
for (Object item : messages) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (!MESSAGE_OBJECT.equals(item.getClass().getName())) {
|
||||
continue;
|
||||
}
|
||||
forwardMessageObject(context, packageName, item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void forwardMessageObject(Context context, String packageName, Object messageObject) {
|
||||
try {
|
||||
Object messageOwner = XposedHelpers.getObjectField(messageObject, "messageOwner");
|
||||
if (messageOwner == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (XposedHelpers.getBooleanField(messageOwner, "out")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int messageId = XposedHelpers.getIntField(messageOwner, "id");
|
||||
long dialogId = extractDialogId(messageObject, messageOwner);
|
||||
if (!rememberMessage(dialogId, messageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String content = extractContent(messageObject, messageOwner);
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = extractTitle(messageObject, messageOwner, dialogId);
|
||||
long timestamp = XposedHelpers.getIntField(messageOwner, "date") * 1000L;
|
||||
if (timestamp <= 0) {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
String sender = extractSenderName(messageObject);
|
||||
String body = content;
|
||||
if (!TextUtils.isEmpty(sender) && !sender.equals(title)) {
|
||||
body = sender + ": " + content;
|
||||
}
|
||||
|
||||
HookForwarder.forward(context, packageName, title, body,
|
||||
HookBridge.SOURCE_XPOSED_TELEGRAM);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " forward failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static long extractDialogId(Object messageObject, Object messageOwner) {
|
||||
try {
|
||||
return ((Number) XposedHelpers.callMethod(messageObject, "getDialogId")).longValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
return XposedHelpers.getLongField(messageOwner, "dialog_id");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
private static String extractContent(Object messageObject, Object messageOwner) {
|
||||
String caption = firstNonEmpty(
|
||||
safeText(XposedHelpers.getObjectField(messageObject, "caption")),
|
||||
safeText(XposedHelpers.getObjectField(messageOwner, "message")),
|
||||
safeText(callMethodSafe(messageObject, "getCaption"))
|
||||
);
|
||||
|
||||
String messageText = firstNonEmpty(
|
||||
safeText(XposedHelpers.getObjectField(messageObject, "messageText")),
|
||||
safeText(callMethodSafe(messageObject, "getMessageText"))
|
||||
);
|
||||
|
||||
String mediaLabel = detectMediaLabel(messageObject, messageOwner);
|
||||
|
||||
if (!TextUtils.isEmpty(caption)) {
|
||||
if (!TextUtils.isEmpty(mediaLabel)) {
|
||||
return mediaLabel + "\n" + caption;
|
||||
}
|
||||
if (!TextUtils.isEmpty(messageText) && !isMediaPlaceholder(messageText)
|
||||
&& !messageText.equals(caption)) {
|
||||
return messageText + "\n" + caption;
|
||||
}
|
||||
return caption;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(messageText) && !isMediaPlaceholder(messageText)) {
|
||||
return messageText;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mediaLabel)) {
|
||||
return mediaLabel;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(messageText)) {
|
||||
return messageText;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String detectMediaLabel(Object messageObject, Object messageOwner) {
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isSticker"))) {
|
||||
return "[贴纸]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isPhoto"))) {
|
||||
return "[图片]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isVideo"))) {
|
||||
return "[视频]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isGif"))) {
|
||||
return "[GIF]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isVoice"))) {
|
||||
return "[语音]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
if (Boolean.TRUE.equals(XposedHelpers.callMethod(messageObject, "isMusic"))) {
|
||||
return "[音频]";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
Object media = XposedHelpers.getObjectField(messageOwner, "media");
|
||||
if (media == null) {
|
||||
return "";
|
||||
}
|
||||
String mediaClass = media.getClass().getName();
|
||||
if (mediaClass.contains("Photo")) {
|
||||
return "[图片]";
|
||||
}
|
||||
if (mediaClass.contains("Document")) {
|
||||
return "[文件]";
|
||||
}
|
||||
if (mediaClass.contains("Video")) {
|
||||
return "[视频]";
|
||||
}
|
||||
if (mediaClass.contains("Geo")) {
|
||||
return "[位置]";
|
||||
}
|
||||
if (mediaClass.contains("Contact")) {
|
||||
return "[联系人]";
|
||||
}
|
||||
return "[媒体消息]";
|
||||
}
|
||||
|
||||
private static boolean isMediaPlaceholder(String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return true;
|
||||
}
|
||||
String value = text.trim();
|
||||
return value.equals("图片")
|
||||
|| value.equals("Photo")
|
||||
|| value.equals("照片")
|
||||
|| value.equals("贴纸")
|
||||
|| value.equals("Sticker")
|
||||
|| value.equals("Video")
|
||||
|| value.equals("视频")
|
||||
|| value.equals("GIF")
|
||||
|| value.equals("动画")
|
||||
|| value.equals("文件")
|
||||
|| value.equals("Document")
|
||||
|| value.equals("语音")
|
||||
|| value.equals("Voice")
|
||||
|| value.equals("音频")
|
||||
|| value.equals("Audio")
|
||||
|| value.equals("位置")
|
||||
|| value.equals("Location")
|
||||
|| value.startsWith("[媒体")
|
||||
|| value.startsWith("[图片")
|
||||
|| value.startsWith("[贴纸")
|
||||
|| value.startsWith("[视频");
|
||||
}
|
||||
|
||||
private static String firstNonEmpty(String... values) {
|
||||
for (String value : values) {
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String callMethodSafe(Object target, String methodName) {
|
||||
try {
|
||||
return safeText(XposedHelpers.callMethod(target, methodName));
|
||||
} catch (Throwable ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractTitle(Object messageObject, Object messageOwner, long dialogId) {
|
||||
String viaController = resolveTitleViaController(messageObject, dialogId);
|
||||
if (!TextUtils.isEmpty(viaController)) {
|
||||
return viaController;
|
||||
}
|
||||
|
||||
try {
|
||||
Object dialogName = XposedHelpers.callMethod(messageObject, "getName");
|
||||
String name = safeText(dialogName);
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
Object chat = XposedHelpers.callMethod(messageObject, "getChat");
|
||||
if (chat != null) {
|
||||
String chatTitle = safeText(XposedHelpers.getObjectField(chat, "title"));
|
||||
if (!TextUtils.isEmpty(chatTitle)) {
|
||||
return chatTitle;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
String localName = safeText(XposedHelpers.getObjectField(messageObject, "localName"));
|
||||
if (!TextUtils.isEmpty(localName)) {
|
||||
return localName;
|
||||
}
|
||||
|
||||
return dialogId != 0 ? ("对话 " + dialogId) : "Telegram";
|
||||
}
|
||||
|
||||
private static String resolveTitleViaController(Object messageObject, long dialogId) {
|
||||
if (dialogId == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int account = XposedHelpers.getIntField(messageObject, "currentAccount");
|
||||
ClassLoader cl = messageObject.getClass().getClassLoader();
|
||||
Class<?> mcClass = XposedHelpers.findClass(
|
||||
"org.telegram.messenger.MessagesController", cl);
|
||||
Object mc = XposedHelpers.callStaticMethod(mcClass, "getInstance", account);
|
||||
|
||||
Object title = null;
|
||||
try {
|
||||
title = XposedHelpers.callMethod(mc, "getPeerTitle", dialogId, false);
|
||||
} catch (Throwable ignored) {
|
||||
title = XposedHelpers.callMethod(mc, "getPeerTitle", dialogId);
|
||||
}
|
||||
|
||||
String text = safeText(title);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (dialogId < 0) {
|
||||
Object chat = XposedHelpers.callMethod(mc, "getChat", -dialogId);
|
||||
if (chat == null) {
|
||||
chat = XposedHelpers.callMethod(mc, "getChat", dialogId);
|
||||
}
|
||||
if (chat != null) {
|
||||
text = safeText(XposedHelpers.getObjectField(chat, "title"));
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object user = XposedHelpers.callMethod(mc, "getUser", dialogId);
|
||||
text = formatUserName(user);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " resolveTitleViaController: " + t.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String formatUserName(Object user) {
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
String first = safeText(XposedHelpers.getObjectField(user, "first_name"));
|
||||
String last = safeText(XposedHelpers.getObjectField(user, "last_name"));
|
||||
String username = safeText(XposedHelpers.getObjectField(user, "username"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(first)) {
|
||||
sb.append(first);
|
||||
}
|
||||
if (!TextUtils.isEmpty(last)) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(last);
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
return sb.toString();
|
||||
}
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
return "@" + username;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String safeText(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
if (!(value instanceof CharSequence) && !(value instanceof Number) && !(value instanceof Boolean)) {
|
||||
String className = value.getClass().getName();
|
||||
if (className.startsWith("org.telegram.tgnet.")
|
||||
|| className.contains("TLRPC$")
|
||||
|| value.getClass().getName().contains("Peer")) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
if (text.contains("TLRPC$") || text.contains("org.telegram.tgnet.")) {
|
||||
return "";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static String extractSenderName(Object messageObject) {
|
||||
try {
|
||||
String fromName = safeText(XposedHelpers.callMethod(messageObject, "getFromName"));
|
||||
if (!TextUtils.isEmpty(fromName)) {
|
||||
return fromName;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static synchronized boolean rememberMessage(long dialogId, int messageId) {
|
||||
if (messageId <= 0) {
|
||||
return true;
|
||||
}
|
||||
String key = dialogId + ":" + messageId;
|
||||
if (RECENT_SET.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
RECENT_SET.add(key);
|
||||
RECENT_KEYS.addLast(key);
|
||||
while (RECENT_KEYS.size() > DEDUP_SIZE) {
|
||||
String oldest = RECENT_KEYS.removeFirst();
|
||||
RECENT_SET.remove(oldest);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Context getContext() {
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
public final class WeChatMessageHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/WeChat";
|
||||
private static final String WCDB_CLASS = "com.tencent.wcdb.database.SQLiteDatabase";
|
||||
|
||||
private WeChatMessageHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
hookWcdbInsert(lpparam, "insert");
|
||||
hookWcdbInsert(lpparam, "insertWithOnConflict");
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " install failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookWcdbInsert(XC_LoadPackage.LoadPackageParam lpparam, String methodName) {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
WCDB_CLASS,
|
||||
lpparam.classLoader,
|
||||
methodName,
|
||||
String.class,
|
||||
String.class,
|
||||
ContentValues.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
handleWeChatInsert(param);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static void handleWeChatInsert(XC_MethodHook.MethodHookParam param) {
|
||||
if (param.args == null || param.args.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object tableArg = param.args[0];
|
||||
Object valuesArg = param.args[2];
|
||||
if (!(tableArg instanceof String) || !(valuesArg instanceof ContentValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String table = (String) tableArg;
|
||||
if (!"message".equalsIgnoreCase(table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues values = (ContentValues) valuesArg;
|
||||
String talker = values.getAsString("talker");
|
||||
String content = values.getAsString("content");
|
||||
Integer type = values.getAsInteger("type");
|
||||
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
if (type != null && (type == 10000 || type == 10002)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HookForwarder.forward(context, "com.tencent.mm", talker, content,
|
||||
HookBridge.SOURCE_XPOSED_WECHAT);
|
||||
}
|
||||
|
||||
private static Context getContext() {
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
9
xposed-module/src/main/res/values/arrays.xml
Normal file
9
xposed-module/src/main/res/values/arrays.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="xposed_scope">
|
||||
<item>org.telegram.messenger</item>
|
||||
<item>org.telegram.messenger.web</item>
|
||||
<item>com.tencent.mm</item>
|
||||
<item>com.google.android.gm</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
5
xposed-module/src/main/res/values/strings.xml
Normal file
5
xposed-module/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="module_name">notiMessage Hook</string>
|
||||
<string name="xposed_description">Hook 目标 App 消息,前台也能同步到 notiMessage。需在 LSPosed 中勾选目标 App 并启用本模块。</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user