From 4856dcdf2fbd8f5bec5851af43c4533faa002175 Mon Sep 17 00:00:00 2001 From: klxiaoniu Date: Thu, 6 Jul 2023 21:09:34 +0800 Subject: [PATCH] wip: +1 in menu for QQNT --- .../dialog/RepeaterPlusIconSettingDialog.java | 4 + .../java/cc/hicore/hook/RepeaterPlus.java | 150 ++++++++++++------ .../java/cc/ioctl/hook/msg/ShowMsgCount.java | 4 +- .../java/io/github/qauxv/util/CustomMenu.java | 96 ----------- .../java/io/github/qauxv/util/CustomMenu.kt | 129 +++++++++++++++ .../java/me/ketal/hook/PicCopyToClipboard.kt | 44 +---- libs/stub/qq-stub | 2 +- 7 files changed, 245 insertions(+), 184 deletions(-) delete mode 100644 app/src/main/java/io/github/qauxv/util/CustomMenu.java create mode 100644 app/src/main/java/io/github/qauxv/util/CustomMenu.kt diff --git a/app/src/main/java/cc/hicore/dialog/RepeaterPlusIconSettingDialog.java b/app/src/main/java/cc/hicore/dialog/RepeaterPlusIconSettingDialog.java index fd94bb1ff6..7535d897b4 100644 --- a/app/src/main/java/cc/hicore/dialog/RepeaterPlusIconSettingDialog.java +++ b/app/src/main/java/cc/hicore/dialog/RepeaterPlusIconSettingDialog.java @@ -151,6 +151,10 @@ public static boolean getIsShowUpper(){ return ConfigManager.getDefaultConfig().getBooleanOrFalse(qn_repeat_show_in_upper_right); } public static boolean getIsShowInMenu(){ + // temporary + if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) { + return false; + } return ConfigManager.getDefaultConfig().getBooleanOrFalse(qn_repeat_show_in_menu); } public static Bitmap getRepeaterIcon() { diff --git a/app/src/main/java/cc/hicore/hook/RepeaterPlus.java b/app/src/main/java/cc/hicore/hook/RepeaterPlus.java index 4b3908e3b9..731348ec08 100644 --- a/app/src/main/java/cc/hicore/hook/RepeaterPlus.java +++ b/app/src/main/java/cc/hicore/hook/RepeaterPlus.java @@ -43,19 +43,26 @@ import cc.hicore.dialog.RepeaterPlusIconSettingDialog; import cc.ioctl.util.HookUtils; import cc.ioctl.util.HostInfo; +import com.tencent.qqnt.kernel.nativeinterface.Contact; +import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; +import io.github.qauxv.R; import io.github.qauxv.base.ISwitchCellAgent; import io.github.qauxv.base.IUiItemAgent; import io.github.qauxv.base.annotation.FunctionHookEntry; import io.github.qauxv.base.annotation.UiItemAgentEntry; +import io.github.qauxv.bridge.AppRuntimeHelper; +import io.github.qauxv.bridge.ntapi.MsgServiceHelper; import io.github.qauxv.dsl.FunctionEntryRouter.Locations.Auxiliary; import io.github.qauxv.hook.BaseFunctionHook; +import io.github.qauxv.util.CustomMenu; import io.github.qauxv.util.Initiator; import io.github.qauxv.util.Log; import io.github.qauxv.util.QQVersion; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -156,56 +163,109 @@ public String[] getUiItemLocation() { @SuppressLint({"WrongConstant", "ResourceType"}) public boolean initOnce() throws Exception { if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) { - // temporary - XC_MethodHook callback = new XC_MethodHook() { - private ImageView img; - private volatile long click_time = 0; - @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { - ImageView imageView; - if (param.args.length == 0) { - Object result = param.getResult(); - if (result instanceof ImageView) { - this.img = (ImageView) result; - this.img.setImageBitmap(RepeaterPlusIconSettingDialog.getRepeaterIcon()); - } - } else if (param.args.length == 3 && (imageView = this.img) != null) { - if (img.getContext().getClass().getName().contains("MultiForwardActivity")) { - return; - } - if (RepeaterPlusIconSettingDialog.getIsDoubleClick()) { - img.setOnClickListener(v -> { - try { - if (System.currentTimeMillis() - 200 > click_time) { - return; + if (!RepeaterPlusIconSettingDialog.getIsShowInMenu()) { + XC_MethodHook callback = new XC_MethodHook() { + private ImageView img; + private volatile long click_time = 0; + + @Override + protected void afterHookedMethod(MethodHookParam param) { + ImageView imageView; + if (param.args.length == 0) { + Object result = param.getResult(); + if (result instanceof ImageView) { + this.img = (ImageView) result; + this.img.setImageBitmap(RepeaterPlusIconSettingDialog.getRepeaterIcon()); + } + } else if (param.args.length == 3 && (imageView = this.img) != null) { + if (img.getContext().getClass().getName().contains("MultiForwardActivity")) { + return; + } + if (RepeaterPlusIconSettingDialog.getIsDoubleClick()) { + img.setOnClickListener(v -> { + try { + if (System.currentTimeMillis() - 200 > click_time) { + return; + } + } finally { + click_time = System.currentTimeMillis(); } - } finally { - click_time = System.currentTimeMillis(); - } - try { - Object a = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.msgfollow.a") - .getDeclaredConstructor(param.thisObject.getClass()).newInstance(param.thisObject); - a.getClass().getMethod("onClick", View.class).invoke(a, v); - } catch (Exception e) { - Log.e(e); - } - }); + try { + // TODO: 存在BUG,建议改为调用IKernelMsgService.forwardMsg + Object a = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.msgfollow.a") + .getDeclaredConstructor(param.thisObject.getClass()).newInstance(param.thisObject); + a.getClass().getMethod("onClick", View.class).invoke(a, v); + } catch (Exception e) { + Log.e(e); + } + }); + } + imageView.setVisibility(0); } - imageView.setVisibility(0); + } + }; + Class clz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.msgfollow.AIOMsgFollowComponent"); + for (Method method : clz.getDeclaredMethods()) { + Class[] parameterTypes = method.getParameterTypes(); + boolean z = true; + boolean z2 = parameterTypes.length == 0 && method.getReturnType().equals(ImageView.class); + if (parameterTypes.length != 3 || !parameterTypes[0].equals(Integer.TYPE) || !parameterTypes[2].equals(List.class)) { + z = z2; + } + if (z) { + XposedBridge.hookMethod(method, callback); } } - }; - Class clz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.msgfollow.AIOMsgFollowComponent"); - for (Method method : clz.getDeclaredMethods()) { - Class[] parameterTypes = method.getParameterTypes(); - boolean z = true; - boolean z2 = parameterTypes.length == 0 && method.getReturnType().equals(ImageView.class); - if (parameterTypes.length != 3 || !parameterTypes[0].equals(Integer.TYPE) || !parameterTypes[2].equals(List.class)) { - z = z2; + + } else { + Class msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem"); + Class picContentComponent = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent"); + //TODO: 添加其他Component + Method listMethod = null; + Method getMsg = null; + Method[] methods = picContentComponent.getDeclaredMethods(); + for (Method method : methods) { + if (method.getReturnType() == List.class && method.getParameterTypes().length == 0) { + listMethod = method; + listMethod.setAccessible(true); + break; + } } - if (z) { - XposedBridge.hookMethod(method, callback); + methods = picContentComponent.getSuperclass().getDeclaredMethods(); + for (Method method : methods) { + if (method.getReturnType() == msgClass && method.getParameterTypes().length == 0) { + getMsg = method; + getMsg.setAccessible(true); + break; + } } + Method finalGetMsg = getMsg; + HookUtils.hookAfterIfEnabled(this, listMethod, param -> { + List list = (List) param.getResult(); + Object msg = finalGetMsg.invoke(param.thisObject); + Object context = param.thisObject.getClass().getMethod("getMContext").invoke(param.thisObject); + Object item = CustomMenu.createItemNt(msg, "+1", R.id.item_repeat, () -> { + //TODO: 复读实现,IKernelMsgService.forwardMsg + try { + // 这里抄的是 8.9.68 Lcom/tencent/mobileqq/aio/msglist/holder/component/c;->t(Lcom/tencent/mobileqq/aio/msg/AIOMsgItem;)V + IKernelMsgService service = MsgServiceHelper.getKernelMsgService(AppRuntimeHelper.getAppRuntime()); + ArrayList msgIds = new ArrayList<>(); + msgIds.add((Long) msg.getClass().getMethod("getMsgId").invoke(msg)); + // context不对,无法继续,请修改... + Object AIOParam = context.getClass().getMethod("f").invoke(context); + Object AIOSession = AIOParam.getClass().getMethod("r").invoke(AIOParam); + Object AIOUtil = Initiator.loadClass("com.tencent.mobileqq.aio.utils.AIOUtil").getDeclaredField("a").get(null); + Contact contact = (Contact) AIOUtil.getClass().getMethod("f", AIOSession.getClass()).invoke(AIOUtil, AIOSession); + ArrayList contacts = new ArrayList<>(); + contacts.add(contact); + service.forwardMsg(msgIds, contact, contacts, null, null); + } catch (Exception e) { + Log.e(e); + } + return Unit.INSTANCE; + }); + list.add(0, item); + }); } return true; } @@ -232,7 +292,7 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { if (context.getClass().getName().contains("MultiForwardActivity")) { return; } - List MessageRecoreList = MField.GetFirstField(param.thisObject, List.class); + List MessageRecoreList = MField.GetFirstField(param.thisObject, List.class); if (MessageRecoreList == null) { return; } diff --git a/app/src/main/java/cc/ioctl/hook/msg/ShowMsgCount.java b/app/src/main/java/cc/ioctl/hook/msg/ShowMsgCount.java index a358a70928..733dd3f6f6 100644 --- a/app/src/main/java/cc/ioctl/hook/msg/ShowMsgCount.java +++ b/app/src/main/java/cc/ioctl/hook/msg/ShowMsgCount.java @@ -73,7 +73,7 @@ public String[] getUiItemLocation() { @Override public boolean initOnce() throws NoSuchMethodException { - Method updateCustomNoteTxt = DexKit.requireMethodFromCache(NCustomWidgetUtil_updateCustomNoteTxt.INSTANCE); + Method updateCustomNoteTxt = null; if (QAppUtils.isQQnt()) { Class clz = DexKit.requireClassFromCache(CCustomWidgetUtil_updateCustomNoteTxt_NT.INSTANCE); for (Method method : clz.getDeclaredMethods()) { @@ -82,6 +82,8 @@ public boolean initOnce() throws NoSuchMethodException { break; } } + } else { + updateCustomNoteTxt = DexKit.requireMethodFromCache(NCustomWidgetUtil_updateCustomNoteTxt.INSTANCE); } XposedBridge.hookMethod(updateCustomNoteTxt, new XC_MethodHook() { @Override diff --git a/app/src/main/java/io/github/qauxv/util/CustomMenu.java b/app/src/main/java/io/github/qauxv/util/CustomMenu.java deleted file mode 100644 index 5aa93ec6d9..0000000000 --- a/app/src/main/java/io/github/qauxv/util/CustomMenu.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * QAuxiliary - An Xposed module for QQ/TIM - * Copyright (C) 2019-2022 qwq233@qwq2333.top - * https://github.com/cinit/QAuxiliary - * - * This software is non-free but opensource software: you can redistribute it - * and/or modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation; either - * version 3 of the License, or any later version and our eula as published - * by QAuxiliary contributors. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * and eula along with this software. If not, see - * - * . - */ -package io.github.qauxv.util; - -import androidx.annotation.NonNull; -import cc.ioctl.util.Reflex; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; - -public class CustomMenu { - - private CustomMenu() { - } - - @NonNull - public static Object createItem(Class clazz, int id, String title, int icon) throws ReflectiveOperationException { - try { - try { - Constructor initWithArgv = clazz.getConstructor(int.class, String.class, int.class); - return initWithArgv.newInstance(id, title, icon); - } catch (NoSuchMethodException unused) { - //no direct constructor, reflex - Object item = createItem(clazz, id, title); - Field f; - f = Reflex.findFieldOrNull(clazz, int.class, "b"); - if (f == null) { - f = Reflex.findField(clazz, int.class, "icon"); - } - f.setAccessible(true); - f.set(item, icon); - return item; - } - } catch (ReflectiveOperationException e) { - Log.w(e.toString()); - //sign... drop icon - return createItem(clazz, id, title); - } - } - - @NonNull - public static Object createItem(Class clazz, int id, String title) throws ReflectiveOperationException { - Object item; - try { - Constructor initWithArgv = clazz.getConstructor(int.class, String.class); - return initWithArgv.newInstance(id, title); - } catch (NoSuchMethodException ignored) { - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - item = clazz.newInstance(); - Field f; - f = Reflex.findFieldOrNull(clazz, int.class, "id"); - if (f == null) { - f = Reflex.findField(clazz, int.class, "a"); - } - f.setAccessible(true); - f.set(item, id); - f = Reflex.findFieldOrNull(clazz, String.class, "title"); - if (f == null) { - f = Reflex.findField(clazz, String.class, "a"); - } - f.setAccessible(true); - f.set(item, title); - return item; - } - - public static void checkArrayElementNonNull(Object[] array) { - if (array == null) { - throw new NullPointerException("array is null"); - } - for (int i = 0; i < array.length; i++) { - if (array[i] == null) { - throw new NullPointerException("array[" + i + "] is null, length=" + array.length); - } - } - } -} diff --git a/app/src/main/java/io/github/qauxv/util/CustomMenu.kt b/app/src/main/java/io/github/qauxv/util/CustomMenu.kt new file mode 100644 index 0000000000..7a0b2a3b06 --- /dev/null +++ b/app/src/main/java/io/github/qauxv/util/CustomMenu.kt @@ -0,0 +1,129 @@ +/* + * QAuxiliary - An Xposed module for QQ/TIM + * Copyright (C) 2019-2022 qwq233@qwq2333.top + * https://github.com/cinit/QAuxiliary + * + * This software is non-free but opensource software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation; either + * version 3 of the License, or any later version and our eula as published + * by QAuxiliary contributors. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * and eula along with this software. If not, see + * + * . + */ +package io.github.qauxv.util + +import android.content.Context +import cc.ioctl.util.Reflex +import com.github.kyuubiran.ezxhelper.utils.findMethod +import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem +import io.github.qauxv.util.dexkit.DexKit +import net.bytebuddy.ByteBuddy +import net.bytebuddy.android.AndroidClassLoadingStrategy +import net.bytebuddy.implementation.FixedValue +import net.bytebuddy.implementation.MethodCall +import net.bytebuddy.matcher.ElementMatchers +import java.lang.reflect.Field + +object CustomMenu { + @Throws(ReflectiveOperationException::class) + fun createItem(clazz: Class<*>, id: Int, title: String?, icon: Int): Any { + return try { + try { + val initWithArgv = clazz.getConstructor(Int::class.javaPrimitiveType, String::class.java, Int::class.javaPrimitiveType) + initWithArgv.newInstance(id, title, icon) + } catch (unused: NoSuchMethodException) { + //no direct constructor, reflex + val item = createItem(clazz, id, title) + var f: Field? = Reflex.findFieldOrNull(clazz, Int::class.javaPrimitiveType, "b") + if (f == null) { + f = Reflex.findField(clazz, Int::class.javaPrimitiveType, "icon") + } + f!!.isAccessible = true + f[item] = icon + item + } + } catch (e: ReflectiveOperationException) { + Log.w(e.toString()) + //sign... drop icon + createItem(clazz, id, title) + } + } + + @JvmStatic + @Throws(ReflectiveOperationException::class) + fun createItem(clazz: Class<*>, id: Int, title: String?): Any { + try { + val initWithArgv = clazz.getConstructor(Int::class.javaPrimitiveType, String::class.java) + return initWithArgv.newInstance(id, title) + } catch (ignored: NoSuchMethodException) { + } catch (e: IllegalAccessException) { + throw AssertionError(e) + } + val item: Any = clazz.newInstance() + var f: Field? = Reflex.findFieldOrNull(clazz, Int::class.javaPrimitiveType, "id") + if (f == null) { + f = Reflex.findField(clazz, Int::class.javaPrimitiveType, "a") + } + f!!.isAccessible = true + f[item] = id + f = Reflex.findFieldOrNull(clazz, String::class.java, "title") + if (f == null) { + f = Reflex.findField(clazz, String::class.java, "a") + } + f!!.isAccessible = true + f[item] = title + return item + } + + + private val strategy by lazy { + AndroidClassLoadingStrategy.Wrapping( + hostInfo.application.getDir( + "generated", + Context.MODE_PRIVATE + ) + ) + } + + @JvmStatic + fun createItemNt(msg: Any, text: String, id: Int, click: () -> Unit): Any { + val msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem") + val absMenuItem = DexKit.loadClassFromCache(AbstractQQCustomMenuItem)!! + val clickName = absMenuItem.findMethod { + returnType == Void.TYPE && parameterTypes.isEmpty() + }.name + val menuItemClass = ByteBuddy() + .subclass(absMenuItem) + .method(ElementMatchers.returns(String::class.java)) + .intercept(FixedValue.value(text)) + .method(ElementMatchers.returns(Int::class.java)) + .intercept(FixedValue.value(id)) + .method(ElementMatchers.named(clickName)) + .intercept(MethodCall.call { click() }) + .make() + .load(absMenuItem.classLoader, strategy) + .loaded + return menuItemClass.getDeclaredConstructor(msgClass) + .newInstance(msg) + } + + fun checkArrayElementNonNull(array: Array?) { + if (array == null) { + throw NullPointerException("array is null") + } + for (i in array.indices) { + if (array[i] == null) { + throw NullPointerException("array[" + i + "] is null, length=" + array.size) + } + } + } +} diff --git a/app/src/main/java/me/ketal/hook/PicCopyToClipboard.kt b/app/src/main/java/me/ketal/hook/PicCopyToClipboard.kt index 9305292ac2..b1e91dfbb1 100644 --- a/app/src/main/java/me/ketal/hook/PicCopyToClipboard.kt +++ b/app/src/main/java/me/ketal/hook/PicCopyToClipboard.kt @@ -26,13 +26,13 @@ import android.content.Context import android.os.Build import android.view.View import cc.hicore.QApp.QAppUtils +import cc.hicore.QQDecodeUtils.DecodeForEncPic import cc.ioctl.util.Reflex import cc.ioctl.util.ui.FaultyDialog +import com.github.kyuubiran.ezxhelper.utils.Log import com.github.kyuubiran.ezxhelper.utils.findMethod import com.github.kyuubiran.ezxhelper.utils.findMethodOrNull import com.github.kyuubiran.ezxhelper.utils.tryOrLogFalse -import cc.hicore.QQDecodeUtils.DecodeForEncPic -import com.github.kyuubiran.ezxhelper.utils.Log import io.github.qauxv.R import io.github.qauxv.base.annotation.FunctionHookEntry import io.github.qauxv.base.annotation.UiItemAgentEntry @@ -47,15 +47,7 @@ import io.github.qauxv.util.Initiator._PicItemBuilder import io.github.qauxv.util.SyncUtils import io.github.qauxv.util.Toasts import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem -import io.github.qauxv.util.dexkit.DexKit -import io.github.qauxv.util.hostInfo import io.github.qauxv.util.isAndroidxFileProviderAvailable -import net.bytebuddy.ByteBuddy -import net.bytebuddy.android.AndroidClassLoadingStrategy -import net.bytebuddy.implementation.FixedValue -import net.bytebuddy.implementation.MethodCall -import net.bytebuddy.matcher.ElementMatchers.named -import net.bytebuddy.matcher.ElementMatchers.returns import xyz.nextalone.util.SystemServiceUtils.copyToClipboard import xyz.nextalone.util.clazz import xyz.nextalone.util.get @@ -79,15 +71,6 @@ object PicCopyToClipboard : CommonSwitchFunctionHook( override val isAvailable: Boolean = isAndroidxFileProviderAvailable - private val strategy by lazy { - AndroidClassLoadingStrategy.Wrapping( - hostInfo.application.getDir( - "generated", - Context.MODE_PRIVATE - ) - ) - } - override fun initOnce() = tryOrLogFalse { if (QAppUtils.isQQnt()) { hookNt() @@ -149,7 +132,7 @@ object PicCopyToClipboard : CommonSwitchFunctionHook( val list = it.result as MutableList val msg = getMsg.invoke(it.thisObject)!! val context = it.thisObject.invoke("getMContext")!! - val item = getMenuItem(msg, "复制图片", R.id.item_copyToClipboard) { + val item = CustomMenu.createItemNt(msg, "复制图片", R.id.item_copyToClipboard) { runCatching { val file = File(getFilePathNt(msg)) onClick(context as Context, file) @@ -161,27 +144,6 @@ object PicCopyToClipboard : CommonSwitchFunctionHook( } } - private fun getMenuItem(msg: Any, text: String, id: Int, click: () -> Unit): Any { - val msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem") - val absMenuItem = DexKit.loadClassFromCache(AbstractQQCustomMenuItem)!! - val clickName = absMenuItem.findMethod { - returnType == Void.TYPE && parameterTypes.isEmpty() - }.name - val menuItemClass = ByteBuddy() - .subclass(absMenuItem) - .method(returns(String::class.java)) - .intercept(FixedValue.value(text)) - .method(returns(Int::class.java)) - .intercept(FixedValue.value(id)) - .method(named(clickName)) - .intercept(MethodCall.call { click() }) - .make() - .load(absMenuItem.classLoader, strategy) - .loaded - return menuItemClass.getDeclaredConstructor(msgClass) - .newInstance(msg) - } - private fun onClick(context: Context, file: File) { if (!file.exists()) { FaultyDialog.show(context, "图片不存在", "尝试打开一次图片") diff --git a/libs/stub/qq-stub b/libs/stub/qq-stub index 5a2a136690..9377c8379c 160000 --- a/libs/stub/qq-stub +++ b/libs/stub/qq-stub @@ -1 +1 @@ -Subproject commit 5a2a136690b1f74bc2406fdca93b2b2c0078cecf +Subproject commit 9377c8379cba32aca96213321bd2dacbf7cbc14e