Skip to content

Commit

Permalink
feat: RepeatToImg on QQ(8.9.88,9.0.75)
Browse files Browse the repository at this point in the history
  • Loading branch information
HdShare committed Jul 13, 2024
1 parent 7152afd commit 20ef3e0
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/com/xiaoniu/dispatcher/MenuBuilderHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import io.github.qauxv.util.Initiator
import me.hd.hook.menu.CopyMarkdown
import me.hd.hook.menu.EditTextContent
import me.hd.hook.menu.RecallMsgRecord
import me.hd.hook.menu.RepeatToImg
import me.ketal.hook.PicCopyToClipboard
import me.qcuncle.hook.TranslateTextMsg
import top.xunflash.hook.MiniAppDirectJump
Expand All @@ -61,6 +62,7 @@ object MenuBuilderHook : BasePersistBackgroundHook() {
EditTextContent,
TranslateTextMsg,
RecallMsgRecord,
RepeatToImg,
)

override fun initOnce(): Boolean {
Expand Down
181 changes: 181 additions & 0 deletions app/src/main/java/me/hd/hook/menu/RepeatToImg.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* QAuxiliary - An Xposed module for QQ/TIM
* Copyright (C) 2019-2024 QAuxiliary developers
* https://github.com/cinit/QAuxiliary
*
* This software is an opensource software: you can redistribute it
* and/or modify it under the terms of the General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version 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 General Public License for more details.
*
* You should have received a copy of the General Public License
* along with this software.
* If not, see
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
*/

package me.hd.hook.menu

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import cc.hicore.message.common.MsgSender
import cc.ioctl.util.LayoutHelper
import cc.ioctl.util.hookAfterIfEnabled
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.xiaoniu.dispatcher.OnMenuBuilder
import com.xiaoniu.util.ContextUtils
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import io.github.qauxv.R
import io.github.qauxv.base.annotation.FunctionHookEntry
import io.github.qauxv.base.annotation.UiItemAgentEntry
import io.github.qauxv.bridge.kernelcompat.ContactCompat
import io.github.qauxv.dsl.FunctionEntryRouter
import io.github.qauxv.hook.CommonSwitchFunctionHook
import io.github.qauxv.ui.CommonContextWrapper
import io.github.qauxv.util.CustomMenu
import io.github.qauxv.util.Initiator
import io.github.qauxv.util.QQVersion
import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem
import io.github.qauxv.util.requireMinQQVersion
import xyz.nextalone.util.findHostView
import java.io.File
import java.io.FileOutputStream
import java.io.IOException


@FunctionHookEntry
@UiItemAgentEntry
object RepeatToImg : CommonSwitchFunctionHook(
targets = arrayOf(AbstractQQCustomMenuItem)
), OnMenuBuilder {

override val name = "复读消息为图片"
override val description = "消息菜单中新增功能"
override val uiItemLocation = FunctionEntryRouter.Locations.Auxiliary.EXPERIMENTAL_CATEGORY
override val isAvailable = requireMinQQVersion(QQVersion.QQ_8_9_88)

private val msgViewHashMap = HashMap<Long, ViewGroup>()

override fun initOnce(): Boolean {
val clazz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.AIOBubbleMsgItemVB")
val method = clazz.declaredMethods.single { method ->
val params = method.parameterTypes
params.size == 4 && params[0] == Int::class.java && params[2] == List::class.java && params[3] == Bundle::class.java
}
hookAfterIfEnabled(method) { param ->
val msg = param.args[1]
val msgRecord = XposedHelpers.callMethod(msg, "getMsgRecord") as MsgRecord
clazz.declaredFields.single { field ->
field.type == View::class.java
}.apply {
isAccessible = true
msgViewHashMap[msgRecord.msgId] = get(param.thisObject) as ViewGroup
}
}
return true
}

private fun getViewBitmap(view: ViewGroup): Bitmap {
return Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888).apply {
view.setBackgroundColor(0xFFF1F1F1.toInt())
view.draw(Canvas(this))
}
}

private fun saveAndSendScreenshot(context: Context, view: ViewGroup, msgRecord: MsgRecord) {
val bitmap = getViewBitmap(view)

val (contentId, avatarId, nickId) = when {
requireMinQQVersion(QQVersion.QQ_9_0_75) -> Triple("ny6", "t6o", "tiq")
requireMinQQVersion(QQVersion.QQ_8_9_88) -> Triple("ntl", "skw", "swf")
else -> Triple("ntl", "skw", "swf")
}
val dp5 = LayoutHelper.dip2px(context, 5f)
val contentView = view.findHostView<LinearLayout>(contentId)!!
val croppedBitmap = if (msgRecord.chatType == 1) {
val avatarView = view.findHostView<FrameLayout>(avatarId)!!
if (msgRecord.sendType == 0) {
Bitmap.createBitmap(
bitmap, 0, avatarView.top - dp5,
contentView.right, contentView.bottom - (avatarView.top - dp5)
)
} else {
Bitmap.createBitmap(
bitmap, contentView.left, avatarView.top - dp5,
bitmap.width - contentView.left, contentView.bottom - (avatarView.top - dp5)
)
}
} else if (msgRecord.chatType == 2) {
val nickView = view.findHostView<FrameLayout>(nickId)!!
if (msgRecord.sendType == 0) {
Bitmap.createBitmap(
bitmap, 0, nickView.top - dp5,
maxOf(contentView.right, nickView.right), contentView.bottom - (nickView.top - dp5)
)
} else {
Bitmap.createBitmap(
bitmap, minOf(contentView.left, nickView.left), nickView.top - dp5,
bitmap.width - minOf(contentView.left, nickView.left), contentView.bottom - (nickView.top - dp5)
)
}
} else {
return@saveAndSendScreenshot
}

val imgFile = File(context.externalCacheDir, "hd_temp/img").apply { parentFile!!.mkdirs() }
try {
FileOutputStream(imgFile).use { fos ->
croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
}
MsgSender.send_pic_by_contact(
ContactCompat(msgRecord.chatType, msgRecord.peerUid, ""),
imgFile.absolutePath
)
} catch (e: IOException) {
e.printStackTrace()
}
}

override val targetComponentTypes = arrayOf(
"com.tencent.mobileqq.aio.msglist.holder.component.anisticker.AIOAniStickerContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.facebubble.AIOFaceBubbleContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.file.AIOFileContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.flashpic.AIOFlashPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.LocationShare.AIOLocationShareComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.markdown.AIOMarkdownContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.marketface.AIOMarketFaceComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.mix.AIOMixContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.poke.AIOPokeContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.ptt.AIOPttContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.reply.AIOReplyComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.video.AIOVideoContentComponent",
"com.tencent.mobileqq.aio.qwallet.AIOQWalletComponent",
)

override fun onGetMenuNt(msg: Any, componentType: String, param: XC_MethodHook.MethodHookParam) {
if (!isEnabled) return
val item = CustomMenu.createItemIconNt(msg, "复读图片", R.drawable.ic_item_repeat_72dp, R.id.item_repeat_to_img) {
val context = CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity())
val msgRecord = XposedHelpers.callMethod(msg, "getMsgRecord") as MsgRecord
val msgView = msgViewHashMap[msgRecord.msgId]
msgView?.let { saveAndSendScreenshot(context, it, msgRecord) }
}
param.result = listOf(item) + param.result as List<*>
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/ids.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<item type="id" name="item_edit_to_send" />
<item type="id" name="item_translate" />
<item type="id" name="item_recall_msgRecord" />
<item type="id" name="item_repeat_to_img" />
<item type="id" name="rootBounceScrollView" />
<item type="id" name="rootMainLayout" />
<item type="id" name="rootMainList" />
Expand Down

0 comments on commit 20ef3e0

Please sign in to comment.