├── app ├── src │ ├── .gitignore │ └── main │ │ ├── assets │ │ └── xposed_init │ │ ├── res │ │ ├── drawable │ │ │ ├── demo.webp │ │ │ ├── tp.webp │ │ │ ├── demo2.webp │ │ │ └── ic_clear.xml │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings_x_raw.xml │ │ │ ├── arrays_x.xml │ │ │ ├── styles.xml │ │ │ ├── strings_raw.xml │ │ │ └── strings_x.xml │ │ ├── values-night │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ ├── values-zh-rTW │ │ │ ├── arrays_x.xml │ │ │ ├── strings_x.xml │ │ │ └── arrays.xml │ │ ├── layout │ │ │ ├── cdn_speedtest_item.xml │ │ │ ├── seekbar_dialog.xml │ │ │ ├── search_bar.xml │ │ │ ├── feature.xml │ │ │ ├── video_choose.xml │ │ │ ├── customize_backup_dialog.xml │ │ │ ├── custom_button.xml │ │ │ └── dialog_color_choose.xml │ │ └── xml │ │ │ └── main_activity.xml │ │ ├── java │ │ └── me │ │ │ └── iacn │ │ │ └── biliroaming │ │ │ ├── hook │ │ │ ├── BaseHook.kt │ │ │ ├── api │ │ │ │ ├── ApiHook.kt │ │ │ │ ├── CardsHook.kt │ │ │ │ ├── SkinHook.kt │ │ │ │ ├── SeasonRcmdHook.kt │ │ │ │ ├── BannerV8AdHook.kt │ │ │ │ └── BannerV3AdHook.kt │ │ │ ├── FullStoryHook.kt │ │ │ ├── PurifyShareHook.kt │ │ │ ├── DialogBlurBackgroundHook.kt │ │ │ ├── PublishToFollowingHook.kt │ │ │ ├── TryWatchVipQualityHook.kt │ │ │ ├── QualityHook.kt │ │ │ ├── BlockUpdateHook.kt │ │ │ ├── VideoQualityHook.kt │ │ │ ├── SpeedHook.kt │ │ │ ├── TeenagersModeHook.kt │ │ │ ├── ChannelTabUIHook.kt │ │ │ ├── BangumiPageAdHook.kt │ │ │ ├── HintHook.kt │ │ │ ├── AllowMiniPlayHook.kt │ │ │ ├── AutoLikeHook.kt │ │ │ ├── VipSectionHook.kt │ │ │ ├── PlayerLongPressHook.kt │ │ │ ├── DarkSwitchHook.kt │ │ │ ├── DanmakuHook.kt │ │ │ ├── P2pHook.kt │ │ │ ├── FavFolderDialogHook.kt │ │ │ ├── StartActivityHook.kt │ │ │ ├── TextFoldHook.kt │ │ │ ├── SplashHook.kt │ │ │ ├── LosslessSettingHook.kt │ │ │ ├── LiveRoomHook.kt │ │ │ ├── DownloadThreadHook.kt │ │ │ ├── OkHttpHook.kt │ │ │ ├── EnvHook.kt │ │ │ ├── MiniProgramHook.kt │ │ │ ├── WebViewHook.kt │ │ │ ├── DrawerHook.kt │ │ │ ├── BLogDebugHook.kt │ │ │ ├── UposReplaceHook.kt │ │ │ ├── CommentImageHook.kt │ │ │ ├── TrialVipQualityHook.kt │ │ │ ├── OkHttpDebugHook.kt │ │ │ ├── SettingHook.kt │ │ │ └── ScreenOrientationHook.kt │ │ │ ├── utils │ │ │ ├── Json.kt │ │ │ ├── Booleans.kt │ │ │ ├── Coroutines.kt │ │ │ ├── UtilsX.kt │ │ │ ├── Hashs.kt │ │ │ ├── StrokeSpan.kt │ │ │ ├── Log.kt │ │ │ ├── DexHelper.kt │ │ │ └── UposReplaceHelper.kt │ │ │ ├── Constant.kt │ │ │ ├── MiscRemoveAdsDialog.kt │ │ │ ├── ColorChooseDialog.kt │ │ │ ├── ARGBColorChooseDialog.kt │ │ │ ├── VideoExportDialog.kt │ │ │ └── TextFoldDialog.kt │ │ ├── jni │ │ └── CMakeLists.txt │ │ └── AndroidManifest.xml ├── .gitignore ├── proguard-rules.pro └── build.gradle.kts ├── imgs ├── icon.png ├── stick1.png ├── stick2.png ├── stick3.png └── stick4.png ├── README.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitmodules ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ ├── new_server.yml │ └── bug_report.yml ├── dependabot.yml └── workflows │ ├── PR.yml │ ├── android.yml │ └── sync.yml ├── settings.gradle.kts ├── gradle.properties └── gradlew.bat /app/src/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /.cxx 4 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | me.iacn.biliroaming.XposedInit 2 | -------------------------------------------------------------------------------- /imgs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/imgs/icon.png -------------------------------------------------------------------------------- /imgs/stick1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/imgs/stick1.png -------------------------------------------------------------------------------- /imgs/stick2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/imgs/stick2.png -------------------------------------------------------------------------------- /imgs/stick3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/imgs/stick3.png -------------------------------------------------------------------------------- /imgs/stick4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/imgs/stick4.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [**Deprecated**] Continued by https://github.com/BiliRoamingX/BiliRoamingX. 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/app/src/main/res/drawable/demo.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/tp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/app/src/main/res/drawable/tp.webp -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/HEAD/app/src/main/res/drawable/demo2.webp -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/jni/dex_builder"] 2 | path = app/src/main/jni/dex_builder 3 | url = git@github.com:LSPosed/DexBuilder.git 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | *.apk 10 | *.jar 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fff 4 | #fa6496 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #303030 4 | #e66e90 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/BaseHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | /** 4 | * Created by iAcn on 2019/3/27 5 | * Email i@iacn.me 6 | */ 7 | abstract class BaseHook(val mClassLoader: ClassLoader) { 8 | abstract fun startHook() 9 | open fun lateInitHook() {} 10 | } -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/ApiHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | interface ApiHook { 4 | val enabled: Boolean 5 | 6 | fun canHandler(api: String): Boolean 7 | fun decodeResponse(): Boolean = true 8 | fun hook(response: String): String 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Telegram 频道 4 | url: https://t.me/biliroaming 5 | about: 可以订阅更新、讨论交流 6 | - name: QQ 频道 7 | url: https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&inviteCode=NVoD5&from=246610&biz=ka 8 | about: 可以订阅更新、讨论交流 -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/arrays_x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 廣告 4 | 直播 5 | 6 | 7 | 評論 8 | 動態 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_x_raw.xml: -------------------------------------------------------------------------------- 1 | 2 | https://github.com/BBSub/ZhConvertDict 3 | https://api.github.com/repos/BBSub/ZhConvertDict/releases/latest 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | buildCache { local { removeUnusedEntriesAfterDays = 1 } } 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven(url = "https://api.xposed.info") 16 | } 17 | } 18 | rootProject.name = "BiliRoaming" 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | target-branch: master 10 | registries: 11 | - maven-google 12 | - gralde-plugin 13 | 14 | registries: 15 | maven-google: 16 | type: maven-repository 17 | url: "https://dl.google.com/dl/android/maven2/" 18 | gralde-plugin: 19 | type: maven-repository 20 | url: "https://plugins.gradle.org/m2/" 21 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/FullStoryHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.replaceMethod 5 | import me.iacn.biliroaming.utils.sPrefs 6 | 7 | class FullStoryHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | if (!sPrefs.getBoolean("disable_story_full", false)) return 10 | instance.playerFullStoryWidgets().forEach { (clazz, method) -> 11 | clazz?.replaceMethod(method, clazz) { false } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Json.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | package me.iacn.biliroaming.utils 3 | 4 | import org.json.JSONObject 5 | 6 | inline fun Map.toJson() = JSONObject(this).toString() 7 | 8 | inline fun Map.toJsonObject() = JSONObject(this) 9 | 10 | fun json(build: JSONObject.() -> Unit) = JSONObject().apply(build) 11 | 12 | context(JSONObject) 13 | infix fun String.by(build: JSONObject.() -> Unit): JSONObject = put(this, JSONObject().build()) 14 | 15 | context(JSONObject) 16 | infix fun String.by(value: Any): JSONObject = put(this, value) 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays_x.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 广告 4 | 直播 5 | 6 | 7 | ad 8 | live 9 | 10 | 11 | 评论 12 | 动态 13 | 14 | 15 | comment 16 | dynamic 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Booleans.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.InvocationKind 5 | import kotlin.contracts.contract 6 | 7 | @OptIn(ExperimentalContracts::class) 8 | inline fun Boolean.yes(action: () -> Unit): Boolean { 9 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 10 | if (this) action() 11 | return this 12 | } 13 | 14 | @OptIn(ExperimentalContracts::class) 15 | inline fun Boolean.no(action: () -> Unit): Boolean { 16 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 17 | if (!this) action() 18 | return this 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/PurifyShareHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.sPrefs 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | 7 | class PurifyShareHook (classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | if (!sPrefs.getBoolean("purify_share", false)) return 10 | instance.shareClickResultClass?.hookBeforeMethod("getContent") { 11 | it.result = null 12 | } 13 | instance.shareClickResultClass?.hookBeforeMethod("getLink") { 14 | it.result = null 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/DialogBlurBackgroundHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Dialog 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.blurBackground 6 | import me.iacn.biliroaming.utils.hookAfterMethod 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class DialogBlurBackgroundHook(mClassLoader: ClassLoader) : BaseHook(mClassLoader) { 10 | override fun startHook() { 11 | if (sPrefs.getBoolean("dialog_blur_background", false).not()) return 12 | Log.d("startHook: DialogBlurBackgroundHook") 13 | Dialog::class.java.hookAfterMethod("show") { 14 | (it.thisObject as Dialog).window?.blurBackground() 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能请求 2 | description: 想要请求添加某个功能 3 | labels: [enhancement] 4 | title: "[Feature] " 5 | body: 6 | - type: textarea 7 | id: reason 8 | attributes: 9 | label: 原因 10 | description: 为什么想要这个功能 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: desc 15 | attributes: 16 | label: 功能简述 17 | description: 想要个怎样的功能 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: logic 22 | attributes: 23 | label: 功能逻辑 24 | description: 如何互交、如何使用等 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: ref 29 | attributes: 30 | label: 实现参考 31 | description: 该功能可能的实现方式,或者其他已经实现该功能的应用等 32 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/PublishToFollowingHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.hookBeforeConstructor 5 | import me.iacn.biliroaming.utils.sPrefs 6 | 7 | class PublishToFollowingHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | if (!sPrefs.getBoolean("disable_auto_select", false)) 10 | return 11 | instance.publishToFollowingConfigClass?.hookBeforeConstructor( 12 | Boolean::class.javaPrimitiveType, 13 | Boolean::class.javaPrimitiveType, 14 | Boolean::class.javaPrimitiveType, 15 | Boolean::class.javaPrimitiveType, 16 | ) { it.args[2]/*autoSelectOnce*/ = false } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/TryWatchVipQualityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class TryWatchVipQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | if (!sPrefs.getBoolean("disable_try_watch_vip_quality", false)) return 11 | 12 | Log.d("startHook: TryWatchVipQualityHook") 13 | instance.canTryWatchVipQuality()?.let { m -> 14 | instance.playerQualityServiceClass?.hookBeforeMethod( 15 | m 16 | ) { 17 | it.result = false 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/QualityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.Log 4 | import me.iacn.biliroaming.utils.hookBeforeMethod 5 | import me.iacn.biliroaming.utils.sPrefs 6 | 7 | class QualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | sPrefs.getString("cn_server_accessKey", null) ?: return 10 | Log.d("startHook: Quality") 11 | 12 | "com.bilibili.lib.accountinfo.model.VipUserInfo".hookBeforeMethod( 13 | mClassLoader, 14 | "isEffectiveVip" 15 | ) { 16 | Thread.currentThread().stackTrace.find { stack -> 17 | stack.className.contains(".quality.") 18 | } ?: return@hookBeforeMethod 19 | it.result = true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | project(biliroaming) 3 | 4 | find_package(cxx REQUIRED CONFIG) 5 | link_libraries(cxx::cxx) 6 | 7 | add_subdirectory(dex_builder) 8 | 9 | add_library(${PROJECT_NAME} SHARED 10 | biliroaming.cc 11 | ) 12 | 13 | target_link_libraries(${PROJECT_NAME} PUBLIC log dex_builder_static) 14 | 15 | if (NOT DEFINED DEBUG_SYMBOLS_PATH) 16 | set(DEBUG_SYMBOLS_PATH ${CMAKE_BINARY_DIR}/symbols) 17 | endif() 18 | 19 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 20 | COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} 21 | COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ 22 | ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME} 23 | COMMAND ${CMAKE_STRIP} --strip-all $) 24 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/BlockUpdateHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.content.Context 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | import me.iacn.biliroaming.utils.new 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class BlockUpdateHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("block_update", false)) return 12 | instance.updateInfoSupplierClass?.hookBeforeMethod( 13 | instance.check(), Context::class.java 14 | ) { param -> 15 | val message = "哼,休想要我更新!<( ̄︶ ̄)>" 16 | param.throwable = instance.latestVersionExceptionClass?.new(message) as? Throwable 17 | ?: Exception(message) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cdn_speedtest_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/VideoQualityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class VideoQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | override fun startHook() { 8 | if (!sPrefs.getBoolean("main_func", false)) return 9 | 10 | val halfScreenQuality = sPrefs.getString("half_screen_quality", "0")?.toInt() ?: 0 11 | val fullScreenQuality = sPrefs.getString("full_screen_quality", "0")?.toInt() ?: 0 12 | if (halfScreenQuality != 0) { 13 | instance.playerPreloadHolderClass?.replaceAllMethods(instance.getPreload()) { null } 14 | } 15 | if (fullScreenQuality != 0) { 16 | instance.playerSettingHelperClass?.replaceMethod(instance.getDefaultQn()) { fullScreenQuality } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | import java.net.URL 8 | 9 | suspend fun fetchJson(url: URL) = withContext(Dispatchers.IO) { 10 | try { 11 | JSONObject(url.readText()) 12 | } catch (e: Throwable) { 13 | null 14 | } 15 | } 16 | 17 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 18 | suspend fun fetchJson(url: String) = fetchJson(URL(url)) 19 | 20 | suspend fun fetchJsonArray(url: URL) = withContext(Dispatchers.IO) { 21 | try { 22 | JSONArray(url.readText()) 23 | } catch (e: Throwable) { 24 | null 25 | } 26 | } 27 | 28 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 29 | suspend fun fetchJsonArray(url: String) = fetchJsonArray(URL(url)) 30 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/CardsHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.sPrefs 4 | import org.json.JSONArray 5 | import org.json.JSONObject 6 | 7 | object CardsHook : ApiHook { 8 | private val cardsApis = arrayOf( 9 | "https://api.bilibili.com/pgc/season/player/cards", 10 | "https://api.bilibili.com/pgc/season/player/ogv/cards" 11 | ) 12 | 13 | override val enabled by lazy { 14 | sPrefs.getBoolean("hidden", false) 15 | && sPrefs.getBoolean("block_up_rcmd_ads", false) 16 | } 17 | 18 | override fun canHandler(api: String) = cardsApis.any { api.startsWith(it) } 19 | override fun decodeResponse() = false 20 | 21 | override fun hook(response: String): String { 22 | return JSONObject().apply { 23 | put("code", 0) 24 | put("data", JSONArray()) 25 | }.toString() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/SpeedHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class SpeedHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | private var lastSet: Any? = null 11 | 12 | override fun startHook() { 13 | Log.d("startHook: SpeedHook") 14 | val speed = sPrefs.getInt("default_speed", 100) 15 | if (speed == 100) return 16 | instance.playerCoreServiceV2Class?.hookBeforeMethod(instance.setDefaultSpeed(), Float::class.javaPrimitiveType) { 17 | if (lastSet != it.thisObject) { 18 | lastSet = it.thisObject 19 | it.args[0] = speed / 100f 20 | Log.toast("已设置倍速为 ${speed}%") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | # org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | org.gradle.jvmargs=-Xmx2g 15 | android.useAndroidX=true 16 | android.enableAppCompileTimeRClass=true 17 | android.experimental.enableNewResourceShrinker.preciseShrinking=true 18 | 19 | appVerName=1.6.12 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_server.yml: -------------------------------------------------------------------------------- 1 | name: 添加公共服务器 2 | description: 把自己的服务器添加到公共服务器列表 3 | labels: [server] 4 | title: "[Server] " 5 | body: 6 | - type: input 7 | id: contact 8 | attributes: 9 | label: 联系方式 10 | description: Telegram、Github 账号等联系方式 11 | validations: 12 | required: true 13 | - type: input 14 | id: domain 15 | attributes: 16 | label: 域名 17 | description: 服务器域名 18 | validations: 19 | required: true 20 | - type: checkboxes 21 | id: areas 22 | attributes: 23 | label: 支持地区 24 | description: 服务器支持解析的地区 25 | options: 26 | - label: 中国大陆 27 | - label: 港澳 28 | - label: 台湾 29 | - label: 东南亚 30 | - type: checkboxes 31 | id: premium 32 | attributes: 33 | label: 带会员专享 34 | options: 35 | - label: 服务器只有带会员用户能解析 36 | - type: input 37 | id: sponsor 38 | attributes: 39 | label: 赞助地址 40 | description: 爱发电等 41 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/TeenagersModeHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.utils.Log 7 | import me.iacn.biliroaming.utils.hookAfterMethod 8 | import me.iacn.biliroaming.utils.sPrefs 9 | 10 | /** 11 | * Created by iAcn on 2019/12/15 12 | * Email i@iacn.me 13 | */ 14 | class TeenagersModeHook(classLoader: ClassLoader) : BaseHook(classLoader) { 15 | override fun startHook() { 16 | if (!sPrefs.getBoolean("teenagers_mode_dialog", false)) return 17 | Log.d("startHook: TeenagersMode") 18 | instance.teenagersModeDialogActivityClass?.hookAfterMethod( 19 | "onCreate", Bundle::class.java 20 | ) { param -> 21 | val activity = param.thisObject as Activity 22 | activity.finish() 23 | Log.d("Teenagers mode dialog has been closed") 24 | Log.toast("已关闭青少年模式对话框") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/seekbar_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 28 | 29 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -repackageclasses "biliroaming" 2 | 3 | -keep class me.iacn.biliroaming.XposedInit { 4 | (); 5 | } 6 | 7 | -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { 8 | ; 9 | } 10 | 11 | -keepclasseswithmembers class me.iacn.biliroaming.utils.DexHelper { 12 | native ; 13 | long token; 14 | java.lang.ClassLoader classLoader; 15 | } 16 | 17 | -keepattributes RuntimeVisible*Annotations 18 | 19 | -keepclassmembers class * { 20 | @android.webkit.JavascriptInterface ; 21 | } 22 | 23 | -keepclassmembers class * implements android.os.Parcelable { 24 | public static final ** CREATOR; 25 | } 26 | 27 | -keepclassmembers class me.iacn.biliroaming.MainActivity$Companion { 28 | boolean isModuleActive(); 29 | } 30 | 31 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 32 | public static void check*(...); 33 | public static void throw*(...); 34 | } 35 | 36 | -assumenosideeffects class java.util.Objects { 37 | public static ** requireNonNull(...); 38 | } 39 | 40 | -allowaccessmodification 41 | -overloadaggressively 42 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/ChannelTabUIHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 7 | import me.iacn.biliroaming.utils.* 8 | 9 | class ChannelTabUIHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("hidden", false) 12 | || !sPrefs.getBoolean("add_channel", false) 13 | ) return 14 | "com.bilibili.pegasus.channelv2.home.category.HomeCategoryFragment" 15 | .from(mClassLoader)?.hookAfterMethod( 16 | "onViewCreated", View::class.java, Bundle::class.java 17 | ) { param -> 18 | val root = param.args[0] as ViewGroup 19 | if (root.context.javaClass == instance.splashActivityClass) { 20 | root.getChildAt(0).visibility = View.GONE 21 | root.clipToPadding = false 22 | root.setPadding(0, 0, 0, 48.dp) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/Constant.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | /** 4 | * Created by iAcn on 2019/4/12 5 | * Email i@iacn.me 6 | */ 7 | object Constant { 8 | const val PINK_PACKAGE_NAME = "tv.danmaku.bili" 9 | const val BLUE_PACKAGE_NAME = "com.bilibili.app.blue" 10 | const val PLAY_PACKAGE_NAME = "com.bilibili.app.in" 11 | const val HD_PACKAGE_NAME = "tv.danmaku.bilibilihd" 12 | val BILIBILI_PACKAGE_NAME = hashMapOf( 13 | "原版" to PINK_PACKAGE_NAME, 14 | "概念版" to BLUE_PACKAGE_NAME, 15 | "play版" to PLAY_PACKAGE_NAME, 16 | "HD版" to HD_PACKAGE_NAME 17 | ) 18 | const val TAG = "BiliRoaming" 19 | const val HOOK_INFO_FILE_NAME = "hookinfo.pb" 20 | const val TYPE_SEASON_ID = 0 21 | const val TYPE_MEDIA_ID = 1 22 | const val TYPE_EPISODE_ID = 2 23 | const val CUSTOM_COLOR_KEY = "biliroaming_custom_color" 24 | const val CURRENT_COLOR_KEY = "theme_entries_current_key" 25 | const val DEFAULT_CUSTOM_COLOR = -0xe6b7d 26 | const val infoUrl = "https://api.bilibili.com/client_info" 27 | const val zoneUrl = "https://api.bilibili.com/x/web-interface/zone" 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/BangumiPageAdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class BangumiPageAdHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | override fun startHook() { 8 | if (!sPrefs.getBoolean("block_bangumi_page_ads", false)) return 9 | Log.d("startHook: BangumiPageAd") 10 | // activity toast ad 11 | "com.bilibili.bangumi.data.page.detail.entity.OGVActivityVo".from(mClassLoader) 12 | ?.hookBeforeAllConstructors { param -> 13 | val args = param.args 14 | for (i in args.indices) { 15 | when (val item = args[i]) { 16 | is Int -> args[i] = 0 17 | is MutableList<*> -> item.clear() 18 | else -> args[i] = null 19 | } 20 | } 21 | } 22 | // mall 23 | instance.bangumiUniformSeasonActivityEntrance()?.let { 24 | instance.bangumiUniformSeasonClass?.replaceMethod(it) { null } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/HintHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.os.Bundle 6 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 7 | import me.iacn.biliroaming.R 8 | import me.iacn.biliroaming.utils.addModuleAssets 9 | import me.iacn.biliroaming.utils.hookAfterMethod 10 | import me.iacn.biliroaming.utils.inflateLayout 11 | import me.iacn.biliroaming.utils.sPrefs 12 | 13 | class HintHook(classLoader: ClassLoader) : BaseHook(classLoader) { 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("show_hint", true)) return 16 | instance.mainActivityClass?.hookAfterMethod("onCreate", Bundle::class.java) { param -> 17 | AlertDialog.Builder(param.thisObject as Activity).run { 18 | context.addModuleAssets() 19 | setTitle("哔哩漫游使用说明") 20 | setView(context.inflateLayout(R.layout.feature)) 21 | setNegativeButton("知道了") { _, _ -> 22 | sPrefs.edit().putBoolean("show_hint", false).apply() 23 | } 24 | show() 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/UtilsX.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.hookInfo 7 | import me.iacn.biliroaming.orNull 8 | import java.io.File 9 | 10 | @Suppress("DEPRECATION") 11 | fun blkvPrefsByName(name: String, multiProcess: Boolean = true): SharedPreferences { 12 | return instance.blkvClass?.callStaticMethodAs( 13 | hookInfo.blkv.getByName.orNull, currentContext, name, multiProcess, 0 14 | ) ?: currentContext.getSharedPreferences(name, Context.MODE_MULTI_PROCESS) 15 | } 16 | 17 | @Suppress("DEPRECATION") 18 | fun blkvPrefsByFile(file: File, multiProcess: Boolean = true): SharedPreferences { 19 | return instance.blkvClass?.callStaticMethodAs( 20 | hookInfo.blkv.getByFile.orNull, currentContext, file, multiProcess, 0 21 | ) ?: currentContext.getSharedPreferences(file.nameWithoutExtension, Context.MODE_MULTI_PROCESS) 22 | } 23 | 24 | val abPrefs by lazy { 25 | val abPath = "prod/blconfig/ab.sp" 26 | val file = File(currentContext.getDir("foundation", Context.MODE_PRIVATE), abPath) 27 | blkvPrefsByFile(file, multiProcess = true) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/AllowMiniPlayHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.from 4 | import me.iacn.biliroaming.utils.getStaticObjectField 5 | import me.iacn.biliroaming.utils.hookBeforeConstructor 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class AllowMiniPlayHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | if (!sPrefs.getBoolean("main_func", false)) return 11 | 12 | if (sPrefs.getBoolean("allow_mini_play", false)) { 13 | "com.bilibili.lib.media.resource.PlayConfig\$PlayMenuConfig".from(mClassLoader) 14 | ?.hookBeforeConstructor( 15 | Boolean::class.javaPrimitiveType, 16 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 17 | ) { param -> 18 | val type = param.args[1] 19 | val miniPlayerType = 20 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 21 | .from(mClassLoader)?.getStaticObjectField("MINIPLAYER") 22 | if (type == miniPlayerType) 23 | param.args[0] = true 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/SkinHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.runCatchingOrNull 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import me.iacn.biliroaming.utils.toJSONObject 6 | import org.json.JSONObject 7 | 8 | object SkinHook : ApiHook { 9 | private const val skinApi = "https://app.bilibili.com/x/resource/show/skin" 10 | 11 | override val enabled by lazy { 12 | sPrefs.getBoolean("hidden", false) 13 | && sPrefs.getBoolean("skin", false) 14 | && !sPrefs.getString("skin_json", null).isNullOrEmpty() 15 | } 16 | 17 | override fun canHandler(api: String) = api.startsWith(skinApi) 18 | 19 | override fun hook(response: String): String { 20 | val skinJson = sPrefs.getString("skin_json", null) 21 | val skin = skinJson.runCatchingOrNull { toJSONObject() } 22 | ?.apply { 23 | if (optString("package_md5").isNotEmpty()) 24 | put("package_md5", JSONObject.NULL) 25 | } ?: return response 26 | return response.toJSONObject() 27 | .apply { 28 | optJSONObject("data") 29 | ?.put("user_equip", skin) 30 | }.toString() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/SeasonRcmdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import org.json.JSONObject 6 | 7 | object SeasonRcmdHook : ApiHook { 8 | private const val rcmdApi = "https://api.bilibili.com/pgc/season/app/related/recommend" 9 | 10 | override val enabled by lazy { 11 | sPrefs.getBoolean("hidden", false) 12 | && sPrefs.getBoolean("remove_video_relate_promote", false) 13 | } 14 | 15 | override fun canHandler(api: String) = api.startsWith(rcmdApi) 16 | 17 | override fun hook(response: String): String { 18 | val json = JSONObject(response) 19 | val cards = json.optJSONObject("result") 20 | ?.optJSONArray("cards") ?: return response 21 | var changed = false 22 | val toRemoveIdxList = mutableListOf() 23 | var index = 0 24 | for (card in cards) { 25 | if (card.optInt("type") == 2) 26 | toRemoveIdxList.add(index) 27 | index++ 28 | } 29 | if (toRemoveIdxList.isNotEmpty()) 30 | changed = true 31 | toRemoveIdxList.reversed().forEach { 32 | cards.remove(it) 33 | } 34 | return if (changed) json.toString() else response 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Hashs.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import java.io.File 4 | import java.security.DigestInputStream 5 | import java.security.MessageDigest 6 | 7 | private val HEX_DIGITS = 8 | charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') 9 | 10 | fun ByteArray.toHexString(): String { 11 | val hexDigits = HEX_DIGITS 12 | val len = size 13 | if (len <= 0) return "" 14 | val ret = CharArray(len shl 1) 15 | var i = 0 16 | var j = 0 17 | while (i < len) { 18 | ret[j++] = hexDigits[this[i].toInt() shr 4 and 0x0f] 19 | ret[j++] = hexDigits[this[i].toInt() and 0x0f] 20 | i++ 21 | } 22 | return String(ret) 23 | } 24 | 25 | private fun hashFile(file: File, algorithm: String): ByteArray? { 26 | return try { 27 | file.inputStream().use { fis -> 28 | val md = MessageDigest.getInstance(algorithm) 29 | val buffer = ByteArray(DEFAULT_BUFFER_SIZE) 30 | DigestInputStream(fis, md).use { 31 | while (true) { 32 | if (it.read(buffer) == -1) 33 | break 34 | } 35 | } 36 | md.digest() 37 | } 38 | } catch (_: Exception) { 39 | null 40 | } 41 | } 42 | 43 | val File.sha256sum: String 44 | get() = hashFile(this, "SHA256")?.toHexString() ?: "" 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/search_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/feature.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 21 | 22 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.view.View 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.* 6 | 7 | class AutoLikeHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | private val likedVideos = HashSet() 9 | 10 | companion object { 11 | var detail: Pair? = null 12 | } 13 | 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("auto_like", false)) return 16 | 17 | Log.d("startHook: AutoLike") 18 | 19 | val likeId = getId("frame_recommend") 20 | val like1 = getId("frame1") 21 | 22 | instance.sectionClass?.hookAfterAllMethods(instance.likeMethod()) { param -> 23 | val sec = param.thisObject ?: return@hookAfterAllMethods 24 | val (aid, like) = detail ?: return@hookAfterAllMethods 25 | if (likedVideos.contains(aid)) return@hookAfterAllMethods 26 | likedVideos.add(aid) 27 | val likeView = sec.javaClass.declaredFields.filter { 28 | View::class.java.isAssignableFrom(it.type) 29 | }.firstNotNullOfOrNull { 30 | sec.getObjectFieldOrNullAs(it.name)?.takeIf { v -> 31 | v.id == likeId || v.id == like1 32 | } 33 | } 34 | if (like == 0) 35 | likeView?.callOnClick() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/VipSectionHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 8 | import me.iacn.biliroaming.utils.findFieldByExactType 9 | import me.iacn.biliroaming.utils.from 10 | import me.iacn.biliroaming.utils.hookAfterMethod 11 | import me.iacn.biliroaming.utils.sPrefs 12 | 13 | class VipSectionHook(classLoader: ClassLoader) : BaseHook(classLoader) { 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("hidden", false) 16 | || (!sPrefs.getBoolean("modify_vip_section_style", false) 17 | && !sPrefs.getBoolean("remove_vip_section", false)) 18 | ) return 19 | val vipEntranceViewClass = 20 | "tv.danmaku.bili.ui.main2.mine.widgets.MineVipEntranceView".from(mClassLoader) 21 | val vipEntranceViewField = 22 | vipEntranceViewClass?.let { instance.homeUserCenterClass?.findFieldByExactType(it) } 23 | instance.homeUserCenterClass?.hookAfterMethod( 24 | "onCreateView", 25 | LayoutInflater::class.java, 26 | ViewGroup::class.java, 27 | Bundle::class.java 28 | ) { 29 | val self = it.thisObject 30 | (vipEntranceViewField?.get(self) as? View)?.visibility = View.GONE 31 | vipEntranceViewField?.set(self, null) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/PlayerLongPressHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.view.MotionEvent 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class PlayerLongPressHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | override fun startHook() { 8 | if (!sPrefs.getBoolean("forbid_player_long_click_accelerate", false)) return 9 | 10 | Log.d("startHook: PlayerLongPress") 11 | 12 | val hooker: Hooker = { param -> param.result = true } 13 | // pre 6.59.0 14 | "tv.danmaku.biliplayerimpl.gesture.GestureService\$mTouchListener\$1".findClassOrNull( 15 | mClassLoader 16 | )?.hookBeforeMethod( 17 | "onLongPress", MotionEvent::class.java, hooker = hooker 18 | ) 19 | // post 6.59.0 20 | arrayOf( 21 | "tv.danmaku.biliplayerimpl.gesture.GestureService\$initInnerLongPressListener\$1\$onLongPress\$1", 22 | "tv.danmaku.biliplayerimpl.gesture.GestureService\$initInnerLongPressListener\$1\$onLongPressEnd\$1", 23 | // post 7.32.0 24 | "com.bilibili.playerbizcommon.gesture.GestureService\$initInnerLongPressListener\$1\$onLongPress\$1", 25 | "com.bilibili.playerbizcommon.gesture.GestureService\$initInnerLongPressListener\$1\$onLongPressEnd\$1" 26 | ).forEach { className -> 27 | className.findClassOrNull(mClassLoader) 28 | ?.hookBeforeMethod("invoke", Object::class.java, hooker = hooker) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/StrokeSpan.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.text.TextPaint 6 | import android.text.style.ReplacementSpan 7 | 8 | class StrokeSpan( 9 | private val fillColor: Int, 10 | private val strokeColor: Int, 11 | private val strokeWidth: Float 12 | ) : ReplacementSpan() { 13 | private fun fillPaint(paint: Paint): TextPaint = 14 | TextPaint(paint).apply { 15 | style = Paint.Style.FILL 16 | color = fillColor 17 | } 18 | 19 | private fun stokePaint(paint: Paint): TextPaint = 20 | TextPaint(paint).apply { 21 | style = Paint.Style.STROKE 22 | color = strokeColor 23 | strokeWidth = this@StrokeSpan.strokeWidth 24 | } 25 | 26 | override fun getSize( 27 | p0: Paint, 28 | p1: CharSequence?, 29 | p2: Int, 30 | p3: Int, 31 | p4: Paint.FontMetricsInt? 32 | ): Int { 33 | return p0.measureText(p1, p2, p3).toInt() 34 | } 35 | 36 | override fun draw( 37 | canvas: Canvas, 38 | text: CharSequence?, 39 | start: Int, 40 | end: Int, 41 | x: Float, 42 | top: Int, 43 | y: Int, 44 | bottom: Int, 45 | paint: Paint 46 | ) { 47 | text ?: return 48 | canvas.drawText(text, start, end, x, y.toFloat(), fillPaint(paint)) 49 | if (strokeWidth > 0) 50 | canvas.drawText(text, start, end, x, y.toFloat(), stokePaint(paint)) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/video_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 17 | 18 | 25 | 26 | 33 | 34 | 42 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | protobuf = "3.24.0" 3 | coroutine = "1.7.3" 4 | kotlin = "1.9.0" 5 | 6 | [plugins] 7 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 8 | agp-app = { id = "com.android.application", version = "8.1.0" } 9 | protobuf = { id = "com.google.protobuf", version = "0.9.4" } 10 | lsplugin-jgit = { id = "org.lsposed.lsplugin.jgit", version = "1.1" } 11 | lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.5" } 12 | lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.1" } 13 | lsplugin-apktransform = { id = "org.lsposed.lsplugin.apktransform", version = "1.2" } 14 | lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version = "1.2" } 15 | 16 | 17 | 18 | [libraries] 19 | xposed = { module = "de.robv.android.xposed:api", version = "82" } 20 | cxx = { module = "dev.rikka.ndk.thirdparty:cxx", version = "1.2.0" } 21 | protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } 22 | protobuf-java = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } 23 | protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } 24 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 25 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine" } 26 | kotlin-coroutines-jdk = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutine" } 27 | androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.0.1" } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/BannerV8AdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import me.iacn.biliroaming.utils.toJSONObject 6 | 7 | object BannerV8AdHook : ApiHook { 8 | private const val feedApi = "https://app.bilibili.com/x/v2/feed/index" 9 | 10 | override val enabled by lazy { 11 | sPrefs.getBoolean("hidden", false) 12 | && sPrefs.getBoolean("purify_banner_ads", false) 13 | } 14 | 15 | override fun canHandler(api: String) = api.startsWith(feedApi) 16 | 17 | override fun hook(response: String): String { 18 | val json = response.toJSONObject() 19 | val items = json.optJSONObject("data") 20 | ?.optJSONArray("items") 21 | ?: return response 22 | var changed = false 23 | for (item in items) { 24 | if (item.optString("card_type") != "banner_v8") 25 | continue 26 | val banners = item.optJSONArray("banner_item") 27 | ?: continue 28 | val toRemoveIdx = mutableListOf() 29 | var index = 0 30 | for (banner in banners) { 31 | if (banner.optString("type") == "ad") 32 | toRemoveIdx.add(index) 33 | index++ 34 | } 35 | if (toRemoveIdx.isNotEmpty()) 36 | changed = true 37 | toRemoveIdx.reversed().forEach { 38 | banners.remove(it) 39 | } 40 | } 41 | return if (changed) json.toString() else response 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/DarkSwitchHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.utils.* 7 | 8 | class DarkSwitchHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | override fun startHook() { 11 | instance.userFragmentClass?.run { 12 | instance.switchDarkModeMethod?.let { 13 | hookBeforeMethod(it, Boolean::class.javaPrimitiveType) { param -> 14 | val activity = param.thisObject 15 | .callMethodOrNullAs("getActivity") 16 | ?: return@hookBeforeMethod 17 | val themeUtils = instance.themeUtilsClass ?: return@hookBeforeMethod 18 | val isDarkFollowSystem = themeUtils.callStaticMethodOrNullAs( 19 | instance.isDarkFollowSystemMethod, activity 20 | ) ?: return@hookBeforeMethod 21 | if (isDarkFollowSystem) { 22 | AlertDialog.Builder(activity) 23 | .setMessage("将关闭深色跟随系统,确定切换?") 24 | .setPositiveButton(android.R.string.ok) { _, _ -> 25 | param.invokeOriginalMethod() 26 | } 27 | .setNegativeButton(android.R.string.cancel, null) 28 | .show() 29 | param.result = null 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/PR.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | env: 10 | CCACHE_DIR: ${{ github.workspace }}/.ccache 11 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 12 | CCACHE_NOHASHDIR: true 13 | CCACHE_MAXSIZE: 1G 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ ubuntu-latest, windows-latest ] 18 | 19 | steps: 20 | - name: Check out 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: 'recursive' 24 | fetch-depth: 0 25 | - name: Set up JDK 17 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | cache: 'gradle' 31 | - name: Set up ccache 32 | uses: hendrikmuhs/ccache-action@v1.2 33 | with: 34 | key: ${{ runner.os }}-${{ github.sha }} 35 | restore-keys: ${{ runner.os }} 36 | - name: Build with Gradle 37 | run: | 38 | echo 'org.gradle.caching=true' >> gradle.properties 39 | echo 'org.gradle.parallel=true' >> gradle.properties 40 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 41 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 42 | echo 'android.native.buildOutput=verbose' >> gradle.properties 43 | ./gradlew assemble 44 | - name: Stop gradle daemon 45 | run: ./gradlew --stop 46 | - name: Upload build artifact 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: ${{ matrix.os }}-artifact 50 | path: | 51 | app/build/outputs 52 | app/release 53 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/DanmakuHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class DanmakuHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | 8 | override fun startHook() { 9 | val blockWeight = sPrefs.getInt("danmaku_filter_weight", 0) 10 | val disableVipDmColorful = sPrefs.getBoolean("disable_vip_dm_colorful", false) 11 | if (blockWeight <= 0 && !disableVipDmColorful) return 12 | instance.dmMossClass?.hookBeforeMethod( 13 | "dmSegMobile", 14 | "com.bapis.bilibili.community.service.dm.v1.DmSegMobileReq", 15 | instance.mossResponseHandlerClass 16 | ) { param -> 17 | param.args[1] = param.args[1].mossResponseHandlerProxy { 18 | if (blockWeight > 0) filterDanmaku(it, blockWeight) 19 | if (disableVipDmColorful) clearVipColorfulSrc(it) 20 | } 21 | } 22 | } 23 | 24 | private fun filterDanmaku(reply: Any?, blockWeight: Int) { 25 | reply?.callMethodAs>("getElemsList")?.filter { 26 | it.callMethodAs("getWeight") >= blockWeight 27 | }?.let { 28 | reply.callMethod("clearElems") 29 | reply.callMethod("addAllElems", it) 30 | } 31 | } 32 | 33 | private fun clearVipColorfulSrc(reply: Any?) { 34 | reply?.callMethodOrNullAs>("getColorfulSrcList")?.filter { 35 | // DmColorfulType 60001 VipGradualColor 36 | it.callMethodAs("getTypeValue") != 60001 37 | }?.let { 38 | reply.callMethod("clearColorfulSrc") 39 | reply.callMethod("addAllColorfulSrc", it) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/P2pHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.utils.* 7 | 8 | class P2pHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | private val blockPcdn = sPrefs.getBoolean("block_pcdn", false) 10 | private val blockPcdnLive = sPrefs.getBoolean("block_pcdn_live", false) 11 | override fun startHook() { 12 | if (!blockPcdn && !blockPcdnLive) return 13 | Log.d("startHook: P2P") 14 | "tv.danmaku.ijk.media.player.IjkMediaPlayer\$IjkMediaPlayerServiceConnection".from( 15 | mClassLoader 16 | )?.replaceMethod("initP2PClient") {} 17 | if (blockPcdn) { 18 | "tv.danmaku.ijk.media.player.P2P".from(mClassLoader)?.run { 19 | hookBeforeMethod("getInstance", Context::class.java, Bundle::class.java) { param -> 20 | param.args[0] = null 21 | param.args[1].callMethod("clear") 22 | } 23 | hookBeforeConstructor(Context::class.java, Bundle::class.java) { param -> 24 | param.args[0] = null 25 | param.args[1].callMethod("clear") 26 | } 27 | } 28 | } 29 | if (blockPcdnLive) { 30 | instance.liveRtcEnable()?.let { 31 | instance.liveRtcEnableClass?.replaceMethod(it) { false } 32 | } 33 | "com.bilibili.bililive.playercore.p2p.P2PType".from(mClassLoader)?.run { 34 | hookBeforeMethod("create", Int::class.javaPrimitiveType) { it.args[0] = 0 } 35 | hookBeforeMethod("createTo", Int::class.javaPrimitiveType) { it.args[0] = 0 } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/BannerV3AdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import org.json.JSONObject 6 | 7 | object BannerV3AdHook : ApiHook { 8 | private val targetApis = arrayOf( 9 | // 追番 10 | "https://api.bilibili.com/pgc/page/bangumi?", 11 | // 影视 12 | "https://api.bilibili.com/pgc/page/cinema/tab?", 13 | // 番剧推荐 14 | "https://api.bilibili.com/pgc/page/?" 15 | ) 16 | 17 | override val enabled by lazy { 18 | sPrefs.getBoolean("hidden", false) 19 | && sPrefs.getBoolean("purify_banner_ads", false) 20 | } 21 | 22 | override fun canHandler(api: String) = targetApis.any { api.startsWith(it) } 23 | 24 | override fun hook(response: String): String { 25 | val json = JSONObject(response) 26 | val modules = json.optJSONObject("result") 27 | ?.optJSONArray("modules") 28 | ?: return response 29 | var changed = false 30 | for (module in modules) { 31 | if (module.optString("style") != "banner_v3") 32 | continue 33 | val items = module.optJSONArray("items") 34 | ?: continue 35 | val toRemoveIdx = mutableListOf() 36 | var index = 0 37 | for (item in items) { 38 | if (item.optJSONObject("source_content") 39 | ?.optJSONObject("ad_content") != null 40 | ) toRemoveIdx.add(index) 41 | index++ 42 | } 43 | if (toRemoveIdx.isNotEmpty()) 44 | changed = true 45 | toRemoveIdx.reversed().forEach { 46 | items.remove(it) 47 | } 48 | } 49 | return if (changed) json.toString() else response 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/FavFolderDialogHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.widget.CheckBox 6 | import me.iacn.biliroaming.from 7 | import me.iacn.biliroaming.hookInfo 8 | import me.iacn.biliroaming.orNull 9 | import me.iacn.biliroaming.utils.* 10 | 11 | class FavFolderDialogHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | override fun startHook() { 13 | if (!sPrefs.getBoolean("disable_auto_subscribe", false)) return 14 | var hooked = false 15 | hookInfo.favFolderDialog.class_.from(mClassLoader)?.hookAfterAllConstructors { param -> 16 | if (hooked) return@hookAfterAllConstructors 17 | val apiCallbackClass = param.thisObject.javaClass.declaredFields 18 | .firstOrNull { f -> f.type.let { it != Context::class.java && !it.isInterface && it.isAbstract } } 19 | ?.also { it.isAccessible = true }?.get(param.thisObject)?.javaClass 20 | ?: return@hookAfterAllConstructors 21 | val onSuccessMethod = apiCallbackClass.declaredMethods.firstOrNull { 22 | !it.isSynthetic && it.parameterTypes.size == 1 23 | } ?: return@hookAfterAllConstructors 24 | val dialogFieldName = apiCallbackClass.declaredFields.firstOrNull { 25 | Dialog::class.java.isAssignableFrom(it.type) 26 | }?.name ?: return@hookAfterAllConstructors 27 | onSuccessMethod.hookAfterMethod { param2 -> 28 | val checkBox = param2.thisObject.getObjectField(dialogFieldName) 29 | ?.getObjectFieldAs(hookInfo.favFolderDialog.checkBox.orNull) 30 | ?: return@hookAfterMethod 31 | if (checkBox.isChecked) 32 | checkBox.toggle() 33 | } 34 | hooked = true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 50 | 53 | 54 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/StartActivityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.app.Instrumentation 5 | import android.content.ComponentName 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import me.iacn.biliroaming.utils.Log 10 | import me.iacn.biliroaming.utils.hookBeforeAllMethods 11 | import me.iacn.biliroaming.utils.hookBeforeMethod 12 | import me.iacn.biliroaming.utils.packageName 13 | import me.iacn.biliroaming.utils.sPrefs 14 | 15 | class StartActivityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 16 | override fun startHook() { 17 | "tv.danmaku.bili.ui.intent.IntentHandlerActivity".hookBeforeMethod(mClassLoader, "onCreate", Bundle::class.java) { param -> 18 | val a = param.thisObject as Activity 19 | val data = a.intent.data ?: return@hookBeforeMethod 20 | a.intent.data = data.buildUpon().encodedQuery(data.encodedQuery?.replace("&-Arouter=story", "")).build() 21 | } 22 | Instrumentation::class.java.hookBeforeAllMethods("execStartActivity") { param -> 23 | val intent = param.args[4] as? Intent ?: return@hookBeforeAllMethods 24 | val uri = intent.dataString ?: return@hookBeforeAllMethods 25 | if (sPrefs.getBoolean( 26 | "replace_story_video", 27 | false 28 | ) && uri.startsWith("bilibili://story/") 29 | ) { 30 | intent.component = ComponentName( 31 | intent.component?.packageName ?: packageName, 32 | "com.bilibili.video.videodetail.VideoDetailsActivity" 33 | ) 34 | intent.data = Uri.parse(uri.replace("bilibili://story/", "bilibili://video/")) 35 | } 36 | if (sPrefs.getBoolean("force_browser", false)) { 37 | if (intent.component?.className?.endsWith("MWebActivity") == true && 38 | intent.data?.authority?.matches(whileListDomain) == false) { 39 | Log.d("force_browser ${intent.data?.authority}") 40 | param.args[4] = Intent(Intent.ACTION_VIEW).apply { 41 | data = intent.data 42 | } 43 | } 44 | } 45 | } 46 | } 47 | companion object { 48 | val whileListDomain = Regex(""".*bilibili\.com|.*b23\.tv""") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/MiscRemoveAdsDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.Activity 4 | import android.content.SharedPreferences 5 | import android.widget.FrameLayout 6 | import android.widget.LinearLayout 7 | import android.widget.ScrollView 8 | import me.iacn.biliroaming.utils.Log 9 | import me.iacn.biliroaming.utils.dp 10 | 11 | class MiscRemoveAdsDialog(activity: Activity, prefs: SharedPreferences) : 12 | BaseWidgetDialog(activity) { 13 | init { 14 | val scrollView = ScrollView(context).apply { 15 | scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY 16 | } 17 | val root = LinearLayout(context).apply { 18 | orientation = LinearLayout.VERTICAL 19 | layoutParams = FrameLayout.LayoutParams( 20 | FrameLayout.LayoutParams.MATCH_PARENT, 21 | FrameLayout.LayoutParams.WRAP_CONTENT 22 | ) 23 | } 24 | scrollView.addView(root) 25 | 26 | val removeSearchAdsSwitch = string(R.string.remove_search_ads_title).let { 27 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 28 | } 29 | removeSearchAdsSwitch.isChecked = prefs.getBoolean("remove_search_ads", false) 30 | 31 | val removeCommentCmSwitch = string(R.string.remove_comment_cm_title).let { 32 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 33 | } 34 | removeCommentCmSwitch.isChecked = prefs.getBoolean("remove_comment_cm", false) 35 | 36 | val blockDmFeedbackSwitch = string(R.string.block_dm_feedback_title).let { 37 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 38 | } 39 | blockDmFeedbackSwitch.isChecked = prefs.getBoolean("block_dm_feedback", false) 40 | 41 | setTitle(string(R.string.misc_remove_ads_title)) 42 | 43 | setPositiveButton(android.R.string.ok) { _, _ -> 44 | prefs.edit().apply { 45 | putBoolean("remove_search_ads", removeSearchAdsSwitch.isChecked) 46 | putBoolean("remove_comment_cm", removeCommentCmSwitch.isChecked) 47 | putBoolean("block_dm_feedback", blockDmFeedbackSwitch.isChecked) 48 | }.apply() 49 | Log.toast(string(R.string.prefs_save_success_and_reboot)) 50 | } 51 | setNegativeButton(android.R.string.cancel, null) 52 | 53 | root.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 54 | 55 | setView(scrollView) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/TextFoldHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class TextFoldHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | private val commentMaxLines by lazy { 8 | sPrefs.getInt("text_fold_comment_max_lines", DEF_COMMENT_MAX_LINES) 9 | } 10 | private val dynMaxLines by lazy { 11 | sPrefs.getInt("text_fold_dyn_max_lines", DEF_DYN_MAX_LINES) 12 | } 13 | private val dynLinesToAll by lazy { 14 | sPrefs.getInt("text_fold_dyn_lines_to_all", DEF_DYN_LINES_TO_ALL) 15 | } 16 | 17 | private var maxLineFieldName = "" 18 | 19 | companion object { 20 | const val DEF_COMMENT_MAX_LINES = 6 21 | const val DEF_DYN_MAX_LINES = 4 22 | const val DEF_DYN_LINES_TO_ALL = 10 23 | } 24 | 25 | override fun startHook() { 26 | if (commentMaxLines != DEF_COMMENT_MAX_LINES) { 27 | "com.bapis.bilibili.main.community.reply.v1.ReplyControl".from(mClassLoader) 28 | ?.replaceMethod("getMaxLine") { commentMaxLines.toLong() } 29 | } 30 | 31 | if (dynMaxLines != DEF_DYN_MAX_LINES) { 32 | instance.ellipsizingTextViewClass?.hookBeforeMethod( 33 | "setMaxLines", 34 | Int::class.javaPrimitiveType 35 | ) { param -> 36 | val maxLines = param.args[0] as Int 37 | if (maxLines == DEF_DYN_MAX_LINES) 38 | param.args[0] = dynMaxLines 39 | } 40 | instance.ellipsizingTextViewClass?.hookAfterAllConstructors { param -> 41 | val fieldName = maxLineFieldName.ifEmpty { 42 | (instance.ellipsizingTextViewClass?.declaredFields 43 | ?.filter { it.type == Int::class.javaPrimitiveType } 44 | ?.find { param.thisObject.getIntField(it.name) == DEF_DYN_MAX_LINES } 45 | ?.name ?: "").also { maxLineFieldName = it } 46 | }.ifEmpty { return@hookAfterAllConstructors } 47 | param.thisObject.setIntField(fieldName, dynMaxLines) 48 | } 49 | } 50 | 51 | if (dynLinesToAll != DEF_DYN_LINES_TO_ALL) { 52 | instance.setLineToAllCountMethod?.let { 53 | instance.ellipsizingTextViewClass?.hookBeforeMethod( 54 | it, Int::class.javaPrimitiveType 55 | ) { param -> param.args[0] = dynLinesToAll } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 反馈 Bug 2 | description: 反馈遇到的问题 3 | labels: [bug] 4 | title: "[Bug] " 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 为了使我们更好地帮助你,请提供以下信息。 10 | - type: textarea 11 | id: desc 12 | attributes: 13 | label: 问题描述 14 | description: 发生了什么情况?有什么现状? 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: steps 19 | attributes: 20 | label: 复现步骤 21 | description: 如何复现 22 | placeholder: | 23 | 1. 打开... 24 | 2. 点击... 25 | 3. 出现...状况 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected 30 | attributes: 31 | label: 预期行为 32 | description: 正常情况下应该发生什么 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: actual 37 | attributes: 38 | label: 实际行为 39 | description: 实际上发生了什么 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: media 44 | attributes: 45 | label: 截图或录屏 46 | description: 问题复现时候的截图或录屏 47 | placeholder: 点击文本框下面小长条可以上传文件 48 | - type: input 49 | id: android-ver 50 | attributes: 51 | label: 安卓版本 52 | placeholder: "12" 53 | validations: 54 | required: true 55 | - type: input 56 | id: romaing-ver 57 | attributes: 58 | label: 哔哩漫游版本 59 | placeholder: 1.6.2 60 | validations: 61 | required: true 62 | - type: dropdown 63 | id: client 64 | attributes: 65 | label: 哔哩哔哩版本 66 | options: 67 | - 粉版(普通版) 68 | - 概念版 69 | - HD 版 70 | - play 版 71 | description: 目前仅支持粉版、概念版、HD 版和 play 版 72 | validations: 73 | required: true 74 | - type: input 75 | id: client-ver 76 | attributes: 77 | label: 哔哩哔哩版本号 78 | description: 非最新版本可能不受理 79 | placeholder: 6.74.0 80 | validations: 81 | required: true 82 | - type: input 83 | id: framework 84 | attributes: 85 | label: 使用的框架和版本 86 | placeholder: LSPosed 1.8.2 87 | validations: 88 | required: true 89 | - type: textarea 90 | id: misc 91 | attributes: 92 | label: 其他 93 | description: 如哪部番、Magisk 版本等 94 | - type: textarea 95 | id: logs 96 | attributes: 97 | label: 日志 98 | description: 请使用漫游自带的导出日志功能或者使用 `adb logcat`。无日志提交会被关闭。 99 | placeholder: 点击文本框下面小长条可以上传文件 100 | validations: 101 | required: true 102 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/SplashHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.net.Uri 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.widget.ImageView 7 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 8 | import me.iacn.biliroaming.utils.* 9 | import java.io.File 10 | 11 | class SplashHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | override fun startHook() { 13 | if (!sPrefs.getBoolean("custom_splash", false) && !sPrefs.getBoolean( 14 | "custom_splash_logo", 15 | false 16 | ) 17 | && !sPrefs.getBoolean("full_splash", false) 18 | ) return 19 | Log.d("startHook: Splash") 20 | 21 | instance.splashInfoClass?.hookAfterMethod( 22 | "getMode" 23 | ) { param -> 24 | param.result = if (sPrefs.getBoolean("full_splash", false)) { 25 | "full" 26 | } else { 27 | param.result 28 | } 29 | } 30 | 31 | instance.brandSplashClass?.hookAfterMethod( 32 | "onViewCreated", 33 | View::class.java, 34 | Bundle::class.java 35 | ) { param -> 36 | val view = param.args[0] as View 37 | if (sPrefs.getBoolean("custom_splash", false)) { 38 | val brandId = getId("brand_splash") 39 | val fullId = getId("full_brand_splash") 40 | val brandSplash = view.findViewById(brandId) 41 | val full = if (fullId != 0) view.findViewById(fullId) else null 42 | val splashImage = File(currentContext.filesDir, SPLASH_IMAGE) 43 | if (splashImage.exists()) { 44 | val uri = Uri.fromFile(splashImage) 45 | brandSplash.setImageURI(uri) 46 | full?.setImageURI(uri) 47 | } else { 48 | brandSplash.alpha = .0f 49 | full?.alpha = .0f 50 | } 51 | } 52 | if (sPrefs.getBoolean("custom_splash_logo", false)) { 53 | val logoId = getId("brand_logo") 54 | val brandLogo = view.findViewById(logoId) 55 | val logoImage = File(currentContext.filesDir, LOGO_IMAGE) 56 | if (logoImage.exists()) 57 | brandLogo.setImageURI(Uri.fromFile(logoImage)) 58 | else 59 | brandLogo.alpha = .0f 60 | } 61 | } 62 | } 63 | 64 | companion object { 65 | const val SPLASH_IMAGE = "biliroaming_splash" 66 | const val LOGO_IMAGE = "biliroaming_logo" 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/xml/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 20 | 21 | 25 | 28 | 29 | 30 | 31 | 34 | 35 | 38 | 39 | 43 | 46 | 47 | 48 | 52 | 53 | 57 | 60 | 61 | 62 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/LosslessSettingHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class LosslessSettingHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | private var losslessEnabled: Boolean 8 | get() = biliPrefs.getBoolean("biliroaming_lossless", false) 9 | set(value) = biliPrefs.edit().putBoolean("biliroaming_lossless", value).apply() 10 | 11 | override fun startHook() { 12 | if (!sPrefs.getBoolean("remember_lossless_setting", false)) 13 | return 14 | instance.playURLMossClass?.run { 15 | hookBeforeMethod( 16 | "playConf", 17 | "com.bapis.bilibili.app.playurl.v1.PlayConfReq", 18 | instance.mossResponseHandlerClass 19 | ) { param -> 20 | param.args[1] = param.args[1].mossResponseHandlerProxy { 21 | it?.callMethod("getPlayConf") 22 | ?.callMethod("getLossLessConf") 23 | ?.callMethod("getConfValue") 24 | ?.callMethod("setSwitchVal", losslessEnabled) 25 | } 26 | } 27 | hookBeforeMethod( 28 | "playConfEdit", 29 | "com.bapis.bilibili.app.playurl.v1.PlayConfEditReq" 30 | ) { param -> 31 | param.args[0].callMethodAs>("getPlayConfList") 32 | .firstOrNull { 33 | it.callMethodAs("getConfTypeValue") == 30 // LOSSLESS 34 | }?.callMethod("getConfValue") 35 | ?.callMethodAs("getSwitchVal") 36 | ?.let { losslessEnabled = it } 37 | } 38 | hookAfterMethod( 39 | "playView", instance.playViewReqClass 40 | ) { param -> 41 | param.result?.callMethod("getPlayConf") 42 | ?.takeIf { it.callMethodAs("hasLossLessConf") } 43 | ?.callMethod("getLossLessConf") 44 | ?.callMethod("getConfValue") 45 | ?.callMethod("setSwitchVal", losslessEnabled) 46 | } 47 | } 48 | instance.playerMossClass?.hookAfterMethod( 49 | "playViewUnite", instance.playViewUniteReqClass 50 | ) { param -> 51 | param.result?.callMethod("getPlayDeviceConf") 52 | ?.callMethodAs>("internalGetMutableDeviceConfs")?.let { 53 | it[30/*LOSSLESS*/]?.callMethod("getConfValue") 54 | ?.callMethod("setSwitchVal", losslessEnabled) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Log.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package me.iacn.biliroaming.utils 4 | 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.widget.Toast 8 | import de.robv.android.xposed.XposedBridge 9 | import me.iacn.biliroaming.BiliBiliPackage 10 | import me.iacn.biliroaming.Constant.TAG 11 | import android.util.Log as ALog 12 | 13 | object Log { 14 | 15 | private val handler by lazy { Handler(Looper.getMainLooper()) } 16 | private var toast: Toast? = null 17 | 18 | fun toast(msg: String, force: Boolean = false, duration: Int = Toast.LENGTH_SHORT, alsoLog: Boolean = true) { 19 | if (!force && !sPrefs.getBoolean("show_info", true)) return 20 | handler.post { 21 | BiliBiliPackage.instance.toastHelperClass?.runCatchingOrNull { 22 | callStaticMethod(BiliBiliPackage.instance.cancelShowToast()) 23 | callStaticMethod( 24 | BiliBiliPackage.instance.showToast(), 25 | currentContext, 26 | "哔哩漫游:$msg", 27 | duration 28 | ) 29 | Unit 30 | } ?: run { 31 | toast?.cancel() 32 | toast = Toast.makeText(currentContext, "", duration).apply { 33 | setText("哔哩漫游:$msg") 34 | show() 35 | } 36 | } 37 | } 38 | if (alsoLog) w(msg) 39 | } 40 | 41 | @JvmStatic 42 | private fun doLog(f: (String, String) -> Int, obj: Any?, toXposed: Boolean = false) { 43 | val str = if (obj is Throwable) ALog.getStackTraceString(obj) else obj.toString() 44 | 45 | if (str.length > maxLength) { 46 | val chunkCount: Int = str.length / maxLength 47 | for (i in 0..chunkCount) { 48 | val max: Int = maxLength * (i + 1) 49 | if (max >= str.length) { 50 | doLog(f, str.substring(maxLength * i)) 51 | } else { 52 | doLog(f, str.substring(maxLength * i, max)) 53 | } 54 | } 55 | } else { 56 | f(TAG, str) 57 | if (toXposed) 58 | XposedBridge.log("$TAG : $str") 59 | } 60 | } 61 | 62 | @JvmStatic 63 | fun d(obj: Any?) { 64 | doLog(ALog::d, obj) 65 | } 66 | 67 | @JvmStatic 68 | fun i(obj: Any?) { 69 | doLog(ALog::i, obj) 70 | } 71 | 72 | @JvmStatic 73 | fun e(obj: Any?) { 74 | doLog(ALog::e, obj, true) 75 | } 76 | 77 | @JvmStatic 78 | fun v(obj: Any?) { 79 | doLog(ALog::v, obj) 80 | } 81 | 82 | @JvmStatic 83 | fun w(obj: Any?) { 84 | doLog(ALog::w, obj) 85 | } 86 | 87 | private const val maxLength = 3000 88 | } 89 | 90 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ me ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | CCACHE_DIR: ${{ github.workspace }}/.ccache 12 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 13 | CCACHE_NOHASHDIR: true 14 | CCACHE_MAXSIZE: 1G 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: 'recursive' 19 | fetch-depth: 0 20 | - name: Setup JDK 17 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: 'temurin' 24 | java-version: 17 25 | cache: 'gradle' 26 | - name: Retrieve version 27 | run: | 28 | echo VERSION=$(echo ${{ github.event.head_commit.id }} | head -c 10) >> $GITHUB_ENV 29 | - name: Set up ccache 30 | uses: hendrikmuhs/ccache-action@v1.2 31 | with: 32 | key: ${{ runner.os }}-${{ github.sha }} 33 | restore-keys: ${{ runner.os }} 34 | - name: Build with Gradle 35 | run: | 36 | echo 'org.gradle.caching=true' >> gradle.properties 37 | echo 'org.gradle.parallel=true' >> gradle.properties 38 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 39 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 40 | echo 'android.native.buildOutput=verbose' >> gradle.properties 41 | ./gradlew -PappVerName=${{ env.VERSION }} assembleRelease assembleDebug 42 | - name: Upload built apk 43 | if: success() 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: snapshot 47 | path: | 48 | app/build/outputs/apk 49 | app/build/outputs/mapping 50 | app/release 51 | - name: Post to channel 52 | if: github.ref == 'refs/heads/master' 53 | env: 54 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO }} 55 | BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} 56 | FILE: app/release/BiliRoaming_${{ env.VERSION }}.apk 57 | COMMIT_MESSAGE: |+ 58 | New push to github\! 59 | ``` 60 | ${{ github.event.head_commit.message }} 61 | ```by `${{ github.event.head_commit.author.name }}` 62 | See commit detail [here](${{ github.event.head_commit.url }}) 63 | Snapshot apk is attached \(unsupported by TAICHI\) 64 | run: | 65 | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'` 66 | curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://release%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F release="@$FILE" 67 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_raw.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | yujincheng08/iAcn/djytw/ 4 | https://api.github.com/repos/zjns/BiliRoamingX/releases 5 | https://github.com/zjns/BiliRoamingX/releases/tag/%1$s 6 | https://github.com/yujincheng08/BiliRoaming/wiki/%E5%85%AC%E5%85%B1%E8%A7%A3%E6%9E%90%E6%9C%8D%E5%8A%A1%E5%99%A8 7 | https://github.com/zjns/BiliRoamingX 8 | https://afdian.net/a/yujincheng08 9 | https://t.me/bb_show 10 | https://github.com/yujincheng08/BiliRoaming/wiki 11 | mqqguild://guild/share?inviteCode=NVoD5&from=246610 12 | upos-sz-mirrorbos.bilivideo.com 13 | upos-sz-mirrorcos.bilivideo.com 14 | upos-sz-mirrorcosb.bilivideo.com 15 | upos-sz-mirrorcoso1.bilivideo.com 16 | upos-sz-mirrorhw.bilivideo.com 17 | upos-sz-mirrorhwb.bilivideo.com 18 | upos-sz-mirrorhwo1.bilivideo.com 19 | upos-sz-mirror08c.bilivideo.com 20 | upos-sz-mirror08h.bilivideo.com 21 | upos-sz-mirror08ct.bilivideo.com 22 | upos-sz-mirrorali.bilivideo.com 23 | upos-sz-mirroralib.bilivideo.com 24 | upos-sz-mirroralio1.bilivideo.com 25 | upos-hz-mirrorakam.akamaized.net 26 | upos-sz-mirroraliov.bilivideo.com 27 | upos-sz-mirrorhwov.bilivideo.com 28 | upos-sz-mirrorcosov.bilivideo.com 29 | cn-hk-eq-bcache-01.bilivideo.com 30 | upos-tf-all-hw.bilivideo.com 31 | upos-tf-all-tx.bilivideo.com 32 | %sKB/s 33 | UPOS 34 | UID 35 | 字幕转换失败,请重试 36 | 转换字典下载失败,请重试 37 | 请注意,站内宣传漫游或脚本会被拉黑 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync Fork 2 | 3 | on: 4 | schedule: 5 | - cron: '1 0-14 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync: 10 | name: Sync 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.x' 18 | 19 | - name: Prepare Python packages 20 | run: | 21 | pip install -U wheel 22 | pip install -U pyrogram tgcrypto 23 | 24 | - name: Get repo name 25 | run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV 26 | 27 | - name: Sync fork 28 | id: sync 29 | uses: zjns/repo-sync@master 30 | with: 31 | token: ${{ secrets.HUB_TOKEN }} 32 | up_repo: 'yujincheng08/BiliRoaming' 33 | up_branch: master 34 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 35 | gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 36 | 37 | - name: Report via Telegram 38 | shell: python 39 | if: always() 40 | env: 41 | API_ID: ${{ secrets.TELEGRAM_API_ID }} 42 | API_HASH: ${{ secrets.TELEGRAM_API_HASH }} 43 | BOT_TOKEN: ${{ secrets.TELEGRAM_BOT }} 44 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO_ME }} 45 | SUCCESS: ${{ job.status == 'success' }} 46 | run: | 47 | import asyncio 48 | import os 49 | from pyrogram import Client 50 | commit_count=${{ steps.sync.outputs.commit_count }} 51 | diff_commits=${{ toJSON(fromJSON(steps.sync.outputs.commits).*.message) }} 52 | if not commit_count: 53 | exit(0) 54 | async def main(): 55 | bot = Client( 56 | "client", 57 | in_memory=True, 58 | api_id=os.environ["API_ID"], 59 | api_hash=os.environ["API_HASH"], 60 | bot_token=os.environ["BOT_TOKEN"], 61 | ) 62 | async with bot: 63 | channel_id = int(os.environ["CHANNEL_ID"]) 64 | repo = os.environ["REPO_NAME"] 65 | repo_url = f"https://github.com/{os.environ['GITHUB_REPOSITORY']}" 66 | success = True if (os.environ["SUCCESS"] == "true") else False 67 | text = f"🎉 Fork sync success!\nRepo: [{repo}]({repo_url})" if (success) else f"😿 Fork sync failed!\nRepo: [{repo}]({repo_url})" 68 | if success: 69 | commits = "\n".join([f"∙ {commit}" for commit in diff_commits[:10]]) 70 | text += f"\n\nSynced commits:\n
{commits}
" 71 | await bot.send_message(chat_id=channel_id, text=text, disable_web_page_preview=True) 72 | async def wait(): 73 | try: 74 | await asyncio.wait_for(main(), timeout=60) 75 | except asyncio.TimeoutError: 76 | print("message send timeout!!!") 77 | exit(1) 78 | asyncio.run(wait()) 79 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/LiveRoomHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import android.view.MotionEvent 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.utils.* 7 | 8 | class LiveRoomHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | if (sPrefs.getBoolean("forbid_switch_live_room", false)) { 11 | instance.livePagerRecyclerViewClass?.replaceMethod( 12 | "onInterceptTouchEvent", 13 | MotionEvent::class.java 14 | ) { false } 15 | } 16 | if (sPrefs.getBoolean("disable_live_room_double_click", false)) { 17 | instance.liveRoomPlayerViewClass?.declaredMethods?.find { it.name == "onDoubleTap" } 18 | ?.hookBeforeMethod { param -> 19 | runCatching { 20 | val player = param.thisObject.callMethod("getPlayerCommonBridge") 21 | ?: return@hookBeforeMethod 22 | val method = if (player.callMethodAs("isPlaying")) 23 | "pause" else "resume" 24 | player.callMethod(method) 25 | }.onSuccess { param.result = true } 26 | } 27 | val lastTouchUpTimeField = 28 | instance.liveRoomPlayerViewClass?.findFirstFieldByExactTypeOrNull(Long::class.javaPrimitiveType!!) 29 | if (lastTouchUpTimeField != null) { 30 | instance.liveRoomPlayerViewClass?.declaredMethods?.filter { m -> 31 | m.isPublic && m.returnType == Void.TYPE && m.parameterTypes.let { it.size == 1 && it[0] == MotionEvent::class.java } 32 | }?.forEach { m -> 33 | m.hookAfterMethod { lastTouchUpTimeField.setLong(it.thisObject, 0L) } 34 | } 35 | } 36 | } 37 | if (!sPrefs.getBoolean("revert_live_room_feed", false)) { 38 | return 39 | } 40 | 41 | instance.liveKvConfigHelperClass?.hookAfterMethod( 42 | "getLocalValue", 43 | String::class.java 44 | ) { param -> 45 | if (param.args[0] == "live_new_room_setting") { 46 | if (param.result != null) { 47 | val obj = (param.result as String).toJSONObject() 48 | if (obj.get("all_new_room_enable") == "2") { 49 | obj.put("all_new_room_enable", "0") 50 | param.result = obj.toString() 51 | } 52 | } 53 | } 54 | } 55 | instance.liveRoomActivityClass?.hookBeforeMethod( 56 | "onCreate", 57 | Bundle::class.java 58 | ) { param -> 59 | val intent = (param.thisObject as android.app.Activity).intent 60 | if (intent.getStringExtra("is_room_feed") == "1") { 61 | intent.putExtra("is_room_feed", "0") 62 | Log.toast("已强制直播间使用旧版样式") 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/DownloadThreadHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.NumberPicker 8 | import android.widget.TextView 9 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 10 | import me.iacn.biliroaming.utils.* 11 | 12 | class DownloadThreadHook(classLoader: ClassLoader) : BaseHook(classLoader) { 13 | override fun startHook() { 14 | if (!sPrefs.getBoolean("custom_download_thread", false)) return 15 | Log.d("startHook: DownloadThread") 16 | instance.downloadThreadListenerClass?.run { 17 | hookBeforeAllConstructors { param -> 18 | val view = param.args.find { it is TextView } as? TextView 19 | ?: return@hookBeforeAllConstructors 20 | val visibility = if (view.tag as Int == 1) { 21 | view.text = "自定义" 22 | View.VISIBLE 23 | } else { 24 | View.INVISIBLE 25 | } 26 | (view.parent as ViewGroup).getChildAt(1).visibility = visibility 27 | } 28 | replaceMethod("onClick", View::class.java) { param -> 29 | var textViewField: String? = null 30 | var viewHostField: String? = null 31 | declaredFields.forEach { 32 | when (it.type) { 33 | instance.downloadThreadViewHostClass -> viewHostField = it.name 34 | TextView::class.java -> textViewField = it.name 35 | } 36 | } 37 | val view = param.thisObject.getObjectFieldAs(textViewField) 38 | if (view.tag as? Int == 1) { 39 | AlertDialog.Builder(view.context).create().run { 40 | setTitle("自定义同时缓存数") 41 | val numberPicker = NumberPicker(context).apply { 42 | minValue = 1 43 | maxValue = 64 44 | wrapSelectorWheel = false 45 | value = param.thisObject.getObjectField(viewHostField) 46 | ?.getIntField(instance.downloadingThread()) 47 | ?: 1 48 | } 49 | setView(numberPicker, 50, 0, 50, 0) 50 | setButton(AlertDialog.BUTTON_POSITIVE, "OK") { _, _ -> 51 | view.tag = numberPicker.value 52 | param.invokeOriginalMethod() 53 | } 54 | show() 55 | } 56 | } else { 57 | param.invokeOriginalMethod() 58 | } 59 | } 60 | } 61 | instance.reportDownloadThreadClass?.replaceMethod( 62 | instance.reportDownloadThread(), 63 | Context::class.java, 64 | Int::class.javaPrimitiveType 65 | ) {} 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/OkHttpHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.hook.api.* 5 | import me.iacn.biliroaming.utils.* 6 | import java.net.HttpURLConnection 7 | 8 | class OkHttpHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | private val apiHooks = mutableListOf() 11 | 12 | init { 13 | apiHooks.add(SeasonRcmdHook) 14 | apiHooks.add(CardsHook) 15 | apiHooks.add(BannerV3AdHook) 16 | apiHooks.add(SkinHook) 17 | if (platform != "android_hd") 18 | apiHooks.add(BannerV8AdHook) 19 | } 20 | 21 | override fun startHook() { 22 | if (apiHooks.all { !it.enabled }) return 23 | 24 | instance.responseClass?.hookAfterAllConstructors out@{ param -> 25 | val response = param.thisObject ?: return@out 26 | val requestField = instance.requestField() ?: return@out 27 | val urlField = instance.urlField() ?: return@out 28 | val request = response.getObjectField(requestField) ?: return@out 29 | val url = request.getObjectField(urlField)?.toString() ?: return@out 30 | for (hook in apiHooks) { 31 | if (!hook.enabled || !hook.canHandler(url)) 32 | continue 33 | val okioClass = instance.okioClass ?: return@out 34 | val bufferedSourceClass = instance.bufferedSourceClass ?: return@out 35 | val codeField = instance.codeField() ?: return@out 36 | val bodyField = instance.bodyField() ?: return@out 37 | val stringMethod = instance.string() ?: return@out 38 | val sourceMethod = instance.source() ?: return@out 39 | val bufferMethod = instance.sourceBuffer() ?: return@out 40 | response.getIntField(codeField).takeIf { it == HttpURLConnection.HTTP_OK } 41 | ?: return@out 42 | 43 | val responseBody = response.getObjectField(bodyField) 44 | val sourceField = responseBody?.javaClass 45 | ?.findFieldByExactTypeOrNull(bufferedSourceClass) ?: return@out 46 | val longType = Long::class.javaPrimitiveType!! 47 | val contentLengthField = responseBody.javaClass.findFieldByExactTypeOrNull(longType) 48 | ?: return@out 49 | val respString = if (hook.decodeResponse()) { 50 | responseBody.callMethod(stringMethod)?.toString() ?: return@out 51 | } else "" 52 | val newResponse = hook.hook(respString) 53 | val stream = newResponse.byteInputStream() 54 | val length = stream.available() 55 | val source = okioClass.callStaticMethod(sourceMethod, stream) ?: return@out 56 | val bufferedSource = okioClass.callStaticMethod(bufferMethod, source) ?: return@out 57 | sourceField.set(responseBody, bufferedSource) 58 | contentLengthField.set(responseBody, length) 59 | break 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/DexHelper.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import java.lang.reflect.Field 4 | import java.lang.reflect.Member 5 | 6 | class DexHelper(private val classLoader: ClassLoader) : AutoCloseable { 7 | 8 | private val token = load(classLoader) 9 | 10 | external fun findMethodUsingString( 11 | str: String, 12 | matchPrefix: Boolean = false, 13 | returnType: Long = -1, 14 | parameterCount: Short = -1, 15 | parameterShorty: String? = null, 16 | declaringClass: Long = -1, 17 | parameterTypes: LongArray? = null, 18 | containsParameterTypes: LongArray? = null, 19 | dexPriority: IntArray? = null, 20 | findFirst: Boolean = true 21 | ): LongArray 22 | 23 | external fun findMethodInvoking( 24 | methodIndex: Long, 25 | returnType: Long = -1, 26 | parameterCount: Short = -1, 27 | parameterShorty: String? = null, 28 | declaringClass: Long = -1, 29 | parameterTypes: LongArray? = null, 30 | containsParameterTypes: LongArray? = null, 31 | dexPriority: IntArray? = null, 32 | findFirst: Boolean = true 33 | ): LongArray 34 | 35 | external fun findMethodInvoked( 36 | methodIndex: Long, 37 | returnType: Long = -1, 38 | parameterCount: Short = -1, 39 | parameterShorty: String? = null, 40 | declaringClass: Long = -1, 41 | parameterTypes: LongArray? = null, 42 | containsParameterTypes: LongArray? = null, 43 | dexPriority: IntArray? = null, 44 | findFirst: Boolean = true 45 | ): LongArray 46 | 47 | external fun findMethodSettingField( 48 | fieldIndex: Long, 49 | returnType: Long = -1, 50 | parameterCount: Short = -1, 51 | parameterShorty: String? = null, 52 | declaringClass: Long = -1, 53 | parameterTypes: LongArray? = null, 54 | containsParameterTypes: LongArray? = null, 55 | dexPriority: IntArray? = null, 56 | findFirst: Boolean = true 57 | ): LongArray 58 | 59 | external fun findMethodGettingField( 60 | fieldIndex: Long, 61 | returnType: Long = -1, 62 | parameterCount: Short = -1, 63 | parameterShorty: String? = null, 64 | declaringClass: Long = -1, 65 | parameterTypes: LongArray? = null, 66 | containsParameterTypes: LongArray? = null, 67 | dexPriority: IntArray? = null, 68 | findFirst: Boolean = true 69 | ): LongArray 70 | 71 | external fun findField( 72 | type: Long, 73 | dexPriority: IntArray? = null, 74 | findFirst: Boolean = true 75 | ): LongArray 76 | 77 | external fun decodeMethodIndex(methodIndex: Long): Member? 78 | 79 | external fun encodeMethodIndex(method: Member): Long 80 | 81 | external fun decodeFieldIndex(fieldIndex: Long): Field? 82 | 83 | external fun encodeFieldIndex(field: Field): Long 84 | 85 | external fun encodeClassIndex(clazz: Class<*>): Long 86 | 87 | external fun decodeClassIndex(classIndex: Long): Class<*>? 88 | 89 | external fun createFullCache() 90 | 91 | external override fun close() 92 | 93 | protected fun finalize() = close() 94 | 95 | private external fun load(classLoader: ClassLoader): Long 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/EnvHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.content.SharedPreferences 4 | import me.iacn.biliroaming.utils.* 5 | import java.util.regex.Pattern 6 | 7 | class EnvHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | Log.d("startHook: Env") 10 | "com.bilibili.lib.blconfig.internal.EnvContext\$preBuiltConfig\$2".hookAfterMethod( 11 | mClassLoader, 12 | "invoke" 13 | ) { param -> 14 | @Suppress("UNCHECKED_CAST") 15 | val result = param.result as MutableMap 16 | for (config in configSet) { 17 | (if (sPrefs.getBoolean( 18 | config.config, 19 | false 20 | ) 21 | ) config.trueValue else config.falseValue) 22 | ?.let { result[config.key] = it } ?: result.remove(config.key) 23 | } 24 | } 25 | "com.bilibili.lib.blconfig.internal.TypedContext\$dataSp\$2".hookAfterMethod( 26 | mClassLoader, 27 | "invoke" 28 | ) { param -> 29 | val result = param.result as SharedPreferences 30 | // this indicates the proper instance 31 | if (!result.contains("bv.enable_bv")) return@hookAfterMethod 32 | for (config in configSet) { 33 | (if (sPrefs.getBoolean( 34 | config.config, 35 | false 36 | ) 37 | ) config.trueValue else config.falseValue) 38 | ?.let { result.edit().putString(config.key, it).apply() } 39 | ?: result.edit().remove(config.key).apply() 40 | } 41 | } 42 | 43 | // // Disable tinker 44 | // "com.tencent.tinker.loader.app.TinkerApplication".findClass(mClassLoader)?.hookBeforeAllConstructors { param -> 45 | // param.args[0] = 0 46 | // } 47 | } 48 | 49 | override fun lateInitHook() { 50 | Log.d("lateHook: Env") 51 | if (sPrefs.getBoolean("enable_av", false)) { 52 | val compatClass = "com.bilibili.droid.BVCompat".findClassOrNull(mClassLoader) 53 | compatClass?.declaredFields?.forEach { 54 | val field = compatClass.getStaticObjectField(it.name) 55 | if (field is Pattern && field.pattern() == "av[1-9]\\d*") 56 | compatClass.setStaticObjectField( 57 | it.name, 58 | Pattern.compile("(av[1-9]\\d*)|(BV1[1-9A-NP-Za-km-z]{9})", field.flags()) 59 | ) 60 | } 61 | } 62 | } 63 | 64 | companion object { 65 | 66 | private val encryptedValueMap = hashMapOf( 67 | "0" to "Irb5O7Q8Ka0ojD4qqScgqg==", 68 | "1" to "Y260Cyvp6HZEboaGO+YGMw==" 69 | ) 70 | 71 | class ConfigTuple( 72 | val key: String, 73 | val config: String, 74 | val trueValue: String?, 75 | val falseValue: String? 76 | ) 77 | 78 | val configSet = listOf( 79 | ConfigTuple( 80 | "bv.enable_bv", 81 | "enable_av", 82 | encryptedValueMap["0"], 83 | encryptedValueMap["1"] 84 | ), 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/MiniProgramHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.Log 6 | import me.iacn.biliroaming.utils.bv2av 7 | import me.iacn.biliroaming.utils.hookBeforeMethod 8 | import me.iacn.biliroaming.utils.sPrefs 9 | import java.net.HttpURLConnection 10 | import java.net.URL 11 | 12 | class MiniProgramHook(classLoader: ClassLoader) : BaseHook(classLoader) { 13 | private val extractUrl = Regex("""(.*)(http\S*)(.*)""") 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("mini_program", false)) return 16 | Log.d("startHook: MiniProgram") 17 | instance.shareWrapperClass?.hookBeforeMethod( 18 | instance.shareWrapper(), 19 | String::class.java, 20 | Bundle::class.java 21 | ) { param -> 22 | val platform = param.args[0] as String 23 | val bundle = param.args[1] as Bundle 24 | if (platform == "COPY") { 25 | bundle.getString("params_content")?.let { content -> 26 | extractUrl.matchEntire(content) 27 | }?.let { 28 | listOf(it.groups[1]?.value, it.groups[2]?.value, it.groups[3]?.value) 29 | }?.let { (prefix, url, postfix) -> 30 | val conn = URL(url).openConnection() as HttpURLConnection 31 | conn.requestMethod = "GET" 32 | conn.instanceFollowRedirects = false 33 | conn.connect() 34 | if (conn.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { 35 | val target = URL(conn.getHeaderField("Location")) 36 | val bv = 37 | target.path.split("/") 38 | .firstOrNull { it.startsWith("BV") && it.length == 12 } 39 | ?: return@hookBeforeMethod 40 | val av = bv2av(bv) 41 | val query = target.query.split("&").map { 42 | it.split("=") 43 | }.filter { 44 | it.size == 2 45 | }.filter { 46 | it[0] == "p" 47 | }.joinToString("") { 48 | it.joinToString("") 49 | } 50 | bundle.putString( 51 | "params_content", 52 | "${prefix}https://b23.tv/av${av}${if (query.isEmpty()) "" else "/${query}"}$postfix" 53 | ) 54 | } 55 | 56 | } 57 | return@hookBeforeMethod 58 | } 59 | if (bundle.getString("params_type") != "type_min_program") return@hookBeforeMethod 60 | bundle.putString("params_type", "type_web") 61 | if (bundle.getString("params_title") == "哔哩哔哩") { 62 | bundle.putString("params_title", bundle.getString("params_content")) 63 | bundle.putString("params_content", "由哔哩漫游分享") 64 | } 65 | if (bundle.getString("params_content")?.startsWith("已观看") == true) { 66 | bundle.putString("params_content", "${bundle.getString("params_content")}\n由哔哩漫游分享") 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/customize_backup_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 |