├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── new_server.yml ├── dependabot.yml └── workflows │ ├── PR.yml │ └── android.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── .gitignore │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── xhook.js │ └── xposed_init │ ├── java │ └── me │ │ └── iacn │ │ └── biliroaming │ │ ├── ARGBColorChooseDialog.kt │ │ ├── BaseWidgetDialog.kt │ │ ├── BiliBiliPackage.kt │ │ ├── ColorChooseDialog.kt │ │ ├── CommentFilterDialog.kt │ │ ├── Constant.kt │ │ ├── CustomSubtitleDialog.kt │ │ ├── DynamicFilterDialog.kt │ │ ├── HomeFilterDialog.kt │ │ ├── MainActivity.kt │ │ ├── SearchFilterDialog.kt │ │ ├── SettingDialog.kt │ │ ├── SpeedTestDialog.kt │ │ ├── VideoExportDialog.kt │ │ ├── XposedInit.kt │ │ ├── hook │ │ ├── AllowMiniPlayHook.kt │ │ ├── AutoLikeHook.kt │ │ ├── BangumiPageAdHook.kt │ │ ├── BangumiPlayUrlHook.kt │ │ ├── BangumiSeasonHook.kt │ │ ├── BaseHook.kt │ │ ├── BlockUpdateHook.kt │ │ ├── CommentImageHook.kt │ │ ├── CopyHook.kt │ │ ├── CoverHook.kt │ │ ├── CustomThemeHook.kt │ │ ├── DanmakuHook.kt │ │ ├── DialogBlurBackgroundHook.kt │ │ ├── DownloadThreadHook.kt │ │ ├── DrawerHook.kt │ │ ├── DynamicHook.kt │ │ ├── EnvHook.kt │ │ ├── FullStoryHook.kt │ │ ├── HintHook.kt │ │ ├── JsonHook.kt │ │ ├── KillDelayBootHook.kt │ │ ├── LiveQualityHook.kt │ │ ├── LiveRoomHook.kt │ │ ├── MultiWindowHook.kt │ │ ├── MusicNotificationHook.kt │ │ ├── P2pHook.kt │ │ ├── PegasusHook.kt │ │ ├── PlayArcConfHook.kt │ │ ├── PlayerLongPressHook.kt │ │ ├── ProtoBufHook.kt │ │ ├── PublishToFollowingHook.kt │ │ ├── QualityHook.kt │ │ ├── SSLHook.kt │ │ ├── SettingHook.kt │ │ ├── ShareHook.kt │ │ ├── SpeedHook.kt │ │ ├── SplashHook.kt │ │ ├── StartActivityHook.kt │ │ ├── SubtitleHook.kt │ │ ├── TeenagersModeHook.kt │ │ ├── TryWatchVipQualityHook.kt │ │ ├── UposReplaceHook.kt │ │ ├── VideoQualityHook.kt │ │ ├── VipSectionHook.kt │ │ └── WebViewHook.kt │ │ ├── network │ │ └── BiliRoamingApi.kt │ │ └── utils │ │ ├── Coroutines.kt │ │ ├── DexHelper.java │ │ ├── KotlinXposedHelper.kt │ │ ├── Log.kt │ │ ├── StrokeSpan.kt │ │ ├── SubtitleHelper.kt │ │ ├── UposReplaceHelper.kt │ │ └── Utils.kt │ ├── jni │ ├── CMakeLists.txt │ └── biliroaming.cc │ ├── proto │ └── me │ │ └── iacn │ │ └── biliroaming │ │ ├── api.proto │ │ └── configs.proto │ └── res │ ├── drawable │ ├── demo.webp │ ├── demo2.webp │ ├── ic_clear.xml │ ├── ic_launcher_foreground.xml │ └── tp.webp │ ├── layout │ ├── cdn_speedtest_item.xml │ ├── custom_button.xml │ ├── custom_subtitle_dialog.xml │ ├── customize_backup_dialog.xml │ ├── dialog_argb_color_choose.xml │ ├── dialog_color_choose.xml │ ├── feature.xml │ ├── search_bar.xml │ ├── seekbar_dialog.xml │ └── video_choose.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── values-night │ ├── colors.xml │ └── styles.xml │ ├── values-zh-rTW │ ├── arrays.xml │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ ├── strings_raw.xml │ └── styles.xml │ └── xml │ ├── main_activity.xml │ └── prefs_setting.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imgs ├── icon.png ├── stick1.png ├── stick2.png ├── stick3.png └── stick4.png └── settings.gradle.kts /.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 | -------------------------------------------------------------------------------- /.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: 可以订阅更新、讨论交流 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | groups: 14 | maven-dependencies: 15 | patterns: 16 | - "*" 17 | registries: 18 | maven-google: 19 | type: maven-repository 20 | url: "https://dl.google.com/dl/android/maven2/" 21 | gralde-plugin: 22 | type: maven-repository 23 | url: "https://plugins.gradle.org/m2/" 24 | -------------------------------------------------------------------------------- /.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 ] 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: Setup Android SDK 32 | uses: android-actions/setup-android@v3 33 | - uses: seanmiddleditch/gha-setup-ninja@master 34 | with: 35 | version: 1.12.0 36 | - name: Set up ccache 37 | uses: hendrikmuhs/ccache-action@v1.2 38 | with: 39 | key: ${{ runner.os }}-${{ github.sha }} 40 | restore-keys: ${{ runner.os }} 41 | - name: Build with Gradle 42 | run: | 43 | sudo rm -rf $ANDROID_HOME/cmake 44 | echo 'org.gradle.caching=true' >> gradle.properties 45 | echo 'org.gradle.parallel=true' >> gradle.properties 46 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 47 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 48 | echo 'android.native.buildOutput=verbose' >> gradle.properties 49 | ./gradlew assemble 50 | - name: Stop gradle daemon 51 | run: ./gradlew --stop 52 | - name: Upload build artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: ${{ matrix.os }}-artifact 56 | path: | 57 | app/build/outputs 58 | app/release 59 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 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: Setup Android SDK 27 | uses: android-actions/setup-android@v3 28 | - uses: seanmiddleditch/gha-setup-ninja@master 29 | with: 30 | version: 1.12.0 31 | - name: Retrieve version 32 | run: | 33 | echo VERSION=$(echo ${{ github.event.head_commit.id }} | head -c 10) >> $GITHUB_ENV 34 | - name: Set up ccache 35 | uses: hendrikmuhs/ccache-action@v1.2 36 | with: 37 | key: ${{ runner.os }}-${{ github.sha }} 38 | restore-keys: ${{ runner.os }} 39 | - name: Build with Gradle 40 | run: | 41 | sudo rm -rf $ANDROID_HOME/cmake 42 | echo 'org.gradle.caching=true' >> gradle.properties 43 | echo 'org.gradle.parallel=true' >> gradle.properties 44 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 45 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 46 | echo 'android.native.buildOutput=verbose' >> gradle.properties 47 | ./gradlew -PappVerName=${{ env.VERSION }} assembleRelease assembleDebug 48 | - name: Upload built apk 49 | if: success() 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: snapshot 53 | path: | 54 | app/build/outputs/apk 55 | app/build/outputs/mapping 56 | app/release 57 | - name: Post to channel 58 | if: github.ref == 'refs/heads/master' 59 | env: 60 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO }} 61 | BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} 62 | FILE: app/release/BiliRoaming_${{ env.VERSION }}.apk 63 | COMMIT_MESSAGE: |+ 64 | New push to github\! 65 | ``` 66 | ${{ github.event.head_commit.message }} 67 | ```by `${{ github.event.head_commit.author.name }}` 68 | See commit detail [here](${{ github.event.head_commit.url }}) 69 | Snapshot apk is attached \(unsupported by TAICHI\) 70 | run: | 71 | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'` 72 | 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" 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | *.apk 10 | *.jar 11 | /.kotlin 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /.cxx 4 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.* 2 | 3 | plugins { 4 | alias(libs.plugins.agp.app) 5 | alias(libs.plugins.kotlin) 6 | alias(libs.plugins.protobuf) 7 | alias(libs.plugins.lsplugin.resopt) 8 | alias(libs.plugins.lsplugin.jgit) 9 | alias(libs.plugins.lsplugin.apksign) 10 | alias(libs.plugins.lsplugin.apktransform) 11 | alias(libs.plugins.lsplugin.cmaker) 12 | } 13 | 14 | val appVerCode = jgit.repo()?.commitCount("refs/remotes/origin/master") ?: 0 15 | val appVerName: String by rootProject 16 | 17 | apksign { 18 | storeFileProperty = "releaseStoreFile" 19 | storePasswordProperty = "releaseStorePassword" 20 | keyAliasProperty = "releaseKeyAlias" 21 | keyPasswordProperty = "releaseKeyPassword" 22 | } 23 | 24 | apktransform { 25 | copy { 26 | when (it.buildType) { 27 | "release" -> file("${it.name}/BiliRoaming_${appVerName}.apk") 28 | else -> null 29 | } 30 | } 31 | } 32 | 33 | cmaker { 34 | default { 35 | targets("biliroaming") 36 | abiFilters("armeabi-v7a", "arm64-v8a", "x86") 37 | arguments += arrayOf( 38 | "-DANDROID_STL=none", 39 | "-DCMAKE_CXX_STANDARD=23", 40 | "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", 41 | ) 42 | cFlags += "-flto" 43 | cppFlags += "-flto" 44 | } 45 | 46 | buildTypes { 47 | arguments += "-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.file("symbols/${it.name}").get().asFile.absolutePath}" 48 | } 49 | } 50 | 51 | android { 52 | namespace = "me.iacn.biliroaming" 53 | compileSdk = 35 54 | buildToolsVersion = "35.0.0" 55 | ndkVersion = "27.2.12479018" 56 | 57 | buildFeatures { 58 | prefab = true 59 | buildConfig = true 60 | } 61 | 62 | defaultConfig { 63 | applicationId = "me.iacn.biliroaming" 64 | minSdk = 24 65 | targetSdk = 35 // Target Android U 66 | versionCode = appVerCode 67 | versionName = appVerName 68 | } 69 | 70 | buildTypes { 71 | release { 72 | isMinifyEnabled = true 73 | isShrinkResources = true 74 | proguardFiles("proguard-rules.pro") 75 | } 76 | } 77 | 78 | compileOptions { 79 | sourceCompatibility(JavaVersion.VERSION_11) 80 | targetCompatibility(JavaVersion.VERSION_11) 81 | } 82 | 83 | kotlinOptions { 84 | jvmTarget = "11" 85 | freeCompilerArgs = listOf( 86 | "-Xno-param-assertions", 87 | "-Xno-call-assertions", 88 | "-Xno-receiver-assertions", 89 | "-language-version=2.0", 90 | ) 91 | } 92 | 93 | sourceSets { 94 | named("main") { 95 | proto { 96 | srcDir("src/main/proto") 97 | include("**/*.proto") 98 | } 99 | } 100 | } 101 | 102 | packaging { 103 | resources { 104 | excludes += "**" 105 | } 106 | } 107 | 108 | lint { 109 | checkReleaseBuilds = false 110 | } 111 | 112 | dependenciesInfo { 113 | includeInApk = false 114 | } 115 | 116 | androidResources { 117 | additionalParameters += arrayOf("--allow-reserved-package-id", "--package-id", "0x23") 118 | } 119 | 120 | externalNativeBuild { 121 | cmake { 122 | path("src/main/jni/CMakeLists.txt") 123 | version = "3.28.0+" 124 | } 125 | } 126 | } 127 | 128 | protobuf { 129 | protoc { 130 | artifact = libs.protobuf.protoc.get().toString() 131 | } 132 | 133 | generateProtoTasks { 134 | all().forEach { task -> 135 | task.builtins { 136 | id("java") { 137 | option("lite") 138 | } 139 | id("kotlin") { 140 | option("lite") 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | configurations.all { 148 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7") 149 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") 150 | } 151 | 152 | dependencies { 153 | compileOnly(libs.xposed) 154 | implementation(libs.protobuf.kotlin) 155 | implementation(libs.protobuf.java) 156 | compileOnly(libs.protobuf.protoc) 157 | implementation(libs.kotlin.stdlib) 158 | implementation(libs.kotlin.coroutines.android) 159 | implementation(libs.kotlin.coroutines.jdk) 160 | implementation(libs.androidx.documentfile) 161 | implementation(libs.cxx) 162 | } 163 | 164 | val adbExecutable: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath 165 | 166 | val restartBiliBili = task("restartBiliBili").apply { 167 | doLast { 168 | exec { 169 | commandLine(adbExecutable, "shell", "am", "force-stop", "tv.danmaku.bili") 170 | } 171 | exec { 172 | commandLine( 173 | adbExecutable, 174 | "shell", 175 | "am", 176 | "start", 177 | "$(pm resolve-activity --components tv.danmaku.bili)" 178 | ) 179 | } 180 | } 181 | } 182 | 183 | afterEvaluate { 184 | tasks.getByPath("installDebug").finalizedBy(restartBiliBili) 185 | } 186 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | generated 2 | -------------------------------------------------------------------------------- /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/assets/xhook.js: -------------------------------------------------------------------------------- 1 | (function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H=[].indexOf||function(a){for(var b=0,c=this.length;b=0||(f=f===b?d[a].length:f,d[a].splice(f,0,c))},c[m]=function(a,c){var f;if(a===b)return void(d={});c===b&&(d[a]=[]),f=e(a).indexOf(c),f!==-1&&e(a).splice(f,1)},c[h]=function(){var b,d,f,g,h,i,j,k;for(b=D(arguments),d=b.shift(),a||(b[0]=z(b[0],y(d))),g=c["on"+d],g&&g.apply(c,b),k=e(d).concat(e("*")),f=i=0,j=k.length;i2)throw"invalid hook";return F[n](d,a,b)},F[c]=function(a,b){if(a.length<2||a.length>3)throw"invalid hook";return F[n](c,a,b)},F.enable=function(){q[u]=t,"function"==typeof r&&(q[g]=r),k&&(q[i]=s)},F.disable=function(){q[u]=F[u],q[g]=F[g],k&&(q[i]=k)},v=F.headers=function(a,b){var c,d,e,f,g,h,i,j,k;switch(null==b&&(b={}),typeof a){case"object":d=[];for(e in a)g=a[e],f=e.toLowerCase(),d.push(f+":\t"+g);return d.join("\n")+"\n";case"string":for(d=a.split("\n"),i=0,j=d.length;ib&&b<4;)k[o]=++b,1===b&&k[h]("loadstart",{}),2===b&&G(),4===b&&(G(),E()),k[h]("readystatechange",{}),4===b&&(t.async===!1?g():setTimeout(g,0))},g=function(){l||k[h]("load",{}),k[h]("loadend",{}),l&&(k[o]=0)},b=0,x=function(a){var b,d;if(4!==a)return void i(a);b=F.listeners(c),(d=function(){var a;return b.length?(a=b.shift(),2===a.length?(a(t,w),d()):3===a.length&&t.async?a(t,w,d):d()):i(4)})()},k=t.xhr=f(),I.onreadystatechange=function(a){try{2===I[o]&&r()}catch(a){}4===I[o]&&(D=!1,r(),q()),x(I[o])},m=function(){l=!0},k[n]("error",m),k[n]("timeout",m),k[n]("abort",m),k[n]("progress",function(){b<3?x(3):k[h]("readystatechange",{})}),("withCredentials"in I||F.addWithCredentials)&&(k.withCredentials=!1),k.status=0,L=e.concat(p);for(J=0,K=L.length;J(R.id.tvHint).apply { 59 | text = if (currentTargetCommentAuthorLevel == 0) "关闭" else context.getString( 60 | R.string.danmaku_filter_weight_hint, 61 | currentTargetCommentAuthorLevel 62 | ) 63 | } 64 | val seekBar = seekBarView.findViewById(R.id.seekBar).apply { 65 | max = 6 66 | setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 67 | override fun onProgressChanged( 68 | seekBar: SeekBar?, progress: Int, fromUser: Boolean 69 | ) { 70 | tvHint.text = 71 | if (progress == 0) "关闭" else context.getString( 72 | R.string.danmaku_filter_weight_hint, 73 | progress 74 | ) 75 | } 76 | 77 | override fun onStartTrackingTouch(seekBar: SeekBar?) {} 78 | override fun onStopTrackingTouch(seekBar: SeekBar?) {} 79 | }) 80 | progress = currentTargetCommentAuthorLevel 81 | } 82 | root.addView(seekBarView) 83 | 84 | setTitle(string(R.string.filter_comment_title)) 85 | 86 | setPositiveButton(android.R.string.ok) { _, _ -> 87 | 88 | val contents = contentGroup.getKeywords() 89 | val contentRegexMode = contentRegexSwitch.isChecked 90 | if (contentRegexMode && contents.runCatching { forEach { it.toRegex() } }.isFailure) { 91 | Log.toast(string(R.string.invalid_regex), force = true) 92 | return@setPositiveButton 93 | } 94 | 95 | prefs.edit().apply { 96 | putStringSet("comment_filter_keyword_content", contents) 97 | putStringSet("comment_filter_keyword_at_upname", upNameGroup.getKeywords()) 98 | putStringSet("comment_filter_keyword_at_uid", uidGroup.getKeywords()) 99 | putBoolean("comment_filter_content_regex_mode", contentRegexMode) 100 | putBoolean("comment_filter_block_at_comment", blockAtCommentSwitch.isChecked) 101 | putLong("target_comment_author_level", seekBar.progress.toLong()) 102 | }.apply() 103 | Log.toast(string(R.string.prefs_save_success_and_reboot)) 104 | } 105 | setNegativeButton(android.R.string.cancel, null) 106 | 107 | root.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 108 | 109 | setView(scrollView) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /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 zoneUrl = "https://api.bilibili.com/x/web-interface/zone" 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/SearchFilterDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.Activity 4 | import android.content.SharedPreferences 5 | import android.view.inputmethod.EditorInfo 6 | import android.widget.* 7 | import me.iacn.biliroaming.utils.Log 8 | import me.iacn.biliroaming.utils.dp 9 | 10 | class SearchFilterDialog(activity: Activity, prefs: SharedPreferences) : 11 | BaseWidgetDialog(activity) { 12 | init { 13 | val scrollView = ScrollView(context).apply { 14 | scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY 15 | } 16 | val root = LinearLayout(context).apply { 17 | orientation = LinearLayout.VERTICAL 18 | layoutParams = FrameLayout.LayoutParams( 19 | FrameLayout.LayoutParams.MATCH_PARENT, 20 | FrameLayout.LayoutParams.WRAP_CONTENT 21 | ) 22 | } 23 | scrollView.addView(root) 24 | 25 | val categoryVideoTitle = categoryTitle(string(R.string.filter_search_video)) 26 | root.addView(categoryVideoTitle) 27 | 28 | val (contentGroup, contentRegexSwitch) = root.addKeywordGroup( 29 | string(R.string.keyword_group_name_content), 40.dp, true 30 | ) 31 | contentRegexSwitch.isChecked = prefs.getBoolean("search_filter_content_regex_mode", false) 32 | val upNameGroup = root.addKeywordGroup(string(R.string.keyword_group_name_up), 40.dp).first 33 | val uidGroup = root.addKeywordGroup( 34 | string(R.string.keyword_group_name_uid), 35 | 40.dp, 36 | inputType = EditorInfo.TYPE_CLASS_NUMBER 37 | ).first 38 | prefs.getStringSet("search_filter_keyword_content", null)?.forEach { 39 | contentGroup.addView(keywordInputItem(contentGroup, it).first) 40 | } 41 | prefs.getStringSet("search_filter_keyword_upname", null)?.forEach { 42 | upNameGroup.addView(keywordInputItem(upNameGroup, it).first) 43 | } 44 | prefs.getStringSet("search_filter_keyword_uid", null)?.forEach { 45 | uidGroup.addView(keywordInputItem(uidGroup, it, EditorInfo.TYPE_CLASS_NUMBER).first) 46 | } 47 | 48 | val removeRelatePromoteSwitch = switchPrefsItem(string(R.string.filter_search_remove_relate_promote)) 49 | .let { root.addView(it.first); it.second } 50 | removeRelatePromoteSwitch.isChecked = prefs.getBoolean("search_filter_remove_relate_promote", false) 51 | 52 | setTitle(string(R.string.filter_search_title)) 53 | 54 | setPositiveButton(android.R.string.ok) { _, _ -> 55 | 56 | val contents = contentGroup.getKeywords() 57 | val contentRegexMode = contentRegexSwitch.isChecked 58 | if (contentRegexMode && contents.runCatching { forEach { it.toRegex() } }.isFailure) { 59 | Log.toast(string(R.string.invalid_regex), force = true) 60 | return@setPositiveButton 61 | } 62 | 63 | prefs.edit().apply { 64 | putStringSet("search_filter_keyword_content", contents) 65 | putStringSet("search_filter_keyword_upname", upNameGroup.getKeywords()) 66 | putStringSet("search_filter_keyword_uid", uidGroup.getKeywords()) 67 | putBoolean("search_filter_content_regex_mode", contentRegexMode) 68 | putBoolean("search_filter_remove_relate_promote", removeRelatePromoteSwitch.isChecked) 69 | }.apply() 70 | Log.toast(string(R.string.prefs_save_success_and_reboot)) 71 | } 72 | setNegativeButton(android.R.string.cancel, null) 73 | 74 | root.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 75 | 76 | setView(scrollView) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/SpeedTestDialog.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package me.iacn.biliroaming 4 | 5 | import android.app.Activity 6 | import android.app.AlertDialog 7 | import android.content.Context 8 | import android.content.SharedPreferences 9 | import android.net.Uri 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.ArrayAdapter 13 | import android.widget.ListView 14 | import android.widget.TextView 15 | import kotlinx.coroutines.* 16 | import kotlinx.coroutines.flow.asFlow 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.flow.toList 19 | import me.iacn.biliroaming.network.BiliRoamingApi 20 | import me.iacn.biliroaming.network.BiliRoamingApi.getPlayUrl 21 | import me.iacn.biliroaming.network.BiliRoamingApi.mainlandTestParams 22 | import me.iacn.biliroaming.network.BiliRoamingApi.overseaTestParams 23 | import me.iacn.biliroaming.utils.* 24 | import org.json.JSONObject 25 | import java.net.URL 26 | import java.util.concurrent.Executors 27 | import java.util.concurrent.TimeUnit 28 | 29 | data class SpeedTestResult(val name: String, val value: String, var speed: String) 30 | 31 | class SpeedTestAdapter(context: Context) : ArrayAdapter(context, 0) { 32 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 33 | return (convertView ?: context.inflateLayout(R.layout.cdn_speedtest_item)).apply { 34 | getItem(position).let { 35 | findViewById(R.id.upos_name).text = it?.name 36 | findViewById(R.id.upos_speed).text = 37 | context.getString(R.string.speed_formatter, it?.speed) 38 | } 39 | } 40 | } 41 | 42 | fun sort() = sort { a, b -> 43 | val aSpeed = a.speed.toLongOrNull() 44 | val bSpeed = b.speed.toLongOrNull() 45 | if (aSpeed == null && bSpeed == null) 46 | 0 47 | else if (aSpeed == null) 48 | 1 49 | else if (bSpeed == null) 50 | -1 51 | else 52 | (bSpeed - aSpeed).toInt() 53 | } 54 | } 55 | 56 | class SpeedTestDialog(activity: Activity, prefs: SharedPreferences) : 57 | AlertDialog.Builder(activity) { 58 | private val scope = MainScope() 59 | private val speedTestDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() 60 | 61 | private val view = ListView(activity) 62 | private val adapter = SpeedTestAdapter(activity) 63 | 64 | init { 65 | view.adapter = adapter 66 | 67 | view.addHeaderView(context.inflateLayout(R.layout.cdn_speedtest_item).apply { 68 | findViewById(R.id.upos_name).text = context.getString(R.string.upos) 69 | findViewById(R.id.upos_speed).text = context.getString(R.string.speed) 70 | }, null, false) 71 | 72 | view.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 73 | 74 | setView(view) 75 | 76 | setPositiveButton("关闭", null) 77 | 78 | setOnDismissListener { 79 | scope.cancel() 80 | } 81 | 82 | view.setOnItemClickListener { _, _, pos, _ -> 83 | val (name, value, _) = adapter.getItem(pos - 1/*headerView*/) 84 | ?: return@setOnItemClickListener 85 | Log.d("Use UPOS Server $name: $value") 86 | prefs.edit().putString("upos_host", value).apply() 87 | Log.toast("已启用 UPOS 服务器:${name}", force = true) 88 | } 89 | 90 | setTitle("CDN 测速") 91 | } 92 | 93 | override fun show(): AlertDialog { 94 | val dialog = super.show() 95 | scope.launch { 96 | dialog.setTitle("正在测速……") 97 | val url = getTestUrl() ?: run { 98 | dialog.setTitle("测速失败") 99 | return@launch 100 | } 101 | context.resources.getStringArray(R.array.upos_entries) 102 | .zip(context.resources.getStringArray(R.array.upos_values)).asFlow().map { 103 | scope.launch { 104 | val item = SpeedTestResult(it.first, it.second, "...") 105 | adapter.add(item) 106 | adapter.sort() 107 | val speed = speedTest(it.second, url) 108 | item.speed = speed.toString() 109 | adapter.sort() 110 | } 111 | }.toList().joinAll() 112 | dialog.setTitle("测速完成") 113 | } 114 | return dialog 115 | } 116 | 117 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 118 | private suspend fun speedTest(upos: String, rawUrl: String) = try { 119 | withContext(speedTestDispatcher) { 120 | withTimeout(5000) { 121 | val url = if (upos == "\$1") URL(rawUrl) else { 122 | URL(Uri.parse(rawUrl).buildUpon().authority(upos).build().toString()) 123 | } 124 | val connection = url.openConnection() 125 | connection.connectTimeout = 5000 126 | connection.readTimeout = 5000 127 | connection.setRequestProperty("User-Agent", "Bilibili Freedoooooom/MarkII") 128 | connection.connect() 129 | val buffer = ByteArray(2048) 130 | var size = 0 131 | val start = System.currentTimeMillis() 132 | connection.getInputStream().use { stream -> 133 | while (isActive) { 134 | val read = stream.read(buffer) 135 | if (read <= 0) break 136 | size += read 137 | } 138 | } 139 | size / (System.currentTimeMillis() - start) // KB/s 140 | } 141 | } 142 | } catch (e: Throwable) { 143 | 0L 144 | } 145 | 146 | private suspend fun getTestUrl() = try { 147 | withContext(speedTestDispatcher) { 148 | withTimeout(5000) { 149 | val cn = runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } == "cn" 150 | val json = if (cn) { 151 | getPlayUrl(overseaTestParams, arrayOf("hk", "tw")) 152 | } else getPlayUrl(mainlandTestParams, arrayOf("cn")) 153 | json?.toJSONObject()?.optJSONObject("dash")?.getJSONArray("audio") 154 | ?.asSequence() 155 | ?.minWithOrNull { a, b -> a.optInt("bandwidth") - b.optInt("bandwidth") } 156 | ?.optString("base_url")?.replace("https", "http") 157 | } 158 | } 159 | } catch (e: BiliRoamingApi.CustomServerException) { 160 | Log.w("请求解析服务器发生错误: ${e.message}") 161 | null 162 | } catch (e: Throwable) { 163 | null 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/VideoExportDialog.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package me.iacn.biliroaming 4 | 5 | import android.annotation.SuppressLint 6 | import android.app.Activity 7 | import android.app.AlertDialog 8 | import android.app.Fragment 9 | import android.content.ActivityNotFoundException 10 | import android.content.Context 11 | import android.content.Intent 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.ArrayAdapter 15 | import android.widget.CheckBox 16 | import android.widget.ListView 17 | import android.widget.TextView 18 | import me.iacn.biliroaming.utils.Log 19 | import me.iacn.biliroaming.utils.inflateLayout 20 | import me.iacn.biliroaming.utils.toJSONObject 21 | import java.io.File 22 | 23 | class VideoExportDialog(activity: Activity, fragment: Fragment) : AlertDialog.Builder(activity) { 24 | companion object { 25 | /** 26 | * 指向视频页的文件 27 | */ 28 | var videosToExport = emptySet() 29 | } 30 | 31 | private val view = ListView(activity) 32 | private val selectedVideos = mutableSetOf() 33 | 34 | init { 35 | val allVideos = mutableListOf() 36 | File(activity.externalCacheDir, "../download").listFiles()?.forEach { video -> 37 | video.listFiles()?.forEach { page -> 38 | try { 39 | val jsonObj = File(page, "entry.json").readText().toJSONObject() 40 | val pageData = jsonObj.optJSONObject("page_data")?.let { 41 | VideoEntry.PageData( 42 | it.optString("part") 43 | ) 44 | } 45 | val ep = jsonObj.optJSONObject("ep")?.let { 46 | VideoEntry.Ep( 47 | it.optLong("av_id"), 48 | it.optString("bvid"), 49 | it.optString("index"), 50 | it.optString("index_title") 51 | ) 52 | } 53 | val videoEntry = VideoEntry( 54 | jsonObj.optString("title"), 55 | jsonObj.optLong("avid"), 56 | jsonObj.optString("bvid"), 57 | pageData, ep 58 | ) 59 | videoEntry.path = page 60 | allVideos.add(videoEntry) 61 | } catch (e: Throwable) { 62 | Log.toast("${e.message}", true, alsoLog = true) 63 | } 64 | } 65 | } 66 | view.adapter = VideoExportAdapter(activity, allVideos, selectedVideos) 67 | view.setPadding(50, 20, 50, 20) 68 | 69 | setNegativeButton("取消", null) 70 | 71 | setPositiveButton("导出") { _, _ -> 72 | videosToExport = selectedVideos 73 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 74 | try { 75 | fragment.startActivityForResult( 76 | Intent.createChooser(intent, "导出视频"), 77 | SettingDialog.VIDEO_EXPORT 78 | ) 79 | } catch (ex: ActivityNotFoundException) { 80 | Log.toast("请安装文件管理器") 81 | } 82 | } 83 | 84 | setView(view) 85 | } 86 | 87 | class VideoEntry( 88 | val title: String, 89 | val avid: Long, 90 | val bvid: String, 91 | val pageData: PageData?, 92 | val ep: Ep?, 93 | var path: File? = null 94 | ) { 95 | class PageData( 96 | val part: String 97 | ) 98 | 99 | class Ep( 100 | val avid: Long, 101 | val bvid: String, 102 | val index: String, 103 | val indexTitle: String 104 | ) 105 | 106 | val aBvid 107 | get() = ep?.bvid ?: bvid 108 | val aid get() = ep?.avid ?: avid 109 | val pageTitle get() = pageData?.part ?: ep?.let { "${it.index} ${it.indexTitle}" } 110 | } 111 | 112 | class VideoExportAdapter( 113 | context: Context, 114 | private val allVideos: List, 115 | private val selectedVideos: MutableSet 116 | ) : ArrayAdapter(context, 0) { 117 | @SuppressLint("SetTextI18n") 118 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 119 | return (convertView ?: context.inflateLayout(R.layout.video_choose)).apply { 120 | allVideos[position].let { 121 | findViewById(R.id.tv_title).text = it.title 122 | findViewById(R.id.tv_pageTitle).text = it.pageTitle 123 | findViewById(R.id.tv_aid).text = "av${it.aid}" 124 | findViewById(R.id.tv_bvid).text = it.aBvid 125 | findViewById(R.id.cb).setOnCheckedChangeListener { _, isChecked -> 126 | if (isChecked) 127 | it.path?.let { it1 -> selectedVideos.add(it1) } 128 | else 129 | selectedVideos.remove(it.path) 130 | } 131 | } 132 | } 133 | } 134 | 135 | override fun getCount() = allVideos.size 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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.hookBeforeAllConstructors 6 | import me.iacn.biliroaming.utils.hookBeforeConstructor 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class AllowMiniPlayHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("main_func", false)) return 12 | 13 | if (sPrefs.getBoolean("allow_mini_play", false)) { 14 | val miniPlayerType = 15 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 16 | .from(mClassLoader)?.getStaticObjectField("MINIPLAYER") 17 | "com.bilibili.lib.media.resource.PlayConfig\$PlayMenuConfig".from(mClassLoader)?.run { 18 | hookBeforeConstructor( 19 | Boolean::class.javaPrimitiveType, 20 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 21 | ) { param -> 22 | val type = param.args[1] 23 | if (type == miniPlayerType) 24 | param.args[0] = true 25 | } 26 | hookBeforeConstructor(Boolean::class.javaPrimitiveType, 27 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType", 28 | List::class.java 29 | ) { param -> 30 | val type = param.args[1] 31 | if (type == miniPlayerType) 32 | param.args[0] = true 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 likeIds = arrayOf( 20 | "frame_recommend", 21 | "frame1", 22 | "frame_like" 23 | ).map { getId(it) } 24 | 25 | instance.likeMethod()?.let { likeMethod -> 26 | instance.sectionClass?.hookAfterAllMethods(likeMethod) { param -> 27 | val sec = param.thisObject ?: return@hookAfterAllMethods 28 | if (!shouldClickLike()) { 29 | return@hookAfterAllMethods 30 | } 31 | val likeView = sec.javaClass.declaredFields.filter { 32 | View::class.java.isAssignableFrom(it.type) 33 | }.firstNotNullOfOrNull { 34 | sec.getObjectFieldOrNullAs(it.name)?.takeIf { v -> 35 | v.id in likeIds 36 | } 37 | } 38 | likeView?.callOnClick() 39 | } 40 | } 41 | instance.bindViewMethod()?.let { bindViewMethod -> 42 | instance.sectionClass?.hookAfterMethod( 43 | bindViewMethod, 44 | instance.viewHolderClass, 45 | instance.continuationClass 46 | ) { param -> 47 | if (!shouldClickLike()) { 48 | return@hookAfterMethod 49 | } 50 | val root = param.args[0].callMethodAs(instance.getRootMethod()) 51 | val likeView = likeIds.firstNotNullOfOrNull { id -> 52 | root.findViewById(id) 53 | } 54 | likeView?.callOnClick() 55 | } 56 | } 57 | } 58 | 59 | private fun shouldClickLike(): Boolean { 60 | val (aid, like) = detail ?: return false 61 | if (likedVideos.contains(aid) || like != 0) { 62 | return false 63 | } 64 | likedVideos.add(aid) 65 | return true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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_view_page_ads", false)) return 9 | Log.d("startHook: BangumiPageAd") 10 | 11 | val oGVActivityVoHooker: Hooker = { param -> 12 | val args = param.args 13 | for (i in args.indices) { 14 | when (val item = args[i]) { 15 | is Int -> args[i] = 0 16 | is MutableList<*> -> item.clear() 17 | else -> args[i] = null 18 | } 19 | } 20 | } 21 | // activity toast ad 22 | "com.bilibili.bangumi.data.page.detail.entity.OGVActivityVo".from(mClassLoader) 23 | ?.hookBeforeAllConstructors(oGVActivityVoHooker) 24 | "com.bilibili.ship.theseus.ogv.activity.OGVActivityVo".from(mClassLoader) 25 | ?.hookBeforeAllConstructors(oGVActivityVoHooker) 26 | 27 | // mall 28 | instance.bangumiUniformSeasonActivityEntrance()?.let { 29 | instance.bangumiUniformSeasonClass?.replaceMethod(it) { null } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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/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/java/me/iacn/biliroaming/hook/CommentImageHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.content.ContentValues 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.os.Environment 7 | import android.os.Parcelable 8 | import android.provider.MediaStore 9 | import android.view.HapticFeedbackConstants 10 | import android.view.View 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.MainScope 13 | import kotlinx.coroutines.launch 14 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 15 | import me.iacn.biliroaming.utils.* 16 | import java.io.File 17 | import java.net.HttpURLConnection 18 | import java.net.URL 19 | 20 | class CommentImageHook(classLoader: ClassLoader) : BaseHook(classLoader) { 21 | companion object { 22 | fun saveImage(url: String) = runCatching { 23 | URL(url).openStream().use { stream -> 24 | val relativePath = "${Environment.DIRECTORY_PICTURES}${File.separator}bili" 25 | val fullFilename = url.substringAfterLast('/') 26 | val filename = fullFilename.substringBeforeLast('.') 27 | 28 | val now = System.currentTimeMillis() 29 | val contentValues = ContentValues().apply { 30 | put(MediaStore.MediaColumns.DISPLAY_NAME, filename) 31 | put( 32 | MediaStore.MediaColumns.MIME_TYPE, 33 | HttpURLConnection.guessContentTypeFromName(fullFilename) ?: "image/png" 34 | ) 35 | put(MediaStore.MediaColumns.DATE_ADDED, now / 1000) 36 | put(MediaStore.MediaColumns.DATE_MODIFIED, now / 1000) 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 38 | put(MediaStore.MediaColumns.DATE_TAKEN, now) 39 | put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) 40 | } else { 41 | val path = File( 42 | Environment.getExternalStoragePublicDirectory( 43 | Environment.DIRECTORY_PICTURES 44 | ), "bili" 45 | ).also { it.mkdirs() } 46 | put(MediaStore.MediaColumns.DATA, File(path, fullFilename).absolutePath) 47 | } 48 | } 49 | val resolver = currentContext.contentResolver 50 | val uri = resolver.insert( 51 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues 52 | ) 53 | runCatching { 54 | resolver.openOutputStream(uri!!)?.use { stream.copyTo(it) } 55 | }.onSuccess { 56 | Log.toast("图片已保存至\n$relativePath${File.separator}$fullFilename", true) 57 | }.onFailure { 58 | Log.e(it) 59 | Log.toast("图片保存失败,可能已经保存或未授予权限", true) 60 | } 61 | } 62 | }.onFailure { 63 | Log.e(it) 64 | Log.toast("图片获取失败", force = true) 65 | } 66 | } 67 | 68 | private val imageViewId = getId("image_view") 69 | private var cacheUrlFieldName = "" 70 | 71 | @Suppress("DEPRECATION") 72 | override fun startHook() { 73 | if (!sPrefs.getBoolean("save_comment_image", false)) return 74 | instance.imageFragmentClass?.hookAfterMethod( 75 | "onViewCreated", View::class.java, Bundle::class.java 76 | ) { param -> 77 | val self = param.thisObject 78 | val view = param.args[0] as? View 79 | val imageItem = self.callMethodOrNullAs("getArguments") 80 | ?.getParcelable("image_item") ?: return@hookAfterMethod 81 | val urlFieldName = cacheUrlFieldName.ifEmpty { 82 | imageItem.javaClass.superclass.findFirstFieldByExactTypeOrNull(String::class.java) 83 | ?.name.orEmpty().also { cacheUrlFieldName = it } 84 | }.ifEmpty { return@hookAfterMethod } 85 | val imageUrl = imageItem.getObjectFieldAs(urlFieldName).takeIf { 86 | !it.isNullOrEmpty() && it.startsWith("http") 87 | }?.substringBefore('@') ?: return@hookAfterMethod 88 | view?.findViewById(imageViewId)?.setOnLongClickListener { 89 | it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) 90 | MainScope().launch(Dispatchers.IO) { 91 | saveImage(imageUrl) 92 | } 93 | true 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/CopyHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.text.SpannableStringBuilder 8 | import android.text.style.ClickableSpan 9 | import android.view.View 10 | import android.widget.FrameLayout 11 | import android.widget.TextView 12 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 13 | import me.iacn.biliroaming.utils.* 14 | import org.json.JSONObject 15 | 16 | class CopyHook(classLoader: ClassLoader) : BaseHook(classLoader) { 17 | companion object { 18 | private val DYNAMIC_COPYABLE_IDS = arrayOf( 19 | "dy_card_text", 20 | "dy_opus_paragraph_desc", 21 | "dy_opus_paragraph_title", 22 | "dy_opus_copy_right_id", 23 | "dy_opus_paragraph_text", 24 | ) 25 | } 26 | 27 | private val enhanceLongClickCopy = sPrefs.getBoolean("comment_copy_enhance", false) 28 | 29 | override fun startHook() { 30 | if (!sPrefs.getBoolean("comment_copy", false)) return 31 | instance.descCopyView().zip(instance.descCopy()).forEach { p -> 32 | val clazz = p.first ?: return@forEach 33 | val method = p.second ?: return@forEach 34 | clazz.replaceMethod( 35 | method, 36 | View::class.java, 37 | ClickableSpan::class.java 38 | ) { param -> 39 | if (!enhanceLongClickCopy) return@replaceMethod Unit 40 | 41 | param.thisObject.getFirstFieldByExactTypeOrNull()?.let { 42 | val view = param.args[0] as View 43 | showCopyDialog(view.context, it, param) 44 | } ?: (param.args[0] as? TextView)?.let { tv -> 45 | showCopyDialog(tv.context, tv.text, param) 46 | } 47 | } 48 | } 49 | 50 | instance.dynamicDescHolderListeners().forEach { c -> 51 | c?.replaceMethod("onLongClick", View::class.java) { param -> 52 | if (!enhanceLongClickCopy) 53 | return@replaceMethod true 54 | val itemView = param.args[0] as? View 55 | DYNAMIC_COPYABLE_IDS.asSequence().firstNotNullOfOrNull { n -> 56 | getId(n).takeIf { it != 0 }?.let { itemView?.findViewById(it) } 57 | }?.let { v -> 58 | (if (instance.ellipsizingTextViewClass?.isInstance(v) == true) { 59 | v.getFirstFieldByExactTypeOrNull() 60 | } else v.text)?.also { text -> 61 | showCopyDialog(v.context, text, param) 62 | } 63 | } ?: Log.toast("找不到动态内容", true) 64 | true 65 | } 66 | } 67 | 68 | val commentCopyHook = fun(param: MethodHookParam, idName: String): Any? { 69 | if (!enhanceLongClickCopy) return true 70 | if (param.args[0] is FrameLayout) return param.invokeOriginalMethod() 71 | (param.args[0] as? View)?.findViewById(getId(idName))?.let { 72 | if (instance.commentSpanTextViewClass?.isInstance(it) == true || 73 | instance.commentSpanEllipsisTextViewClass?.isInstance(it) == true 74 | ) it else null 75 | }?.let { view -> 76 | view.getFirstFieldByExactTypeOrNull()?.also { text -> 77 | showCopyDialog(view.context, text, param) 78 | } 79 | } ?: Log.toast("找不到评论内容", true) 80 | return true 81 | } 82 | instance.commentCopyClass?.replaceMethod("onLongClick", View::class.java) { 83 | commentCopyHook(it, "message") 84 | } 85 | instance.commentCopyNewClass?.replaceMethod("onLongClick", View::class.java) { 86 | commentCopyHook(it, "comment_message") 87 | } 88 | 89 | instance.comment3CopyClass?.let { c -> 90 | instance.comment3Copy()?.let { m -> 91 | instance.comment3ViewIndex().let { i -> 92 | c.replaceAllMethods(m) { param -> 93 | if (!enhanceLongClickCopy) return@replaceAllMethods true 94 | val view = param.args[i] as View 95 | view.getFirstFieldByExactTypeOrNull()?.also { text -> 96 | showCopyDialog(view.context, text, param) 97 | } 98 | return@replaceAllMethods true 99 | } 100 | } 101 | } 102 | } 103 | 104 | if (!enhanceLongClickCopy) return 105 | "com.bilibili.bplus.im.conversation.ConversationActivity".from(mClassLoader) 106 | ?.declaredMethods?.find { 107 | it.name == instance.onOperateClick() && it.parameterTypes.size == 8 108 | }?.hookBeforeMethod { param -> 109 | if (param.args.last() == param.args.first()) { 110 | val activity = param.thisObject as Activity 111 | val json = param.args[1].callMethodOrNullAs(instance.getContentString()) ?: "" 112 | val text = runCatchingOrNull { json.toJSONObject() }?.run { 113 | optString("content").ifEmpty { 114 | buildString { 115 | appendLine(optString("title").trim()) 116 | appendLine(optString("text").trim()) 117 | optJSONArray("modules")?.run { 118 | asSequence().map { 119 | it.optString("title") + ":" + it.optString("detail") 120 | }.joinToString("\n").run { 121 | append(this) 122 | } 123 | } 124 | }.run { removeSuffix("\n") } 125 | } 126 | } ?: return@hookBeforeMethod 127 | showCopyDialog(activity, text, param) 128 | param.args[6].callMethodOrNull("dismiss") 129 | param.result = null 130 | } 131 | } 132 | } 133 | 134 | private fun showCopyDialog(context: Context, text: CharSequence, param: MethodHookParam) { 135 | val appDialogTheme = getResId("AppTheme.Dialog.Alert", "style") 136 | AlertDialog.Builder(context, appDialogTheme).run { 137 | setTitle("自由复制内容") 138 | setMessage(text) 139 | setPositiveButton("分享") { _, _ -> 140 | context.startActivity( 141 | Intent.createChooser( 142 | Intent().apply { 143 | action = Intent.ACTION_SEND 144 | putExtra(Intent.EXTRA_TEXT, text) 145 | type = "text/plain" 146 | }, "分享评论内容" 147 | ) 148 | ) 149 | } 150 | setNeutralButton("复制全部") { _, _ -> 151 | param.invokeOriginalMethod() 152 | } 153 | setNegativeButton(android.R.string.cancel, null) 154 | show() 155 | }.apply { 156 | findViewById(android.R.id.message).setTextIsSelectable(true) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/DrawerHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.Gravity 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 9 | import me.iacn.biliroaming.utils.* 10 | 11 | class DrawerHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | 13 | private var drawerLayout: Any? = null 14 | private var navView: View? = null 15 | 16 | override fun startHook() { 17 | if (!sPrefs.getBoolean("drawer", false)) return 18 | 19 | Log.d("startHook: DrawerHook") 20 | 21 | runCatching { 22 | instance.kanbanCallbackClass?.new(null)?.callMethod(instance.kanbanCallback(), null) 23 | } 24 | 25 | instance.mainActivityClass?.hookAfterMethod("onCreate", Bundle::class.java) { param -> 26 | val self = param.thisObject as Activity 27 | val view = self.findViewById(android.R.id.content).getChildAt(0) 28 | (view.parent as ViewGroup).removeViewInLayout(view) 29 | drawerLayout = instance.drawerLayoutClass?.new(self) 30 | drawerLayout?.callMethod("addView", view, 0, view.layoutParams) 31 | 32 | val homeFragment = instance.homeUserCenterClass?.new() 33 | val fragmentManager = self.callMethod("getSupportFragmentManager") 34 | fragmentManager?.callMethod("beginTransaction")?.callMethod("add", homeFragment, "home") 35 | ?.callMethod("commit") 36 | fragmentManager?.callMethod("executePendingTransactions") 37 | 38 | self.setContentView(drawerLayout as View) 39 | } 40 | 41 | val createHooker: Hooker = { param -> 42 | val self = param.thisObject as Activity 43 | val fragmentManager = self.callMethod("getSupportFragmentManager") 44 | navView = fragmentManager?.callMethod("findFragmentByTag", "home") 45 | ?.callMethodAs("getView") 46 | 47 | val layoutParams = instance.drawerLayoutParamsClass?.new( 48 | ViewGroup.MarginLayoutParams( 49 | ViewGroup.MarginLayoutParams.MATCH_PARENT, 50 | ViewGroup.MarginLayoutParams.MATCH_PARENT 51 | ) 52 | ) 53 | layoutParams?.javaClass?.fields?.get(0)?.set(layoutParams, Gravity.START) 54 | navView?.parent ?: drawerLayout?.callMethod("addView", navView, 1, layoutParams) 55 | } 56 | 57 | instance.mainActivityClass?.runCatching { 58 | getDeclaredMethod( 59 | "onPostCreate", 60 | Bundle::class.java 61 | ) 62 | }?.onSuccess { it.hookAfterMethod(createHooker) } 63 | 64 | instance.mainActivityClass?.runCatching { getDeclaredMethod("onStart") } 65 | ?.onSuccess { it.hookAfterMethod(createHooker) } 66 | 67 | instance.mainActivityClass?.replaceMethod("onBackPressed") { param -> 68 | try { 69 | if (drawerLayout?.callMethodAs(instance.isDrawerOpen(), navView) == true) { 70 | drawerLayout?.callMethod(instance.closeDrawer(), navView, true) 71 | } else { 72 | param.invokeOriginalMethod() 73 | } 74 | } catch (e: Throwable) { 75 | param.invokeOriginalMethod() 76 | } 77 | } 78 | 79 | "tv.danmaku.bili.ui.main2.basic.BaseMainFrameFragment".hookAfterMethod( 80 | mClassLoader, 81 | "onViewCreated", 82 | View::class.java, 83 | Bundle::class.java 84 | ) { param -> 85 | val id = getId("avatar_layout") 86 | (param.args[0] as View).findViewById(id)?.setOnClickListener { 87 | try { 88 | drawerLayout?.callMethod(instance.openDrawer(), navView, true) 89 | } catch (e: Throwable) { 90 | Log.e(e) 91 | } 92 | } 93 | } 94 | 95 | } 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.lang.reflect.Proxy 6 | import java.util.regex.Pattern 7 | 8 | class EnvHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | Log.d("startHook: Env") 11 | "com.bilibili.lib.blconfig.internal.EnvContext\$preBuiltConfig\$2".hookAfterMethod( 12 | mClassLoader, 13 | "invoke" 14 | ) { param -> 15 | @Suppress("UNCHECKED_CAST") 16 | val result = param.result as MutableMap 17 | for (config in configSet) { 18 | (if (sPrefs.getBoolean( 19 | config.config, 20 | false 21 | ) 22 | ) config.trueValue else config.falseValue) 23 | ?.let { result[config.key] = it } ?: result.remove(config.key) 24 | } 25 | } 26 | "com.bilibili.lib.blconfig.internal.TypedContext\$dataSp\$2".hookAfterMethod( 27 | mClassLoader, 28 | "invoke" 29 | ) { param -> 30 | val result = param.result as SharedPreferences 31 | // this indicates the proper instance 32 | if (!result.contains("bv.enable_bv")) return@hookAfterMethod 33 | for (config in configSet) { 34 | (if (sPrefs.getBoolean( 35 | config.config, 36 | false 37 | ) 38 | ) config.trueValue else config.falseValue) 39 | ?.let { result.edit().putString(config.key, it).apply() } 40 | ?: result.edit().remove(config.key).apply() 41 | } 42 | } 43 | 44 | "com.bilibili.lib.blconfig.internal.OverrideConfig".findClassOrNull(mClassLoader) 45 | ?.hookBeforeAllConstructors { param -> 46 | val delegate = param.args.getOrNull(0) ?: return@hookBeforeAllConstructors 47 | val realConfig = param.args.getOrNull(1) ?: return@hookBeforeAllConstructors 48 | val delegateClass = delegate.javaClass 49 | param.args[0] = Proxy.newProxyInstance( 50 | delegateClass.classLoader, 51 | delegateClass.interfaces 52 | ) { _, m, a -> 53 | val args = a ?: emptyArray() 54 | if (m.name == "getConfig") { 55 | var result: Any? = null 56 | val key = args[0] 57 | for (config in configSet) { 58 | if (sPrefs.getBoolean(config.config, false) && config.key == key) { 59 | result = realConfig.callMethodOrNull("get", *args) 60 | } 61 | } 62 | result ?: m(delegate, *args) 63 | } else { 64 | m(delegate, *args) 65 | } 66 | } 67 | } 68 | 69 | // // Disable tinker 70 | // "com.tencent.tinker.loader.app.TinkerApplication".findClass(mClassLoader)?.hookBeforeAllConstructors { param -> 71 | // param.args[0] = 0 72 | // } 73 | } 74 | 75 | override fun lateInitHook() { 76 | Log.d("lateHook: Env") 77 | if (sPrefs.getBoolean("enable_av", false)) { 78 | val compatClass = "com.bilibili.droid.BVCompat".findClassOrNull(mClassLoader) 79 | compatClass?.declaredFields?.forEach { 80 | val field = compatClass.getStaticObjectField(it.name) 81 | if (field is Pattern && field.pattern() == "av[1-9]\\d*") 82 | compatClass.setStaticObjectField( 83 | it.name, 84 | Pattern.compile("(av[1-9]\\d*)|(BV1[1-9A-NP-Za-km-z]{9})", field.flags()) 85 | ) 86 | } 87 | } 88 | } 89 | 90 | companion object { 91 | 92 | private val encryptedValueMap = hashMapOf( 93 | "0" to "Irb5O7Q8Ka0ojD4qqScgqg==", 94 | "1" to "Y260Cyvp6HZEboaGO+YGMw==" 95 | ) 96 | 97 | class ConfigTuple( 98 | val key: String, 99 | val config: String, 100 | val trueValue: String?, 101 | val falseValue: String? 102 | ) 103 | 104 | val configSet = listOf( 105 | ConfigTuple( 106 | "bv.enable_bv", 107 | "enable_av", 108 | encryptedValueMap["0"], 109 | encryptedValueMap["1"] 110 | ), 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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/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/hook/KillDelayBootHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.Constant 5 | import me.iacn.biliroaming.utils.hookAfterMethod 6 | import me.iacn.biliroaming.utils.packageName 7 | 8 | class KillDelayBootHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | instance.gripperBootExpClass?.hookAfterMethod( 11 | if (packageName == Constant.PLAY_PACKAGE_NAME) "b" else "getDelayMillis" 12 | ) { param -> 13 | param.result = -1L 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /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/MultiWindowHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.hookBeforeAllMethods 6 | import me.iacn.biliroaming.utils.replaceMethod 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class MultiWindowHook(mClassLoader: ClassLoader) : BaseHook(mClassLoader) { 10 | override fun startHook() { 11 | if (sPrefs.getBoolean("fake_non_multiwindow", false).not()) return 12 | Log.d("startHook: MultiWindowHook") 13 | Activity::class.java 14 | .getDeclaredMethod("isInMultiWindowMode") 15 | .replaceMethod { false } 16 | Activity::class.java 17 | .hookBeforeAllMethods("onMultiWindowModeChanged") { param -> 18 | param.args[0] = false 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/MusicNotificationHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import de.robv.android.xposed.XposedHelpers 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.findClassOrNull 6 | import me.iacn.biliroaming.utils.hookBeforeMethod 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class MusicNotificationHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("music_notification", false)) return 12 | 13 | Log.d("startHook: MusicNotification") 14 | 15 | "com.bilibili.lib.blconfig.ConfigManager\$Companion".findClassOrNull(mClassLoader)?.run { 16 | hookBeforeMethod( 17 | "isHitFF", 18 | String::class.java 19 | ) { param -> 20 | (param.args[0] as String).run { 21 | if (this == "ff_background_use_system_media_controls") { 22 | param.result = true 23 | } 24 | } 25 | } 26 | } 27 | 28 | "com.bilibili.lib.dd.DeviceDecision".findClassOrNull(mClassLoader)?.hookBeforeMethod( 29 | "getBoolean", 30 | String::class.java, 31 | Boolean::class.javaPrimitiveType 32 | ) { param -> 33 | if (param.args[0] == "dd_enable_system_media_control") { 34 | param.result = true 35 | } 36 | } 37 | 38 | // Play store 39 | "com.bilibili.lib.blconfig.ConfigManager\$a".findClassOrNull(mClassLoader)?.run { 40 | XposedHelpers.findMethodExactIfExists(this, "g", String::class.java)?.hookBeforeMethod { 41 | (it.args[0] as String).run { 42 | if (this == "ff_background_use_system_media_controls") { 43 | it.result = true 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /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/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/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/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/java/me/iacn/biliroaming/hook/SSLHook.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package me.iacn.biliroaming.hook 4 | 5 | import android.annotation.SuppressLint 6 | import android.net.http.SslError 7 | import android.webkit.SslErrorHandler 8 | import android.webkit.WebView 9 | import me.iacn.biliroaming.utils.* 10 | import org.apache.http.conn.scheme.HostNameResolver 11 | import org.apache.http.conn.ssl.SSLSocketFactory 12 | import java.net.Socket 13 | import java.security.KeyStore 14 | import java.security.SecureRandom 15 | import java.security.cert.X509Certificate 16 | import javax.net.ssl.* 17 | 18 | class SSLHook(classLoader: ClassLoader) : BaseHook(classLoader) { 19 | override fun startHook() { 20 | Log.d("startHook: Ssl") 21 | 22 | val emptyTrustManagers = arrayOf(object : X509TrustManager { 23 | @SuppressLint("TrustAllX509TrustManager") 24 | override fun checkClientTrusted(chain: Array, authType: String) { 25 | } 26 | 27 | @SuppressLint("TrustAllX509TrustManager") 28 | override fun checkServerTrusted(chain: Array, authType: String) { 29 | } 30 | 31 | override fun getAcceptedIssuers(): Array = emptyArray() 32 | 33 | @Suppress("unused", "UNUSED_PARAMETER") 34 | fun checkServerTrusted( 35 | chain: Array, 36 | authType: String, 37 | host: String 38 | ): List = emptyList() 39 | }) 40 | 41 | "javax.net.ssl.TrustManagerFactory".hookBeforeMethod( 42 | mClassLoader, 43 | "getTrustManagers" 44 | ) { param -> 45 | param.result = emptyTrustManagers 46 | } 47 | 48 | "javax.net.ssl.SSLContext".hookBeforeMethod( 49 | mClassLoader, 50 | "init", 51 | "javax.net.ssl.KeyManager[]", 52 | "javax.net.ssl.TrustManager[]", 53 | SecureRandom::class.java 54 | ) { param -> 55 | param.args[0] = null 56 | param.args[1] = emptyTrustManagers 57 | param.args[2] = null 58 | } 59 | 60 | "javax.net.ssl.HttpsURLConnection".hookBeforeMethod( 61 | mClassLoader, 62 | "setSSLSocketFactory", 63 | javax.net.ssl.SSLSocketFactory::class.java 64 | ) { param -> 65 | param.args[0] = "javax.net.ssl.SSLSocketFactory".findClass(mClassLoader).new() 66 | } 67 | 68 | "org.apache.http.conn.scheme.SchemeRegistry".findClassOrNull(mClassLoader) 69 | ?.hookBeforeMethod("register", "org.apache.http.conn.scheme.Scheme") { param -> 70 | if (param.args[0].callMethodAs("getName") == "https") { 71 | param.args[0] = param.args[0].javaClass.new( 72 | "https", 73 | SSLSocketFactory.getSocketFactory(), 74 | 443 75 | ) 76 | } 77 | } 78 | 79 | "org.apache.http.conn.ssl.HttpsURLConnection".findClassOrNull(mClassLoader)?.run { 80 | hookBeforeMethod("setDefaultHostnameVerifier", HostnameVerifier::class.java) { param -> 81 | param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER 82 | } 83 | 84 | hookBeforeMethod("setHostnameVerifier", HostnameVerifier::class.java) { param -> 85 | param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER 86 | } 87 | 88 | } 89 | "org.apache.http.conn.ssl.SSLSocketFactory".hookBeforeMethod( 90 | mClassLoader, 91 | "getSocketFactory" 92 | ) { param -> 93 | param.result = SSLSocketFactory::class.java.new() 94 | } 95 | 96 | "org.apache.http.conn.ssl.SSLSocketFactory".findClassOrNull(mClassLoader) 97 | ?.hookAfterConstructor( 98 | String::class.java, 99 | KeyStore::class.java, 100 | String::class.java, 101 | KeyStore::class.java, 102 | SecureRandom::class.java, 103 | HostNameResolver::class.java 104 | ) { param -> 105 | val algorithm = param.args[0] as? String 106 | val keystore = param.args[1] as? KeyStore 107 | val keystorePassword = param.args[2] as? String 108 | val random = param.args[4] as? SecureRandom 109 | 110 | @Suppress("UNCHECKED_CAST") val trustManagers = 111 | emptyTrustManagers as Array 112 | 113 | val keyManagers = keystore?.let { 114 | SSLSocketFactory::class.java.callStaticMethodAs>( 115 | "createKeyManagers", 116 | keystore, 117 | keystorePassword 118 | ) 119 | } 120 | 121 | 122 | param.thisObject.setObjectField("sslcontext", SSLContext.getInstance(algorithm)) 123 | param.thisObject.getObjectField("sslcontext") 124 | ?.callMethod("init", keyManagers, trustManagers, random) 125 | param.thisObject.setObjectField( 126 | "socketfactory", 127 | param.thisObject.getObjectField("sslcontext")?.callMethod("getSocketFactory") 128 | ) 129 | } 130 | 131 | "org.apache.http.conn.ssl.SSLSocketFactory".hookAfterMethod( 132 | mClassLoader, 133 | "isSecure", 134 | Socket::class.java 135 | ) { param -> 136 | param.result = true 137 | } 138 | 139 | "okhttp3.CertificatePinner".findClassOrNull(mClassLoader)?.run { 140 | (runCatchingOrNull { getDeclaredMethod("findMatchingPins", String::class.java) } 141 | ?: declaredMethods.firstOrNull { it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java && it.returnType == List::class.java })?.hookBeforeMethod { param -> 142 | param.args[0] = "" 143 | } 144 | } 145 | 146 | "android.webkit.WebViewClient".findClassOrNull(mClassLoader)?.run { 147 | replaceMethod( 148 | "onReceivedSslError", 149 | WebView::class.java, 150 | SslErrorHandler::class.java, 151 | SslError::class.java 152 | ) { param -> 153 | (param.args[1] as SslErrorHandler).proceed() 154 | null 155 | } 156 | replaceMethod( 157 | "onReceivedError", 158 | WebView::class.java, 159 | Int::class.javaPrimitiveType, 160 | String::class.java, 161 | String::class.java 162 | ) { 163 | null 164 | } 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/SettingHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 9 | import me.iacn.biliroaming.SettingDialog 10 | import me.iacn.biliroaming.utils.* 11 | import java.lang.reflect.Constructor 12 | import java.lang.reflect.Proxy 13 | 14 | 15 | class SettingHook(classLoader: ClassLoader) : BaseHook(classLoader) { 16 | private var startSetting = false 17 | 18 | override fun startHook() { 19 | Log.d("startHook: Setting") 20 | 21 | instance.splashActivityClass?.hookBeforeMethod("onCreate", Bundle::class.java) { param -> 22 | val self = param.thisObject as Activity 23 | startSetting = self.intent.hasExtra(START_SETTING_KEY) 24 | } 25 | 26 | instance.mainActivityClass?.hookAfterMethod("onResume") { param -> 27 | if (startSetting) { 28 | startSetting = false 29 | SettingDialog.show(param.thisObject as Activity) 30 | } 31 | } 32 | 33 | instance.mainActivityClass?.hookBeforeMethod( 34 | "onCreate", 35 | Bundle::class.java 36 | ) { param -> 37 | val bundle = param.args[0] as? Bundle 38 | bundle?.remove("android:fragments") 39 | } 40 | 41 | instance.drawerClass?.hookAfterMethod( 42 | "onCreateView", 43 | LayoutInflater::class.java, 44 | ViewGroup::class.java, 45 | Bundle::class.java 46 | ) { param -> 47 | val navSettingId = getId("nav_settings") 48 | val nav = 49 | param.thisObject.javaClass.declaredFields.first { it.type.name == "android.support.design.widget.NavigationView" }.name 50 | (param.thisObject.getObjectField(nav) 51 | ?: param.result).callMethodAs("findViewById", navSettingId) 52 | .setOnLongClickListener { 53 | SettingDialog.show(param.thisObject.callMethodAs("getActivity")) 54 | true 55 | } 56 | } 57 | 58 | instance.homeCenters().forEach { (c, m) -> 59 | c?.hookBeforeAllMethods(m) { param -> 60 | @Suppress("UNCHECKED_CAST") 61 | val list = param.args[1] as? MutableList 62 | ?: param.args[1]?.getObjectFieldOrNullAs>("moreSectionList") 63 | ?: return@hookBeforeAllMethods 64 | 65 | val itemList = list.lastOrNull()?.let { 66 | if (it.javaClass != instance.menuGroupItemClass) it.getObjectFieldOrNullAs>( 67 | "itemList" 68 | ) else list 69 | } ?: list 70 | 71 | val item = instance.menuGroupItemClass?.new() ?: return@hookBeforeAllMethods 72 | item.setIntField("id", SETTING_ID) 73 | .setObjectField("title", "哔哩漫游设置") 74 | .setObjectField( 75 | "icon", 76 | "https://i0.hdslb.com/bfs/album/276769577d2a5db1d9f914364abad7c5253086f6.png" 77 | ) 78 | .setObjectField("uri", SETTING_URI) 79 | .setIntField("visible", 1) 80 | itemList.forEach { 81 | if (try { 82 | it.getIntField("id") == SETTING_ID 83 | } catch (t: Throwable) { 84 | it.getLongField("id") == SETTING_ID.toLong() 85 | } 86 | ) return@hookBeforeAllMethods 87 | } 88 | itemList.add(item) 89 | } 90 | } 91 | 92 | instance.settingRouterClass?.hookBeforeAllConstructors { param -> 93 | if (param.args[1] != SETTING_URI) return@hookBeforeAllConstructors 94 | val routerType = (param.method as Constructor<*>).parameterTypes[3] 95 | param.args[3] = Proxy.newProxyInstance( 96 | routerType.classLoader, 97 | arrayOf(routerType) 98 | ) { _, method, _ -> 99 | val returnType = method.returnType 100 | Proxy.newProxyInstance( 101 | returnType.classLoader, 102 | arrayOf(returnType) 103 | ) { _, method2, args -> 104 | when (method2.returnType) { 105 | Boolean::class.javaPrimitiveType -> false 106 | else -> { 107 | if (method2.parameterTypes.isNotEmpty() && 108 | method2.parameterTypes[0].name == "android.app.Activity" 109 | ) { 110 | val currentActivity = args[0] as Activity 111 | SettingDialog.show(currentActivity) 112 | } 113 | null 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | companion object { 122 | const val START_SETTING_KEY = "biliroaming_start_setting" 123 | const val SETTING_URI = "bilibili://biliroaming" 124 | const val SETTING_ID = 114514 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/ShareHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.net.Uri 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.getObjectField 8 | import me.iacn.biliroaming.utils.hookAfterMethod 9 | import me.iacn.biliroaming.utils.sPrefs 10 | import me.iacn.biliroaming.utils.setObjectField 11 | import java.net.HttpURLConnection 12 | import java.net.URL 13 | 14 | class ShareHook(classLoader: ClassLoader) : BaseHook(classLoader) { 15 | private val contentUrlPattern = Regex("""[\s\S]*(https?://(?:bili2233\.cn|b23\.tv)/\S*)$""") 16 | 17 | private fun String.resolveB23URL(): String { 18 | val conn = URL(this).openConnection() as HttpURLConnection 19 | conn.requestMethod = "GET" 20 | conn.instanceFollowRedirects = false 21 | conn.connect() 22 | if (conn.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { 23 | return conn.getHeaderField("Location") 24 | } 25 | return this 26 | } 27 | 28 | private fun transformUrl(url: String, transformAv: Boolean): String { 29 | val target = Uri.parse(url) 30 | val bv = if (transformAv) { 31 | target.path?.split("/")?.firstOrNull { it.startsWith("BV") && it.length == 12 } 32 | } else { 33 | null 34 | } 35 | val av = bv?.let { "av${bv2av(bv)}" } 36 | val newUrl = target.buildUpon() 37 | if (av != null) { 38 | newUrl.path(target.path!!.replace(bv, av)) 39 | } 40 | val encodedQuery = target.encodedQuery 41 | if (encodedQuery != null) { 42 | val query = encodedQuery.split("&").map { 43 | it.split("=") 44 | }.filter { 45 | it.size == 2 46 | }.mapNotNull { 47 | when { 48 | it[0] == "p" || it[0] == "t" -> "${it[0]}=${it[1]}" 49 | it[0] == "start_progress" -> "start_progress=${it[1]}&t=${it[1].toLong() / 1000}" 50 | else -> null 51 | } 52 | }.joinToString("&", postfix = "&unique_k=2333") 53 | newUrl.encodedQuery(query) 54 | } else { 55 | newUrl.appendQueryParameter("unique_k", "2333") 56 | } 57 | return newUrl.build().toString() 58 | } 59 | 60 | override fun startHook() { 61 | val miniProgramEnabled = sPrefs.getBoolean("mini_program", false) 62 | val purifyShareEnabled = sPrefs.getBoolean("purify_share", false) 63 | if (!miniProgramEnabled && !purifyShareEnabled) return 64 | Log.d("startHook: ShareHook") 65 | instance.shareClickResultClass?.apply { 66 | if (purifyShareEnabled) { 67 | hookAfterMethod("getLink") { param -> 68 | (param.result as? String)?.takeIf { 69 | it.startsWith("https://bili2233.cn") || it.startsWith("http://bili2233.cn") || it.startsWith("https://b23.tv") || it.startsWith("http://b23.tv") 70 | }?.let { 71 | val targetUrl = Uri.parse(it).buildUpon().query("").build().toString() 72 | param.result = targetUrl.resolveB23URL().also { r -> param.thisObject.setObjectField("link", r) } 73 | } 74 | } 75 | hookAfterMethod("getContent") { param -> 76 | val content = param.result as? String 77 | content?.let { 78 | contentUrlPattern.matchEntire(it)?.groups?.get(1)?.value 79 | }?.let { contentUrl -> 80 | val resolvedUrl = (param.thisObject.getObjectField("link")?.let { it as String } ?: contentUrl) 81 | .let { 82 | if (it.startsWith("https://bili2233.cn") || it.startsWith("http://bili2233.cn") || it.startsWith("https://b23.tv") || it.startsWith("http://b23.tv")) 83 | it.resolveB23URL() 84 | else it 85 | } 86 | param.result = content.replace(contentUrl, transformUrl(resolvedUrl, miniProgramEnabled)).also { r -> 87 | param.thisObject.setObjectField("content", r) 88 | } 89 | } 90 | } 91 | } 92 | if (!miniProgramEnabled) return@apply 93 | // ShareMode Definition 94 | // 1: PARAMS_TYPE_TEXT 95 | // 2: PARAMS_TYPE_AUDIO 96 | // 4: PARAMS_TYPE_VIDEO 97 | // 5: PARAMS_TYPE_IMAGE 98 | // 6 / 7: PARAMS_TYPE_MIN_PROGRAM 99 | // 21: PARAMS_TYPE_PURE_IMAGE 100 | // Others: PARAMS_TYPE_WEB 101 | hookAfterMethod("getShareMode") { param -> 102 | if (param.result == 6 || param.result == 7) { 103 | param.result = 0 104 | param.thisObject.apply { 105 | getObjectField("title")?.takeIf { it == "哔哩哔哩" }?.let { title -> 106 | setObjectField("title", getObjectField("content")) 107 | setObjectField("content", "由哔哩漫游分享") 108 | } 109 | getObjectField("content")?.let { it as String } 110 | ?.takeIf { it.startsWith("已观看") }?.let { content -> 111 | setObjectField("content", "$content\n由哔哩漫游分享") 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /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.callMethod 6 | import me.iacn.biliroaming.utils.callMethodOrNullAs 7 | import me.iacn.biliroaming.utils.getObjectField 8 | import me.iacn.biliroaming.utils.hookAfterAllConstructors 9 | import me.iacn.biliroaming.utils.sPrefs 10 | 11 | class SpeedHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | override fun startHook() { 13 | Log.d("startHook: SpeedHook") 14 | val defaultPlaybackSpeed = sPrefs.getInt("default_speed", 100) / 100f 15 | if (defaultPlaybackSpeed == 1f) return 16 | instance.playSpeedManager?.hookAfterAllConstructors { 17 | for (f in it.thisObject.javaClass.declaredFields) { 18 | val o = it.thisObject.getObjectField(f.name) 19 | val v = o?.callMethodOrNullAs("getValue") ?: continue 20 | if (v != 1f) continue 21 | o.callMethod("setValue", defaultPlaybackSpeed) 22 | Log.toast("已设置 $defaultPlaybackSpeed 倍速") 23 | break 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/SplashHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.content.res.Configuration 4 | import android.graphics.Color 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.view.View 8 | import android.widget.ImageView 9 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 10 | import me.iacn.biliroaming.utils.* 11 | import java.io.File 12 | 13 | class SplashHook(classLoader: ClassLoader) : BaseHook(classLoader) { 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("custom_splash", false) && !sPrefs.getBoolean( 16 | "custom_splash_logo", 17 | false 18 | ) 19 | && !sPrefs.getBoolean("full_splash", false) && !sPrefs.getBoolean("auto_dark_splash", false) 20 | ) return 21 | Log.d("startHook: Splash") 22 | 23 | instance.splashInfoClass?.hookAfterMethod( 24 | "getMode" 25 | ) { param -> 26 | param.result = if (sPrefs.getBoolean("full_splash", false)) { 27 | "full" 28 | } else { 29 | param.result 30 | } 31 | } 32 | 33 | instance.brandSplashClass?.hookAfterMethod( 34 | "onViewCreated", 35 | View::class.java, 36 | Bundle::class.java 37 | ) { param -> 38 | val view = param.args[0] as View 39 | val containerId = getId("splash_container") 40 | if (sPrefs.getBoolean("auto_dark_splash", false)) 41 | view.findViewById(containerId) 42 | .setBackgroundColor( 43 | if (view.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) 44 | Color.BLACK 45 | else Color.WHITE 46 | ) 47 | if (sPrefs.getBoolean("custom_splash", false)) { 48 | val brandId = getId("brand_splash") 49 | val fullId = getId("full_brand_splash") 50 | val brandSplash = view.findViewById(brandId) 51 | val full = if (fullId != 0) view.findViewById(fullId) else null 52 | val splashImage = File(currentContext.filesDir, SPLASH_IMAGE) 53 | if (splashImage.exists()) { 54 | val uri = Uri.fromFile(splashImage) 55 | brandSplash.setImageURI(uri) 56 | full?.setImageURI(uri) 57 | } else { 58 | brandSplash.alpha = .0f 59 | full?.alpha = .0f 60 | } 61 | } 62 | if (sPrefs.getBoolean("custom_splash_logo", false)) { 63 | val logoId = getId("brand_logo") 64 | val brandLogo = view.findViewById(logoId) 65 | val logoImage = File(currentContext.filesDir, LOGO_IMAGE) 66 | if (logoImage.exists()) 67 | brandLogo.setImageURI(Uri.fromFile(logoImage)) 68 | else 69 | brandLogo.alpha = .0f 70 | } 71 | } 72 | } 73 | 74 | companion object { 75 | const val SPLASH_IMAGE = "biliroaming_splash" 76 | const val LOGO_IMAGE = "biliroaming_logo" 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /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.BiliBiliPackage 10 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 11 | import me.iacn.biliroaming.utils.Log 12 | import me.iacn.biliroaming.utils.hookBeforeAllMethods 13 | import me.iacn.biliroaming.utils.hookBeforeMethod 14 | import me.iacn.biliroaming.utils.packageName 15 | import me.iacn.biliroaming.utils.sPrefs 16 | import me.iacn.biliroaming.utils.toJSONObject 17 | import kotlin.math.floor 18 | 19 | class StartActivityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 20 | 21 | private fun fixIntentUri(original: Uri): Uri { 22 | val fixedUri = Uri.parse(original.toString().replace("bilibili://story/", "bilibili://united_video/")).buildUpon() 23 | .clearQuery() 24 | .appendQueryParameter("from_spmid", original.getQueryParameter("from_spmid")) 25 | .appendQueryParameter("aid", original.path?.split("/")?.last() ?: "") 26 | .appendQueryParameter("bvid", "") 27 | .build() 28 | return fixedUri 29 | } 30 | 31 | override fun startHook() { 32 | "tv.danmaku.bili.ui.intent.IntentHandlerActivity".hookBeforeMethod(mClassLoader, "onCreate", Bundle::class.java) { param -> 33 | val a = param.thisObject as Activity 34 | val data = a.intent.data ?: return@hookBeforeMethod 35 | a.intent.data = data.buildUpon().encodedQuery(data.encodedQuery?.replace("&-Arouter=story", "")?.replace("&-Atype=story", "")).build() 36 | } 37 | Instrumentation::class.java.hookBeforeAllMethods("execStartActivity") { param -> 38 | val intent = param.args[4] as? Intent ?: return@hookBeforeAllMethods 39 | val uri = intent.dataString ?: return@hookBeforeAllMethods 40 | if (sPrefs.getBoolean( 41 | "replace_story_video", 42 | false 43 | ) && uri.startsWith("bilibili://story/") 44 | ) { 45 | if (instance.hasUnitedVideoActivity) { 46 | intent.data?.let { 47 | try { 48 | val cid = intent.data?.getQueryParameter("player_preload").toJSONObject().getLong("cid") 49 | intent.data = fixIntentUri(Uri.parse(intent.dataString)) 50 | // fix extra 51 | val pre = Uri.parse(intent.dataString).buildUpon().clearQuery().build().toString() 52 | val aid = pre.split("/").last().toLong() 53 | intent.removeExtra("player_preload") 54 | intent.putExtra("player_preload", floor(Math.random()*1000000000).toInt().toString()) 55 | intent.putExtra("blrouter.targeturl", pre) 56 | intent.putExtra("blrouter.pagename", "bilibili://united_video/") 57 | intent.putExtra("jumpFrom", 7) 58 | intent.putExtra("", aid) 59 | intent.putExtra("aid", aid) 60 | intent.putExtra("cid", cid) 61 | intent.putExtra("bvid", "") 62 | intent.putExtra("from", 7) 63 | intent.putExtra("blrouter.targeturl", pre) 64 | intent.putExtra("blrouter.matchrule", "bilibili://united_video/") 65 | // fix component 66 | intent.component = ComponentName( 67 | intent.component?.packageName ?: packageName, 68 | "com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity" 69 | ) 70 | } catch (e: Exception) { 71 | Log.e("replaceStoryVideo fix intent failed!!!") 72 | Log.e(e) 73 | } 74 | } 75 | return@hookBeforeAllMethods 76 | } 77 | // 兼容旧版 78 | intent.component = ComponentName( 79 | intent.component?.packageName ?: packageName, 80 | "com.bilibili.video.videodetail.VideoDetailsActivity" 81 | ) 82 | intent.data = Uri.parse(uri.replace("bilibili://story/", "bilibili://video/")) 83 | } 84 | if (sPrefs.getBoolean("force_browser", false)) { 85 | if (intent.component?.className?.endsWith("MWebActivity") == true && 86 | intent.data?.authority?.matches(whileListDomain) == false) { 87 | Log.d("force_browser ${intent.data?.authority}") 88 | param.args[4] = Intent(Intent.ACTION_VIEW).apply { 89 | data = intent.data 90 | } 91 | } 92 | } 93 | } 94 | } 95 | companion object { 96 | val whileListDomain = Regex(""".*bilibili\.com|.*b23\.tv""") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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/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.replaceMethod 7 | import me.iacn.biliroaming.utils.sPrefs 8 | 9 | class TryWatchVipQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("disable_try_watch_vip_quality", false)) return 12 | 13 | instance.vipQualityTrialService?.replaceMethod(instance.canTrialMethod()) { false } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/UposReplaceHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.Log 4 | import me.iacn.biliroaming.utils.UposReplaceHelper.enableLivePcdnBlock 5 | import me.iacn.biliroaming.utils.UposReplaceHelper.enablePcdnBlock 6 | import me.iacn.biliroaming.utils.UposReplaceHelper.enableUposReplace 7 | import me.iacn.biliroaming.utils.UposReplaceHelper.forceUpos 8 | import me.iacn.biliroaming.utils.UposReplaceHelper.gotchaRegex 9 | import me.iacn.biliroaming.utils.UposReplaceHelper.initVideoUposList 10 | import me.iacn.biliroaming.utils.UposReplaceHelper.isNeedReplaceVideoUpos 11 | import me.iacn.biliroaming.utils.UposReplaceHelper.isOverseaUpos 12 | import me.iacn.biliroaming.utils.UposReplaceHelper.isPCdnUpos 13 | import me.iacn.biliroaming.utils.UposReplaceHelper.liveUpos 14 | import me.iacn.biliroaming.utils.UposReplaceHelper.replaceUpos 15 | import me.iacn.biliroaming.utils.UposReplaceHelper.videoUposBackups 16 | import me.iacn.biliroaming.utils.from 17 | import me.iacn.biliroaming.utils.getObjectFieldOrNull 18 | import me.iacn.biliroaming.utils.getObjectFieldOrNullAs 19 | import me.iacn.biliroaming.utils.hookBeforeConstructor 20 | import me.iacn.biliroaming.utils.hookBeforeMethod 21 | import me.iacn.biliroaming.utils.setObjectField 22 | 23 | 24 | class UposReplaceHook(classLoader: ClassLoader) : BaseHook(classLoader) { 25 | override fun startHook() { 26 | if (!enableUposReplace || !(forceUpos || enablePcdnBlock || enableLivePcdnBlock)) return 27 | Log.d("startHook: UposReplaceHook") 28 | "tv.danmaku.ijk.media.player.IjkMediaAsset\$MediaAssertSegment\$Builder".from(mClassLoader) 29 | ?.run { 30 | hookBeforeConstructor(String::class.java, Int::class.javaPrimitiveType) { param -> 31 | val baseUrl = param.args[0] as String 32 | if (baseUrl.contains("live-bvc")) { 33 | if (enableLivePcdnBlock && !baseUrl.contains(gotchaRegex)) { 34 | param.args[0] = baseUrl.replaceUpos(liveUpos) 35 | } 36 | } else if (baseUrl.isNeedReplaceVideoUpos()) { 37 | param.args[0] = baseUrl.replaceUpos() 38 | } 39 | } 40 | 41 | if (!(enablePcdnBlock || forceUpos)) return@run 42 | hookBeforeMethod("setBackupUrls", MutableCollection::class.java) { param -> 43 | val mediaAssertSegment = param.thisObject.getObjectFieldOrNull("target") 44 | val baseUrl = 45 | mediaAssertSegment?.getObjectFieldOrNullAs("url").orEmpty() 46 | if (baseUrl.isEmpty()) return@hookBeforeMethod 47 | val backupUrls = if (param.args[0] == null) { 48 | if (baseUrl.contains("live-bvc")) return@hookBeforeMethod else { 49 | emptyList() 50 | } 51 | } else { 52 | @Suppress("UNCHECKED_CAST") 53 | (param.args[0] as List).filter { !it.isPCdnUpos() } 54 | .takeIf { backupUrls -> 55 | backupUrls.isEmpty() || !backupUrls.any { it.contains("live-bvc") } 56 | } ?: return@hookBeforeMethod 57 | } 58 | reconstructBackupUposList( 59 | baseUrl, backupUrls, mediaAssertSegment 60 | ).takeIf { it.isNotEmpty() }?.let { 61 | param.args[0] = it 62 | } 63 | } 64 | } 65 | } 66 | 67 | override fun lateInitHook() { 68 | initVideoUposList(mClassLoader) 69 | } 70 | 71 | private fun reconstructBackupUposList( 72 | baseUrl: String, backupUrls: List, mediaAssertSegment: Any? 73 | ): List { 74 | val rawUrl = backupUrls.firstOrNull() ?: baseUrl 75 | return if (baseUrl.isPCdnUpos()) { 76 | if (backupUrls.isNotEmpty()) { 77 | mediaAssertSegment?.setObjectField("url", rawUrl.replaceUpos()) 78 | listOf(rawUrl.replaceUpos(videoUposBackups[0], rawUrl.isOverseaUpos()), baseUrl) 79 | } else emptyList() 80 | } else { 81 | if (enablePcdnBlock || forceUpos || backupUrls.isEmpty() || rawUrl.isOverseaUpos()) { 82 | listOf( 83 | rawUrl.replaceUpos(videoUposBackups[0]), rawUrl.replaceUpos(videoUposBackups[1]) 84 | ) 85 | } else emptyList() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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 | instance.playerQualityServices().forEach { (clazz, getDefaultQnThumb) -> 15 | clazz?.replaceAllMethods(getDefaultQnThumb) { halfScreenQuality } 16 | } 17 | } 18 | if (fullScreenQuality != 0) { 19 | instance.playerSettingHelperClass?.replaceMethod(instance.getDefaultQn()) { fullScreenQuality } 20 | } 21 | 22 | if (halfScreenQuality != 0 || fullScreenQuality != 0) { 23 | instance.autoSupremumQualityClass?.hookBeforeConstructor( 24 | *Array(6) { Int::class.javaPrimitiveType } 25 | ) { param -> 26 | if (halfScreenQuality != 0) { 27 | param.args[0] = halfScreenQuality // loginHalf 28 | 29 | param.args[3] = halfScreenQuality // unloginHalf 30 | param.args[4] = halfScreenQuality // unloginFull 31 | param.args[5] = halfScreenQuality // unloginMobileFull 32 | } 33 | if (fullScreenQuality != 0) { 34 | param.args[1] = fullScreenQuality // loginFull 35 | param.args[2] = fullScreenQuality // loginMobileFull 36 | } 37 | } 38 | instance.qualityStrategyProviderClass?.hookBeforeMethod( 39 | instance.selectQuality(), 40 | instance.autoSupremumQualityClass, 41 | Boolean::class.javaPrimitiveType, // isFullscreen 42 | Boolean::class.javaPrimitiveType // isVideoPortrait 43 | ) { param -> 44 | // videoQuality = when { 45 | // isVideoPortrait && isFullscreen -> loginFull 46 | // isVideoPortrait && !isFullscreen -> unloginFull 47 | // isFullscreen -> loginHalf 48 | // else -> unloginHalf 49 | // } 50 | param.args[2] = true 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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("remove_vip_section", false) 17 | ) return 18 | val vipEntranceViewClass = 19 | "tv.danmaku.bili.ui.main2.mine.widgets.MineVipEntranceView".from(mClassLoader) 20 | val vipEntranceViewField = 21 | vipEntranceViewClass?.let { instance.homeUserCenterClass?.findFieldByExactType(it) } 22 | instance.homeUserCenterClass?.hookAfterMethod( 23 | "onCreateView", 24 | LayoutInflater::class.java, 25 | ViewGroup::class.java, 26 | Bundle::class.java 27 | ) { 28 | val self = it.thisObject 29 | (vipEntranceViewField?.get(self) as? View)?.visibility = View.GONE 30 | vipEntranceViewField?.set(self, null) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/WebViewHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.graphics.Bitmap 4 | import android.webkit.JavascriptInterface 5 | import android.webkit.WebView 6 | import android.webkit.WebViewClient 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.MainScope 9 | import kotlinx.coroutines.launch 10 | import me.iacn.biliroaming.BuildConfig 11 | import me.iacn.biliroaming.XposedInit.Companion.moduleRes 12 | import me.iacn.biliroaming.utils.* 13 | 14 | 15 | class WebViewHook(classLoader: ClassLoader) : BaseHook(classLoader) { 16 | private val hookedClient = HashSet>() 17 | 18 | private val jsHooker = object : Any() { 19 | @Suppress("UNUSED") 20 | @JavascriptInterface 21 | fun hook(url: String, text: String): String { 22 | return this@WebViewHook.hook(url, text) 23 | } 24 | 25 | @Suppress("UNUSED") 26 | @JavascriptInterface 27 | fun saveImage(url: String) { 28 | MainScope().launch(Dispatchers.IO) { 29 | CommentImageHook.saveImage(url) 30 | } 31 | } 32 | } 33 | 34 | private val js by lazy { 35 | runCatchingOrNull { 36 | moduleRes.assets.open("xhook.js") 37 | .use { it.bufferedReader().readText() } 38 | } ?: "" 39 | } 40 | 41 | override fun startHook() { 42 | Log.d("startHook: WebView") 43 | WebView::class.java.hookBeforeMethod( 44 | "setWebViewClient", WebViewClient::class.java 45 | ) { param -> 46 | val clazz = param.args[0].javaClass 47 | (param.thisObject as WebView).addJavascriptInterface(jsHooker, "hooker") 48 | if (hookedClient.contains(clazz)) return@hookBeforeMethod 49 | try { 50 | clazz.getDeclaredMethod( 51 | "onPageStarted", 52 | WebView::class.java, String::class.java, Bitmap::class.java 53 | ).hookBeforeMethod { p -> 54 | val webView = p.args[0] as WebView 55 | webView.evaluateJavascript("""(function(){$js})()""".trimMargin(), null) 56 | } 57 | if (sPrefs.getBoolean("save_comment_image", false)) { 58 | clazz.getDeclaredMethod( 59 | "onPageFinished", 60 | WebView::class.java, String::class.java 61 | ).hookBeforeMethod { p -> 62 | val webView = p.args[0] as WebView 63 | val url = p.args[1] as String 64 | if (url.startsWith("https://www.bilibili.com/h5/note-app/view")) { 65 | webView.evaluateJavascript( 66 | """(function(){for(var i=0;i{hooker.saveImage(e.target.currentSrc);})}}})()""", 67 | null 68 | ) 69 | } 70 | } 71 | } 72 | hookedClient.add(clazz) 73 | Log.d("hook webview $clazz") 74 | } catch (_: NoSuchMethodException) { 75 | } 76 | } 77 | } 78 | 79 | @Suppress("UNUSED_PARAMETER") 80 | fun hook(url: String, text: String): String { 81 | return text 82 | } 83 | 84 | override fun lateInitHook() { 85 | if (BuildConfig.DEBUG) { 86 | WebView.setWebContentsDebuggingEnabled(true) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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.JSONObject 6 | import java.net.URL 7 | 8 | suspend fun fetchJson(url: URL) = withContext(Dispatchers.IO) { 9 | try { 10 | JSONObject(url.readText()) 11 | } catch (e: Throwable) { 12 | null 13 | } 14 | } 15 | 16 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 17 | suspend fun fetchJson(url: String) = fetchJson(URL(url)) 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/DexHelper.java: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import java.io.Closeable; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Member; 9 | 10 | public class DexHelper implements AutoCloseable, Closeable { 11 | public static final int NO_CLASS_INDEX = -1; 12 | private final ClassLoader classLoader; 13 | private final long token; 14 | 15 | public DexHelper(@NonNull ClassLoader classLoader) { 16 | this.classLoader = classLoader; 17 | token = load(classLoader); 18 | } 19 | 20 | @NonNull 21 | public native long[] findMethodUsingString( 22 | @NonNull String str, boolean matchPrefix, long returnType, short parameterCount, 23 | @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes, 24 | @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst); 25 | 26 | @NonNull 27 | public native long[] findMethodInvoking(long methodIndex, long returnType, 28 | short parameterCount, @Nullable String parameterShorty, 29 | long declaringClass, @Nullable long[] parameterTypes, 30 | @Nullable long[] containsParameterTypes, 31 | @Nullable int[] dexPriority, boolean findFirst); 32 | 33 | @NonNull 34 | public native long[] findMethodInvoked(long methodIndex, long returnType, 35 | short parameterCount, @Nullable String parameterShorty, 36 | long declaringClass, @Nullable long[] parameterTypes, 37 | @Nullable long[] containsParameterTypes, 38 | @Nullable int[] dexPriority, boolean findFirst); 39 | 40 | @NonNull 41 | public native long[] findMethodSettingField( 42 | long fieldIndex, long returnType, short parameterCount, 43 | @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes, 44 | @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst); 45 | 46 | @NonNull 47 | public native long[] findMethodGettingField( 48 | long fieldIndex, long returnType, short parameterCount, 49 | @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes, 50 | @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst); 51 | 52 | @NonNull 53 | public native long[] findField(long type, @Nullable int[] dexPriority, boolean findFirst); 54 | 55 | @Nullable 56 | public native Member decodeMethodIndex(long methodIndex); 57 | 58 | public native long encodeMethodIndex(@NonNull Member method); 59 | 60 | @Nullable 61 | public native Field decodeFieldIndex(long fieldIndex); 62 | 63 | public native long encodeFieldIndex(@NonNull Field field); 64 | 65 | public native long encodeClassIndex(@NonNull Class clazz); 66 | 67 | @Nullable 68 | public native Class decodeClassIndex(long classIndex); 69 | 70 | public native void createFullCache(); 71 | 72 | @Override 73 | public native void close(); 74 | 75 | @Override 76 | protected void finalize() { 77 | close(); 78 | } 79 | 80 | private native long load(@NonNull ClassLoader classLoader); 81 | } 82 | 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/java/me/iacn/biliroaming/utils/SubtitleHelper.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.BitmapFactory.Options 6 | import me.iacn.biliroaming.R 7 | import me.iacn.biliroaming.XposedInit.Companion.moduleRes 8 | import org.json.JSONArray 9 | import org.json.JSONObject 10 | import java.io.* 11 | import java.net.URL 12 | import java.nio.ByteBuffer 13 | import java.util.zip.GZIPInputStream 14 | 15 | class TrieNode(val key: Char, val level: Int = 0) { 16 | private val children = hashMapOf>() 17 | 18 | val isLeaf get() = value != null 19 | var value: V? = null 20 | 21 | fun getOrAddChild(k: Char) = children.computeIfAbsent(k) { TrieNode(k, level + 1) } 22 | 23 | fun child(k: Char) = children[k] 24 | } 25 | 26 | class Trie { 27 | private val root = TrieNode(key = '\u0000') 28 | 29 | fun add(w: String, value: T) { 30 | if (w.isEmpty()) return 31 | var p = root 32 | for (c in w.toCharArray()) 33 | p = p.getOrAddChild(c) 34 | p.value = value 35 | } 36 | 37 | fun bestMatch(sen: CharArray): TrieNode? { 38 | var node: TrieNode = root 39 | var leaf: TrieNode? = null 40 | for (c in sen) { 41 | node = node.child(c) ?: break 42 | if (node.isLeaf) leaf = node 43 | } 44 | return leaf 45 | } 46 | } 47 | 48 | class Dictionary( 49 | private val chars: Map, 50 | private val dict: Trie, 51 | private val maxLen: Int 52 | ) { 53 | private fun convert(reader: Reader, writer: Writer) { 54 | val `in` = PushbackReader(reader.buffered(), maxLen) 55 | val buf = CharArray(maxLen) 56 | var len: Int 57 | 58 | while (true) { 59 | len = `in`.read(buf) 60 | if (len == -1) break 61 | val node = dict.bestMatch(buf) 62 | if (node != null) { 63 | val offset = node.level 64 | node.value?.let { writer.write(it) } 65 | `in`.unread(buf, offset, len - offset) 66 | } else { 67 | `in`.unread(buf, 0, len) 68 | val ch = `in`.read().toChar() 69 | writer.write(chars.getOrDefault(ch, ch).code) 70 | } 71 | } 72 | } 73 | 74 | fun convert(str: String) = StringWriter().also { 75 | convert(str.reader(), it) 76 | }.toString() 77 | 78 | companion object { 79 | private const val SHARP = '#' 80 | private const val EQUAL = '=' 81 | 82 | fun loadDictionary(mappingFile: File): Dictionary { 83 | val charMap = HashMap(4096) 84 | val dict = Trie() 85 | var maxLen = 2 86 | mappingFile.bufferedReader().useLines { lines -> 87 | lines.filterNot { it.isBlank() || it.trimStart().startsWith(SHARP) } 88 | .map { it.split(EQUAL, limit = 2) }.filter { it.size == 2 }.forEach { (k, v) -> 89 | if (k.length == 1 && v.length == 1) { 90 | charMap[k[0]] = v[0] 91 | } else { 92 | maxLen = k.length.coerceAtLeast(maxLen) 93 | dict.add(k, v) 94 | } 95 | } 96 | } 97 | return Dictionary(charMap, dict, maxLen) 98 | } 99 | } 100 | } 101 | 102 | object SubtitleHelper { 103 | private val dictFile by lazy { File(currentContext.filesDir, "t2cn.txt") } 104 | private val dictionary by lazy { Dictionary.loadDictionary(dictFile) } 105 | private const val dictUrl = 106 | "https://archive.biliimg.com/bfs/archive/566adec17e127bf92aed21832db0206ccecc8caa.png" 107 | 108 | // !!! Do not remove symbol '\' for "\}", Android need it 109 | @Suppress("RegExpRedundantEscape") 110 | private val noStyleRegex = 111 | Regex("""\{\\?\\an\d+\}|]*>|<\\?/font>||<\\?/i>||<\\?/b>||<\\?/u>""") 112 | val dictExist get() = dictFile.isFile 113 | 114 | @Synchronized 115 | fun downloadDict(): Boolean { 116 | if (dictExist) return true 117 | runCatching { 118 | val buffer = URL(dictUrl).openStream().buffered().use { 119 | val options = Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 } 120 | val bitmap = BitmapFactory.decodeStream(it, null, options) 121 | ByteBuffer.allocate(bitmap!!.byteCount).apply { 122 | bitmap.let { b -> b.copyPixelsToBuffer(this); b.recycle() } 123 | rewind() 124 | } 125 | } 126 | val bytes = ByteArray(buffer.int).also { buffer.get(it) } 127 | dictFile.outputStream().use { o -> 128 | GZIPInputStream(bytes.inputStream()).use { it.copyTo(o) } 129 | } 130 | }.onSuccess { 131 | return true 132 | }.onFailure { 133 | Log.e(it) 134 | dictFile.delete() 135 | } 136 | return false 137 | } 138 | 139 | fun convert(json: String): String { 140 | val subJson = JSONObject(json) 141 | var subBody = subJson.optJSONArray("body") ?: return json 142 | val subText = subBody.asSequence().map { it.optString("content") } 143 | .joinToString("\u0000").run { 144 | // Remove srt style, bilibili not support it 145 | if (contains("\\an") || contains("") || contains("") || contains("") 147 | ) replace(noStyleRegex, "") else this 148 | } 149 | val converted = dictionary.convert(subText) 150 | val lines = converted.split('\u0000') 151 | subBody.asSequence().zip(lines.asSequence()).forEach { (obj, line) -> 152 | obj.put("content", line) 153 | } 154 | subBody = subBody.appendInfo(moduleRes.getString(R.string.subtitle_append_info)) 155 | return subJson.apply { 156 | put("body", subBody) 157 | }.toString() 158 | } 159 | 160 | fun errorResponse(content: String) = JSONObject().apply { 161 | put("body", JSONArray().apply { 162 | put(JSONObject().apply { 163 | put("from", 0) 164 | put("location", 2) 165 | put("to", 9999) 166 | put("content", content) 167 | }) 168 | }) 169 | }.toString() 170 | 171 | private fun JSONArray.appendInfo(content: String): JSONArray { 172 | if (length() == 0) return this 173 | val firstLine = optJSONObject(0) 174 | ?: return this 175 | val lastLine = optJSONObject(length() - 1) 176 | ?: return this 177 | val firstFrom = firstLine.optDouble("from") 178 | .takeIf { !it.isNaN() } ?: return this 179 | val lastTo = lastLine.optDouble("to") 180 | .takeIf { !it.isNaN() } ?: return this 181 | val minDuration = 1.0 182 | val maxDuration = 5.0 183 | val interval = 0.3 184 | val appendStart = firstFrom >= minDuration + interval 185 | val from = if (appendStart) 0.0 else lastTo + interval 186 | val to = if (appendStart) { 187 | from + (firstFrom - interval).coerceAtMost(maxDuration) 188 | } else from + maxDuration 189 | val info = JSONObject().apply { 190 | put("from", from) 191 | put("location", 2) 192 | put("to", to) 193 | put("content", content) 194 | } 195 | return if (appendStart) { 196 | JSONArray().apply { 197 | put(info) 198 | for (jo in this@appendInfo) { 199 | put(jo) 200 | } 201 | } 202 | } else apply { put(info) } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/UposReplaceHelper.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import android.net.Uri 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.MainScope 6 | import kotlinx.coroutines.future.future 7 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 8 | import me.iacn.biliroaming.R 9 | import me.iacn.biliroaming.UGCPlayViewReply 10 | import me.iacn.biliroaming.XposedInit 11 | import java.util.concurrent.CompletableFuture 12 | import java.util.concurrent.TimeUnit 13 | 14 | object UposReplaceHelper { 15 | private val aliHost = string(R.string.ali_host) 16 | private val cosHost = string(R.string.cos_host) 17 | private val hwHost = string(R.string.hw_host) 18 | private val aliovHost = string(R.string.aliov_host) 19 | private val hwovHost = string(R.string.hwov_host) 20 | private val hkBcacheHost = string(R.string.hk_bcache_host) 21 | 22 | val isLocatedCn by lazy { 23 | (runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } ?: "cn") == "cn" 24 | } 25 | 26 | val forceUpos = sPrefs.getBoolean("force_upos", false) 27 | val enablePcdnBlock = sPrefs.getBoolean("block_pcdn", false) 28 | val enableLivePcdnBlock = sPrefs.getBoolean("block_pcdn_live", false) 29 | 30 | private lateinit var videoUposList: CompletableFuture> 31 | private val mainVideoUpos = 32 | sPrefs.getString("upos_host", null) ?: if (isLocatedCn) hwHost else aliovHost 33 | private val serverList = XposedInit.moduleRes.getStringArray(R.array.upos_values) 34 | private val extraVideoUposList = when (serverList.indexOf(mainVideoUpos)) { 35 | in 1..3 -> listOf(hwHost, cosHost) 36 | in 5..7 -> listOf(hwHost, aliHost) 37 | in 8..15 -> listOf(aliHost, cosHost) 38 | else -> listOf(aliHost, hkBcacheHost) 39 | } 40 | private val videoUposBase by lazy { 41 | runCatchingOrNull { videoUposList.get(500L, TimeUnit.MILLISECONDS) }?.get(0) 42 | ?: mainVideoUpos 43 | } 44 | val videoUposBackups by lazy { 45 | runCatchingOrNull { videoUposList.get(500L, TimeUnit.MILLISECONDS) }?.subList(1, 3) 46 | ?: extraVideoUposList 47 | } 48 | const val liveUpos = "c1--cn-gotcha01.bilivideo.com" 49 | 50 | val enableUposReplace = (mainVideoUpos != "\$1") 51 | 52 | private val overseaVideoUposRegex by lazy { 53 | Regex("""(akamai|(ali|hw|cos)\w*ov|hk-eq-bcache|bstar1)""") 54 | } 55 | private val urlBwRegex by lazy { Regex("""(bw=[^&]*)""") } 56 | private val ipPCdnRegex by lazy { Regex("""^https?://\d{1,3}\.\d{1,3}""") } 57 | val gotchaRegex by lazy { Regex("""https?://\w*--\w*-gotcha\d*\.bilivideo""") } 58 | 59 | fun initVideoUposList(mClassLoader: ClassLoader) { 60 | videoUposList = MainScope().future(Dispatchers.IO) { 61 | val bCacheRegex = Regex("""cn-.*\.bilivideo""") 62 | mutableListOf(mainVideoUpos).apply { 63 | // 8K video sample, without area limitation, reply probably contains Mirror CDN 64 | val playViewReply = instance.playViewReqClass?.new()?.apply { 65 | callMethod("setAid", 355749246L) 66 | callMethod("setCid", 1115447032L) 67 | callMethod("setQn", 127) 68 | callMethod("setFnval", 4048) 69 | callMethod("setFourk", true) 70 | callMethod("setForceHost", 2) 71 | }?.let { playViewReqUgc -> 72 | instance.playURLMossClass?.new()?.callMethod("playView", playViewReqUgc) 73 | ?.callMethodAs("toByteArray")?.let { 74 | UGCPlayViewReply.parseFrom(it) 75 | } 76 | } 77 | val officialList = playViewReply?.videoInfo?.let { info -> 78 | mutableListOf().apply { 79 | info.streamListList?.forEach { stream -> 80 | add(stream.dashVideo.baseUrl) 81 | addAll(stream.dashVideo.backupUrlList) 82 | } 83 | info.dashAudioList?.forEach { dashItem -> 84 | add(dashItem.baseUrl) 85 | addAll(dashItem.backupUrlList) 86 | } 87 | } 88 | }?.mapNotNull { Uri.parse(it).encodedAuthority }?.distinct() 89 | ?.filter { !it.isPCdnUpos() }.orEmpty() 90 | addAll(officialList.filter { !(it.contains(bCacheRegex) || it == mainVideoUpos) } 91 | .ifEmpty { officialList }) 92 | addAll(extraVideoUposList) 93 | }.also { hookTf(mClassLoader) } 94 | } 95 | } 96 | 97 | fun String.isPCdnUpos() = 98 | contains("szbdyd.com") || contains(".mcdn.bilivideo") || contains(ipPCdnRegex) 99 | 100 | fun String.isOverseaUpos() = isLocatedCn == contains(overseaVideoUposRegex) 101 | 102 | fun String.isNeedReplaceVideoUpos() = 103 | if (contains(".mcdn.bilivideo") || contains(ipPCdnRegex)) { 104 | // IP:Port type PCDN currently only exists in Live and Thai Video. 105 | // Cannot simply replace IP:Port or 'mcdn.bilivideo' like PCDN's host 106 | false 107 | } else { 108 | // only 'szbdyd.com' like PCDN can be replace 109 | (forceUpos && startsWith("http")) || (enablePcdnBlock && contains("szbdyd.com")) || isOverseaUpos() 110 | } 111 | 112 | fun String.replaceUpos( 113 | upos: String = videoUposBase, needReplace: Boolean = true 114 | ): String { 115 | fun String.replaceUposBw(): String = replace(urlBwRegex, "bw=1280000") 116 | 117 | return if (needReplace) { 118 | val uri = Uri.parse(this) 119 | val newUpos = uri.getQueryParameter("xy_usource") ?: upos 120 | uri.replaceUpos(newUpos).toString().replaceUposBw() 121 | } else replaceUposBw() 122 | } 123 | 124 | fun Uri.replaceUpos(upos: String): Uri = buildUpon().authority(upos).build() 125 | 126 | private fun hookTf(mClassLoader: ClassLoader) { 127 | if (!(enablePcdnBlock || forceUpos)) return 128 | // fake grpc TF header then only reply with mirror type playurl 129 | "com.bilibili.lib.moss.utils.RuntimeHelper".from(mClassLoader) 130 | ?.hookAfterMethod("tf") { param -> 131 | val result = param.result 132 | if (result.callMethodOrNullAs("getNumber") != 0) return@hookAfterMethod 133 | result.javaClass.callStaticMethodOrNull("forNumber", 1)?.let { 134 | param.result = it 135 | } 136 | } 137 | } 138 | 139 | private fun string(resId: Int) = XposedInit.moduleRes.getString(resId) 140 | } 141 | -------------------------------------------------------------------------------- /app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | project(biliroaming) 3 | 4 | set(CMAKE_CXX_SCAN_FOR_MODULES ON) 5 | 6 | find_package(cxx REQUIRED CONFIG) 7 | link_libraries(cxx::cxx) 8 | 9 | add_subdirectory(dex_builder) 10 | 11 | add_library(${PROJECT_NAME} SHARED 12 | biliroaming.cc 13 | ) 14 | 15 | target_link_libraries(${PROJECT_NAME} PUBLIC log dex_builder_static) 16 | 17 | if (NOT DEFINED DEBUG_SYMBOLS_PATH) 18 | set(DEBUG_SYMBOLS_PATH ${CMAKE_BINARY_DIR}/symbols) 19 | endif() 20 | 21 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 22 | COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} 23 | COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ 24 | ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME} 25 | COMMAND ${CMAKE_STRIP} --strip-all $) 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/app/src/main/res/drawable/demo.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/app/src/main/res/drawable/demo2.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/app/src/main/res/drawable/tp.webp -------------------------------------------------------------------------------- /app/src/main/res/layout/cdn_speedtest_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 20 | 21 | 25 | 26 | 33 | 34 | 45 | 46 | 47 | 51 | 52 | 59 | 60 | 67 | 68 | 69 | 70 | 74 | 75 | 82 | 83 | 92 | 93 | 94 | 98 | 99 | 106 | 107 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /app/src/main/res/layout/customize_backup_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 20 | 21 | 31 | 32 | 38 | 39 | 49 | 50 | 56 | 57 | 67 | 68 | 74 | 75 | 85 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_argb_color_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 16 | 17 | 23 | 24 | 28 | 29 | 30 | 36 | 37 | 43 | 44 | 54 | 55 | 56 | 57 | 64 | 65 | 71 | 72 | 78 | 79 | 88 | 89 | 90 | 91 | 98 | 99 | 105 | 106 | 112 | 113 | 122 | 123 | 124 | 125 | 132 | 133 | 139 | 140 | 146 | 147 | 156 | 157 | 158 | 159 | 166 | 167 | 173 | 174 | 180 | 181 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_color_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 17 | 18 | 24 | 25 | 31 | 32 | 42 | 43 | 44 | 45 | 52 | 53 | 59 | 60 | 66 | 67 | 76 | 77 | 78 | 79 | 86 | 87 | 93 | 94 | 100 | 101 | 110 | 111 | 112 | 113 | 120 | 121 | 127 | 128 | 134 | 135 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /app/src/main/res/layout/feature.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 21 | 22 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/search_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/seekbar_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/video_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 17 | 18 | 25 | 26 | 33 | 34 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #303030 4 | #e66e90 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 不取代 5 | ali(阿里) 6 | alib(阿里) 7 | alio1(阿里) 8 | bos(百度) 9 | cos(騰訊) 10 | cosb(騰訊) 11 | coso1(騰訊) 12 | hw(華為) 13 | hwb(華為) 14 | hwo1(華為) 15 | 08c(華為) 16 | 08h(華為) 17 | 08ct(華為) 18 | tf_hw(華為) 19 | tf_tx(騰訊) 20 | akamai(Akamai海外) 21 | aliov(阿里海外) 22 | cosov(騰訊海外) 23 | hwov(華為海外) 24 | hk_bcache(Bilibili海外) 25 | 26 | 27 | 28 | 直播 29 | 推薦 30 | 熱門 31 | 番劇(動畫) 32 | 影視 33 | 韓綜 34 | 其它 35 | 36 | 37 | 38 | 廣告(含推廣) 39 | 遊戲 40 | 橫幅(輪播圖) 41 | 通知(追番更新、活動提示) 42 | 文章 43 | 動態 44 | 豎版影片 45 | 直播 46 | 內聯影片 47 | 番劇、電影、電視劇、紀錄片…… 48 | 大卡(單列顯示的) 49 | 中卡 50 | 小卡(雙列顯示的) 51 | 52 | 53 | 54 | 分頁:首頁 55 | 分頁:動態 56 | 分頁:投稿 57 | 分頁:商品 58 | 分頁:追番 59 | 分頁:課堂 60 | 直播 61 | 充電 62 | 大航海 63 | 推廣櫥窗 64 | 影片 65 | 專欄 66 | 音訊 67 | 最近追番 68 | 最近投幣 69 | 最近按讚 70 | 最近追漫 71 | 玩的遊戲 72 | 課堂 73 | 粉絲裝扮 74 | 收藏夾 75 | 漫畫 76 | 合集 77 | 數字藏品展現 78 | NFT大頭貼 79 | 80 | 81 | 82 | UP主推薦廣告 83 | 相關遊戲 84 | 85 | 86 | 87 | 轉發 88 | 投稿影片 89 | 番劇、電影等 90 | 付費內容 1 91 | 付費內容 2 92 | 摺疊 93 | 文字 94 | 圖文 95 | 文章 96 | 音訊 97 | 通用 方形 98 | 通用 豎形 99 | 直播 100 | 播放列表 101 | 廣告 102 | 小程式 103 | 訂閱 104 | 新訂閱 105 | 直播推薦 106 | 橫幅 107 | 合集 108 | 故事 109 | 話題推薦 110 | 付費更新 111 | 話題集合 112 | 通知 113 | 114 | 115 | 購物卡片 116 | 購物精選 117 | 關注提醒 118 | 直播預約 119 | 投餵支持 120 | 滾動橫幅 121 | 電池任務 122 | 正在去買 123 | 124 | 125 | 預設 126 | 跟隨全螢幕解析度 127 | 240P 128 | 360P 129 | 480P 130 | 720P 131 | 720P 60幀 132 | 1080P 133 | 1080P 高碼率 134 | 1080P 60幀 135 | 4K 136 | 8K 137 | 138 | 139 | 預設 140 | 240P 141 | 360P 142 | 480P 143 | 720P 144 | 720P 60幀 145 | 1080P 146 | 1080P 高碼率 147 | 1080P 60幀 148 | 4K 149 | 8K 150 | 151 | 152 | 預設(使用雲控值) 153 | 16:9 154 | 4:3 155 | 156 | 157 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fff 4 | #fa6496 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_raw.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | yujincheng08/iAcn/djytw/ 4 | https://api.github.com/repos/yujincheng08/BiliRoaming/releases/latest 5 | https://github.com/yujincheng08/BiliRoaming/releases/latest 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/yujincheng08/BiliRoaming 8 | https://afdian.net/a/yujincheng08 9 | https://t.me/biliroaming 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 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | 49 | 53 | 56 | 57 | 58 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/build.gradle.kts -------------------------------------------------------------------------------- /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 | android.useAndroidX=true 15 | android.enableAppCompileTimeRClass=true 16 | 17 | appVerName=1.6.13 18 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | protobuf = "4.30.2" 3 | coroutine = "1.10.2" 4 | kotlin = "2.1.20" 5 | 6 | [plugins] 7 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 8 | agp-app = { id = "com.android.application", version = "8.9.1" } 9 | protobuf = { id = "com.google.protobuf", version = "0.9.5" } 10 | lsplugin-jgit = { id = "org.lsposed.lsplugin.jgit", version = "1.1" } 11 | lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.6" } 12 | lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" } 13 | lsplugin-apktransform = { id = "org.lsposed.lsplugin.apktransform", version = "1.2" } 14 | lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version = "1.2" } 15 | 16 | [libraries] 17 | xposed = { module = "de.robv.android.xposed:api", version = "82" } 18 | cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" } 19 | protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } 20 | protobuf-java = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } 21 | protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } 22 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 23 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine" } 24 | kotlin-coroutines-jdk = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutine" } 25 | androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.0.1" } 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /imgs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/imgs/icon.png -------------------------------------------------------------------------------- /imgs/stick1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/imgs/stick1.png -------------------------------------------------------------------------------- /imgs/stick2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/imgs/stick2.png -------------------------------------------------------------------------------- /imgs/stick3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/imgs/stick3.png -------------------------------------------------------------------------------- /imgs/stick4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yujincheng08/BiliRoaming/6f49faa817d17672ab1202de6c5ca2a347688e38/imgs/stick4.png -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------