Skip to content

Commit

Permalink
feat: MiniAppDirectJump supports user self-configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
Xunflash committed Dec 7, 2024
1 parent 3ee39fc commit 38c3113
Showing 1 changed file with 227 additions and 33 deletions.
260 changes: 227 additions & 33 deletions app/src/main/java/top/xunflash/hook/MiniAppDirectJump.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,46 @@

package top.xunflash.hook

import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.appcompat.widget.SwitchCompat
import cc.hicore.QApp.QAppUtils
import cc.ioctl.util.Reflex
import cc.ioctl.util.afterHookIfEnabled
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.xiaoniu.dispatcher.OnMenuBuilder
import com.xiaoniu.util.ContextUtils
import io.github.qauxv.util.xpcompat.XC_MethodHook.MethodHookParam
import io.github.qauxv.util.xpcompat.XposedBridge
import io.github.qauxv.util.xpcompat.XposedHelpers
import io.github.qauxv.R
import io.github.qauxv.base.IUiItemAgent
import io.github.qauxv.base.annotation.FunctionHookEntry
import io.github.qauxv.base.annotation.UiItemAgentEntry
import io.github.qauxv.dsl.FunctionEntryRouter
import io.github.qauxv.hook.CommonSwitchFunctionHook
import io.github.qauxv.hook.CommonConfigFunctionHook
import io.github.qauxv.util.CustomMenu
import io.github.qauxv.util.CustomMenu.createItemIconNt
import io.github.qauxv.util.Initiator
import io.github.qauxv.util.Toasts
import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem
import io.github.qauxv.util.dexkit.CArkAppItemBubbleBuilder
import io.github.qauxv.util.dexkit.DexKit
import io.github.qauxv.util.xpcompat.XC_MethodHook.MethodHookParam
import io.github.qauxv.util.xpcompat.XposedBridge
import io.github.qauxv.util.xpcompat.XposedHelpers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
Expand All @@ -60,18 +70,183 @@ import java.lang.reflect.Array
import java.net.HttpURLConnection
import java.net.URL


@FunctionHookEntry
@UiItemAgentEntry
object MiniAppDirectJump : CommonSwitchFunctionHook("MiniAppDirectJump::BaseChatPie", arrayOf(CArkAppItemBubbleBuilder, AbstractQQCustomMenuItem)),
object MiniAppDirectJump : CommonConfigFunctionHook(targets = arrayOf(CArkAppItemBubbleBuilder, AbstractQQCustomMenuItem)),
OnMenuBuilder {
override val name: String = "小程序/分享卡片跳转APP"

override val description: String = "长按小程序/分享卡片增加直接打开APP的菜单项,省去打开卡顿的小程序/网页的步骤(暂时仅支持哔哩哔哩)"
data class AppConfig(
var packageNames: List<String>,
var regex: String,
var parseShortUrls: Boolean
)

override val name: String = "小程序/分享卡片跳转APP"
override val valueState: MutableStateFlow<String?> by lazy {
MutableStateFlow(if (isEnabled) "已开启" else "禁用")
}
override val description: String = "长按小程序/分享卡片增加直接跳转到APP的菜单项,支持配置多个应用"
override val uiItemLocation = FunctionEntryRouter.Locations.Auxiliary.MESSAGE_CATEGORY

// 借用 cc/ioctl/hook/msg/CopyCardMsg.kt
override val onUiItemClickListener: (IUiItemAgent, Activity, View) -> Unit = { _, activity, _ ->
showConfigDialog(activity)
}

private var appConfigs = mutableListOf(
AppConfig(
listOf("tv.danmaku.bili", "com.bilibili.app.in", "tv.danmaku.bilibilihd", "com.bilibili.app.blue"),
"""https?:\\?/\\?/((b23\.tv)|([\w.]*bilibili\.com))[^"]*""",
true
),
AppConfig(
listOf("com.xingin.xhs"),
"""(?<="jumpUrl":")https?:\\?/\\?/[^"]*""",
false
),
AppConfig(
listOf("com.coolapk.market"),
"""(?<="jumpUrl":")https?:\\?/\\?/[^"]*""",
false
),
AppConfig(
listOf("com.zhihu.android"),
"""(?<="jumpUrl":")https?:\\?/\\?/[^"]*""",
false
)
)

@SuppressLint("SetTextI18n")
private fun showConfigDialog(ctx: Context) {
val currEnabled = isEnabled
val configsView = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
val padding = (16 * ctx.resources.displayMetrics.density).toInt()
setPadding(padding, padding, padding, padding)
}

val funcSwitch = SwitchCompat(ctx).apply {
isChecked = currEnabled
textSize = 16f
text = "功能开关 (开启后才生效)"
}
configsView.addView(funcSwitch)


val appConfigsContainer = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
}
configsView.addView(appConfigsContainer)

fun createAppConfigView(config: AppConfig? = null): View {
return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 16, 0, 16)
}
layoutParams = params

addView(View(ctx).apply {
setBackgroundColor(Color.LTGRAY)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
1
)
})

val packageNameEdit = EditText(ctx).apply {
setText(config?.packageNames?.joinToString(",") ?: "")
hint = "应用包名(英文逗号分隔,优先检查的放在前面)"
textSize = 14f
}
addView(packageNameEdit, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
))

val regexEdit = EditText(ctx).apply {
setText(config?.regex ?: "")
hint = "匹配正则表达式(可从卡片消息json字符串中获取链接)"
textSize = 14f
}
addView(regexEdit)

