diff --git a/app/src/main/java/me/singleneuron/util/NonNTMessageStyleNotification.kt b/app/src/main/java/me/singleneuron/util/NonNTMessageStyleNotification.kt new file mode 100644 index 0000000000..5e54bed47f --- /dev/null +++ b/app/src/main/java/me/singleneuron/util/NonNTMessageStyleNotification.kt @@ -0,0 +1,376 @@ +/* + * QAuxiliary - An Xposed module for QQ/TIM + * Copyright (C) 2019-2023 QAuxiliary developers + * 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 me.singleneuron.util + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.os.Build +import android.view.WindowInsets +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.MessagingStyle +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import cc.chenhe.qqnotifyevo.utils.NotifyChannel +import cc.chenhe.qqnotifyevo.utils.getChannelId +import cc.ioctl.util.Reflex +import cc.ioctl.util.hookAfterIfEnabled +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XC_MethodReplacement +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import io.github.qauxv.bridge.AppRuntimeHelper +import io.github.qauxv.bridge.ChatActivityFacade +import io.github.qauxv.bridge.SessionInfoImpl +import io.github.qauxv.util.Initiator +import io.github.qauxv.util.LicenseStatus +import io.github.qauxv.util.hostInfo +import moe.zapic.hook.MessagingStyleNotification +import xyz.nextalone.util.clazz +import xyz.nextalone.util.method + +// FIXME: current not working: channel not assigned or overwritten + +class NonNTMessageStyleNotification(private val parent: MessagingStyleNotification) { + private val numRegex = Regex("""\((\d+)\S{1,3}新消息\)?$""") + private val senderName = Regex("""^.*?: """) + private val activityName = "com.tencent.mobileqq.activity.miniaio.MiniChatActivity" + + private val historyMessage: HashMap = HashMap() + private var windowHeight = -1 + + var isEnabled: Boolean + get() = this.parent.isEnabled + set(_) {} + + @Throws(Exception::class) + fun hook(): Boolean { + val buildNotification = Reflex.findSingleMethod( + Initiator._MobileQQServiceExtend()!!, + android.app.Notification::class.java, + false, + Intent::class.java, + Bitmap::class.java, + String::class.java, + String::class.java, + String::class.java + ) + val context = hostInfo.application + parent.hookAfterIfEnabled(buildNotification) { param -> + val intent = param.args[0] as Intent + // TODO: 2022-07-14 uin may be null + val uin = intent.getStringExtra("uin") + ?: intent.getStringExtra("param_uin")!! + val isTroop = intent.getIntExtra( + "uintype", + intent.getIntExtra("param_uinType", -1) + ) + if (isTroop != 0 && isTroop != 1 && isTroop != 3000) return@hookAfterIfEnabled + val bitmap = param.args[1] as Bitmap? + var title = numRegex.replace(param.args[3] as String, "") + var text = param.args[4] as String + val key = when (isTroop) { + 1 -> "group_$uin" + else -> "private_$uin" + } + val oldNotification = param.result as Notification + val notificationId = + intent.getIntExtra("KEY_NOTIFY_ID_FROM_PROCESSOR", -113) + + var channelId: NotifyChannel = NotifyChannel.FRIEND + if (title.contains("[特别关心]")) { + channelId = NotifyChannel.FRIEND_SPECIAL + title = title.removePrefix("[特别关心]") + } + + var messageStyle = historyMessage[notificationId] + if (messageStyle == null) { + messageStyle = MessagingStyle( + Person.Builder() + .setName(title) + .setIcon(IconCompat.createWithBitmap(bitmap!!)) + .setImportant(channelId == NotifyChannel.FRIEND_SPECIAL) + .setKey(uin) + .build() + ) + historyMessage[notificationId] = messageStyle + } + + var person: Person? = null + + if (isTroop == 1) { + val sender = senderName.find(text)?.value?.replace(": ", "") + text = senderName.replace(text, "") + val senderUin = intent.getStringExtra("param_fromuin")!! + /*throwOrTrue { + val senderUin = intent.getStringExtra("param_fromuin") + bitmap = face.getBitmapFromCache(TYPE_USER,senderUin) + }*/ + person = Person.Builder() + .setName(sender) + .setKey(senderUin) + .setIcon(parent.avatarHelper.getAvatar(senderUin)) + .build() + messageStyle.conversationTitle = title + messageStyle.isGroupConversation = true + channelId = NotifyChannel.GROUP + } + + val message = MessagingStyle.Message(text, oldNotification.`when`, person) + messageStyle.addMessage(message) + + //Log.d(historyMessage.toString()) + val builder = NotificationCompat.Builder(context, oldNotification) + .setContentTitle(null) + .setContentText(null) + .setLargeIcon(null) + .setStyle(messageStyle) + if (isTroop == 1) { + builder.setLargeIcon(bitmap) + } + + val remoteInput: RemoteInput = RemoteInput.Builder("KEY_REPLY").run { + setLabel("回复") + build() + } + val replyIntent = Intent(context, "com.tencent.mobileqq.servlet.NotificationClickReceiver".clazz).apply { + putExtra("TO_UIN", uin) + putExtra("UIN_TYPE", isTroop) + putExtra("NOTIFY_ID", notificationId) + } + + val replyPendingIntent: PendingIntent = + PendingIntent.getBroadcast( + context, + uin.toLong().toInt(), + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 + ) + val replyAction = NotificationCompat.Action.Builder( + android.R.drawable.ic_menu_edit, + "回复", + replyPendingIntent + ) + .addRemoteInput(remoteInput) + .build() + builder.addAction(replyAction) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val newIntent = intent.clone() as Intent + newIntent.component = ComponentName( + context, + activityName.clazz!! + ) + newIntent.putExtra("key_mini_from", 2) + newIntent.putExtra("minaio_height_ration", 1f) + newIntent.putExtra("minaio_scaled_ration", 1f) + newIntent.putExtra( + "public_fragment_class", + "com.tencent.mobileqq.activity.miniaio.MiniChatFragment" + ) + val bubbleIntent = PendingIntent.getActivity( + context, + uin.toLong().toInt(), // uin may be lager than Int.MAX_VALUE but small than 2^32-1 + newIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + + val bubbleData = NotificationCompat.BubbleMetadata.Builder( + bubbleIntent, + // FIXME: 2022-06-24 handle NPE if bitmap is null + IconCompat.createWithBitmap(bitmap!!) + ) + .setDesiredHeight(600) + .build() + + val shortcut = + ShortcutInfoCompat.Builder(context, key) + .setIntent(intent) + .setLongLived(true) + .setShortLabel(title) + .setIcon(bubbleData.icon!!) + .build() + + ShortcutManagerCompat.pushDynamicShortcut( + context, + shortcut + ) + + builder.apply { + setShortcutInfo(shortcut) + bubbleMetadata = bubbleData + setChannelId(getChannelId(channelId)) + } + } + param.result = builder.build() + } + + XposedHelpers.findAndHookMethod( + "com.tencent.mobileqq.servlet.NotificationClickReceiver".clazz, + "onReceive", + Context::class.java, + Intent::class.java, + object : XC_MethodHook() { + @SuppressLint("NotificationPermission") + override fun beforeHookedMethod(param: MethodHookParam) { + val ctx = param.args[0] as Context + val intent = param.args[1] as Intent + val uinType = intent.getIntExtra("UIN_TYPE", -1) + if (uinType != -1) { + param.result = null + val uin = intent.getStringExtra("TO_UIN") ?: return + val result = RemoteInput.getResultsFromIntent(intent)?.getString("KEY_REPLY") ?: return + val selfUin = AppRuntimeHelper.getAccount() + // send message + ChatActivityFacade.sendMessage( + AppRuntimeHelper.getQQAppInterface(), + hostInfo.application, + SessionInfoImpl.createSessionInfo(uin, uinType), + result + ) + + // update exist notification + val notifyId = intent.getIntExtra("NOTIFY_ID", -113) + val notificationManager = ctx.getSystemService(NotificationManager::class.java) + val origin = notificationManager.activeNotifications.find { + it.id == notifyId + } ?: return + val msg = historyMessage[notifyId] ?: return + val sendMsg: MessagingStyle.Message = MessagingStyle.Message( + result, + System.currentTimeMillis(), + Person.Builder().setName("我").setIcon(parent.avatarHelper.getAvatar(selfUin)).setKey(selfUin).build() + ) + msg.addMessage(sendMsg) + historyMessage[notifyId] = msg + val newNotification = + NotificationCompat.Builder(ctx, origin.notification) + .setStyle(msg) + .setSilent(true) + .build() + notificationManager.notify(origin.id, newNotification) + } + } + } + ) + + XposedHelpers.findAndHookMethod( + "com.tencent.commonsdk.util.notification.QQNotificationManager".clazz, + "cancel", String::class.java, Int::class.javaPrimitiveType, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + if (!isEnabled or LicenseStatus.sDisableCommonHooks) return + if (param.args[0] as String != "MobileQQServiceWrapper.showMsgNotification") { + historyMessage.remove(param.args[1] as Int) + } else { + // stop QQ cancel the old message to prevent message flashing in notification area + param.result = null + } + } + } + ) + XposedHelpers.findAndHookMethod( + "com.tencent.commonsdk.util.notification.QQNotificationManager".clazz, + "cancelAll", + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + if (!isEnabled or LicenseStatus.sDisableCommonHooks) return + historyMessage.clear() + } + } + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Fix the height of launch from bubble + XposedHelpers.findAndHookMethod( + "com.tencent.widget.immersive.ImmersiveUtils".clazz, + "getStatusBarHeight", + Context::class.java, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + if (!isEnabled or LicenseStatus.sDisableCommonHooks) return + if ((param.args[0] as? Activity)?.isLaunchedFromBubble == true) + param.result = 0 + } + }) + // Don't clear notification when launching from bubble + XposedHelpers.findAndHookMethod("com.tencent.mobileqq.app.lifecycle.BaseActivityLifecycleCallbackImpl".clazz, + "doOnWindowFocusChanged", + Activity::class.java, + Boolean::class.javaPrimitiveType, + object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any? { + val id = Thread.currentThread().id + val unhook = if (isEnabled && !LicenseStatus.sDisableCommonHooks && + param.args[1] as Boolean && + (param.args[0] as Activity).isLaunchedFromBubble + ) + XposedHelpers.findAndHookMethod("com.tencent.mobileqq.app.QQAppInterface".clazz, + "removeNotification", + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + if (id == Thread.currentThread().id) param.result = null + } + } + ) else null + val result = XposedBridge.invokeOriginalMethod( + param.method, + param.thisObject, + param.args + ) + unhook?.unhook() + return result + } + } + ) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + XposedBridge.hookMethod(activityName.clazz!!.method("doOnStart")!!, object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam?) { + val activity = param!!.thisObject as Activity + val rootView = activity.window.decorView + windowHeight = activity.window.attributes.height + rootView.setOnApplyWindowInsetsListener { _, insets -> + val attr = activity.window.attributes + if (insets.isVisible(WindowInsets.Type.ime()) && attr.height != windowHeight) { + attr.height = windowHeight + activity.window.attributes = attr + } + insets + } + } + }) + } + return true + } +} diff --git a/app/src/main/java/moe/zapic/hook/MessagingStyleNotification.kt b/app/src/main/java/moe/zapic/hook/MessagingStyleNotification.kt index 994c47cbd3..5571ebee24 100644 --- a/app/src/main/java/moe/zapic/hook/MessagingStyleNotification.kt +++ b/app/src/main/java/moe/zapic/hook/MessagingStyleNotification.kt @@ -22,32 +22,17 @@ package moe.zapic.hook -import android.annotation.SuppressLint -import android.app.Activity import android.app.Notification import android.app.NotificationChannel import android.app.NotificationChannelGroup import android.app.NotificationManager import android.app.PendingIntent -import android.content.ComponentName -import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode -import android.graphics.Rect -import android.graphics.RectF import android.os.Build -import android.util.LruCache -import android.view.WindowInsets import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person -import androidx.core.app.RemoteInput import androidx.core.content.LocusIdCompat import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -61,31 +46,22 @@ import cc.ioctl.util.Reflex import cc.ioctl.util.hookAfterIfEnabled import cc.ioctl.util.hookBeforeIfEnabled import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers 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.ChatActivityFacade -import io.github.qauxv.bridge.FaceImpl -import io.github.qauxv.bridge.SessionInfoImpl import io.github.qauxv.dsl.FunctionEntryRouter import io.github.qauxv.hook.CommonSwitchFunctionHook import io.github.qauxv.ui.ResUtils -import io.github.qauxv.util.Initiator import io.github.qauxv.util.LicenseStatus -import io.github.qauxv.util.Log import io.github.qauxv.util.QQVersion import io.github.qauxv.util.SyncUtils import io.github.qauxv.util.hostInfo +import me.singleneuron.util.NonNTMessageStyleNotification import moe.zapic.util.QQAvatarHelper import moe.zapic.util.QQRecentContactInfo import xyz.nextalone.util.clazz import xyz.nextalone.util.get -import xyz.nextalone.util.hookAfter -import xyz.nextalone.util.method -import java.io.File import java.lang.reflect.Method import java.util.WeakHashMap @@ -101,7 +77,7 @@ object MessagingStyleNotification : CommonSwitchFunctionHook(SyncUtils.PROC_ANY) private val notificationInfoMap = WeakHashMap>() private val historyMessage: HashMap = HashMap() - private val avatarHelper = QQAvatarHelper() + val avatarHelper = QQAvatarHelper() @Throws(Exception::class) override fun initOnce(): Boolean { @@ -110,15 +86,14 @@ object MessagingStyleNotification : CommonSwitchFunctionHook(SyncUtils.PROC_ANY) } if (!HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) { - return hookNonNt() + return NonNTMessageStyleNotification(this).hook() } - - val cNotificationFacade = "com.tencent.qqnt.notification.NotificationFacade".clazz!! + val cNotificationFacede = "com.tencent.qqnt.notification.NotificationFacade".clazz!! val cAppRuntime = "mqq.app.AppRuntime".clazz!! val cCommonInfo = "com.tencent.qqnt.kernel.nativeinterface.NotificationCommonInfo".clazz!! val cRecentInfo = "com.tencent.qqnt.kernel.nativeinterface.RecentContactInfo".clazz!! val postNotification = Reflex.findSingleMethod( - cNotificationFacade, + cNotificationFacede, null, false, Notification::class.java, @@ -126,12 +101,12 @@ object MessagingStyleNotification : CommonSwitchFunctionHook(SyncUtils.PROC_ANY) ) lateinit var buildNotification: Method lateinit var recentInfoBuilder: Method - cNotificationFacade.declaredMethods.forEach { + cNotificationFacede.declaredMethods.forEach { if (it.parameterTypes.size != 3) return@forEach if (it.parameterTypes[0] != cAppRuntime) return@forEach if (it.parameterTypes[2] == cCommonInfo) { buildNotification = it - } else if (it.parameterTypes[1] == cRecentInfo){ + } else if (it.parameterTypes[1] == cRecentInfo) { recentInfoBuilder = it } } @@ -297,383 +272,11 @@ object MessagingStyleNotification : CommonSwitchFunctionHook(SyncUtils.PROC_ANY) if (notificationChannels.any { notificationChannel -> notificationManager.getNotificationChannel(notificationChannel.id) == null }) { - Log.i("QNotifyEvolutionXp: Creating channels...") + XposedBridge.log("QNotifyEvolutionXp: Creating channels...") notificationManager.createNotificationChannelGroup(notificationChannelGroup) notificationManager.createNotificationChannels(notificationChannels) } } - private fun hookNonNt() : Boolean { - val numRegex = Regex("""\((\d+)\S{1,3}新消息\)?$""") - val senderName = Regex("""^.*?: """) - val activityName = "com.tencent.mobileqq.activity.miniaio.MiniChatActivity" - val avatarCachePath: String? - - val historyMessage: HashMap = HashMap() - val avatarCache: LruCache = LruCache(50) - var windowHeight: Int - - val buildNotification = Reflex.findSingleMethod( - Initiator._MobileQQServiceExtend()!!, - android.app.Notification::class.java, - false, - Intent::class.java, - Bitmap::class.java, - String::class.java, - String::class.java, - String::class.java - ) - val context = hostInfo.application - avatarCachePath = File( - context.getExternalFilesDir(null)?.parent, - "Tencent/MobileQQ/head/_hd" - ).absolutePath - - hookAfterIfEnabled(buildNotification) { param -> - val intent = param.args[0] as Intent - // TODO: 2022-07-14 uin may be null - val uin = intent.getStringExtra("uin") - ?: intent.getStringExtra("param_uin")!! - val isTroop = intent.getIntExtra( - "uintype", - intent.getIntExtra("param_uinType", -1) - ) - if (isTroop != 0 && isTroop != 1 && isTroop != 3000) return@hookAfterIfEnabled - val bitmap = param.args[1] as Bitmap? - var title = numRegex.replace(param.args[3] as String, "") - var text = param.args[4] as String - val key = when (isTroop) { - 1 -> "group_$uin" - else -> "private_$uin" - } - val oldNotification = param.result as Notification - val notificationId = - intent.getIntExtra("KEY_NOTIFY_ID_FROM_PROCESSOR", -113) - - var channelId: NotifyChannel = NotifyChannel.FRIEND - if (title.contains("[特别关心]")) { - channelId = NotifyChannel.FRIEND_SPECIAL - title = title.removePrefix("[特别关心]") - } - - var messageStyle = historyMessage[notificationId] - if (messageStyle == null) { - messageStyle = MessagingStyle( - Person.Builder() - .setName(title) - .setIcon(IconCompat.createWithBitmap(bitmap!!)) - .setImportant(channelId == NotifyChannel.FRIEND_SPECIAL) - .setKey(uin) - .build() - ) - historyMessage[notificationId] = messageStyle - } - - var person: Person? = null - - if (isTroop == 1) { - val sender = senderName.find(text)?.value?.replace(": ", "") - text = senderName.replace(text, "") - val senderUin = intent.getStringExtra("param_fromuin")!! - /*throwOrTrue { - val senderUin = intent.getStringExtra("param_fromuin") - bitmap = face.getBitmapFromCache(TYPE_USER,senderUin) - }*/ - person = Person.Builder() - .setName(sender) - .setKey(senderUin) - .setIcon(getAvatar(senderUin, avatarCachePath, avatarCache)) - .build() - messageStyle.conversationTitle = title - messageStyle.isGroupConversation = true - channelId = NotifyChannel.GROUP - } - - val message = MessagingStyle.Message(text, oldNotification.`when`, person) - messageStyle.addMessage(message) - - val builder = NotificationCompat.Builder(context, oldNotification) - .setContentTitle(null) - .setContentText(null) - .setLargeIcon(null) - .setStyle(messageStyle) - if (isTroop == 1) { - builder.setLargeIcon(bitmap) - } - - val remoteInput: RemoteInput = RemoteInput.Builder("KEY_REPLY").run { - setLabel("回复") - build() - } - val replyIntent = Intent(context, "com.tencent.mobileqq.servlet.NotificationClickReceiver".clazz).apply { - putExtra("TO_UIN", uin) - putExtra("UIN_TYPE", isTroop) - putExtra("NOTIFY_ID", notificationId) - } - - val replyPendingIntent: PendingIntent = - PendingIntent.getBroadcast( - context, - uin.toLong().toInt(), - replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 - ) - val replyAction = NotificationCompat.Action.Builder( - android.R.drawable.ic_menu_edit, - "回复", - replyPendingIntent - ) - .addRemoteInput(remoteInput) - .build() - builder.addAction(replyAction) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val newIntent = intent.clone() as Intent - newIntent.component = ComponentName( - context, - activityName.clazz!! - ) - newIntent.putExtra("key_mini_from", 2) - newIntent.putExtra("minaio_height_ration", 1f) - newIntent.putExtra("minaio_scaled_ration", 1f) - newIntent.putExtra( - "public_fragment_class", - "com.tencent.mobileqq.activity.miniaio.MiniChatFragment" - ) - val bubbleIntent = PendingIntent.getActivity( - context, - uin.toLong().toInt(), // uin may be lager than Int.MAX_VALUE but small than 2^32-1 - newIntent, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT - ) - - val bubbleData = NotificationCompat.BubbleMetadata.Builder( - bubbleIntent, - // FIXME: 2022-06-24 handle NPE if bitmap is null - IconCompat.createWithBitmap(bitmap!!) - ) - .setDesiredHeight(600) - .build() - - val shortcut = - ShortcutInfoCompat.Builder(context, key) - .setIntent(intent) - .setLongLived(true) - .setShortLabel(title) - .setIcon(bubbleData.icon!!) - .build() - - ShortcutManagerCompat.pushDynamicShortcut( - context, - shortcut - ) - - builder.apply { - setShortcutInfo(shortcut) - bubbleMetadata = bubbleData - setChannelId(getChannelId(channelId)) - } - } - Log.i("QNotifyEvolutionXp: send as channel " + channelId.name) - param.result = builder.build() - } - - XposedHelpers.findAndHookMethod( - "com.tencent.mobileqq.servlet.NotificationClickReceiver".clazz, - "onReceive", - Context::class.java, - Intent::class.java, - object : XC_MethodHook() { - @SuppressLint("NotificationPermission") - override fun beforeHookedMethod(param: MethodHookParam) { - val ctx = param.args[0] as Context - val intent = param.args[1] as Intent - val uinType = intent.getIntExtra("UIN_TYPE", -1) - if (uinType != -1) { - param.result = null - val uin = intent.getStringExtra("TO_UIN") ?: return - val result = RemoteInput.getResultsFromIntent(intent)?.getCharSequence("KEY_REPLY") ?: return - val selfUin = AppRuntimeHelper.getAccount() - // send message - ChatActivityFacade.sendMessage( - AppRuntimeHelper.getQQAppInterface(), - hostInfo.application, - SessionInfoImpl.createSessionInfo(uin, uinType), - result.toString() - ) - - // update exist notification - val notifyId = intent.getIntExtra("NOTIFY_ID", -113) - val notificationManager = ctx.getSystemService(NotificationManager::class.java) - val origin = notificationManager.activeNotifications.find { - it.id == notifyId - } ?: return - val msg = historyMessage[notifyId]?: return - val sendMsg: MessagingStyle.Message = MessagingStyle.Message( - result, - System.currentTimeMillis(), - Person.Builder().setName("我").setIcon(getAvatar(selfUin, avatarCachePath, avatarCache)).setKey(selfUin).build() - ) - msg.addMessage(sendMsg) - historyMessage[notifyId] = msg - val newNotification = - NotificationCompat.Builder(ctx, origin.notification) - .setStyle(msg) - .setSilent(true) - .build() - notificationManager.notify(origin.id, newNotification) - } - } - } - ) - XposedHelpers.findAndHookMethod( - "com.tencent.commonsdk.util.notification.QQNotificationManager".clazz, - "cancel", String::class.java, Int::class.javaPrimitiveType, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - if (!isEnabled or LicenseStatus.sDisableCommonHooks) return - if (param.args[0] as String != "MobileQQServiceWrapper.showMsgNotification") { - historyMessage.remove(param.args[1] as Int) - } else { - // stop QQ cancel the old message to prevent message flashing in notification area - param.result = null - } - } - } - ) - XposedHelpers.findAndHookMethod( - "com.tencent.commonsdk.util.notification.QQNotificationManager".clazz, - "cancelAll", - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - if (!isEnabled or LicenseStatus.sDisableCommonHooks) return - historyMessage.clear() - } - } - ) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Fix the height of launch from bubble - XposedHelpers.findAndHookMethod( - "com.tencent.widget.immersive.ImmersiveUtils".clazz, - "getStatusBarHeight", - Context::class.java, - object : XC_MethodHook() { - override fun afterHookedMethod(param: MethodHookParam) { - if (!isEnabled or LicenseStatus.sDisableCommonHooks) return - if ((param.args[0] as? Activity)?.isLaunchedFromBubble == true) - param.result = 0 - } - }) - // Don't clear notification when launching from bubble - XposedHelpers.findAndHookMethod("com.tencent.mobileqq.app.lifecycle.BaseActivityLifecycleCallbackImpl".clazz, - "doOnWindowFocusChanged", - Activity::class.java, - Boolean::class.javaPrimitiveType, - object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Any? { - val id = Thread.currentThread().id - val unhook = if (isEnabled && !LicenseStatus.sDisableCommonHooks && - param.args[1] as Boolean && - (param.args[0] as Activity).isLaunchedFromBubble - ) - XposedHelpers.findAndHookMethod("com.tencent.mobileqq.app.QQAppInterface".clazz, - "removeNotification", - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - if (id == Thread.currentThread().id) param.result = null - } - } - ) else null - val result = XposedBridge.invokeOriginalMethod( - param.method, - param.thisObject, - param.args - ) - unhook?.unhook() - return result - } - } - ) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - activityName.clazz!!.method("doOnStart")!!.hookAfter(this) { - val activity = it.thisObject as Activity - val rootView = activity.window.decorView - windowHeight = activity.window.attributes.height - rootView.setOnApplyWindowInsetsListener { _, insets -> - val attr = activity.window.attributes - if (insets.isVisible(WindowInsets.Type.ime()) && attr.height != windowHeight) { - attr.height = windowHeight - activity.window.attributes = attr - } - insets - } - } - } - return true - } - - // Non NT uses below - private fun toMD5(uin: String): String { - val toMD5Method = "com.tencent.qphone.base.util.MD5".clazz!!.getMethod("toMD5", String::class.java) - return toMD5Method.invoke(null, uin) as String - } - - private fun getCroppedBitmap(bm: Bitmap): Bitmap { - var w: Int = bm.width - var h: Int = bm.height - - val radius = if (w < h) w else h - w = radius - h = radius - - val bmOut = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bmOut) - - val paint = Paint() - paint.isAntiAlias = true - paint.color = -0xbdbdbe - - val rect = Rect(0, 0, w, h) - val rectF = RectF(rect) - - canvas.drawARGB(0, 0, 0, 0) - canvas.drawCircle( - rectF.left + rectF.width() / 2, rectF.top + rectF.height() / 2, - (radius / 2).toFloat(), paint - ) - - paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) - canvas.drawBitmap(bm, rect, rect, paint) - - return bmOut - } - - private fun getAvatarFromFile(uin: String, avatarCachePath: String): Bitmap? { - val md5 = toMD5(toMD5(toMD5(uin) + uin) + uin) - val file = File(avatarCachePath, "$md5.jpg_") - if (file.isFile) { - return getCroppedBitmap(BitmapFactory.decodeFile(file.absolutePath)) - } - return null - } - - private fun getAvatar(uin: String, avatarCachePath: String, avatarCache: LruCache): IconCompat? { - if (avatarCache[uin] == null) { - var cached = getAvatarFromFile(uin,avatarCachePath) - if (cached == null) { - val face = FaceImpl.getInstance() - cached = face.getBitmapFromCache(1, uin) - if (cached == null) { - face.requestDecodeFace(1, uin) - return null - } - } - avatarCache.put(uin, IconCompat.createWithBitmap(cached)) - } - return avatarCache[uin] - } }