├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── new_server.yml ├── dependabot.yml └── workflows │ ├── PR.yml │ ├── android.yml │ ├── check-bilibili-version.yml │ └── sync.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 │ │ ├── Constant.kt │ │ ├── CustomSubtitleDialog.kt │ │ ├── DynamicFilterDialog.kt │ │ ├── HomeFilterDialog.kt │ │ ├── MainActivity.kt │ │ ├── MiscRemoveAdsDialog.kt │ │ ├── SettingDialog.kt │ │ ├── SpeedTestDialog.kt │ │ ├── TextFoldDialog.kt │ │ ├── VideoExportDialog.kt │ │ ├── XposedInit.kt │ │ ├── hook │ │ ├── AllowMiniPlayHook.kt │ │ ├── AppUpgradeHook.kt │ │ ├── AutoLikeHook.kt │ │ ├── BLogDebugHook.kt │ │ ├── BangumiPageAdHook.kt │ │ ├── BangumiPlayUrlHook.kt │ │ ├── BangumiSeasonHook.kt │ │ ├── BaseHook.kt │ │ ├── BlockUpdateHook.kt │ │ ├── ChannelTabUIHook.kt │ │ ├── CommentImageHook.kt │ │ ├── CopyHook.kt │ │ ├── CoverHook.kt │ │ ├── CustomThemeHook.kt │ │ ├── DanmakuHook.kt │ │ ├── DarkSwitchHook.kt │ │ ├── DialogBlurBackgroundHook.kt │ │ ├── DownloadThreadHook.kt │ │ ├── DrawerHook.kt │ │ ├── DynamicHook.kt │ │ ├── EnvHook.kt │ │ ├── FavFolderDialogHook.kt │ │ ├── FullStoryHook.kt │ │ ├── HintHook.kt │ │ ├── JsonHook.kt │ │ ├── LiveRoomHook.kt │ │ ├── LosslessSettingHook.kt │ │ ├── MiniProgramHook.kt │ │ ├── MossDebugHook.kt │ │ ├── MusicNotificationHook.kt │ │ ├── OkHttpDebugHook.kt │ │ ├── OkHttpHook.kt │ │ ├── P2pHook.kt │ │ ├── PegasusHook.kt │ │ ├── PlayArcConfHook.kt │ │ ├── PlaybackSpeedHook.kt │ │ ├── PlayerLongPressHook.kt │ │ ├── ProtoBufHook.kt │ │ ├── PublishToFollowingHook.kt │ │ ├── PurifyShareHook.kt │ │ ├── QualityHook.kt │ │ ├── SSLHook.kt │ │ ├── ScreenOrientationHook.kt │ │ ├── SettingHook.kt │ │ ├── SpeedHook.kt │ │ ├── SplashHook.kt │ │ ├── StartActivityHook.kt │ │ ├── SubtitleDownloadHook.kt │ │ ├── SubtitleHook.kt │ │ ├── TeenagersModeHook.kt │ │ ├── TextFoldHook.kt │ │ ├── TrialVipQualityHook.kt │ │ ├── TryWatchVipQualityHook.kt │ │ ├── UposReplaceHook.kt │ │ ├── VideoQualityHook.kt │ │ ├── VipSectionHook.kt │ │ ├── WebViewHook.kt │ │ └── api │ │ │ ├── ApiHook.kt │ │ │ ├── BannerV3AdHook.kt │ │ │ ├── BannerV8AdHook.kt │ │ │ ├── CardsHook.kt │ │ │ ├── SeasonRcmdHook.kt │ │ │ └── SkinHook.kt │ │ ├── network │ │ └── BiliRoamingApi.kt │ │ └── utils │ │ ├── Booleans.kt │ │ ├── Coroutines.kt │ │ ├── DexHelper.kt │ │ ├── Hashs.kt │ │ ├── Json.kt │ │ ├── KotlinXposedHelper.kt │ │ ├── Log.kt │ │ ├── StrokeSpan.kt │ │ ├── SubtitleHelper.kt │ │ ├── UposReplaceHelper.kt │ │ ├── Utils.kt │ │ └── UtilsX.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 │ ├── arrays_x.xml │ ├── strings.xml │ └── strings_x.xml │ ├── values │ ├── arrays.xml │ ├── arrays_x.xml │ ├── colors.xml │ ├── strings.xml │ ├── strings_raw.xml │ ├── strings_x.xml │ ├── strings_x_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 | 14 | registries: 15 | maven-google: 16 | type: maven-repository 17 | url: "https://dl.google.com/dl/android/maven2/" 18 | gralde-plugin: 19 | type: maven-repository 20 | url: "https://plugins.gradle.org/m2/" 21 | -------------------------------------------------------------------------------- /.github/workflows/PR.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | env: 10 | CCACHE_DIR: ${{ github.workspace }}/.ccache 11 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 12 | CCACHE_NOHASHDIR: true 13 | CCACHE_MAXSIZE: 1G 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ ubuntu-latest, windows-latest ] 18 | 19 | steps: 20 | - name: Check out 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: 'recursive' 24 | fetch-depth: 0 25 | - name: Set up JDK 17 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | cache: 'gradle' 31 | - name: Set up ccache 32 | uses: hendrikmuhs/ccache-action@v1.2 33 | with: 34 | key: ${{ runner.os }}-${{ github.sha }} 35 | restore-keys: ${{ runner.os }} 36 | - name: Build with Gradle 37 | run: | 38 | echo 'org.gradle.caching=true' >> gradle.properties 39 | echo 'org.gradle.parallel=true' >> gradle.properties 40 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 41 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 42 | echo 'android.native.buildOutput=verbose' >> gradle.properties 43 | ./gradlew assemble 44 | - name: Stop gradle daemon 45 | run: ./gradlew --stop 46 | - name: Upload build artifact 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: ${{ matrix.os }}-artifact 50 | path: | 51 | app/build/outputs 52 | app/release 53 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ me ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | CCACHE_DIR: ${{ github.workspace }}/.ccache 12 | CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" 13 | CCACHE_NOHASHDIR: true 14 | CCACHE_MAXSIZE: 1G 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: 'recursive' 19 | fetch-depth: 0 20 | - name: Setup JDK 17 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: 'temurin' 24 | java-version: 17 25 | cache: 'gradle' 26 | - name: Retrieve version 27 | run: | 28 | echo VERSION=$(echo ${{ github.event.head_commit.id }} | head -c 10) >> $GITHUB_ENV 29 | - name: Set up ccache 30 | uses: hendrikmuhs/ccache-action@v1.2 31 | with: 32 | key: ${{ runner.os }}-${{ github.sha }} 33 | restore-keys: ${{ runner.os }} 34 | - name: Build with Gradle 35 | run: | 36 | echo 'org.gradle.caching=true' >> gradle.properties 37 | echo 'org.gradle.parallel=true' >> gradle.properties 38 | echo 'org.gradle.vfs.watch=true' >> gradle.properties 39 | echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties 40 | echo 'android.native.buildOutput=verbose' >> gradle.properties 41 | ./gradlew -PappVerName=${{ env.VERSION }} assembleRelease assembleDebug 42 | - name: Upload built apk 43 | if: success() 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: snapshot 47 | path: | 48 | app/build/outputs/apk 49 | app/build/outputs/mapping 50 | app/release 51 | - name: Post to channel 52 | if: github.ref == 'refs/heads/master' 53 | env: 54 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO }} 55 | BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} 56 | FILE: app/release/BiliRoaming_${{ env.VERSION }}.apk 57 | COMMIT_MESSAGE: |+ 58 | New push to github\! 59 | ``` 60 | ${{ github.event.head_commit.message }} 61 | ```by `${{ github.event.head_commit.author.name }}` 62 | See commit detail [here](${{ github.event.head_commit.url }}) 63 | Snapshot apk is attached \(unsupported by TAICHI\) 64 | run: | 65 | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'` 66 | curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://release%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D" -F release="@$FILE" 67 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync Fork 2 | 3 | on: 4 | schedule: 5 | - cron: '1 0-14 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync: 10 | name: Sync 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.x' 18 | 19 | - name: Prepare Python packages 20 | run: | 21 | pip install -U wheel 22 | pip install -U pyrogram tgcrypto 23 | 24 | - name: Get repo name 25 | run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV 26 | 27 | - name: Sync fork 28 | id: sync 29 | uses: zjns/repo-sync@master 30 | with: 31 | token: ${{ secrets.HUB_TOKEN }} 32 | up_repo: 'yujincheng08/BiliRoaming' 33 | up_branch: master 34 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 35 | gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} 36 | 37 | - name: Report via Telegram 38 | shell: python 39 | if: always() 40 | env: 41 | API_ID: ${{ secrets.TELEGRAM_API_ID }} 42 | API_HASH: ${{ secrets.TELEGRAM_API_HASH }} 43 | BOT_TOKEN: ${{ secrets.TELEGRAM_BOT }} 44 | CHANNEL_ID: ${{ secrets.TELEGRAM_TO_ME }} 45 | SUCCESS: ${{ job.status == 'success' }} 46 | run: | 47 | import asyncio 48 | import os 49 | from pyrogram import Client 50 | commit_count=${{ steps.sync.outputs.commit_count }} 51 | diff_commits=${{ toJSON(fromJSON(steps.sync.outputs.commits).*.message) }} 52 | if not commit_count: 53 | exit(0) 54 | async def main(): 55 | bot = Client( 56 | "client", 57 | in_memory=True, 58 | api_id=os.environ["API_ID"], 59 | api_hash=os.environ["API_HASH"], 60 | bot_token=os.environ["BOT_TOKEN"], 61 | ) 62 | async with bot: 63 | channel_id = int(os.environ["CHANNEL_ID"]) 64 | repo = os.environ["REPO_NAME"] 65 | repo_url = f"https://github.com/{os.environ['GITHUB_REPOSITORY']}" 66 | success = True if (os.environ["SUCCESS"] == "true") else False 67 | text = f"🎉 Fork sync success!\nRepo: [{repo}]({repo_url})" if (success) else f"😿 Fork sync failed!\nRepo: [{repo}]({repo_url})" 68 | if success: 69 | commits = "\n".join([f"∙ {commit}" for commit in diff_commits[:10]]) 70 | text += f"\n\nSynced commits:\n
{commits}
" 71 | await bot.send_message(chat_id=channel_id, text=text, disable_web_page_preview=True) 72 | async def wait(): 73 | try: 74 | await asyncio.wait_for(main(), timeout=60) 75 | except asyncio.TimeoutError: 76 | print("message send timeout!!!") 77 | exit(1) 78 | asyncio.run(wait()) 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | *.apk 10 | *.jar 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [**Deprecated**] Continued by https://github.com/BiliRoamingX/BiliRoamingX. 2 | -------------------------------------------------------------------------------- /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/me") ?: 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}/BiliRoamingX-v${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 += "-DANDROID_STL=none" 38 | cppFlags += "-Wno-c++2b-extensions" 39 | } 40 | 41 | buildTypes { 42 | arguments += "-DDEBUG_SYMBOLS_PATH=${project.buildDir.absolutePath}/symbols/${it.name}" 43 | } 44 | } 45 | 46 | android { 47 | namespace = "me.iacn.biliroaming" 48 | compileSdk = 34 49 | buildToolsVersion = "34.0.0" 50 | ndkVersion = "25.2.9519653" 51 | 52 | buildFeatures { 53 | prefab = true 54 | buildConfig = true 55 | } 56 | 57 | defaultConfig { 58 | applicationId = "me.iacn.biliroamingx" 59 | minSdk = 24 60 | targetSdk = 34 // Target Android U 61 | versionCode = appVerCode 62 | versionName = appVerName 63 | } 64 | 65 | buildTypes { 66 | release { 67 | isMinifyEnabled = true 68 | isShrinkResources = true 69 | proguardFiles("proguard-rules.pro") 70 | } 71 | } 72 | 73 | compileOptions { 74 | sourceCompatibility(JavaVersion.VERSION_11) 75 | targetCompatibility(JavaVersion.VERSION_11) 76 | } 77 | 78 | kotlinOptions { 79 | jvmTarget = "11" 80 | freeCompilerArgs = listOf( 81 | "-Xcontext-receivers", 82 | "-Xno-param-assertions", 83 | "-Xno-call-assertions", 84 | "-Xno-receiver-assertions", 85 | "-opt-in=kotlin.RequiresOptIn", 86 | "-language-version=2.0", 87 | ) 88 | } 89 | 90 | sourceSets { 91 | named("main") { 92 | proto { 93 | srcDir("src/main/proto") 94 | include("**/*.proto") 95 | } 96 | } 97 | } 98 | 99 | packaging { 100 | resources { 101 | excludes += "**" 102 | } 103 | } 104 | 105 | lint { 106 | checkReleaseBuilds = false 107 | } 108 | 109 | dependenciesInfo { 110 | includeInApk = false 111 | } 112 | 113 | androidResources { 114 | additionalParameters += arrayOf("--allow-reserved-package-id", "--package-id", "0x23") 115 | } 116 | 117 | externalNativeBuild { 118 | cmake { 119 | path("src/main/jni/CMakeLists.txt") 120 | version = "3.22.1+" 121 | } 122 | } 123 | } 124 | 125 | protobuf { 126 | protoc { 127 | artifact = libs.protobuf.protoc.get().toString() 128 | } 129 | 130 | generateProtoTasks { 131 | all().forEach { task -> 132 | task.builtins { 133 | id("java") { 134 | option("lite") 135 | } 136 | id("kotlin") { 137 | option("lite") 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | configurations.all { 145 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7") 146 | exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") 147 | } 148 | 149 | dependencies { 150 | compileOnly(libs.xposed) 151 | implementation(libs.protobuf.kotlin) 152 | implementation(libs.protobuf.java) 153 | compileOnly(libs.protobuf.protoc) 154 | implementation(libs.kotlin.stdlib) 155 | implementation(libs.kotlin.coroutines.android) 156 | implementation(libs.kotlin.coroutines.jdk) 157 | implementation(libs.androidx.documentfile) 158 | implementation(libs.cxx) 159 | } 160 | 161 | val adbExecutable: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath 162 | 163 | val restartBiliBili = task("restartBiliBili").apply { 164 | doLast { 165 | exec { 166 | commandLine(adbExecutable, "shell", "am", "force-stop", "tv.danmaku.bili") 167 | } 168 | exec { 169 | commandLine( 170 | adbExecutable, 171 | "shell", 172 | "am", 173 | "start", 174 | "$(pm resolve-activity --components tv.danmaku.bili)" 175 | ) 176 | } 177 | } 178 | } 179 | 180 | afterEvaluate { 181 | tasks.getByPath("installDebug").finalizedBy(restartBiliBili) 182 | } 183 | -------------------------------------------------------------------------------- /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/xposed_init: -------------------------------------------------------------------------------- 1 | me.iacn.biliroaming.XposedInit 2 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/ARGBColorChooseDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.text.Editable 7 | import android.text.TextWatcher 8 | import android.view.View 9 | import android.widget.EditText 10 | import android.widget.SeekBar 11 | import android.widget.SeekBar.OnSeekBarChangeListener 12 | import android.widget.TextView 13 | import me.iacn.biliroaming.utils.inflateLayout 14 | 15 | /** 16 | * Created by iAcn on 2019/7/14 17 | * Email i@iacn.me 18 | * 19 | * Copy & Modify from ColorChooseDialog on 2021/7/6 20 | */ 21 | class ARGBColorChooseDialog(context: Context, defColor: Int) : AlertDialog.Builder(context) { 22 | private val view = context.inflateLayout(R.layout.dialog_argb_color_choose) 23 | private val sampleView: View = view.findViewById(R.id.view_sample2) 24 | private val etColor: EditText = view.findViewById(R.id.et_color2) 25 | private val sbColorA: SeekBar = view.findViewById(R.id.sb_colorA2) 26 | private val sbColorR: SeekBar = view.findViewById(R.id.sb_colorR2) 27 | private val sbColorG: SeekBar = view.findViewById(R.id.sb_colorG2) 28 | private val sbColorB: SeekBar = view.findViewById(R.id.sb_colorB2) 29 | private val tvColorA: TextView = view.findViewById(R.id.tv_colorA2) 30 | private val tvColorR: TextView = view.findViewById(R.id.tv_colorR2) 31 | private val tvColorG: TextView = view.findViewById(R.id.tv_colorG2) 32 | private val tvColorB: TextView = view.findViewById(R.id.tv_colorB2) 33 | val color: Int 34 | get() = Color.argb( 35 | sbColorA.progress, 36 | sbColorR.progress, 37 | sbColorG.progress, 38 | sbColorB.progress 39 | ) 40 | 41 | private fun setEditTextListener() { 42 | etColor.addTextChangedListener(object : TextWatcher { 43 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 44 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 45 | updateValue(handleUnknownColor(s.toString())) 46 | } 47 | 48 | override fun afterTextChanged(s: Editable) {} 49 | }) 50 | } 51 | 52 | private fun setSeekBarListener() { 53 | val listener: OnSeekBarChangeListener = object : OnSeekBarChangeListener { 54 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 55 | if (fromUser) { 56 | val color = Color.argb( 57 | sbColorA.progress, 58 | sbColorR.progress, 59 | sbColorG.progress, 60 | sbColorB.progress 61 | ) 62 | etColor.setText(String.format("%08X", 0xFFFFFFFF.toInt() and color)) 63 | } 64 | tvColorA.text = sbColorA.progress.toString() 65 | tvColorR.text = sbColorR.progress.toString() 66 | tvColorG.text = sbColorG.progress.toString() 67 | tvColorB.text = sbColorB.progress.toString() 68 | } 69 | 70 | override fun onStartTrackingTouch(seekBar: SeekBar) {} 71 | override fun onStopTrackingTouch(seekBar: SeekBar) {} 72 | } 73 | sbColorA.setOnSeekBarChangeListener(listener) 74 | sbColorR.setOnSeekBarChangeListener(listener) 75 | sbColorG.setOnSeekBarChangeListener(listener) 76 | sbColorB.setOnSeekBarChangeListener(listener) 77 | } 78 | 79 | private fun updateValue(color: Int) { 80 | sampleView.setBackgroundColor(color) 81 | val progressA = Color.alpha(color) 82 | val progressR = Color.red(color) 83 | val progressG = Color.green(color) 84 | val progressB = Color.blue(color) 85 | sbColorA.progress = progressA 86 | sbColorR.progress = progressR 87 | sbColorG.progress = progressG 88 | sbColorB.progress = progressB 89 | tvColorA.text = progressA.toString() 90 | tvColorR.text = progressR.toString() 91 | tvColorG.text = progressG.toString() 92 | tvColorB.text = progressB.toString() 93 | } 94 | 95 | private fun handleUnknownColor(color: String) = 96 | try { 97 | Color.parseColor("#$color") 98 | } catch (e: IllegalArgumentException) { 99 | Color.BLACK 100 | } 101 | 102 | init { 103 | setView(view) 104 | setEditTextListener() 105 | setSeekBarListener() 106 | updateValue(defColor) 107 | etColor.setText(String.format("%08X", 0xFFFFFFFF.toInt() and defColor)) 108 | setTitle("拾色器") 109 | setNegativeButton("取消", null) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/ColorChooseDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.text.Editable 7 | import android.text.TextWatcher 8 | import android.view.View 9 | import android.widget.EditText 10 | import android.widget.SeekBar 11 | import android.widget.SeekBar.OnSeekBarChangeListener 12 | import android.widget.TextView 13 | import me.iacn.biliroaming.utils.inflateLayout 14 | 15 | /** 16 | * Created by iAcn on 2019/7/14 17 | * Email i@iacn.me 18 | */ 19 | class ColorChooseDialog(context: Context, defColor: Int) : AlertDialog.Builder(context) { 20 | private val view = context.inflateLayout(R.layout.dialog_color_choose) 21 | private val sampleView: View = view.findViewById(R.id.view_sample) 22 | private val etColor: EditText = view.findViewById(R.id.et_color) 23 | private val sbColorR: SeekBar = view.findViewById(R.id.sb_colorR) 24 | private val sbColorG: SeekBar = view.findViewById(R.id.sb_colorG) 25 | private val sbColorB: SeekBar = view.findViewById(R.id.sb_colorB) 26 | private val tvColorR: TextView = view.findViewById(R.id.tv_colorR) 27 | private val tvColorG: TextView = view.findViewById(R.id.tv_colorG) 28 | private val tvColorB: TextView = view.findViewById(R.id.tv_colorB) 29 | val color: Int 30 | get() = Color.rgb(sbColorR.progress, sbColorG.progress, sbColorB.progress) 31 | 32 | private fun setEditTextListener() { 33 | etColor.addTextChangedListener(object : TextWatcher { 34 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 35 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 36 | updateValue(handleUnknownColor(s.toString())) 37 | } 38 | 39 | override fun afterTextChanged(s: Editable) {} 40 | }) 41 | } 42 | 43 | private fun setSeekBarListener() { 44 | val listener: OnSeekBarChangeListener = object : OnSeekBarChangeListener { 45 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 46 | if (fromUser) { 47 | val color = Color.rgb(sbColorR.progress, sbColorG.progress, sbColorB.progress) 48 | etColor.setText(String.format("%06X", 0xFFFFFF and color)) 49 | } 50 | tvColorR.text = sbColorR.progress.toString() 51 | tvColorG.text = sbColorG.progress.toString() 52 | tvColorB.text = sbColorB.progress.toString() 53 | } 54 | 55 | override fun onStartTrackingTouch(seekBar: SeekBar) {} 56 | override fun onStopTrackingTouch(seekBar: SeekBar) {} 57 | } 58 | sbColorR.setOnSeekBarChangeListener(listener) 59 | sbColorG.setOnSeekBarChangeListener(listener) 60 | sbColorB.setOnSeekBarChangeListener(listener) 61 | } 62 | 63 | private fun updateValue(color: Int) { 64 | sampleView.setBackgroundColor(color) 65 | val progressR = Color.red(color) 66 | val progressG = Color.green(color) 67 | val progressB = Color.blue(color) 68 | sbColorR.progress = progressR 69 | sbColorG.progress = progressG 70 | sbColorB.progress = progressB 71 | tvColorR.text = progressR.toString() 72 | tvColorG.text = progressG.toString() 73 | tvColorB.text = progressB.toString() 74 | } 75 | 76 | private fun handleUnknownColor(color: String) = 77 | try { 78 | Color.parseColor("#$color") 79 | } catch (e: IllegalArgumentException) { 80 | Color.BLACK 81 | } 82 | 83 | init { 84 | setView(view) 85 | setEditTextListener() 86 | setSeekBarListener() 87 | updateValue(defColor) 88 | etColor.setText(String.format("%06X", 0xFFFFFF and defColor)) 89 | setTitle("自选颜色") 90 | setNegativeButton("取消", null) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/Constant.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | /** 4 | * Created by iAcn on 2019/4/12 5 | * Email i@iacn.me 6 | */ 7 | object Constant { 8 | const val PINK_PACKAGE_NAME = "tv.danmaku.bili" 9 | const val BLUE_PACKAGE_NAME = "com.bilibili.app.blue" 10 | const val PLAY_PACKAGE_NAME = "com.bilibili.app.in" 11 | const val HD_PACKAGE_NAME = "tv.danmaku.bilibilihd" 12 | val BILIBILI_PACKAGE_NAME = hashMapOf( 13 | "原版" to PINK_PACKAGE_NAME, 14 | "概念版" to BLUE_PACKAGE_NAME, 15 | "play版" to PLAY_PACKAGE_NAME, 16 | "HD版" to HD_PACKAGE_NAME 17 | ) 18 | const val TAG = "BiliRoaming" 19 | const val HOOK_INFO_FILE_NAME = "hookinfo.pb" 20 | const val TYPE_SEASON_ID = 0 21 | const val TYPE_MEDIA_ID = 1 22 | const val TYPE_EPISODE_ID = 2 23 | const val CUSTOM_COLOR_KEY = "biliroaming_custom_color" 24 | const val CURRENT_COLOR_KEY = "theme_entries_current_key" 25 | const val DEFAULT_CUSTOM_COLOR = -0xe6b7d 26 | const val infoUrl = "https://api.bilibili.com/client_info" 27 | const val zoneUrl = "https://api.bilibili.com/x/web-interface/zone" 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/MiscRemoveAdsDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.Activity 4 | import android.content.SharedPreferences 5 | import android.widget.FrameLayout 6 | import android.widget.LinearLayout 7 | import android.widget.ScrollView 8 | import me.iacn.biliroaming.utils.Log 9 | import me.iacn.biliroaming.utils.dp 10 | 11 | class MiscRemoveAdsDialog(activity: Activity, prefs: SharedPreferences) : 12 | BaseWidgetDialog(activity) { 13 | init { 14 | val scrollView = ScrollView(context).apply { 15 | scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY 16 | } 17 | val root = LinearLayout(context).apply { 18 | orientation = LinearLayout.VERTICAL 19 | layoutParams = FrameLayout.LayoutParams( 20 | FrameLayout.LayoutParams.MATCH_PARENT, 21 | FrameLayout.LayoutParams.WRAP_CONTENT 22 | ) 23 | } 24 | scrollView.addView(root) 25 | 26 | val removeSearchAdsSwitch = string(R.string.remove_search_ads_title).let { 27 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 28 | } 29 | removeSearchAdsSwitch.isChecked = prefs.getBoolean("remove_search_ads", false) 30 | 31 | val removeCommentCmSwitch = string(R.string.remove_comment_cm_title).let { 32 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 33 | } 34 | removeCommentCmSwitch.isChecked = prefs.getBoolean("remove_comment_cm", false) 35 | 36 | val blockDmFeedbackSwitch = string(R.string.block_dm_feedback_title).let { 37 | switchPrefsItem(it).let { p -> root.addView(p.first); p.second } 38 | } 39 | blockDmFeedbackSwitch.isChecked = prefs.getBoolean("block_dm_feedback", false) 40 | 41 | setTitle(string(R.string.misc_remove_ads_title)) 42 | 43 | setPositiveButton(android.R.string.ok) { _, _ -> 44 | prefs.edit().apply { 45 | putBoolean("remove_search_ads", removeSearchAdsSwitch.isChecked) 46 | putBoolean("remove_comment_cm", removeCommentCmSwitch.isChecked) 47 | putBoolean("block_dm_feedback", blockDmFeedbackSwitch.isChecked) 48 | }.apply() 49 | Log.toast(string(R.string.prefs_save_success_and_reboot)) 50 | } 51 | setNegativeButton(android.R.string.cancel, null) 52 | 53 | root.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 54 | 55 | setView(scrollView) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/TextFoldDialog.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.content.SharedPreferences 6 | import android.text.TextUtils 7 | import android.util.TypedValue 8 | import android.widget.* 9 | import me.iacn.biliroaming.hook.TextFoldHook 10 | import me.iacn.biliroaming.utils.dp 11 | import me.iacn.biliroaming.utils.sPrefs 12 | 13 | class TextFoldDialog(val activity: Activity, prefs: SharedPreferences) : 14 | AlertDialog.Builder(activity) { 15 | init { 16 | val scrollView = ScrollView(context).apply { 17 | scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY 18 | } 19 | val root = LinearLayout(context).apply { 20 | orientation = LinearLayout.VERTICAL 21 | layoutParams = FrameLayout.LayoutParams( 22 | FrameLayout.LayoutParams.MATCH_PARENT, 23 | FrameLayout.LayoutParams.WRAP_CONTENT 24 | ) 25 | } 26 | scrollView.addView(root) 27 | 28 | val commentMaxLines = 29 | prefs.getInt("text_fold_comment_max_lines", TextFoldHook.DEF_COMMENT_MAX_LINES) 30 | val commentMaxLinesTitle = string(R.string.text_fold_comment_max_lines_title) 31 | val commentMaxLinesItem = seekBarItem(commentMaxLinesTitle, current = commentMaxLines).let { 32 | root.addView(it.first) 33 | it.second 34 | } 35 | val dynMaxLines = prefs.getInt("text_fold_dyn_max_lines", TextFoldHook.DEF_DYN_MAX_LINES) 36 | val dynMaxLinesTitle = string(R.string.text_fold_dyn_max_lines_title) 37 | val dynMaxLinesItem = seekBarItem(dynMaxLinesTitle, current = dynMaxLines).let { 38 | root.addView(it.first) 39 | it.second 40 | } 41 | val dynLinesToAll = 42 | prefs.getInt("text_fold_dyn_lines_to_all", TextFoldHook.DEF_DYN_LINES_TO_ALL) 43 | val dynLinesToAllTitle = string(R.string.text_fold_dyn_lines_to_all_title) 44 | val dynLinesToAllItem = seekBarItem(dynLinesToAllTitle, current = dynLinesToAll).let { 45 | root.addView(it.first) 46 | it.second 47 | } 48 | 49 | setTitle(string(R.string.text_fold_title)) 50 | 51 | setPositiveButton(android.R.string.ok) { _, _ -> 52 | sPrefs.edit().apply { 53 | putInt( 54 | "text_fold_comment_max_lines", 55 | commentMaxLinesItem.progress.takeIf { it != 0 } 56 | ?: TextFoldHook.DEF_COMMENT_MAX_LINES 57 | ) 58 | putInt( 59 | "text_fold_dyn_max_lines", 60 | dynMaxLinesItem.progress.takeIf { it != 0 } 61 | ?: TextFoldHook.DEF_DYN_MAX_LINES 62 | ) 63 | putInt( 64 | "text_fold_dyn_lines_to_all", 65 | dynLinesToAllItem.progress.takeIf { it != 0 } 66 | ?: TextFoldHook.DEF_DYN_LINES_TO_ALL 67 | ) 68 | }.apply() 69 | } 70 | 71 | setNegativeButton(android.R.string.cancel, null) 72 | 73 | root.setPadding(16.dp, 10.dp, 16.dp, 10.dp) 74 | 75 | setView(scrollView) 76 | } 77 | 78 | private fun string(resId: Int) = context.getString(resId) 79 | 80 | private fun seekBarItem( 81 | name: String, 82 | current: Int, 83 | indicator: String = string(R.string.text_fold_line), 84 | max: Int = 100, 85 | ): Pair { 86 | val layout = LinearLayout(activity).apply { 87 | orientation = LinearLayout.VERTICAL 88 | layoutParams = LinearLayout.LayoutParams( 89 | LinearLayout.LayoutParams.MATCH_PARENT, 90 | LinearLayout.LayoutParams.WRAP_CONTENT 91 | ) 92 | setPadding(0, 8.dp, 0, 8.dp) 93 | } 94 | val nameView = TextView(activity).apply { 95 | text = name 96 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 16F) 97 | setSingleLine() 98 | ellipsize = TextUtils.TruncateAt.END 99 | layoutParams = LinearLayout.LayoutParams( 100 | LinearLayout.LayoutParams.WRAP_CONTENT, 101 | LinearLayout.LayoutParams.WRAP_CONTENT 102 | ) 103 | } 104 | val progressView = TextView(activity).apply { 105 | text = indicator.format(current) 106 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 14F) 107 | TypedValue().apply { 108 | context.theme.resolveAttribute(android.R.attr.textColorSecondary, this, true) 109 | }.data.let { setTextColor(it) } 110 | layoutParams = LinearLayout.LayoutParams( 111 | LinearLayout.LayoutParams.WRAP_CONTENT, 112 | LinearLayout.LayoutParams.WRAP_CONTENT 113 | ) 114 | } 115 | val seekBarView = SeekBar(activity).apply { 116 | progress = current 117 | this.max = max 118 | layoutParams = LinearLayout.LayoutParams( 119 | LinearLayout.LayoutParams.MATCH_PARENT, 120 | LinearLayout.LayoutParams.WRAP_CONTENT 121 | ).apply { 122 | topMargin = 8.dp 123 | } 124 | setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 125 | override fun onProgressChanged( 126 | seekBar: SeekBar?, 127 | progress: Int, 128 | fromUser: Boolean 129 | ) { 130 | progressView.text = indicator.format(progress) 131 | } 132 | 133 | override fun onStartTrackingTouch(seekBar: SeekBar?) {} 134 | override fun onStopTrackingTouch(seekBar: SeekBar?) {} 135 | }) 136 | } 137 | layout.addView(nameView) 138 | layout.addView(progressView) 139 | layout.addView(seekBarView) 140 | return Pair(layout, seekBarView) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /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.hookBeforeConstructor 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class AllowMiniPlayHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | if (!sPrefs.getBoolean("main_func", false)) return 11 | 12 | if (sPrefs.getBoolean("allow_mini_play", false)) { 13 | "com.bilibili.lib.media.resource.PlayConfig\$PlayMenuConfig".from(mClassLoader) 14 | ?.hookBeforeConstructor( 15 | Boolean::class.javaPrimitiveType, 16 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 17 | ) { param -> 18 | val type = param.args[1] 19 | val miniPlayerType = 20 | "com.bilibili.lib.media.resource.PlayConfig\$PlayConfigType" 21 | .from(mClassLoader)?.getStaticObjectField("MINIPLAYER") 22 | if (type == miniPlayerType) 23 | param.args[0] = true 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.view.View 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.* 6 | 7 | class AutoLikeHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | private val likedVideos = HashSet() 9 | 10 | companion object { 11 | var detail: Pair? = null 12 | } 13 | 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("auto_like", false)) return 16 | 17 | Log.d("startHook: AutoLike") 18 | 19 | val likeId = getId("frame_recommend") 20 | val like1 = getId("frame1") 21 | 22 | instance.sectionClass?.hookAfterAllMethods(instance.likeMethod()) { param -> 23 | val sec = param.thisObject ?: return@hookAfterAllMethods 24 | val (aid, like) = detail ?: return@hookAfterAllMethods 25 | if (likedVideos.contains(aid)) return@hookAfterAllMethods 26 | likedVideos.add(aid) 27 | val likeView = sec.javaClass.declaredFields.filter { 28 | View::class.java.isAssignableFrom(it.type) 29 | }.firstNotNullOfOrNull { 30 | sec.getObjectFieldOrNullAs(it.name)?.takeIf { v -> 31 | v.id == likeId || v.id == like1 32 | } 33 | } 34 | if (like == 0) 35 | likeView?.callOnClick() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/BLogDebugHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.from 4 | import me.iacn.biliroaming.utils.hookBeforeMethod 5 | import java.lang.reflect.Method 6 | 7 | class BLogDebugHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | val stringClass = String::class.java 10 | val throwableClass = Throwable::class.java 11 | val objectArrayClass = Array::class.java 12 | "tv.danmaku.android.log.BLog".from(mClassLoader)?.declaredMethods?.filter { m -> 13 | m.parameterTypes.let { 14 | it.size == 2 && it[0] == stringClass && (it[1] == stringClass || it[1] == throwableClass) 15 | || it.size == 3 && it[0] == stringClass && it[1] == stringClass 16 | && (it[2] == throwableClass || it[2] == objectArrayClass) 17 | } 18 | }?.forEach { m -> 19 | m.hookBeforeMethod { param -> 20 | val method = param.method as Method 21 | val methodParamTypes = method.parameterTypes 22 | val methodParamCount = method.parameterTypes.size 23 | 24 | fun Any?.traceString() = 25 | android.util.Log.getStackTraceString(this as? Throwable) 26 | 27 | val tag = "BiliRoaming." + param.args[0] as String 28 | if (methodParamCount == 2 && methodParamTypes[1] == stringClass) { 29 | val messages = param.args[1].toString().chunked(3000) 30 | when (method.name) { 31 | "i" -> messages.forEach { android.util.Log.i(tag, it) } 32 | "d" -> messages.forEach { android.util.Log.d(tag, it) } 33 | "w" -> messages.forEach { android.util.Log.w(tag, it) } 34 | "e" -> messages.forEach { android.util.Log.e(tag, it) } 35 | "v" -> messages.forEach { android.util.Log.v(tag, it) } 36 | } 37 | } else if (methodParamCount == 2 && methodParamTypes[1] == throwableClass) { 38 | when (method.name) { 39 | "i" -> android.util.Log.i(tag, param.args[1].traceString()) 40 | "d" -> android.util.Log.d(tag, param.args[1].traceString()) 41 | "w" -> android.util.Log.w(tag, param.args[1].traceString()) 42 | "e" -> android.util.Log.e(tag, param.args[1].traceString()) 43 | "v" -> android.util.Log.v(tag, param.args[1].traceString()) 44 | } 45 | } else if (methodParamCount == 3 && methodParamTypes[2] == throwableClass) { 46 | val message = (param.args[1] as? String).orEmpty() 47 | val throwable = param.args[2] as? Throwable 48 | when (method.name) { 49 | "i" -> android.util.Log.i(tag, message, throwable) 50 | "d" -> android.util.Log.d(tag, message, throwable) 51 | "w" -> android.util.Log.w(tag, message, throwable) 52 | "e" -> android.util.Log.e(tag, message, throwable) 53 | "v" -> android.util.Log.v(tag, message, throwable) 54 | } 55 | } else if (methodParamCount == 3 && methodParamTypes[2] == objectArrayClass 56 | && method.name.endsWith("fmt") 57 | ) { 58 | @Suppress("UNCHECKED_CAST") 59 | val formats = param.args[2] as Array 60 | val message = (param.args[1] as? String).orEmpty() 61 | when (method.name[0].toString()) { 62 | "i" -> android.util.Log.i(tag, message.format(*formats)) 63 | "d" -> android.util.Log.d(tag, message.format(*formats)) 64 | "w" -> android.util.Log.w(tag, message.format(*formats)) 65 | "e" -> android.util.Log.e(tag, message.format(*formats)) 66 | "v" -> android.util.Log.v(tag, message.format(*formats)) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/BangumiPageAdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class BangumiPageAdHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | override fun startHook() { 8 | if (!sPrefs.getBoolean("block_bangumi_page_ads", false)) return 9 | Log.d("startHook: BangumiPageAd") 10 | // activity toast ad 11 | "com.bilibili.bangumi.data.page.detail.entity.OGVActivityVo".from(mClassLoader) 12 | ?.hookBeforeAllConstructors { param -> 13 | val args = param.args 14 | for (i in args.indices) { 15 | when (val item = args[i]) { 16 | is Int -> args[i] = 0 17 | is MutableList<*> -> item.clear() 18 | else -> args[i] = null 19 | } 20 | } 21 | } 22 | // mall 23 | instance.bangumiUniformSeasonActivityEntrance()?.let { 24 | instance.bangumiUniformSeasonClass?.replaceMethod(it) { null } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/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/ChannelTabUIHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 7 | import me.iacn.biliroaming.utils.* 8 | 9 | class ChannelTabUIHook(classLoader: ClassLoader) : BaseHook(classLoader) { 10 | override fun startHook() { 11 | if (!sPrefs.getBoolean("hidden", false) 12 | || !sPrefs.getBoolean("add_channel", false) 13 | ) return 14 | "com.bilibili.pegasus.channelv2.home.category.HomeCategoryFragment" 15 | .from(mClassLoader)?.hookAfterMethod( 16 | "onViewCreated", View::class.java, Bundle::class.java 17 | ) { param -> 18 | val root = param.args[0] as ViewGroup 19 | if (root.context.javaClass == instance.splashActivityClass) { 20 | root.getChildAt(0).visibility = View.GONE 21 | root.clipToPadding = false 22 | root.setPadding(0, 0, 0, 48.dp) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/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}bilibili" 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 | ), "bilibili" 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/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/DarkSwitchHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.utils.* 7 | 8 | class DarkSwitchHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | override fun startHook() { 11 | instance.userFragmentClass?.run { 12 | instance.switchDarkModeMethod?.let { 13 | hookBeforeMethod(it, Boolean::class.javaPrimitiveType) { param -> 14 | val activity = param.thisObject 15 | .callMethodOrNullAs("getActivity") 16 | ?: return@hookBeforeMethod 17 | val themeUtils = instance.themeUtilsClass ?: return@hookBeforeMethod 18 | val isDarkFollowSystem = themeUtils.callStaticMethodOrNullAs( 19 | instance.isDarkFollowSystemMethod, activity 20 | ) ?: return@hookBeforeMethod 21 | if (isDarkFollowSystem) { 22 | AlertDialog.Builder(activity) 23 | .setMessage("将关闭深色跟随系统,确定切换?") 24 | .setPositiveButton(android.R.string.ok) { _, _ -> 25 | param.invokeOriginalMethod() 26 | } 27 | .setNegativeButton(android.R.string.cancel, null) 28 | .show() 29 | param.result = null 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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.util.regex.Pattern 6 | 7 | class EnvHook(classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | Log.d("startHook: Env") 10 | "com.bilibili.lib.blconfig.internal.EnvContext\$preBuiltConfig\$2".hookAfterMethod( 11 | mClassLoader, 12 | "invoke" 13 | ) { param -> 14 | @Suppress("UNCHECKED_CAST") 15 | val result = param.result as MutableMap 16 | for (config in configSet) { 17 | (if (sPrefs.getBoolean( 18 | config.config, 19 | false 20 | ) 21 | ) config.trueValue else config.falseValue) 22 | ?.let { result[config.key] = it } ?: result.remove(config.key) 23 | } 24 | } 25 | "com.bilibili.lib.blconfig.internal.TypedContext\$dataSp\$2".hookAfterMethod( 26 | mClassLoader, 27 | "invoke" 28 | ) { param -> 29 | val result = param.result as SharedPreferences 30 | // this indicates the proper instance 31 | if (!result.contains("bv.enable_bv")) return@hookAfterMethod 32 | for (config in configSet) { 33 | (if (sPrefs.getBoolean( 34 | config.config, 35 | false 36 | ) 37 | ) config.trueValue else config.falseValue) 38 | ?.let { result.edit().putString(config.key, it).apply() } 39 | ?: result.edit().remove(config.key).apply() 40 | } 41 | } 42 | 43 | // // Disable tinker 44 | // "com.tencent.tinker.loader.app.TinkerApplication".findClass(mClassLoader)?.hookBeforeAllConstructors { param -> 45 | // param.args[0] = 0 46 | // } 47 | } 48 | 49 | override fun lateInitHook() { 50 | Log.d("lateHook: Env") 51 | if (sPrefs.getBoolean("enable_av", false)) { 52 | val compatClass = "com.bilibili.droid.BVCompat".findClassOrNull(mClassLoader) 53 | compatClass?.declaredFields?.forEach { 54 | val field = compatClass.getStaticObjectField(it.name) 55 | if (field is Pattern && field.pattern() == "av[1-9]\\d*") 56 | compatClass.setStaticObjectField( 57 | it.name, 58 | Pattern.compile("(av[1-9]\\d*)|(BV1[1-9A-NP-Za-km-z]{9})", field.flags()) 59 | ) 60 | } 61 | } 62 | } 63 | 64 | companion object { 65 | 66 | private val encryptedValueMap = hashMapOf( 67 | "0" to "Irb5O7Q8Ka0ojD4qqScgqg==", 68 | "1" to "Y260Cyvp6HZEboaGO+YGMw==" 69 | ) 70 | 71 | class ConfigTuple( 72 | val key: String, 73 | val config: String, 74 | val trueValue: String?, 75 | val falseValue: String? 76 | ) 77 | 78 | val configSet = listOf( 79 | ConfigTuple( 80 | "bv.enable_bv", 81 | "enable_av", 82 | encryptedValueMap["0"], 83 | encryptedValueMap["1"] 84 | ), 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/FavFolderDialogHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.widget.CheckBox 6 | import me.iacn.biliroaming.from 7 | import me.iacn.biliroaming.hookInfo 8 | import me.iacn.biliroaming.orNull 9 | import me.iacn.biliroaming.utils.* 10 | 11 | class FavFolderDialogHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | override fun startHook() { 13 | if (!sPrefs.getBoolean("disable_auto_subscribe", false)) return 14 | var hooked = false 15 | hookInfo.favFolderDialog.class_.from(mClassLoader)?.hookAfterAllConstructors { param -> 16 | if (hooked) return@hookAfterAllConstructors 17 | val apiCallbackClass = param.thisObject.javaClass.declaredFields 18 | .firstOrNull { f -> f.type.let { it != Context::class.java && !it.isInterface && it.isAbstract } } 19 | ?.also { it.isAccessible = true }?.get(param.thisObject)?.javaClass 20 | ?: return@hookAfterAllConstructors 21 | val onSuccessMethod = apiCallbackClass.declaredMethods.firstOrNull { 22 | !it.isSynthetic && it.parameterTypes.size == 1 23 | } ?: return@hookAfterAllConstructors 24 | val dialogFieldName = apiCallbackClass.declaredFields.firstOrNull { 25 | Dialog::class.java.isAssignableFrom(it.type) 26 | }?.name ?: return@hookAfterAllConstructors 27 | onSuccessMethod.hookAfterMethod { param2 -> 28 | val checkBox = param2.thisObject.getObjectField(dialogFieldName) 29 | ?.getObjectFieldAs(hookInfo.favFolderDialog.checkBox.orNull) 30 | ?: return@hookAfterMethod 31 | if (checkBox.isChecked) 32 | checkBox.toggle() 33 | } 34 | hooked = true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/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/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/LosslessSettingHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class LosslessSettingHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | private var losslessEnabled: Boolean 8 | get() = biliPrefs.getBoolean("biliroaming_lossless", false) 9 | set(value) = biliPrefs.edit().putBoolean("biliroaming_lossless", value).apply() 10 | 11 | override fun startHook() { 12 | if (!sPrefs.getBoolean("remember_lossless_setting", false)) 13 | return 14 | instance.playURLMossClass?.run { 15 | hookBeforeMethod( 16 | "playConf", 17 | "com.bapis.bilibili.app.playurl.v1.PlayConfReq", 18 | instance.mossResponseHandlerClass 19 | ) { param -> 20 | param.args[1] = param.args[1].mossResponseHandlerProxy { 21 | it?.callMethod("getPlayConf") 22 | ?.callMethod("getLossLessConf") 23 | ?.callMethod("getConfValue") 24 | ?.callMethod("setSwitchVal", losslessEnabled) 25 | } 26 | } 27 | hookBeforeMethod( 28 | "playConfEdit", 29 | "com.bapis.bilibili.app.playurl.v1.PlayConfEditReq" 30 | ) { param -> 31 | param.args[0].callMethodAs>("getPlayConfList") 32 | .firstOrNull { 33 | it.callMethodAs("getConfTypeValue") == 30 // LOSSLESS 34 | }?.callMethod("getConfValue") 35 | ?.callMethodAs("getSwitchVal") 36 | ?.let { losslessEnabled = it } 37 | } 38 | hookAfterMethod( 39 | "playView", instance.playViewReqClass 40 | ) { param -> 41 | param.result?.callMethod("getPlayConf") 42 | ?.takeIf { it.callMethodAs("hasLossLessConf") } 43 | ?.callMethod("getLossLessConf") 44 | ?.callMethod("getConfValue") 45 | ?.callMethod("setSwitchVal", losslessEnabled) 46 | } 47 | } 48 | instance.playerMossClass?.hookAfterMethod( 49 | "playViewUnite", instance.playViewUniteReqClass 50 | ) { param -> 51 | param.result?.callMethod("getPlayDeviceConf") 52 | ?.callMethodAs>("internalGetMutableDeviceConfs")?.let { 53 | it[30/*LOSSLESS*/]?.callMethod("getConfValue") 54 | ?.callMethod("setSwitchVal", losslessEnabled) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/MiniProgramHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.Log 6 | import me.iacn.biliroaming.utils.bv2av 7 | import me.iacn.biliroaming.utils.hookBeforeMethod 8 | import me.iacn.biliroaming.utils.sPrefs 9 | import java.net.HttpURLConnection 10 | import java.net.URL 11 | 12 | class MiniProgramHook(classLoader: ClassLoader) : BaseHook(classLoader) { 13 | private val extractUrl = Regex("""(.*)(http\S*)(.*)""") 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("mini_program", false)) return 16 | Log.d("startHook: MiniProgram") 17 | instance.shareWrapperClass?.hookBeforeMethod( 18 | instance.shareWrapper(), 19 | String::class.java, 20 | Bundle::class.java 21 | ) { param -> 22 | val platform = param.args[0] as String 23 | val bundle = param.args[1] as Bundle 24 | if (platform == "COPY") { 25 | bundle.getString("params_content")?.let { content -> 26 | extractUrl.matchEntire(content) 27 | }?.let { 28 | listOf(it.groups[1]?.value, it.groups[2]?.value, it.groups[3]?.value) 29 | }?.let { (prefix, url, postfix) -> 30 | val conn = URL(url).openConnection() as HttpURLConnection 31 | conn.requestMethod = "GET" 32 | conn.instanceFollowRedirects = false 33 | conn.connect() 34 | if (conn.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { 35 | val target = URL(conn.getHeaderField("Location")) 36 | val bv = 37 | target.path.split("/") 38 | .firstOrNull { it.startsWith("BV") && it.length == 12 } 39 | ?: return@hookBeforeMethod 40 | val av = bv2av(bv) 41 | val query = target.query.split("&").map { 42 | it.split("=") 43 | }.filter { 44 | it.size == 2 45 | }.filter { 46 | it[0] == "p" 47 | }.joinToString("") { 48 | it.joinToString("") 49 | } 50 | bundle.putString( 51 | "params_content", 52 | "${prefix}https://b23.tv/av${av}${if (query.isEmpty()) "" else "/${query}"}$postfix" 53 | ) 54 | } 55 | 56 | } 57 | return@hookBeforeMethod 58 | } 59 | if (bundle.getString("params_type") != "type_min_program") return@hookBeforeMethod 60 | bundle.putString("params_type", "type_web") 61 | if (bundle.getString("params_title") == "哔哩哔哩") { 62 | bundle.putString("params_title", bundle.getString("params_content")) 63 | bundle.putString("params_content", "由哔哩漫游分享") 64 | } 65 | if (bundle.getString("params_content")?.startsWith("已观看") == true) { 66 | bundle.putString("params_content", "${bundle.getString("params_content")}\n由哔哩漫游分享") 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/OkHttpDebugHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | import java.io.Closeable 6 | import java.io.EOFException 7 | import java.io.IOException 8 | import java.lang.reflect.Proxy 9 | 10 | class OkHttpDebugHook(classLoader: ClassLoader) : BaseHook(classLoader) { 11 | 12 | private val gzipSourceClass by Weak { "okio.GzipSource" from mClassLoader } 13 | private val bufferClass by Weak { "okio.Buffer" from mClassLoader } 14 | 15 | override fun startHook() { 16 | instance.realCallClass?.hookAfterMethod(instance.execute()) { param -> 17 | val response = param.result 18 | runCatchingOrNull { 19 | logResponse(response, false) 20 | } 21 | } 22 | instance.realCallClass?.hookBeforeMethod( 23 | instance.enqueue(), instance.callbackClass 24 | ) { param -> 25 | val callback = param.args[0] 26 | param.args[0] = Proxy.newProxyInstance( 27 | callback.javaClass.classLoader, 28 | arrayOf(instance.callbackClass) 29 | ) { _, m, args -> 30 | if (m.parameterTypes.size == 2 && !IOException::class.java.isAssignableFrom(m.parameterTypes[1])) { 31 | val response = args[1] 32 | runCatchingOrNull { 33 | logResponse(response, true) 34 | } 35 | } 36 | m(callback, *args) 37 | } 38 | } 39 | } 40 | 41 | private fun logResponse(response: Any?, async: Boolean) { 42 | response ?: return 43 | val gzipSourceClass = gzipSourceClass ?: return 44 | val bufferClass = bufferClass ?: return 45 | val request = response.getObjectField(instance.requestField()) 46 | val method = request?.getObjectField(instance.methodFiled()) 47 | val url = request?.getObjectField(instance.urlField())?.toString() 48 | val protocol = response.getObjectField(instance.protocolField()) 49 | Log.d("############################## ${if (async) "async" else "blocking"}") 50 | Log.d("--> $method $url $protocol") 51 | val headers = response.getObjectField(instance.headersField()) 52 | if (bodyHasUnknownEncoding(headers)) { 53 | Log.d("<-- END HTTP (encoded body omitted)") 54 | return 55 | } else if (method == "HEAD") { 56 | Log.d("<-- END HTTP") 57 | return 58 | } 59 | val responseBody = response.getObjectField(instance.bodyField()) 60 | val source = responseBody?.callMethod(instance.bodySource()) 61 | source?.callMethod("request", Long.MAX_VALUE) 62 | var buffer = source?.callMethod("buffer") ?: return 63 | var gzippedLength: Long? = null 64 | val contentEncoding = headers?.callMethod(instance.getHeader(), "Content-Encoding") 65 | ?.toString() ?: "" 66 | if ("gzip".equals(contentEncoding, ignoreCase = true)) { 67 | gzippedLength = buffer.callMethodAs("size") 68 | (gzipSourceClass.new(buffer.callMethod("clone")) as Closeable).use { gzippedResponseBody -> 69 | buffer = bufferClass.new() 70 | buffer.callMethod("writeAll", gzippedResponseBody) 71 | } 72 | } 73 | val size = buffer.callMethodAs("size") 74 | if (!buffer.isProbablyUtf8()) { 75 | Log.d("") 76 | Log.d("<-- END HTTP (binary $size-byte body omitted)") 77 | return 78 | } 79 | Log.d("") 80 | val responseString = buffer.callMethod("clone") 81 | ?.callMethod("readString", Charsets.UTF_8) 82 | Log.d(responseString) 83 | 84 | if (gzippedLength != null) { 85 | Log.d("<-- END HTTP ($size-byte, $gzippedLength-gzipped-byte body)") 86 | } else { 87 | Log.d("<-- END HTTP ($size-byte body)") 88 | } 89 | Log.d("##############################") 90 | } 91 | 92 | private fun bodyHasUnknownEncoding(headers: Any?): Boolean { 93 | val contentEncoding = headers?.callMethodAs( 94 | instance.getHeader(), "Content-Encoding" 95 | ) ?: return false 96 | return !contentEncoding.equals("identity", ignoreCase = true) && 97 | !contentEncoding.equals("gzip", ignoreCase = true) 98 | } 99 | 100 | private fun Any.isProbablyUtf8(): Boolean { 101 | try { 102 | val prefix = bufferClass?.new() ?: return false 103 | val byteCount = callMethodAs("size").coerceAtMost(64) 104 | callMethod("copyTo", prefix, 0, byteCount) 105 | for (i in 0 until 16) { 106 | if (prefix.callMethodAs("exhausted")) { 107 | break 108 | } 109 | val codePoint = prefix.callMethodAs("readUtf8CodePoint") 110 | if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { 111 | return false 112 | } 113 | } 114 | return true 115 | } catch (_: EOFException) { 116 | return false // Truncated UTF-8 sequence. 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/OkHttpHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.hook.api.* 5 | import me.iacn.biliroaming.utils.* 6 | import java.net.HttpURLConnection 7 | 8 | class OkHttpHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | private val apiHooks = mutableListOf() 11 | 12 | init { 13 | apiHooks.add(SeasonRcmdHook) 14 | apiHooks.add(CardsHook) 15 | apiHooks.add(BannerV3AdHook) 16 | apiHooks.add(SkinHook) 17 | if (platform != "android_hd") 18 | apiHooks.add(BannerV8AdHook) 19 | } 20 | 21 | override fun startHook() { 22 | if (apiHooks.all { !it.enabled }) return 23 | 24 | instance.responseClass?.hookAfterAllConstructors out@{ param -> 25 | val response = param.thisObject ?: return@out 26 | val requestField = instance.requestField() ?: return@out 27 | val urlField = instance.urlField() ?: return@out 28 | val request = response.getObjectField(requestField) ?: return@out 29 | val url = request.getObjectField(urlField)?.toString() ?: return@out 30 | for (hook in apiHooks) { 31 | if (!hook.enabled || !hook.canHandler(url)) 32 | continue 33 | val okioClass = instance.okioClass ?: return@out 34 | val bufferedSourceClass = instance.bufferedSourceClass ?: return@out 35 | val codeField = instance.codeField() ?: return@out 36 | val bodyField = instance.bodyField() ?: return@out 37 | val stringMethod = instance.string() ?: return@out 38 | val sourceMethod = instance.source() ?: return@out 39 | val bufferMethod = instance.sourceBuffer() ?: return@out 40 | response.getIntField(codeField).takeIf { it == HttpURLConnection.HTTP_OK } 41 | ?: return@out 42 | 43 | val responseBody = response.getObjectField(bodyField) 44 | val sourceField = responseBody?.javaClass 45 | ?.findFieldByExactTypeOrNull(bufferedSourceClass) ?: return@out 46 | val longType = Long::class.javaPrimitiveType!! 47 | val contentLengthField = responseBody.javaClass.findFieldByExactTypeOrNull(longType) 48 | ?: return@out 49 | val respString = if (hook.decodeResponse()) { 50 | responseBody.callMethod(stringMethod)?.toString() ?: return@out 51 | } else "" 52 | val newResponse = hook.hook(respString) 53 | val stream = newResponse.byteInputStream() 54 | val length = stream.available() 55 | val source = okioClass.callStaticMethod(sourceMethod, stream) ?: return@out 56 | val bufferedSource = okioClass.callStaticMethod(bufferMethod, source) ?: return@out 57 | sourceField.set(responseBody, bufferedSource) 58 | contentLengthField.set(responseBody, length) 59 | break 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /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/PurifyShareHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.utils.sPrefs 4 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | 7 | class PurifyShareHook (classLoader: ClassLoader) : BaseHook(classLoader) { 8 | override fun startHook() { 9 | if (!sPrefs.getBoolean("purify_share", false)) return 10 | instance.shareClickResultClass?.hookBeforeMethod("getContent") { 11 | it.result = null 12 | } 13 | instance.shareClickResultClass?.hookBeforeMethod("getLink") { 14 | it.result = null 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/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/ScreenOrientationHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.content.pm.ActivityInfo 5 | import android.content.res.Configuration 6 | import android.os.Bundle 7 | import android.view.View 8 | import de.robv.android.xposed.XC_MethodHook 9 | import me.iacn.biliroaming.from 10 | import me.iacn.biliroaming.hookInfo 11 | import me.iacn.biliroaming.orNull 12 | import me.iacn.biliroaming.utils.* 13 | 14 | class ScreenOrientationHook(classLoader: ClassLoader) : BaseHook(classLoader) { 15 | private val oldExpandId by lazy { getId("bbplayer_halfscreen_expand") } 16 | private val newExpandId by lazy { getId("gemini_halfscreen_expand") } 17 | 18 | private var shouldIgnoreBack = false 19 | private var shouldIgnoreClick = false 20 | 21 | override fun startHook() { 22 | if (!sPrefs.getBoolean("unlock_screen_orientation", false)) 23 | return 24 | "com.bilibili.multitypeplayerV2.MultiTypeVideoContentActivity".from(mClassLoader) 25 | ?.hookAfterMethod("onCreate", Bundle::class.java) { param -> 26 | val activity = param.thisObject as Activity 27 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 28 | } 29 | "com.bilibili.video.videodetail.VideoDetailsActivity".from(mClassLoader) 30 | ?.hookAfterMethod("onCreate", Bundle::class.java) { param -> 31 | val activity = param.thisObject as Activity 32 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 33 | } 34 | hookInfo.orientationProcessor.run { 35 | class_.from(mClassLoader)?.let { 36 | it.replaceMethod(startGravitySensor.orNull) { null } 37 | it.replaceMethod(correctOrientation.orNull, it) { null } 38 | it.hookAfterMethod( 39 | switchOrientation.orNull, Int::class.javaPrimitiveType 40 | ) { param -> 41 | val activity = param.thisObject.getObjectFieldAs(activity.orNull) 42 | if (activity.requestedOrientation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { 43 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 44 | } 45 | } 46 | } 47 | } 48 | "com.bilibili.app.gemini.player.widget.story.GeminiPlayerFullscreenWidget" 49 | .from(mClassLoader)?.hookBeforeMethod("onClick", View::class.java) { 50 | shouldIgnoreClick = Thread.currentThread().stackTrace.none { 51 | it.methodName == "onConfigurationChanged" 52 | } 53 | } 54 | "com.bilibili.bangumi.ui.page.detail.playerV2.widget.halfscreen.PgcPlayerFullscreenWidget" 55 | .from(mClassLoader)?.hookBeforeMethod("onClick", View::class.java) { 56 | shouldIgnoreClick = Thread.currentThread().stackTrace.none { 57 | it.methodName == "onConfigurationChanged" 58 | } 59 | } 60 | "com.bilibili.bangumi.ui.page.detail.playerV2.widget.landscape.PgcPlayerBackWidget" 61 | .from(mClassLoader)?.hookBeforeMethod("onClick", View::class.java) { 62 | shouldIgnoreBack = Thread.currentThread().stackTrace.none { 63 | it.methodName == "onConfigurationChanged" 64 | } 65 | } 66 | "com.bilibili.bangumi.ui.page.detail.BangumiDetailActivityV3".from(mClassLoader)?.run { 67 | hookBeforeMethod("onBackPressed") { 68 | shouldIgnoreBack = Thread.currentThread().stackTrace.none { 69 | it.methodName == "onConfigurationChanged" 70 | } 71 | } 72 | hookAfterMethod("onCreate", Bundle::class.java) { param -> 73 | shouldIgnoreBack = false 74 | shouldIgnoreClick = false 75 | val activity = param.thisObject as Activity 76 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 77 | } 78 | hookBeforeMethod("onConfigurationChanged", Configuration::class.java) { param -> 79 | val activity = param.thisObject as Activity 80 | val newConfig = param.args[0] as Configuration 81 | if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { 82 | if (!shouldIgnoreClick) { 83 | (activity.findViewById(newExpandId) 84 | ?: activity.findViewById(oldExpandId)) 85 | ?.callOnClick() 86 | } else { 87 | shouldIgnoreClick = false 88 | } 89 | } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { 90 | if (!shouldIgnoreBack) { 91 | @Suppress("DEPRECATION") 92 | activity.onBackPressed() 93 | } else { 94 | shouldIgnoreBack = false 95 | } 96 | } 97 | } 98 | } 99 | hookInfo.screenLayoutHelper.class_.from(mClassLoader)?.declaredMethods?.find { 100 | it.name == hookInfo.screenLayoutHelper.onStateChange.orNull 101 | }?.run { 102 | var orientationHook: XC_MethodHook.Unhook? = null 103 | hookBeforeMethod { 104 | val shouldSkip = Thread.currentThread().stackTrace.map { it.methodName }.let { 105 | ("onClick" in it || "onBackPressed" in it) && "onConfigurationChanged" !in it 106 | } 107 | if (shouldSkip) return@hookBeforeMethod 108 | orientationHook = Activity::class.java.replaceMethod( 109 | "setRequestedOrientation", 110 | Int::class.javaPrimitiveType 111 | ) { null } 112 | } 113 | hookAfterMethod { param -> 114 | orientationHook?.unhook() 115 | orientationHook = null 116 | val activity = param.thisObject.getObjectFieldAs( 117 | hookInfo.screenLayoutHelper.activity.orNull 118 | ) 119 | if (activity.requestedOrientation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { 120 | activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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/SpeedHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class SpeedHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | 10 | private var lastSet: Any? = null 11 | 12 | override fun startHook() { 13 | Log.d("startHook: SpeedHook") 14 | val speed = sPrefs.getInt("default_speed", 100) 15 | if (speed == 100) return 16 | instance.playerCoreServiceV2Class?.hookBeforeMethod(instance.setDefaultSpeed(), Float::class.javaPrimitiveType) { 17 | if (lastSet != it.thisObject) { 18 | lastSet = it.thisObject 19 | it.args[0] = speed / 100f 20 | Log.toast("已设置倍速为 ${speed}%") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/SplashHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.net.Uri 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.widget.ImageView 7 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 8 | import me.iacn.biliroaming.utils.* 9 | import java.io.File 10 | 11 | class SplashHook(classLoader: ClassLoader) : BaseHook(classLoader) { 12 | override fun startHook() { 13 | if (!sPrefs.getBoolean("custom_splash", false) && !sPrefs.getBoolean( 14 | "custom_splash_logo", 15 | false 16 | ) 17 | && !sPrefs.getBoolean("full_splash", false) 18 | ) return 19 | Log.d("startHook: Splash") 20 | 21 | instance.splashInfoClass?.hookAfterMethod( 22 | "getMode" 23 | ) { param -> 24 | param.result = if (sPrefs.getBoolean("full_splash", false)) { 25 | "full" 26 | } else { 27 | param.result 28 | } 29 | } 30 | 31 | instance.brandSplashClass?.hookAfterMethod( 32 | "onViewCreated", 33 | View::class.java, 34 | Bundle::class.java 35 | ) { param -> 36 | val view = param.args[0] as View 37 | if (sPrefs.getBoolean("custom_splash", false)) { 38 | val brandId = getId("brand_splash") 39 | val fullId = getId("full_brand_splash") 40 | val brandSplash = view.findViewById(brandId) 41 | val full = if (fullId != 0) view.findViewById(fullId) else null 42 | val splashImage = File(currentContext.filesDir, SPLASH_IMAGE) 43 | if (splashImage.exists()) { 44 | val uri = Uri.fromFile(splashImage) 45 | brandSplash.setImageURI(uri) 46 | full?.setImageURI(uri) 47 | } else { 48 | brandSplash.alpha = .0f 49 | full?.alpha = .0f 50 | } 51 | } 52 | if (sPrefs.getBoolean("custom_splash_logo", false)) { 53 | val logoId = getId("brand_logo") 54 | val brandLogo = view.findViewById(logoId) 55 | val logoImage = File(currentContext.filesDir, LOGO_IMAGE) 56 | if (logoImage.exists()) 57 | brandLogo.setImageURI(Uri.fromFile(logoImage)) 58 | else 59 | brandLogo.alpha = .0f 60 | } 61 | } 62 | } 63 | 64 | companion object { 65 | const val SPLASH_IMAGE = "biliroaming_splash" 66 | const val LOGO_IMAGE = "biliroaming_logo" 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/StartActivityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.app.Activity 4 | import android.app.Instrumentation 5 | import android.content.ComponentName 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import me.iacn.biliroaming.utils.Log 10 | import me.iacn.biliroaming.utils.hookBeforeAllMethods 11 | import me.iacn.biliroaming.utils.hookBeforeMethod 12 | import me.iacn.biliroaming.utils.packageName 13 | import me.iacn.biliroaming.utils.sPrefs 14 | 15 | class StartActivityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 16 | override fun startHook() { 17 | "tv.danmaku.bili.ui.intent.IntentHandlerActivity".hookBeforeMethod(mClassLoader, "onCreate", Bundle::class.java) { param -> 18 | val a = param.thisObject as Activity 19 | val data = a.intent.data ?: return@hookBeforeMethod 20 | a.intent.data = data.buildUpon().encodedQuery(data.encodedQuery?.replace("&-Arouter=story", "")).build() 21 | } 22 | Instrumentation::class.java.hookBeforeAllMethods("execStartActivity") { param -> 23 | val intent = param.args[4] as? Intent ?: return@hookBeforeAllMethods 24 | val uri = intent.dataString ?: return@hookBeforeAllMethods 25 | if (sPrefs.getBoolean( 26 | "replace_story_video", 27 | false 28 | ) && uri.startsWith("bilibili://story/") 29 | ) { 30 | intent.component = ComponentName( 31 | intent.component?.packageName ?: packageName, 32 | "com.bilibili.video.videodetail.VideoDetailsActivity" 33 | ) 34 | intent.data = Uri.parse(uri.replace("bilibili://story/", "bilibili://video/")) 35 | } 36 | if (sPrefs.getBoolean("force_browser", false)) { 37 | if (intent.component?.className?.endsWith("MWebActivity") == true && 38 | intent.data?.authority?.matches(whileListDomain) == false) { 39 | Log.d("force_browser ${intent.data?.authority}") 40 | param.args[4] = Intent(Intent.ACTION_VIEW).apply { 41 | data = intent.data 42 | } 43 | } 44 | } 45 | } 46 | } 47 | companion object { 48 | val whileListDomain = Regex(""".*bilibili\.com|.*b23\.tv""") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/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/TextFoldHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.* 5 | 6 | class TextFoldHook(classLoader: ClassLoader) : BaseHook(classLoader) { 7 | private val commentMaxLines by lazy { 8 | sPrefs.getInt("text_fold_comment_max_lines", DEF_COMMENT_MAX_LINES) 9 | } 10 | private val dynMaxLines by lazy { 11 | sPrefs.getInt("text_fold_dyn_max_lines", DEF_DYN_MAX_LINES) 12 | } 13 | private val dynLinesToAll by lazy { 14 | sPrefs.getInt("text_fold_dyn_lines_to_all", DEF_DYN_LINES_TO_ALL) 15 | } 16 | 17 | private var maxLineFieldName = "" 18 | 19 | companion object { 20 | const val DEF_COMMENT_MAX_LINES = 6 21 | const val DEF_DYN_MAX_LINES = 4 22 | const val DEF_DYN_LINES_TO_ALL = 10 23 | } 24 | 25 | override fun startHook() { 26 | if (commentMaxLines != DEF_COMMENT_MAX_LINES) { 27 | "com.bapis.bilibili.main.community.reply.v1.ReplyControl".from(mClassLoader) 28 | ?.replaceMethod("getMaxLine") { commentMaxLines.toLong() } 29 | } 30 | 31 | if (dynMaxLines != DEF_DYN_MAX_LINES) { 32 | instance.ellipsizingTextViewClass?.hookBeforeMethod( 33 | "setMaxLines", 34 | Int::class.javaPrimitiveType 35 | ) { param -> 36 | val maxLines = param.args[0] as Int 37 | if (maxLines == DEF_DYN_MAX_LINES) 38 | param.args[0] = dynMaxLines 39 | } 40 | instance.ellipsizingTextViewClass?.hookAfterAllConstructors { param -> 41 | val fieldName = maxLineFieldName.ifEmpty { 42 | (instance.ellipsizingTextViewClass?.declaredFields 43 | ?.filter { it.type == Int::class.javaPrimitiveType } 44 | ?.find { param.thisObject.getIntField(it.name) == DEF_DYN_MAX_LINES } 45 | ?.name ?: "").also { maxLineFieldName = it } 46 | }.ifEmpty { return@hookAfterAllConstructors } 47 | param.thisObject.setIntField(fieldName, dynMaxLines) 48 | } 49 | } 50 | 51 | if (dynLinesToAll != DEF_DYN_LINES_TO_ALL) { 52 | instance.setLineToAllCountMethod?.let { 53 | instance.ellipsizingTextViewClass?.hookBeforeMethod( 54 | it, Int::class.javaPrimitiveType 55 | ) { param -> param.args[0] = dynLinesToAll } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/TrialVipQualityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import de.robv.android.xposed.XC_MethodHook 6 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 7 | import me.iacn.biliroaming.from 8 | import me.iacn.biliroaming.hookInfo 9 | import me.iacn.biliroaming.orNull 10 | import me.iacn.biliroaming.utils.* 11 | 12 | class TrialVipQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 13 | 14 | private val biliAccountInfoClass by Weak { hookInfo.biliAccountInfo.class_ from mClassLoader } 15 | private val isEffectiveVip: Boolean 16 | get() = biliAccountInfoClass?.callStaticMethod(hookInfo.biliAccountInfo.get.orNull) 17 | ?.callMethodAs(hookInfo.biliAccountInfo.isEffectiveVip.orNull) ?: false 18 | 19 | private val vipFreeText by lazy { 20 | currentContext.getString(getResId("try_listening_tips", "string")) // 限免中 21 | } 22 | private val trialingText by lazy { 23 | currentContext.getString(getResId("player_try_watching", "string")) // 试看中 24 | } 25 | private val toTrialText by lazy { 26 | currentContext.getString(getResId("player_try_watch_enable", "string")) // 可试看 27 | } 28 | private val Number.dp2px: Int 29 | get() = (this.toFloat() * (currentContext.resources.displayMetrics.density + 0.5F)).toInt() 30 | 31 | override fun startHook() { 32 | val hidden = sPrefs.getBoolean("hidden", false) 33 | val trialVipQuality = sPrefs.getBoolean("trial_vip_quality", false) 34 | val forceOldPlayer = sPrefs.getBoolean("force_old_player", false) 35 | if (forceOldPlayer) { 36 | "tv.danmaku.biliplayerv2.GeminiPlayerFFKt".from(mClassLoader)?.declaredMethods?.find { 37 | it.parameterTypes.isEmpty() && it.returnType == Boolean::class.javaPrimitiveType 38 | }?.replaceMethod { false } 39 | } 40 | if (forceOldPlayer || (hidden && trialVipQuality)) { 41 | hookInfo.playerController.class_.from(mClassLoader)?.declaredMethods?.find { 42 | it.name == hookInfo.playerController.getPlayer.orNull 43 | }?.hookBeforeMethod { 44 | if (forceOldPlayer || !isEffectiveVip) 45 | it.args[1] = false 46 | } 47 | } 48 | if (!hidden || !trialVipQuality) return 49 | instance.playURLMossClass?.hookAfterMethod( 50 | XC_MethodHook.PRIORITY_LOWEST, 51 | "playView", instance.playViewReqClass 52 | ) { param -> 53 | param.result ?: return@hookAfterMethod 54 | if (isEffectiveVip || param.args[0].callMethodAs("getDownload") >= 1) 55 | return@hookAfterMethod 56 | makeVipFree(param.result.callMethod("getVideoInfo")) 57 | } 58 | 59 | instance.playerMossClass?.hookAfterMethod( 60 | XC_MethodHook.PRIORITY_LOWEST, 61 | "playViewUnite", instance.playViewUniteReqClass 62 | ) { param -> 63 | param.result ?: return@hookAfterMethod 64 | if (isEffectiveVip || param.args[0].callMethod("getVod") 65 | ?.callMethodAs("getDownload").let { it != null && it >= 1 } 66 | ) return@hookAfterMethod 67 | makeVipFree(param.result.callMethod("getVodInfo")) 68 | } 69 | 70 | hookInfo.qualityViewHolderList.forEach { info -> 71 | info.class_.from(mClassLoader)?.declaredMethods 72 | ?.find { it.name == info.bindOnline.orNull } 73 | ?.hookAfterMethod { param -> 74 | val selected = param.args[1] as Boolean 75 | val strokeBadge = param.args[3] as TextView 76 | val solidBadge = param.args[4] as TextView 77 | if (!isEffectiveVip && solidBadge.text.toString() == vipFreeText) { 78 | solidBadge.visibility = View.GONE 79 | val strokeText = if (selected) trialingText else toTrialText 80 | strokeBadge.text = strokeText 81 | strokeBadge.visibility = View.VISIBLE 82 | if (strokeText.length > 2) { 83 | strokeBadge.setPadding(4.dp2px, 1.dp2px, 4.dp2px, 2.dp2px) 84 | } else { 85 | strokeBadge.setPadding(8.5.dp2px, 1.dp2px, 8.5.dp2px, 2.dp2px) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | private fun makeVipFree(videoInfo: Any?) { 93 | videoInfo ?: return 94 | videoInfo.callMethodAs>("getStreamListList") 95 | .filter { it.callMethodAs("hasDashVideo") || it.callMethodAs("hasSegmentVideo") } 96 | .forEach { 97 | it.callMethod("getStreamInfo")?.run { 98 | if (callMethodAs("getNeedVip")) { 99 | callMethod("setNeedVip", false) 100 | callMethod("setVipFree", true) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/TryWatchVipQualityHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 4 | import me.iacn.biliroaming.utils.Log 5 | import me.iacn.biliroaming.utils.hookBeforeMethod 6 | import me.iacn.biliroaming.utils.sPrefs 7 | 8 | class TryWatchVipQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 9 | override fun startHook() { 10 | if (!sPrefs.getBoolean("disable_try_watch_vip_quality", false)) return 11 | 12 | Log.d("startHook: TryWatchVipQualityHook") 13 | instance.canTryWatchVipQuality()?.let { m -> 14 | instance.playerQualityServiceClass?.hookBeforeMethod( 15 | m 16 | ) { 17 | it.result = false 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/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 | } 15 | if (fullScreenQuality != 0) { 16 | instance.playerSettingHelperClass?.replaceMethod(instance.getDefaultQn()) { fullScreenQuality } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/VipSectionHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 8 | import me.iacn.biliroaming.utils.findFieldByExactType 9 | import me.iacn.biliroaming.utils.from 10 | import me.iacn.biliroaming.utils.hookAfterMethod 11 | import me.iacn.biliroaming.utils.sPrefs 12 | 13 | class VipSectionHook(classLoader: ClassLoader) : BaseHook(classLoader) { 14 | override fun startHook() { 15 | if (!sPrefs.getBoolean("hidden", false) 16 | || (!sPrefs.getBoolean("modify_vip_section_style", false) 17 | && !sPrefs.getBoolean("remove_vip_section", false)) 18 | ) return 19 | val vipEntranceViewClass = 20 | "tv.danmaku.bili.ui.main2.mine.widgets.MineVipEntranceView".from(mClassLoader) 21 | val vipEntranceViewField = 22 | vipEntranceViewClass?.let { instance.homeUserCenterClass?.findFieldByExactType(it) } 23 | instance.homeUserCenterClass?.hookAfterMethod( 24 | "onCreateView", 25 | LayoutInflater::class.java, 26 | ViewGroup::class.java, 27 | Bundle::class.java 28 | ) { 29 | val self = it.thisObject 30 | (vipEntranceViewField?.get(self) as? View)?.visibility = View.GONE 31 | vipEntranceViewField?.set(self, null) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/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/hook/api/ApiHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | interface ApiHook { 4 | val enabled: Boolean 5 | 6 | fun canHandler(api: String): Boolean 7 | fun decodeResponse(): Boolean = true 8 | fun hook(response: String): String 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/BannerV3AdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import org.json.JSONObject 6 | 7 | object BannerV3AdHook : ApiHook { 8 | private val targetApis = arrayOf( 9 | // 追番 10 | "https://api.bilibili.com/pgc/page/bangumi?", 11 | // 影视 12 | "https://api.bilibili.com/pgc/page/cinema/tab?", 13 | // 番剧推荐 14 | "https://api.bilibili.com/pgc/page/?" 15 | ) 16 | 17 | override val enabled by lazy { 18 | sPrefs.getBoolean("hidden", false) 19 | && sPrefs.getBoolean("purify_banner_ads", false) 20 | } 21 | 22 | override fun canHandler(api: String) = targetApis.any { api.startsWith(it) } 23 | 24 | override fun hook(response: String): String { 25 | val json = JSONObject(response) 26 | val modules = json.optJSONObject("result") 27 | ?.optJSONArray("modules") 28 | ?: return response 29 | var changed = false 30 | for (module in modules) { 31 | if (module.optString("style") != "banner_v3") 32 | continue 33 | val items = module.optJSONArray("items") 34 | ?: continue 35 | val toRemoveIdx = mutableListOf() 36 | var index = 0 37 | for (item in items) { 38 | if (item.optJSONObject("source_content") 39 | ?.optJSONObject("ad_content") != null 40 | ) toRemoveIdx.add(index) 41 | index++ 42 | } 43 | if (toRemoveIdx.isNotEmpty()) 44 | changed = true 45 | toRemoveIdx.reversed().forEach { 46 | items.remove(it) 47 | } 48 | } 49 | return if (changed) json.toString() else response 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/BannerV8AdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import me.iacn.biliroaming.utils.toJSONObject 6 | 7 | object BannerV8AdHook : ApiHook { 8 | private const val feedApi = "https://app.bilibili.com/x/v2/feed/index" 9 | 10 | override val enabled by lazy { 11 | sPrefs.getBoolean("hidden", false) 12 | && sPrefs.getBoolean("purify_banner_ads", false) 13 | } 14 | 15 | override fun canHandler(api: String) = api.startsWith(feedApi) 16 | 17 | override fun hook(response: String): String { 18 | val json = response.toJSONObject() 19 | val items = json.optJSONObject("data") 20 | ?.optJSONArray("items") 21 | ?: return response 22 | var changed = false 23 | for (item in items) { 24 | if (item.optString("card_type") != "banner_v8") 25 | continue 26 | val banners = item.optJSONArray("banner_item") 27 | ?: continue 28 | val toRemoveIdx = mutableListOf() 29 | var index = 0 30 | for (banner in banners) { 31 | if (banner.optString("type") == "ad") 32 | toRemoveIdx.add(index) 33 | index++ 34 | } 35 | if (toRemoveIdx.isNotEmpty()) 36 | changed = true 37 | toRemoveIdx.reversed().forEach { 38 | banners.remove(it) 39 | } 40 | } 41 | return if (changed) json.toString() else response 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/CardsHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.sPrefs 4 | import org.json.JSONArray 5 | import org.json.JSONObject 6 | 7 | object CardsHook : ApiHook { 8 | private val cardsApis = arrayOf( 9 | "https://api.bilibili.com/pgc/season/player/cards", 10 | "https://api.bilibili.com/pgc/season/player/ogv/cards" 11 | ) 12 | 13 | override val enabled by lazy { 14 | sPrefs.getBoolean("hidden", false) 15 | && sPrefs.getBoolean("block_up_rcmd_ads", false) 16 | } 17 | 18 | override fun canHandler(api: String) = cardsApis.any { api.startsWith(it) } 19 | override fun decodeResponse() = false 20 | 21 | override fun hook(response: String): String { 22 | return JSONObject().apply { 23 | put("code", 0) 24 | put("data", JSONArray()) 25 | }.toString() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/SeasonRcmdHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.iterator 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import org.json.JSONObject 6 | 7 | object SeasonRcmdHook : ApiHook { 8 | private const val rcmdApi = "https://api.bilibili.com/pgc/season/app/related/recommend" 9 | 10 | override val enabled by lazy { 11 | sPrefs.getBoolean("hidden", false) 12 | && sPrefs.getBoolean("remove_video_relate_promote", false) 13 | } 14 | 15 | override fun canHandler(api: String) = api.startsWith(rcmdApi) 16 | 17 | override fun hook(response: String): String { 18 | val json = JSONObject(response) 19 | val cards = json.optJSONObject("result") 20 | ?.optJSONArray("cards") ?: return response 21 | var changed = false 22 | val toRemoveIdxList = mutableListOf() 23 | var index = 0 24 | for (card in cards) { 25 | if (card.optInt("type") == 2) 26 | toRemoveIdxList.add(index) 27 | index++ 28 | } 29 | if (toRemoveIdxList.isNotEmpty()) 30 | changed = true 31 | toRemoveIdxList.reversed().forEach { 32 | cards.remove(it) 33 | } 34 | return if (changed) json.toString() else response 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/hook/api/SkinHook.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.hook.api 2 | 3 | import me.iacn.biliroaming.utils.runCatchingOrNull 4 | import me.iacn.biliroaming.utils.sPrefs 5 | import me.iacn.biliroaming.utils.toJSONObject 6 | import org.json.JSONObject 7 | 8 | object SkinHook : ApiHook { 9 | private const val skinApi = "https://app.bilibili.com/x/resource/show/skin" 10 | 11 | override val enabled by lazy { 12 | sPrefs.getBoolean("hidden", false) 13 | && sPrefs.getBoolean("skin", false) 14 | && !sPrefs.getString("skin_json", null).isNullOrEmpty() 15 | } 16 | 17 | override fun canHandler(api: String) = api.startsWith(skinApi) 18 | 19 | override fun hook(response: String): String { 20 | val skinJson = sPrefs.getString("skin_json", null) 21 | val skin = skinJson.runCatchingOrNull { toJSONObject() } 22 | ?.apply { 23 | if (optString("package_md5").isNotEmpty()) 24 | put("package_md5", JSONObject.NULL) 25 | } ?: return response 26 | return response.toJSONObject() 27 | .apply { 28 | optJSONObject("data") 29 | ?.put("user_equip", skin) 30 | }.toString() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Booleans.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.InvocationKind 5 | import kotlin.contracts.contract 6 | 7 | @OptIn(ExperimentalContracts::class) 8 | inline fun Boolean.yes(action: () -> Unit): Boolean { 9 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 10 | if (this) action() 11 | return this 12 | } 13 | 14 | @OptIn(ExperimentalContracts::class) 15 | inline fun Boolean.no(action: () -> Unit): Boolean { 16 | contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 17 | if (!this) action() 18 | return this 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | import java.net.URL 8 | 9 | suspend fun fetchJson(url: URL) = withContext(Dispatchers.IO) { 10 | try { 11 | JSONObject(url.readText()) 12 | } catch (e: Throwable) { 13 | null 14 | } 15 | } 16 | 17 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 18 | suspend fun fetchJson(url: String) = fetchJson(URL(url)) 19 | 20 | suspend fun fetchJsonArray(url: URL) = withContext(Dispatchers.IO) { 21 | try { 22 | JSONArray(url.readText()) 23 | } catch (e: Throwable) { 24 | null 25 | } 26 | } 27 | 28 | @Suppress("BlockingMethodInNonBlockingContext") // Fuck JetBrain 29 | suspend fun fetchJsonArray(url: String) = fetchJsonArray(URL(url)) 30 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/DexHelper.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import java.lang.reflect.Field 4 | import java.lang.reflect.Member 5 | 6 | class DexHelper(private val classLoader: ClassLoader) : AutoCloseable { 7 | 8 | private val token = load(classLoader) 9 | 10 | external fun findMethodUsingString( 11 | str: String, 12 | matchPrefix: Boolean = false, 13 | returnType: Long = -1, 14 | parameterCount: Short = -1, 15 | parameterShorty: String? = null, 16 | declaringClass: Long = -1, 17 | parameterTypes: LongArray? = null, 18 | containsParameterTypes: LongArray? = null, 19 | dexPriority: IntArray? = null, 20 | findFirst: Boolean = true 21 | ): LongArray 22 | 23 | external fun findMethodInvoking( 24 | methodIndex: Long, 25 | returnType: Long = -1, 26 | parameterCount: Short = -1, 27 | parameterShorty: String? = null, 28 | declaringClass: Long = -1, 29 | parameterTypes: LongArray? = null, 30 | containsParameterTypes: LongArray? = null, 31 | dexPriority: IntArray? = null, 32 | findFirst: Boolean = true 33 | ): LongArray 34 | 35 | external fun findMethodInvoked( 36 | methodIndex: Long, 37 | returnType: Long = -1, 38 | parameterCount: Short = -1, 39 | parameterShorty: String? = null, 40 | declaringClass: Long = -1, 41 | parameterTypes: LongArray? = null, 42 | containsParameterTypes: LongArray? = null, 43 | dexPriority: IntArray? = null, 44 | findFirst: Boolean = true 45 | ): LongArray 46 | 47 | external fun findMethodSettingField( 48 | fieldIndex: Long, 49 | returnType: Long = -1, 50 | parameterCount: Short = -1, 51 | parameterShorty: String? = null, 52 | declaringClass: Long = -1, 53 | parameterTypes: LongArray? = null, 54 | containsParameterTypes: LongArray? = null, 55 | dexPriority: IntArray? = null, 56 | findFirst: Boolean = true 57 | ): LongArray 58 | 59 | external fun findMethodGettingField( 60 | fieldIndex: Long, 61 | returnType: Long = -1, 62 | parameterCount: Short = -1, 63 | parameterShorty: String? = null, 64 | declaringClass: Long = -1, 65 | parameterTypes: LongArray? = null, 66 | containsParameterTypes: LongArray? = null, 67 | dexPriority: IntArray? = null, 68 | findFirst: Boolean = true 69 | ): LongArray 70 | 71 | external fun findField( 72 | type: Long, 73 | dexPriority: IntArray? = null, 74 | findFirst: Boolean = true 75 | ): LongArray 76 | 77 | external fun decodeMethodIndex(methodIndex: Long): Member? 78 | 79 | external fun encodeMethodIndex(method: Member): Long 80 | 81 | external fun decodeFieldIndex(fieldIndex: Long): Field? 82 | 83 | external fun encodeFieldIndex(field: Field): Long 84 | 85 | external fun encodeClassIndex(clazz: Class<*>): Long 86 | 87 | external fun decodeClassIndex(classIndex: Long): Class<*>? 88 | 89 | external fun createFullCache() 90 | 91 | external override fun close() 92 | 93 | protected fun finalize() = close() 94 | 95 | private external fun load(classLoader: ClassLoader): Long 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Hashs.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import java.io.File 4 | import java.security.DigestInputStream 5 | import java.security.MessageDigest 6 | 7 | private val HEX_DIGITS = 8 | charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') 9 | 10 | fun ByteArray.toHexString(): String { 11 | val hexDigits = HEX_DIGITS 12 | val len = size 13 | if (len <= 0) return "" 14 | val ret = CharArray(len shl 1) 15 | var i = 0 16 | var j = 0 17 | while (i < len) { 18 | ret[j++] = hexDigits[this[i].toInt() shr 4 and 0x0f] 19 | ret[j++] = hexDigits[this[i].toInt() and 0x0f] 20 | i++ 21 | } 22 | return String(ret) 23 | } 24 | 25 | private fun hashFile(file: File, algorithm: String): ByteArray? { 26 | return try { 27 | file.inputStream().use { fis -> 28 | val md = MessageDigest.getInstance(algorithm) 29 | val buffer = ByteArray(DEFAULT_BUFFER_SIZE) 30 | DigestInputStream(fis, md).use { 31 | while (true) { 32 | if (it.read(buffer) == -1) 33 | break 34 | } 35 | } 36 | md.digest() 37 | } 38 | } catch (_: Exception) { 39 | null 40 | } 41 | } 42 | 43 | val File.sha256sum: String 44 | get() = hashFile(this, "SHA256")?.toHexString() ?: "" 45 | -------------------------------------------------------------------------------- /app/src/main/java/me/iacn/biliroaming/utils/Json.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | package me.iacn.biliroaming.utils 3 | 4 | import org.json.JSONObject 5 | 6 | inline fun Map.toJson() = JSONObject(this).toString() 7 | 8 | inline fun Map.toJsonObject() = JSONObject(this) 9 | 10 | fun json(build: JSONObject.() -> Unit) = JSONObject().apply(build) 11 | 12 | context(JSONObject) 13 | infix fun String.by(build: JSONObject.() -> Unit): JSONObject = put(this, JSONObject().build()) 14 | 15 | context(JSONObject) 16 | infix fun String.by(value: Any): JSONObject = put(this, value) 17 | -------------------------------------------------------------------------------- /app/src/main/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/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 || (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/java/me/iacn/biliroaming/utils/UtilsX.kt: -------------------------------------------------------------------------------- 1 | package me.iacn.biliroaming.utils 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import me.iacn.biliroaming.BiliBiliPackage.Companion.instance 6 | import me.iacn.biliroaming.hookInfo 7 | import me.iacn.biliroaming.orNull 8 | import java.io.File 9 | 10 | @Suppress("DEPRECATION") 11 | fun blkvPrefsByName(name: String, multiProcess: Boolean = true): SharedPreferences { 12 | return instance.blkvClass?.callStaticMethodAs( 13 | hookInfo.blkv.getByName.orNull, currentContext, name, multiProcess, 0 14 | ) ?: currentContext.getSharedPreferences(name, Context.MODE_MULTI_PROCESS) 15 | } 16 | 17 | @Suppress("DEPRECATION") 18 | fun blkvPrefsByFile(file: File, multiProcess: Boolean = true): SharedPreferences { 19 | return instance.blkvClass?.callStaticMethodAs( 20 | hookInfo.blkv.getByFile.orNull, currentContext, file, multiProcess, 0 21 | ) ?: currentContext.getSharedPreferences(file.nameWithoutExtension, Context.MODE_MULTI_PROCESS) 22 | } 23 | 24 | val abPrefs by lazy { 25 | val abPath = "prod/blconfig/ab.sp" 26 | val file = File(currentContext.getDir("foundation", Context.MODE_PRIVATE), abPath) 27 | blkvPrefsByFile(file, multiProcess = true) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | project(biliroaming) 3 | 4 | find_package(cxx REQUIRED CONFIG) 5 | link_libraries(cxx::cxx) 6 | 7 | add_subdirectory(dex_builder) 8 | 9 | add_library(${PROJECT_NAME} SHARED 10 | biliroaming.cc 11 | ) 12 | 13 | target_link_libraries(${PROJECT_NAME} PUBLIC log dex_builder_static) 14 | 15 | if (NOT DEFINED DEBUG_SYMBOLS_PATH) 16 | set(DEBUG_SYMBOLS_PATH ${CMAKE_BINARY_DIR}/symbols) 17 | endif() 18 | 19 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 20 | COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} 21 | COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ 22 | ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME} 23 | COMMAND ${CMAKE_STRIP} --strip-all $) 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/app/src/main/res/drawable/demo.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/demo2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/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/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/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 |