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]
- }
}