├── .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 |
22 |
23 |
30 |
31 |
41 |
42 |
48 |
49 |
59 |
60 |
66 |
67 |
77 |
78 |
84 |
85 |
95 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_color_choose.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
17 |
18 |
24 |
25 |
31 |
32 |
42 |
43 |
44 |
45 |
52 |
53 |
59 |
60 |
66 |
67 |
76 |
77 |
78 |
79 |
86 |
87 |
93 |
94 |
100 |
101 |
110 |
111 |
112 |
113 |
120 |
121 |
127 |
128 |
134 |
135 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/feature.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
16 |
17 |
21 |
22 |
28 |
29 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/search_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
25 |
26 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/seekbar_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/video_choose.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
17 |
18 |
25 |
26 |
33 |
34 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #303030
4 | #e66e90
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rTW/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 不取代
5 | - ali(阿里)
6 | - alib(阿里)
7 | - alio1(阿里)
8 | - bos(百度)
9 | - cos(騰訊)
10 | - cosb(騰訊)
11 | - coso1(騰訊)
12 | - hw(華為)
13 | - hwb(華為)
14 | - hwo1(華為)
15 | - 08c(華為)
16 | - 08h(華為)
17 | - 08ct(華為)
18 | - tf_hw(華為)
19 | - tf_tx(騰訊)
20 | - akamai(Akamai海外)
21 | - aliov(阿里海外)
22 | - cosov(騰訊海外)
23 | - hwov(華為海外)
24 | - hk_bcache(Bilibili海外)
25 |
26 |
27 |
28 | - 直播
29 | - 推薦
30 | - 熱門
31 | - 番劇
32 | - 影視
33 | - 韓綜
34 | - 其它
35 |
36 |
37 |
38 | - 廣告
39 | - 遊戲
40 | - 橫幅(輪播圖)
41 | - 通知(追番更新、活動提示)
42 | - 文章
43 | - 動態
44 | - 豎版影片
45 | - 直播
46 | - 內聯影片
47 | - 番劇、電影、電視劇、紀錄片……
48 | - 大卡(單列顯示的)
49 | - 中卡
50 | - 小卡(雙列顯示的)
51 |
52 |
53 |
54 | - 分頁:首頁
55 | - 分頁:動態
56 | - 分頁:投稿
57 | - 分頁:商品
58 | - 分頁:追番
59 | - 分頁:課堂
60 | - 直播
61 | - 充電
62 | - 大航海
63 | - 推廣櫥窗
64 | - 影片
65 | - 專欄
66 | - 音訊
67 | - 最近追番
68 | - 最近投幣
69 | - 最近按讚
70 | - 最近追漫
71 | - 玩的遊戲
72 | - 課堂
73 | - 粉絲裝扮
74 | - 收藏夾
75 | - comic list
76 | - ugc season list
77 | - 數字藏品展現
78 | - NFT大頭貼
79 |
80 |
81 |
82 | - UP主推薦廣告
83 | - 相關遊戲
84 |
85 |
86 |
87 | - 轉發
88 | - 投稿影片
89 | - 番劇、電影等
90 | - 付費內容 1
91 | - 付費內容 2
92 | - 摺疊
93 | - 文字
94 | - 圖文
95 | - 文章
96 | - 音訊
97 | - 通用 方形
98 | - 通用 豎形
99 | - 直播
100 | - 播放列表
101 | - 廣告
102 | - 小程式
103 | - 訂閱
104 | - 新訂閱
105 | - 直播推薦
106 | - 橫幅
107 | - 合集
108 | - 故事
109 | - 話題推薦
110 | - 付費更新
111 | - 話題集合
112 | - 通知
113 |
114 |
115 | - 購物卡片
116 | - 購物精選
117 | - 關注提醒
118 | - 直播預約
119 | - 投餵支持
120 | - 滾動橫幅
121 | - 電池任務
122 | - 正在去買
123 |
124 |
125 | - 預設
126 | - 跟隨全螢幕解析度
127 | - 240P
128 | - 360P
129 | - 480P
130 | - 720P
131 | - 720P 60幀
132 | - 1080P
133 | - 1080P 高碼率
134 | - 1080P 60幀
135 | - 4K
136 | - 8K
137 |
138 |
139 | - 預設
140 | - 240P
141 | - 360P
142 | - 480P
143 | - 720P
144 | - 720P 60幀
145 | - 1080P
146 | - 1080P 高碼率
147 | - 1080P 60幀
148 | - 4K
149 | - 8K
150 |
151 |
152 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rTW/arrays_x.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - 廣告
4 | - 直播
5 |
6 |
7 | - 評論
8 | - 動態
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rTW/strings_x.xml:
--------------------------------------------------------------------------------
1 |
2 | 字幕轉換錯誤回饋
3 | 說明
4 | 本模組與原版保持同步更新,並額外添加一些功能,這些功能帶有 X 字樣。\n內建使用可在應用內收到新的內建版更新。
5 | 啟用番劇字幕下載功能
6 | 需開啟解鎖番劇限制選項,此功能才會生效。功能入口:播放詳情頁右上角三點 -> 字幕下載。字幕載入完成後入口才能顯示。
7 | 匯入自製主題
8 | 重啟後,須等待十幾秒下載資源,然後再重啟
9 | 從json文件匯入
10 | 淨化輪播圖廣告
11 | 淨化推薦、追番、影視等版塊輪播圖廣告
12 | 文字摺疊控制
13 | 支持自訂評論及動態開始摺疊的文字行數
14 | 評論開始摺疊行數
15 | 動態開始摺疊行數
16 | 動態全文閱讀觸發行數
17 | %1$s行
18 | 去廣告雜項
19 | 移除搜尋推廣
20 | 移除評論頁頂部推廣橫條
21 | 修改我的大會員樣式
22 | 我的頁面將大會員樣式從橫幅修改為正常樣式
23 | 自訂播放速度列表
24 | 同時套用到影片及聽影片播放速度列表
25 | 空格分隔,例如:2 1.5 1,空表示預設
26 | 輸入無效!
27 | 必須包含預設倍速1!
28 | 重啟生效
29 | 預設播放速度
30 | 僅套用到影片播放
31 | 例如:1.5,空表示預設
32 | 長按播放速度
33 | 自訂播放器長按播放速度
34 | 封鎖播放結束時彈幕氛圍回饋
35 | 過濾故事模式影片
36 | 過濾故事模式(豎向)下的影片
37 | 解除螢幕方向鎖定
38 | 解除影片播放頁螢幕方向鎖定,允許系統根據目前裝置姿態顯示旋轉建議按鈕
39 | 記住無損設定開關狀態
40 | 記住播放器無損設定開關狀態
41 | 無限試用會員畫質
42 | 透過將影片串流訊息設定為限免達到無限試用會員畫質目的,目前只對大部分UP主投稿影片有效,啟用後針對非會員用戶投稿影片播放會強制使用舊版播放器
43 | 新增頻道按鈕
44 | 在底欄新增頻道按鈕(重啟兩次生效)
45 | 隱藏關注按鈕
46 | 隱藏評論、動態等右上角的關注按鈕
47 | 禁止自動訂閱合集
48 | 禁止收藏有合集的影片時自動勾選訂閱合集
49 | 強制使用舊版播放器
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays_x.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - 广告
4 | - 直播
5 |
6 |
7 | - ad
8 | - live
9 |
10 |
11 | - 评论
12 | - 动态
13 |
14 |
15 | - comment
16 | - dynamic
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #fff
4 | #fa6496
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_raw.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | yujincheng08/iAcn/djytw/
4 | https://api.github.com/repos/zjns/BiliRoamingX/releases
5 | https://github.com/zjns/BiliRoamingX/releases/tag/%1$s
6 | https://github.com/yujincheng08/BiliRoaming/wiki/%E5%85%AC%E5%85%B1%E8%A7%A3%E6%9E%90%E6%9C%8D%E5%8A%A1%E5%99%A8
7 | https://github.com/zjns/BiliRoamingX
8 | https://afdian.net/a/yujincheng08
9 | https://t.me/bb_show
10 | https://github.com/yujincheng08/BiliRoaming/wiki
11 | mqqguild://guild/share?inviteCode=NVoD5&from=246610
12 | upos-sz-mirrorbos.bilivideo.com
13 | upos-sz-mirrorcos.bilivideo.com
14 | upos-sz-mirrorcosb.bilivideo.com
15 | upos-sz-mirrorcoso1.bilivideo.com
16 | upos-sz-mirrorhw.bilivideo.com
17 | upos-sz-mirrorhwb.bilivideo.com
18 | upos-sz-mirrorhwo1.bilivideo.com
19 | upos-sz-mirror08c.bilivideo.com
20 | upos-sz-mirror08h.bilivideo.com
21 | upos-sz-mirror08ct.bilivideo.com
22 | upos-sz-mirrorali.bilivideo.com
23 | upos-sz-mirroralib.bilivideo.com
24 | upos-sz-mirroralio1.bilivideo.com
25 | upos-hz-mirrorakam.akamaized.net
26 | upos-sz-mirroraliov.bilivideo.com
27 | upos-sz-mirrorhwov.bilivideo.com
28 | upos-sz-mirrorcosov.bilivideo.com
29 | cn-hk-eq-bcache-01.bilivideo.com
30 | upos-tf-all-hw.bilivideo.com
31 | upos-tf-all-tx.bilivideo.com
32 | %sKB/s
33 | UPOS
34 | UID
35 | 字幕转换失败,请重试
36 | 转换字典下载失败,请重试
37 | 请注意,站内宣传漫游或脚本会被拉黑
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_x.xml:
--------------------------------------------------------------------------------
1 |
2 | 字幕转换错误反馈
3 | 说明
4 | 本模块与原版保持同步更新,并额外添加一些功能,这些功能带有 X 字样。\n内置使用可在应用内收到新的内置版更新。
5 | 启用字幕下载功能
6 | 需开启解锁番剧限制选项,此功能才会生效。功能入口:播放详情页右上角三点 -> 字幕下载。字幕加载完成后入口才能显示。
7 | 导入自制主题
8 | 重启后,须等待十几秒下载资源,然后再重启
9 | 从json文件导入
10 | 净化轮播图广告
11 | 净化推荐、追番、影视等版块轮播图广告
12 | 文本折叠控制
13 | 支持自定义评论及动态开始折叠的文本行数
14 | 评论开始折叠行数
15 | 动态开始折叠行数
16 | 动态全文阅读触发行数
17 | %1$s行
18 | 去广告杂项
19 | 移除搜索推广
20 | 移除评论页顶部推广横条
21 | 修改我的大会员样式
22 | 我的页面将大会员样式从横幅修改为正常样式
23 | 自定义播放速度列表
24 | 同时应用到视频及听视频播放速度列表
25 | 空格分隔,例如:2 1.5 1,空表示默认
26 | 输入无效!
27 | 必须包含默认倍速1!
28 | 重启生效
29 | 默认播放速度
30 | 仅应用到视频播放
31 | 例如:1.5,空表示默认
32 | 长按播放速度
33 | 自定义播放器长按播放速度
34 | 屏蔽播放结束时弹幕氛围反馈
35 | 过滤故事模式视频
36 | 过滤故事模式(竖屏)下的视频
37 | 解除屏幕方向锁定
38 | 解除视频播放页屏幕方向锁定,允许系统根据当前设备姿态显示旋转建议按钮
39 | 记住无损设置开关状态
40 | 记住播放器无损设置开关状态
41 | 无限试用会员画质
42 | 通过将视频流信息设置为限免达到无限试用会员画质目的,目前只对大部分UP主投稿视频有效,启用后针对非会员用户投稿视频播放会强制使用旧版播放器
43 | 添加频道按钮
44 | 在底栏添加频道按钮(重启两次生效)
45 | 隐藏关注按钮
46 | 隐藏评论、动态等右上角的关注按钮
47 | 禁止自动订阅合集
48 | 禁止收藏有合集的视频时自动勾选订阅合集
49 | 强制使用旧版播放器
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_x_raw.xml:
--------------------------------------------------------------------------------
1 |
2 | https://github.com/BBSub/ZhConvertDict
3 | https://api.github.com/repos/BBSub/ZhConvertDict/releases/latest
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
20 |
21 |
25 |
28 |
29 |
30 |
31 |
34 |
35 |
38 |
39 |
43 |
46 |
47 |
48 |
52 |
53 |
57 |
60 |
61 |
62 |
66 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/build.gradle.kts
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | # org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | org.gradle.jvmargs=-Xmx2g
15 | android.useAndroidX=true
16 | android.enableAppCompileTimeRClass=true
17 | android.experimental.enableNewResourceShrinker.preciseShrinking=true
18 |
19 | appVerName=1.6.12
20 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | protobuf = "3.24.0"
3 | coroutine = "1.7.3"
4 | kotlin = "1.9.0"
5 |
6 | [plugins]
7 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
8 | agp-app = { id = "com.android.application", version = "8.1.0" }
9 | protobuf = { id = "com.google.protobuf", version = "0.9.4" }
10 | lsplugin-jgit = { id = "org.lsposed.lsplugin.jgit", version = "1.1" }
11 | lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.5" }
12 | lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.1" }
13 | lsplugin-apktransform = { id = "org.lsposed.lsplugin.apktransform", version = "1.2" }
14 | lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version = "1.2" }
15 |
16 |
17 |
18 | [libraries]
19 | xposed = { module = "de.robv.android.xposed:api", version = "82" }
20 | cxx = { module = "dev.rikka.ndk.thirdparty:cxx", version = "1.2.0" }
21 | protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
22 | protobuf-java = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
23 | protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
24 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
25 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine" }
26 | kotlin-coroutines-jdk = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutine" }
27 | androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.0.1" }
28 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/imgs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/imgs/icon.png
--------------------------------------------------------------------------------
/imgs/stick1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/imgs/stick1.png
--------------------------------------------------------------------------------
/imgs/stick2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/imgs/stick2.png
--------------------------------------------------------------------------------
/imgs/stick3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/imgs/stick3.png
--------------------------------------------------------------------------------
/imgs/stick4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zjns/BiliRoamingX/01a0f53d8d1f7c31e5c65dc38674b579507f7daf/imgs/stick4.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":app")
2 | buildCache { local { removeUnusedEntriesAfterDays = 1 } }
3 | pluginManagement {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | gradlePluginPortal()
8 | }
9 | }
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | maven(url = "https://api.xposed.info")
16 | }
17 | }
18 | rootProject.name = "BiliRoaming"
19 |
--------------------------------------------------------------------------------