From 38c311391221408d9964a758d9858510b9f492ca Mon Sep 17 00:00:00 2001 From: Xunflash Date: Sat, 7 Dec 2024 21:58:02 +1100 Subject: [PATCH] feat: MiniAppDirectJump supports user self-configuration --- .../top/xunflash/hook/MiniAppDirectJump.kt | 260 +++++++++++++++--- 1 file changed, 227 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/top/xunflash/hook/MiniAppDirectJump.kt b/app/src/main/java/top/xunflash/hook/MiniAppDirectJump.kt index 4cb702482a..3f56c0b753 100644 --- a/app/src/main/java/top/xunflash/hook/MiniAppDirectJump.kt +++ b/app/src/main/java/top/xunflash/hook/MiniAppDirectJump.kt @@ -22,27 +22,33 @@ 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 @@ -50,8 +56,12 @@ 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 @@ -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, + var regex: String, + var parseShortUrls: Boolean + ) + override val name: String = "小程序/分享卡片跳转APP" + override val valueState: MutableStateFlow 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 @@ -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) } } @@ -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") @@ -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 list.add(1, item) } -} +} \ No newline at end of file