diff --git a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt index b41bc26726..f3e26cb3c5 100644 --- a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt +++ b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt @@ -49,8 +49,9 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex val rxGeneralResponseClass by Weak { "com.bilibili.okretro.call.rxjava.RxGeneralResponse" from mClassLoader } val fastJsonClass by Weak { mHookInfo.fastJson.class_ from mClassLoader } val bangumiUniformSeasonClass by Weak { mHookInfo.bangumiSeason from mClassLoader } - val sectionClass by Weak { mHookInfo.section.class_ from mClassLoader } - val viewHolderClass by Weak { mHookInfo.section.viewHolder from mClassLoader } + val kingPositionComponentClass by Weak { mHookInfo.autoLike.kingPositionComponent.class_ from mClassLoader } + val storyAbsControllerClass by Weak { mHookInfo.autoLike.storyAbsController.class_ from mClassLoader } + val storyDetailClass by Weak { "com.bilibili.video.story.StoryDetail" from mClassLoader } val retrofitResponseClass by Weak { mHookInfo.retrofitResponse from mClassLoader } val themeHelperClass by Weak { mHookInfo.themeHelper.class_ from mClassLoader } val themeIdHelperClass by Weak { mHookInfo.themeIdHelper.class_ from mClassLoader } @@ -179,6 +180,8 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex val fastjsonFieldAnnotation by Weak { "com.alibaba.fastjson.annotation.JSONField" from mClassLoader } val gsonFieldAnnotation by Weak { "com.google.gson.annotations.SerializedName" from mClassLoader } val pegasusParserClass by Weak { mHookInfo.pegasusParser from mClassLoader } + val resolveClientCompanionClass by Weak { mHookInfo.resolveClientCompanion.class_ from mClassLoader } + val videoDownloadEntryClass by Weak { "com.bilibili.videodownloader.model.VideoDownloadEntry" from mClassLoader } // for v8.17.0+ val useNewMossFunc = instance.viewMossClass?.declaredMethods?.any { @@ -237,11 +240,11 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex fun requestField() = mHookInfo.okHttp.response.request.orNull - fun likeMethod() = mHookInfo.section.like.orNull + fun componentMapField() = mHookInfo.autoLike.kingPositionComponent.componentMap.orNull - fun bindViewMethod() = mHookInfo.section.bindView.orNull + fun setMDataMethod() = mHookInfo.autoLike.storyAbsController.setMData.orNull - fun getRootMethod() = mHookInfo.section.getRoot.orNull + fun getMPlayerMethod() = mHookInfo.autoLike.storyAbsController.getMPlayer.orNull fun themeName() = mHookInfo.themeName.field.orNull @@ -358,6 +361,10 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex fun getDataSPMethod() = mHookInfo.dataSP.get.orNull + fun buildCommonResolverParamsMethod() = + mHookInfo.resolveClientCompanion.buildCommonResolverParams.orNull + + fun setExtraContentMethod() = mHookInfo.gCommonResolverParams.setExtraContent.orNull private fun readHookInfo(context: Context): Configs.HookInfo { try { @@ -852,68 +859,74 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex }?.name ?: return@field } } - section = section { - val kingPositionComponentClass = dexHelper.findMethodUsingString( - "LikeClicked(view=", - false, - -1, - -1, - null, - -1, - null, - null, - null, - true - ).asSequence().firstNotNullOfOrNull { - dexHelper.decodeMethodIndex(it)?.declaringClass?.run { - var outerClass = this - while (outerClass.declaringClass != null) { - outerClass = outerClass.declaringClass!! + autoLike = autoLike { + kingPositionComponent = kingPositionComponent { + val kingPositionComponentClass = dexHelper.findMethodUsingString( + "LikeClicked(view=", + false, + -1, + -1, + null, + -1, + null, + null, + null, + true + ).asSequence().firstNotNullOfOrNull { + dexHelper.decodeMethodIndex(it)?.declaringClass?.run { + var outerClass = this + while (outerClass.declaringClass != null) { + outerClass = outerClass.declaringClass!! + } + outerClass } - outerClass - } - } - val genericInterface = kingPositionComponentClass?.genericInterfaces?.getOrNull(0) - val parameterType = (genericInterface as? ParameterizedType)?.actualTypeArguments?.getOrNull(0) - val viewHolderClass = if (parameterType is ParameterizedType) { - parameterType.rawType - } else { - parameterType - }?.let { it as? Class<*> } - val getRootMethod = viewHolderClass?.declaredMethods?.firstOrNull { - it.returnType == View::class.java && it.parameterCount == 0 - } - val bindViewMethod = kingPositionComponentClass?.declaredMethods?.firstOrNull { - it.isPublic && it.parameterCount == 2 - && it.parameterTypes[0] == viewHolderClass - } - if (kingPositionComponentClass != null && viewHolderClass != null && bindViewMethod != null && getRootMethod != null) { + } ?: return@kingPositionComponent + val componentMapField = kingPositionComponentClass.declaredFields.firstOrNull { + it.type == Map::class.java + } ?: return@kingPositionComponent class_ = class_ { name = kingPositionComponentClass.name } - viewHolder = class_ { name = viewHolderClass.name } - bindView = method { name = bindViewMethod.name } - getRoot = method { name = getRootMethod.name } - return@section + componentMap = field { name = componentMapField.name } + } + storyAbsController = storyAbsController { + val storyAbsControllerClass = dexHelper.findMethodUsingString( + "notify player state fail", + false, + -1, + -1, + null, + -1, + null, + null, + null, + true + ).asSequence().mapNotNull { + dexHelper.decodeMethodIndex(it)?.declaringClass + }.firstOrNull() ?: return@storyAbsController + val storyPagerPlayerInterface = dexHelper.findMethodUsingString( + " play item but is inactivate", + false, + -1, + -1, + null, + -1, + null, + null, + null, + true + ).asSequence().mapNotNull { + dexHelper.decodeMethodIndex(it)?.declaringClass?.interfaces?.getOrNull(0) + }.firstOrNull() ?: return@storyAbsController + val storyDetailClass = "com.bilibili.video.story.StoryDetail".from(classloader) + val setMDataMethod = storyAbsControllerClass.declaredMethods.firstOrNull { + it.returnType == Void.TYPE && it.parameterCount == 1 && it.parameterTypes[0] == storyDetailClass + } ?: return@storyAbsController + val getMPlayerMethod = storyAbsControllerClass.declaredMethods.firstOrNull { + storyPagerPlayerInterface.isAssignableFrom(it.returnType) && it.parameterCount == 0 + } ?: return@storyAbsController + class_ = class_ { name = storyAbsControllerClass.name } + setMData = method { name = setMDataMethod.name } + getMPlayer = method { name = getMPlayerMethod.name } } - - val sectionClass = dexHelper.findMethodUsingString( - "ActionViewHolder", - false, - -1, - -1, - null, - -1, - null, - null, - null, - true - ).firstOrNull()?.let { - dexHelper.decodeMethodIndex(it) - }?.declaringClass ?: return@section - val likeMethod = sectionClass?.superclass?.declaredMethods?.find { - it.parameterTypes.size == 1 && it.returnType == Void.TYPE && !it.isFinal - } ?: return@section - class_ = class_ { name = sectionClass.name } - like = method { name = likeMethod.name } } signQuery = signQuery { val signedQueryClass = @@ -1930,7 +1943,7 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex }.let { playerQualityService.addAll(it.toList()) } playerSettingHelper = playerSettingHelper { val getDefaultQnMethod = dexHelper.findMethodUsingString( - "get free data failed", + "quality settings:", false, -1, -1, @@ -2329,6 +2342,46 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex ?: return@class_ name = getPegasusParser.declaringClass.name } + resolveClientCompanion = resolveClientCompanion { + val resolveClientClass = dexHelper.findMethodUsingString( + "Invalid segment id: %s, segment list size:%s", + false, + -1, + -1, + null, + -1, + null, + null, + null, + true + ).asSequence().firstNotNullOfOrNull { + dexHelper.decodeMethodIndex(it)?.declaringClass + } ?: return@resolveClientCompanion + val resolveClientCompanionClass = "${resolveClientClass.name}\$a".from(classloader) + ?: return@resolveClientCompanion + val paramsClass = + "com.bilibili.app.gemini.base.player.GeminiCommonResolverParams".from( + classloader + ) + val videoDownloadEntryClass = + "com.bilibili.videodownloader.model.VideoDownloadEntry".from(classloader) + val buildParamsMethod = resolveClientCompanionClass.declaredMethods.firstOrNull { + it.returnType == paramsClass && it.parameterCount == 1 && it.parameterTypes[0] == videoDownloadEntryClass + } ?: return@resolveClientCompanion + class_ = class_ { name = resolveClientCompanionClass.name } + buildCommonResolverParams = method { name = buildParamsMethod.name } + } + gCommonResolverParams = gCommonResolverParams { + val commonResolverParamsClass = + "com.bilibili.app.gemini.base.player.GeminiCommonResolverParams".from( + classloader + ) ?: return@gCommonResolverParams + val setExtraContentMethod = commonResolverParamsClass.declaredMethods?.firstOrNull { + it.returnType == Void.TYPE && it.parameterCount == 1 && it.parameterTypes[0] == Map::class.java + } ?: return@gCommonResolverParams + class_ = class_ { name = commonResolverParamsClass.name } + setExtraContent = method { name = setExtraContentMethod.name } + } dexHelper.close() } diff --git a/app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt index 121014b19e..361224d601 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt @@ -1,6 +1,8 @@ package me.iacn.biliroaming.hook import android.view.View +import android.widget.LinearLayout +import de.robv.android.xposed.XC_MethodHook.Unhook import me.iacn.biliroaming.BiliBiliPackage.Companion.instance import me.iacn.biliroaming.utils.* @@ -11,47 +13,98 @@ class AutoLikeHook(classLoader: ClassLoader) : BaseHook(classLoader) { var detail: Pair? = null } + private val likeIds by lazy { + arrayOf( + "frame_like", + "like_layout" + ).map { getId(it) } + } + override fun startHook() { if (!sPrefs.getBoolean("auto_like", false)) return Log.d("startHook: AutoLike") - val likeIds = arrayOf( - "frame_recommend", - "frame1", - "frame_like" - ).map { getId(it) } + val unhookSet = arrayOf?>(null) + unhookSet[0] = instance.kingPositionComponentClass?.hookAfterAllConstructors { param -> + val kingPositionComponent = param.thisObject + val componentMap = + kingPositionComponent.getObjectFieldAs>(instance.componentMapField()) + + val likeComponentClass = componentMap.keys.filterNotNull().firstNotNullOfOrNull { + val componentClass = it.javaClass + componentClass.declaredFields.firstOrNull { + it.type == Runnable::class.java + } ?: return@firstNotNullOfOrNull null + componentClass + } ?: return@hookAfterAllConstructors + + val rebindView = likeComponentClass.declaredMethods.firstOrNull { + it.returnType == Void.TYPE && it.parameterCount == 5 + && it.parameterTypes[0] == likeComponentClass + && it.parameterTypes[4] == LinearLayout::class.java + } ?: return@hookAfterAllConstructors + + rebindView.hookAfterMethod { p -> + performLike(p.args[4] as View) + } + + unhookSet[0]?.forEach { it.unhook() } + } - instance.likeMethod()?.let { likeMethod -> - instance.sectionClass?.hookAfterAllMethods(likeMethod) { param -> - val sec = param.thisObject ?: return@hookAfterAllMethods - if (!shouldClickLike()) { - return@hookAfterAllMethods - } - val likeView = sec.javaClass.declaredFields.filter { - View::class.java.isAssignableFrom(it.type) - }.firstNotNullOfOrNull { - sec.getObjectFieldOrNullAs(it.name)?.takeIf { v -> - v.id in likeIds - } - } - likeView?.callOnClick() + // 番剧分集切换 + instance.viewUniteMossClass?.hookBeforeMethod( + "arcRefresh", + "com.bapis.bilibili.app.viewunite.v1.ArcRefreshReq", + instance.mossResponseHandlerClass + ) { param -> + val req = param.args[0] + val aid = req.callMethodAs("getAid") + ?: req.callMethodAs("getBvid")?.let { bv2av(it) } + if (aid == null || aid <= 0L) { + return@hookBeforeMethod } + param.args[1] = param.args[1].mossResponseHandlerReplaceProxy { reply -> + reply ?: return@mossResponseHandlerReplaceProxy null + val like = reply.callMethod("getReqUser")?.callMethodAs("getLike") + ?: return@mossResponseHandlerReplaceProxy null + detail = aid to like + null + } + } + + // 竖屏模式 + instance.storyAbsControllerClass?.hookAfterMethod( + instance.setMDataMethod(), + instance.storyDetailClass + ) { param -> + if (param.thisObject.callMethod(instance.getMPlayerMethod()) == null) { + return@hookAfterMethod + } + val storyDetail = instance.fastJsonClass?.callStaticMethodAs( + "toJSONString", + param.args[0] + ).toJSONObject() + val playerArgs = storyDetail.optJSONObject("player_args") + val aid = playerArgs?.optLong("aid") + val reqUser = storyDetail.optJSONObject("req_user") + val like = reqUser?.optBoolean("like") + if (aid == null || like == null) { + return@hookAfterMethod + } + detail = aid to if (like) 1 else 0 + + performLike(param.thisObject as View) + } + } + + private fun performLike(root: View) { + val likeView = likeIds.firstNotNullOfOrNull { + root.findViewById(it) } - instance.bindViewMethod()?.let { bindViewMethod -> - instance.sectionClass?.hookAfterMethod( - bindViewMethod, - instance.viewHolderClass, - instance.continuationClass - ) { param -> - if (!shouldClickLike()) { - return@hookAfterMethod - } - val root = param.args[0].callMethodAs(instance.getRootMethod()) - val likeView = likeIds.firstNotNullOfOrNull { id -> - root.findViewById(id) - } - likeView?.callOnClick() + likeView?.post { + if (shouldClickLike()) { + likeView.callOnClick() } } } diff --git a/app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt index 51aa74743d..94e47dfd21 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt @@ -269,6 +269,24 @@ class BangumiPlayUrlHook(classLoader: ClassLoader) : BaseHook(classLoader) { } } } + + // 修复下载时提示获取剧集信息失败 + instance.resolveClientCompanionClass?.hookAfterMethod( + instance.buildCommonResolverParamsMethod(), + instance.videoDownloadEntryClass + ) { param -> + val entry = param.args[0].callMethodAs("toJsonObject") + val seasonId = entry?.optString("season_id") + val epId = entry?.optJSONObject("ep")?.optString("episode_id") + val extraMap = buildMap { + seasonId?.let { put("season_id", it) } + epId?.let { put("ep_id", it) } + }.ifEmpty { + return@hookAfterMethod + } + param.result.callMethod(instance.setExtraContentMethod(), extraMap) + } + instance.playerMossClass?.run { var isDownload = false hookBeforeMethod( @@ -354,10 +372,11 @@ class BangumiPlayUrlHook(classLoader: ClassLoader) : BaseHook(classLoader) { } ?: throw CustomServerException(mapOf("未知错误" to "请检查哔哩漫游设置中解析服务器设置。")) } catch (e: CustomServerException) { - param.result = showPlayerErrorUnite( - response, supplement, - "请求解析服务器发生错误", e.message, true - ) + // v8.48.0+ 打开缓存弹窗导致应用崩溃 +// param.result = showPlayerErrorUnite( +// response, supplement, +// "请求解析服务器发生错误", e.message, true +// ) Log.toast("请求解析服务器发生错误: ${e.message}", alsoLog = true) } } else if (isDownload) { diff --git a/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt index 5fed4a0550..77614a54ac 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt @@ -112,17 +112,25 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { debug { "originalLiveUrl: $originalUri" } - newQn = findQuality( - JSONArray(originalUri.getQueryParameter("accept_quality")), - liveQuality - ).toString() - debug { "newQn: $newQn" } - - param.args[0] = originalUri.removeQuery { name -> - name.startsWith("playurl") - }.also { - debug { "newLiveUrl: $it" } + if (originalUri.getQueryParameter("no_playurl") == "1") { + newQn = liveQuality.toString() + } else { + newQn = findQuality( + JSONArray(originalUri.getQueryParameter("accept_quality")), + liveQuality + ).toString() + + param.args[0] = originalUri.modified( + removeIf = { name -> + name.startsWith("playurl") + }, + append = mapOf("no_playurl" to "1") + ).also { + debug { "newLiveUrl: $it" } + } } + + debug { "newQn: $newQn" } } } @@ -143,14 +151,17 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { } } - private fun Uri.removeQuery(predicate: (String) -> Boolean): Uri { + private fun Uri.modified(removeIf: (String) -> Boolean, append: Map): Uri { val newBuilder = buildUpon().clearQuery() for (name in queryParameterNames) { val value = getQueryParameter(name) ?: "" - if (!predicate(name)) { + if (!removeIf(name)) { newBuilder.appendQueryParameter(name, value) } } + append.forEach { (k, v) -> + newBuilder.appendQueryParameter(k, v.toString()) + } return newBuilder.build() } diff --git a/app/src/main/java/me/iacn/biliroaming/hook/ProtoBufHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/ProtoBufHook.kt index 84163894c2..474e18f01f 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/ProtoBufHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/ProtoBufHook.kt @@ -376,7 +376,11 @@ class ProtoBufHook(classLoader: ClassLoader) : BaseHook(classLoader) { ?.callMethodAs("getAid") ?: -1L val like = viewReply.callMethod("getReqUser") ?.callMethodAs("getLike") ?: -1 - AutoLikeHook.detail = aid to like + + if (aid > 0 && like != -1) { + AutoLikeHook.detail = aid to like + } + BangumiPlayUrlHook.qnApplied.set(false) // 视频详情页荣誉卡片 diff --git a/app/src/main/proto/me/iacn/biliroaming/configs.proto b/app/src/main/proto/me/iacn/biliroaming/configs.proto index d4617ede42..0c700d9bc4 100644 --- a/app/src/main/proto/me/iacn/biliroaming/configs.proto +++ b/app/src/main/proto/me/iacn/biliroaming/configs.proto @@ -159,12 +159,20 @@ message SignQuery { optional Method method = 2; } -message Section { +message AutoLike { + optional KingPositionComponent kingPositionComponent = 1; + optional StoryAbsController storyAbsController = 2; +} + +message KingPositionComponent { + optional Class class = 1; + optional Field componentMap = 2; +} + +message StoryAbsController { optional Class class = 1; - optional Method like = 2; - optional Class view_holder = 3; - optional Method bind_view = 4; - optional Method get_root = 5; + optional Method setMData = 2; + optional Method getMPlayer = 3; } message Drawer { @@ -312,6 +320,16 @@ message DataSP { optional Method get = 2; } +message ResolveClientCompanion { + optional Class class = 1; + optional Method buildCommonResolverParams = 2; +} + +message GCommonResolverParams { + optional Class class = 1; + optional Method setExtraContent = 2; +} + message HookInfo { int64 last_update_time = 1; optional MapIds map_ids = 2; @@ -330,7 +348,7 @@ message HookInfo { optional ThemeProcessor theme_processor = 20; optional Class theme_list_click = 22; optional ThemeName theme_name = 24; - optional Section section = 25; + optional AutoLike autoLike = 25; optional SignQuery sign_query = 27; optional Class general_response = 28; optional Drawer drawer = 30; @@ -381,4 +399,6 @@ message HookInfo { optional PreBuiltConfig preBuiltConfig = 99; optional DataSP dataSP = 100; optional Class pegasus_parser = 101; + optional ResolveClientCompanion resolveClientCompanion = 102; + optional GCommonResolverParams gCommonResolverParams = 103; }