val parseShortLinksSwitch = SwitchCompat(ctx).apply {
isChecked = config?.parseShortUrls ?: false
textSize = 14f
text = "解析短链接还原"
}
addView(parseShortLinksSwitch)


val deleteButton = Button(ctx).apply {
text = "删除"
setOnClickListener {
appConfigsContainer.removeView(this@apply.parent as View)
}
}
addView(deleteButton)

tag = object {
fun getConfig() = AppConfig(
packageNameEdit.text.toString()
.split(",")
.map { it.trim() }
.filter { it.isNotEmpty() },
regexEdit.text.toString(),
parseShortLinksSwitch.isChecked
)
}
}
}

appConfigs.forEach { config ->
appConfigsContainer.addView(createAppConfigView(config))
}

val addButton = Button(ctx).apply {
text = "添加应用配置"
setOnClickListener {
appConfigsContainer.addView(createAppConfigView())
}
}
configsView.addView(addButton)

val scrollView = ScrollView(ctx).apply {
addView(configsView)
}

AlertDialog.Builder(ctx).apply {
setTitle("跳转配置")
setView(scrollView)
setCancelable(false)
setPositiveButton("确定") { _, _ ->
val newEnabled = funcSwitch.isChecked
if (newEnabled != currEnabled) {
isEnabled = newEnabled
valueState.value = if (newEnabled) "已开启" else "禁用"
}

appConfigs.clear()
for (i in 0 until appConfigsContainer.childCount) {
val view = appConfigsContainer.getChildAt(i)
val config = (view.tag as? Any)?.let {
it.javaClass.getDeclaredMethod("getConfig").invoke(it) as? AppConfig
} ?: continue
if (config.packageNames.isNotEmpty() && config.regex.isNotBlank()) {
appConfigs.add(config)
}
}

Toasts.success(ctx, "已保存配置")
}
setNegativeButton("取消", null)
show()
}
}

override fun initOnce() = throwOrTrue {
if (QAppUtils.isQQnt()) {
return@throwOrTrue
Expand Down Expand Up @@ -115,7 +290,7 @@ object MiniAppDirectJump : CommonSwitchFunctionHook("MiniAppDirectJump::BaseChat
val text = Reflex.invokeVirtual(
Reflex.getInstanceObjectOrNull(chatMessage, "ark_app_message"), "toAppXml"
) as String
toBiliApp(ctx, text)
toApp(ctx, text)
}
}

Expand All @@ -131,32 +306,51 @@ object MiniAppDirectJump : CommonSwitchFunctionHook("MiniAppDirectJump::BaseChat
}
}.getOrNull() ?: shortUrl

private fun toBiliApp(ctx: Activity, text: String) = MainScope().launch {
fun isPackageInstalled(packageName: String) = try {
ctx.packageManager.getApplicationInfo(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}
private fun toApp(ctx: Activity, text: String) = MainScope().launch {
var foundValidApp = false
for (appConfig in appConfigs) {
try {
val regex = appConfig.regex.toRegex()
val matchResult = regex.find(text) ?: continue
val url = matchResult.value.replace("\\/", "/")
val isShortLink = matchResult.groupValues.getOrNull(2)?.isNotEmpty() ?: false

val bilibiliPackages = arrayOf("tv.danmaku.bili", "com.bilibili.app.in", "tv.danmaku.bilibilihd", "com.bilibili.app.blue")
val firstValidPackage = bilibiliPackages.firstOrNull { isPackageInstalled(it) }
?: run { Toasts.info(ctx, "未找到可打开应用");return@launch }
val resolvedUrl = if (isShortLink && appConfig.parseShortUrls) {
Toasts.info(ctx, "解析短链接中")
withContext(Dispatchers.IO) { resolveShortUrl(url) }
} else url

val bilibiliLinkRegex = """https?:\\?/\\?/((b23\.tv)|([\w.]*bilibili\.com))[^"]*""".toRegex()
val (url, isShortLink) = bilibiliLinkRegex.find(text)?.let {
it.value.replace("\\/", "/") to it.groupValues[2].isNotEmpty()
} ?: run { Toasts.info(ctx, "链接解析失败");return@launch }
val resolvedUrl = if (isShortLink) withContext(Dispatchers.IO) { resolveShortUrl(url) } else url
for (packageName in appConfig.packageNames) {
if (!isPackageInstalled(ctx, packageName)) {
continue
}

try {
val uri = Uri.parse(resolvedUrl)
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(firstValidPackage)
try {
val uri = Uri.parse(resolvedUrl)
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(packageName)
}
if (ctx.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()) {
ctx.startActivity(intent)
foundValidApp = true
break
}
} catch (e: ActivityNotFoundException) {
continue
}
}
//if (foundValidApp) break
} catch (e: Exception) {
continue
}
ctx.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toasts.info(ctx, "应用打开失败")
}
if (!foundValidApp) Toasts.info(ctx, "未找到可打开的应用")
}

private fun isPackageInstalled(ctx: Context, packageName: String) = try {
ctx.packageManager.getApplicationInfo(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}

override val targetComponentTypes = arrayOf("com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent")
Expand All @@ -168,9 +362,9 @@ object MiniAppDirectJump : CommonSwitchFunctionHook("MiniAppDirectJump::BaseChat
val element = (msg.javaClass.declaredMethods.first {
it.returnType == MsgElement::class.java && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }.invoke(msg) as MsgElement).arkElement
toBiliApp(ctx, element.bytesData)
toApp(ctx, element.bytesData)
}
val list = param.result as MutableList<Any>
list.add(1, item)
}
}
}

0 comments on commit 38c3113

Please sign in to comment.