├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ ├── Purge.js
│ ├── native_init
│ └── xposed_init
│ ├── cpp
│ ├── CMakeLists.txt
│ ├── check
│ │ ├── anti-xposed.c
│ │ ├── anti-xposed.h
│ │ ├── art.h
│ │ ├── check.c
│ │ ├── check.h
│ │ ├── classloader.cpp
│ │ ├── classloader.h
│ │ ├── hash.c
│ │ ├── hash.h
│ │ ├── inline.c
│ │ ├── inline.h
│ │ ├── main.c
│ │ ├── main.h
│ │ ├── path.c
│ │ ├── path.h
│ │ ├── plt.c
│ │ └── plt.h
│ └── hide
│ │ ├── hide.cpp
│ │ └── hide.h
│ ├── java
│ └── gm
│ │ └── tieba
│ │ └── tabswitch
│ │ ├── Constants.kt
│ │ ├── XposedContext.kt
│ │ ├── XposedInit.kt
│ │ ├── dao
│ │ ├── AcRule.kt
│ │ ├── AcRules.kt
│ │ ├── Adp.kt
│ │ └── Preferences.kt
│ │ ├── hooker
│ │ ├── IHooker.kt
│ │ ├── Obfuscated.kt
│ │ ├── TSPreference.kt
│ │ ├── TSPreferenceHelper.kt
│ │ ├── add
│ │ │ ├── HistoryCache.kt
│ │ │ ├── Ripple.kt
│ │ │ ├── SaveImages.kt
│ │ │ └── SelectClipboard.kt
│ │ ├── auto
│ │ │ ├── AgreeNum.kt
│ │ │ ├── AutoSign.kt
│ │ │ ├── AutoSignHelper.kt
│ │ │ ├── FrsTab.kt
│ │ │ ├── MsgCenterTab.kt
│ │ │ ├── NotificationDetect.kt
│ │ │ ├── OpenSign.kt
│ │ │ ├── OriginSrc.kt
│ │ │ └── TransitionAnimation.kt
│ │ ├── deobfuscation
│ │ │ ├── Deobfuscation.kt
│ │ │ ├── DeobfuscationHelper.kt
│ │ │ ├── DeobfuscationHooker.kt
│ │ │ └── Matcher.kt
│ │ ├── eliminate
│ │ │ ├── ContentFilter.kt
│ │ │ ├── FoldTopCardView.kt
│ │ │ ├── FollowFilter.kt
│ │ │ ├── FragmentTab.kt
│ │ │ ├── FrsPageFilter.kt
│ │ │ ├── PersonalizedFilter.kt
│ │ │ ├── Purge.kt
│ │ │ ├── PurgeEnter.kt
│ │ │ ├── PurgeMy.kt
│ │ │ ├── PurgeVideo.kt
│ │ │ ├── RedTip.kt
│ │ │ ├── RegexFilter.kt
│ │ │ ├── RemoveUpdate.kt
│ │ │ └── UserFilter.kt
│ │ └── extra
│ │ │ ├── AutoRefresh.kt
│ │ │ ├── ForbidGesture.kt
│ │ │ ├── Hide.java
│ │ │ ├── LogRedirect.kt
│ │ │ ├── NativeCheck.java
│ │ │ ├── StackTrace.java
│ │ │ └── TraceChecker.java
│ │ ├── util
│ │ ├── DisplayUtils.kt
│ │ ├── FileUtils.kt
│ │ ├── Parser.kt
│ │ └── ReflectUtils.kt
│ │ └── widget
│ │ ├── NavigationBar.kt
│ │ ├── Switch.kt
│ │ └── TbToast.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ └── values
│ ├── arrays.xml
│ └── strings.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | if: ${{ !startsWith(github.event.head_commit.message, '[skip ci]') }}
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4.1.1
17 | with:
18 | submodules: 'recursive'
19 | fetch-depth: 0
20 |
21 | - name: Setup JDK 17
22 | uses: actions/setup-java@v4.0.0
23 | with:
24 | java-version: 17
25 | distribution: 'temurin'
26 |
27 | - name: Build with Gradle
28 | run: bash ./gradlew assembleRelease
29 |
30 | - name: Sign Android release
31 | if: success()
32 | id: sign
33 | uses: r0adkll/sign-android-release@v1
34 | env:
35 | BUILD_TOOLS_VERSION: "33.0.2"
36 | with:
37 | releaseDirectory: app/build/outputs/apk/release
38 | signingKeyBase64: ${{ secrets.SIGNING_KEY }}
39 | alias: ${{ secrets.ALIAS }}
40 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
41 | keyPassword: ${{ secrets.KEY_PASSWORD }}
42 |
43 | - name: Retrieve filename
44 | if: success()
45 | run: echo "FILENAME=$(basename ${{ steps.sign.outputs.signedReleaseFile }})" >> $GITHUB_ENV
46 | shell: bash
47 |
48 | - name: Upload built apk
49 | if: success()
50 | id: upload
51 | uses: actions/upload-artifact@v4
52 | with:
53 | name: ${{ env.FILENAME }}
54 | path: ${{ steps.sign.outputs.signedReleaseFile }}
55 |
56 | - name: Write job summary
57 | if: success()
58 | run: echo "### [下载链接](${{ steps.upload.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
59 |
60 | - name: Send commit to telegram
61 | uses: appleboy/telegram-action@master
62 | with:
63 | to: ${{ secrets.TELEGRAM_TO }}
64 | token: ${{ secrets.TELEGRAM_TOKEN }}
65 | format: markdown
66 | message: |+
67 | New push to github!
68 | *${{ github.event.head_commit.message }}* by ${{ github.event.head_commit.author.name }}
69 | See commit detail [here](${{ github.event.head_commit.url }})
70 | Snapshot apk is attached
71 | document: ${{ github.workspace }}/${{ steps.sign.outputs.signedReleaseFile }}
72 |
73 | skipped:
74 | runs-on: ubuntu-latest
75 | if: ${{ startsWith(github.event.head_commit.message, '[skip ci]') }}
76 | steps:
77 | - name: Send commit to telegram
78 | uses: appleboy/telegram-action@master
79 | with:
80 | to: ${{ secrets.TELEGRAM_TO }}
81 | token: ${{ secrets.TELEGRAM_TOKEN }}
82 | format: markdown
83 | message: |+
84 | New push to github!
85 | *${{ github.event.head_commit.message }}* by ${{ github.event.head_commit.author.name }}
86 | See commit detail [here](${{ github.event.head_commit.url }})
87 | This push skipped building
88 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | build
4 | local.properties
5 | app/.cxx
6 | app/gm.jks
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TiebaTS
2 |
3 | 提供修改百度贴吧底栏等个性化功能。An Xposed module for Baidu Tieba with personalized functions.
4 |
5 | [](https://github.com/GuhDoy/TiebaTS/actions)
6 | [](https://t.me/TabSwitch)
7 | [](https://github.com/GuhDoy/TiebaTS)
8 | [](https://github.com/GuhDoy/TiebaTS/releases/latest)
9 |
10 | ## 功能
11 |
12 | - 自定义主页导航栏
13 | - 禁用 Flutter
14 | - 净化进吧
15 | - 净化我的
16 | - 隐藏小红点
17 | - 过滤首页推荐
18 | - 过滤帖子回复
19 | - 过滤吧页面
20 | - 进吧增加收藏、历史
21 | - 我的收藏增加搜索、吧名
22 | - 浏览历史增加搜索
23 | - 搜索楼中楼增加查看主题贴
24 | - 楼层增加点按效果
25 | - 长按下载保存全部图片
26 | - 备注关注的人
27 | - 自动签到
28 | - 自动切换夜间模式
29 | - 吧页面起始页面改为最新
30 | - 自动查看原图
31 | - 使用媒体存储保存图片
32 | - 禁用帖子手势
33 | - 用赞踩差数代替赞数
34 | - 禁止监听内部图片内容变化
35 |
36 | ## 项目地址
37 |
38 | [https://github.com/GuhDoy/TiebaTS](https://github.com/GuhDoy/TiebaTS)
39 |
40 | ## 特别鸣谢
41 |
42 | 详见[LicenseActivity.java](https://github.com/GuhDoy/TiebaTS/blob/full/app/src/main/java/gm/tieba/tabswitch/ui/LicenseActivity.java)
43 |
44 | ## 下载
45 |
46 | [Github Release](https://github.com/GuhDoy/TiebaTS/releases/latest)
47 |
48 | ## 协议
49 |
50 | 复制或参考hook规则用于开发百度贴吧模块需遵守[GNU General Public Licence, version 3](https://choosealicense.com/licenses/gpl-3.0/)
51 |
52 | 复制或参考其它代码用于开发其它模块或软件需遵守[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html)
53 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.ByteArrayOutputStream
2 |
3 | plugins {
4 | id("com.android.application")
5 | id("org.jetbrains.kotlin.android")
6 | id("com.google.devtools.ksp")
7 | }
8 |
9 | fun String.runCommand(currentWorkingDir: File = file("./")): String {
10 | val byteOut = ByteArrayOutputStream()
11 | project.exec {
12 | workingDir = currentWorkingDir
13 | commandLine = this@runCommand.split("\\s".toRegex())
14 | standardOutput = byteOut
15 | }
16 | return String(byteOut.toByteArray()).trim()
17 | }
18 |
19 | val gitCommitCount = "git rev-list --count HEAD".runCommand().toInt()
20 | val latestTag = "git describe --abbrev=0 --tags".runCommand()
21 | val commitCountSinceLatestTag = ("git rev-list --count $latestTag..HEAD").runCommand()
22 | val sdk = 34
23 |
24 | android {
25 | compileSdk = sdk
26 | buildToolsVersion = "34.0.0"
27 | ndkVersion = "26.0.10792818"
28 |
29 | defaultConfig {
30 | applicationId = "gm.tieba.tabswitch"
31 | minSdk = 28
32 | targetSdk = sdk
33 | versionCode = gitCommitCount
34 | versionName = "3.0.3-beta"
35 | if (versionName!!.contains("alpha") || versionName!!.contains("beta")) {
36 | versionNameSuffix = ".$commitCountSinceLatestTag"
37 | }
38 | buildConfigField("String", "TARGET_VERSION", "\"12.74.1.1\"")
39 | buildConfigField("String", "MIN_VERSION", "\"12.53.1.0\"")
40 |
41 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
42 | externalNativeBuild {
43 | cmake {
44 | abiFilters("arm64-v8a")
45 | arguments("-DANDROID_STL=none")
46 | }
47 | }
48 | }
49 | applicationVariants.all {
50 | outputs
51 | .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
52 | .all { output ->
53 | output.outputFileName = "TS_${defaultConfig.versionName}${defaultConfig.versionNameSuffix ?: ""}_${name}.apk"
54 | false
55 | }
56 | }
57 | buildTypes {
58 | release {
59 | isMinifyEnabled = true
60 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
61 | }
62 | }
63 | compileOptions {
64 | sourceCompatibility = JavaVersion.VERSION_17
65 | targetCompatibility = JavaVersion.VERSION_17
66 | }
67 | externalNativeBuild {
68 | cmake {
69 | path("src/main/cpp/CMakeLists.txt")
70 | }
71 | }
72 | packaging {
73 | resources.excludes.addAll(listOf("/META-INF/**", "/kotlin/**", "/okhttp3/**"))
74 | jniLibs.excludes.addAll(listOf("**/liblog.so", "/lib/x86/**", "/lib/x86_64/**"))
75 | }
76 | buildFeatures {
77 | prefab = true
78 | buildConfig = true
79 | }
80 | lint {
81 | checkDependencies = true
82 | }
83 | namespace = "gm.tieba.tabswitch"
84 | }
85 |
86 | dependencies {
87 | compileOnly("de.robv.android.xposed:api:82")
88 | api("androidx.annotation:annotation:1.7.1")
89 |
90 | val roomVersion = "2.6.1"
91 | implementation("androidx.room:room-runtime:$roomVersion")
92 | annotationProcessor("androidx.room:room-compiler:$roomVersion")
93 | implementation("androidx.room:room-ktx:$roomVersion")
94 | ksp("androidx.room:room-compiler:$roomVersion")
95 |
96 | implementation("org.luckypray:dexkit:2.0.1")
97 | implementation("com.squareup.okhttp3:okhttp:4.12.0")
98 | implementation("dev.rikka.ndk.thirdparty:cxx:1.2.0")
99 | }
100 |
101 | val adbExecutable: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath
102 |
103 | tasks.register("restartTieba") {
104 | doLast {
105 | exec {
106 | commandLine(adbExecutable, "shell", "am", "force-stop", "com.baidu.tieba")
107 | }
108 | exec {
109 | commandLine(adbExecutable, "shell", "am", "start", "$(pm resolve-activity --components com.baidu.tieba)")
110 | }
111 | }
112 | }
113 |
114 | afterEvaluate {
115 | tasks.named("installDebug").configure {
116 | finalizedBy(tasks.named("restartTieba"))
117 | }
118 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -allowaccessmodification
2 | -overloadaggressively
3 |
4 | -keep class gm.tieba.tabswitch.XposedInit
5 |
6 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics {
7 | public static void check*(...);
8 | public static void throw*(...);
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
13 |
16 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/assets/Purge.js:
--------------------------------------------------------------------------------
1 | (function (send) {
2 | XMLHttpRequest.prototype.send = function () {
3 | var callback = this.onreadystatechange;
4 | this.onreadystatechange = function () {
5 | if (this.readyState == 4) {
6 | let propertiesToDelete = [];
7 | if (
8 | // 吧页面更多板块
9 | this.responseURL.match(
10 | /https?:\/\/tieba\.baidu\.com\/c\/f\/frs\/frsBottom.*/g
11 | )
12 | ) {
13 | propertiesToDelete = [
14 | "frs_bottom",
15 | "activityhead",
16 | "live_fuse_forum",
17 | "card_activity",
18 | "ai_chatroom_guide",
19 | "friend_forum",
20 | "game_card_guide",
21 | "area_data",
22 | ];
23 | } else if (
24 | // 一键签到页面
25 | this.responseURL.match(
26 | /https?:\/\/tieba\.baidu\.com\/c\/f\/forum\/getforumlist.*/g
27 | )
28 | ) {
29 | propertiesToDelete = ["advert"];
30 | }
31 | if (propertiesToDelete.length > 0) {
32 | res = JSON.parse(this.response);
33 | propertiesToDelete.forEach((property) => {
34 | delete res[property];
35 | });
36 | Object.defineProperty(this, "response", { writable: true });
37 | Object.defineProperty(this, "responseText", {
38 | writable: true,
39 | });
40 | this.response = this.responseText = JSON.stringify(res);
41 | }
42 | }
43 | if (callback) {
44 | callback.apply(this, arguments);
45 | }
46 | };
47 | send.apply(this, arguments);
48 | };
49 | })(XMLHttpRequest.prototype.send);
50 |
--------------------------------------------------------------------------------
/app/src/main/assets/native_init:
--------------------------------------------------------------------------------
1 | libhide.so
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | gm.tieba.tabswitch.XposedInit
--------------------------------------------------------------------------------
/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.18.1)
2 | project(TS)
3 | set(CMAKE_CXX_STANDARD 20)
4 |
5 | set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics")
6 | set(LINKER_FLAGS "-Wl,--hash-style=both")
7 |
8 | if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
9 | message("Builing Release...")
10 |
11 | set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden")
12 | set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections")
13 | elseif (CMAKE_BUILD_TYPE STREQUAL "Debug")
14 | message("Builing Debug...")
15 |
16 | add_definitions(-DDEBUG)
17 | endif ()
18 |
19 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}")
20 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}")
21 |
22 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}")
23 | set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}")
24 |
25 | find_package(cxx REQUIRED CONFIG)
26 | find_library(log-lib log)
27 |
28 | aux_source_directory(check CHECK)
29 | add_library(check SHARED ${CHECK})
30 | target_link_libraries(check ${log-lib} cxx::cxx)
31 |
32 | aux_source_directory(hide HIDE)
33 | add_library(hide SHARED ${HIDE})
34 | target_link_libraries(hide ${log-lib} cxx::cxx)
35 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/anti-xposed.c:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019/3/7.
3 | //
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include "anti-xposed.h"
12 | #include "plt.h"
13 |
14 | #ifndef NELEM
15 | #define NELEM(x) (sizeof(x) / sizeof((x)[0]))
16 | #endif
17 |
18 | #define likely(x) __builtin_expect(!!(x), 1)
19 |
20 | static jclass originalXposedClass;
21 |
22 | static inline void fill_java_lang_VMClassLoader(char v[]) {
23 | // java/lang/VMClassLoader
24 | static unsigned int m = 0;
25 |
26 | if (m == 0) {
27 | m = 19;
28 | } else if (m == 23) {
29 | m = 29;
30 | }
31 |
32 | v[0x0] = 'n';
33 | v[0x1] = 'd';
34 | v[0x2] = 'p';
35 | v[0x3] = 'f';
36 | v[0x4] = '\'';
37 | v[0x5] = 'e';
38 | v[0x6] = 'k';
39 | v[0x7] = 'e';
40 | v[0x8] = 'k';
41 | v[0x9] = '"';
42 | v[0xa] = 'X';
43 | v[0xb] = 'B';
44 | v[0xc] = 'S';
45 | v[0xd] = '}';
46 | v[0xe] = 's';
47 | v[0xf] = 's';
48 | v[0x10] = 'r';
49 | v[0x11] = 'N';
50 | v[0x12] = 'l';
51 | v[0x13] = 'e';
52 | v[0x14] = 'a';
53 | v[0x15] = 'c';
54 | v[0x16] = 'u';
55 | for (unsigned int i = 0; i < 0x17; ++i) {
56 | v[i] ^= ((i + 0x17) % m);
57 | }
58 | v[0x17] = '\0';
59 | }
60 |
61 | static inline void fill_de_robv_android_xposed_XposedBridge(char v[]) {
62 | // de/robv/android/xposed/XposedBridge
63 | static unsigned int m = 0;
64 |
65 | if (m == 0) {
66 | m = 31;
67 | } else if (m == 37) {
68 | m = 41;
69 | }
70 |
71 | v[0x0] = '`';
72 | v[0x1] = '`';
73 | v[0x2] = ')';
74 | v[0x3] = 'u';
75 | v[0x4] = 'g';
76 | v[0x5] = 'k';
77 | v[0x6] = '|';
78 | v[0x7] = '$';
79 | v[0x8] = 'm';
80 | v[0x9] = 'c';
81 | v[0xa] = 'j';
82 | v[0xb] = '}';
83 | v[0xc] = '\x7f';
84 | v[0xd] = 'x';
85 | v[0xe] = 'v';
86 | v[0xf] = '<';
87 | v[0x10] = 'l';
88 | v[0x11] = 'e';
89 | v[0x12] = 'y';
90 | v[0x13] = 'd';
91 | v[0x14] = '}';
92 | v[0x15] = '}';
93 | v[0x16] = '5';
94 | v[0x17] = 'C';
95 | v[0x18] = 'l';
96 | v[0x19] = 'r';
97 | v[0x1a] = 'm';
98 | v[0x1b] = 'e';
99 | v[0x1c] = 'e';
100 | v[0x1d] = '@';
101 | v[0x1e] = 'q';
102 | v[0x1f] = 'm';
103 | v[0x20] = 'a';
104 | v[0x21] = 'a';
105 | v[0x22] = 'b';
106 | for (unsigned int i = 0; i < 0x23; ++i) {
107 | v[i] ^= ((i + 0x23) % m);
108 | }
109 | v[0x23] = '\0';
110 | }
111 |
112 | static inline void fill_findLoadedClass(char v[]) {
113 | // findLoadedClass
114 | static unsigned int m = 0;
115 |
116 | if (m == 0) {
117 | m = 13;
118 | } else if (m == 17) {
119 | m = 19;
120 | }
121 |
122 | v[0x0] = 'd';
123 | v[0x1] = 'j';
124 | v[0x2] = 'j';
125 | v[0x3] = 'a';
126 | v[0x4] = 'J';
127 | v[0x5] = 'h';
128 | v[0x6] = 'i';
129 | v[0x7] = 'm';
130 | v[0x8] = 'o';
131 | v[0x9] = 'o';
132 | v[0xa] = 'O';
133 | v[0xb] = 'l';
134 | v[0xc] = '`';
135 | v[0xd] = 'q';
136 | v[0xe] = 'p';
137 | for (unsigned int i = 0; i < 0xf; ++i) {
138 | v[i] ^= ((i + 0xf) % m);
139 | }
140 | v[0xf] = '\0';
141 | }
142 |
143 | static inline void fill_findLoadedClass_signature(char v[]) {
144 | // (Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;
145 | static unsigned int m = 0;
146 |
147 | if (m == 0) {
148 | m = 59;
149 | } else if (m == 61) {
150 | m = 67;
151 | }
152 |
153 | v[0x0] = ')';
154 | v[0x1] = 'N';
155 | v[0x2] = 'i';
156 | v[0x3] = 'e';
157 | v[0x4] = 's';
158 | v[0x5] = 'g';
159 | v[0x6] = '(';
160 | v[0x7] = 'd';
161 | v[0x8] = 'h';
162 | v[0x9] = 'd';
163 | v[0xa] = 'l';
164 | v[0xb] = '#';
165 | v[0xc] = 'N';
166 | v[0xd] = 'b';
167 | v[0xe] = 'n';
168 | v[0xf] = 'c';
169 | v[0x10] = 'b';
170 | v[0x11] = '^';
171 | v[0x12] = '|';
172 | v[0x13] = 'u';
173 | v[0x14] = 'q';
174 | v[0x15] = 's';
175 | v[0x16] = 'e';
176 | v[0x17] = '#';
177 | v[0x18] = 'U';
178 | v[0x19] = 'p';
179 | v[0x1a] = 'z';
180 | v[0x1b] = 'j';
181 | v[0x1c] = '|';
182 | v[0x1d] = '1';
183 | v[0x1e] = 's';
184 | v[0x1f] = 'A';
185 | v[0x20] = 'O';
186 | v[0x21] = 'E';
187 | v[0x22] = '\x0c';
188 | v[0x23] = 'w';
189 | v[0x24] = 'Q';
190 | v[0x25] = 'T';
191 | v[0x26] = 'N';
192 | v[0x27] = 'F';
193 | v[0x28] = 'N';
194 | v[0x29] = '\x11';
195 | v[0x2a] = '\x02';
196 | v[0x2b] = '`';
197 | v[0x2c] = 'G';
198 | v[0x2d] = 'O';
199 | v[0x2e] = 'Y';
200 | v[0x2f] = 'Q';
201 | v[0x30] = '\x1e';
202 | v[0x31] = '^';
203 | v[0x32] = 'R';
204 | v[0x33] = 'Z';
205 | v[0x34] = 'R';
206 | v[0x35] = '\x19';
207 | v[0x36] = 't';
208 | v[0x37] = 'T';
209 | v[0x38] = 'X';
210 | v[0x39] = 'I';
211 | v[0x3a] = 's';
212 | v[0x3b] = ':';
213 | for (unsigned int i = 0; i < 0x3c; ++i) {
214 | v[i] ^= ((i + 0x3c) % m);
215 | }
216 | v[0x3c] = '\0';
217 | }
218 |
219 | static inline void fill_invokeOriginalMethodNative_signature(char v[]) {
220 | // (Ljava/lang/reflect/Member;I[Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
221 | static unsigned int m = 0;
222 |
223 | if (m == 0) {
224 | m = 113;
225 | } else if (m == 127) {
226 | m = 131;
227 | }
228 |
229 | v[0x0] = '.';
230 | v[0x1] = 'K';
231 | v[0x2] = 'b';
232 | v[0x3] = 'h';
233 | v[0x4] = '|';
234 | v[0x5] = 'j';
235 | v[0x6] = '#';
236 | v[0x7] = 'a';
237 | v[0x8] = 'o';
238 | v[0x9] = 'a';
239 | v[0xa] = 'w';
240 | v[0xb] = '>';
241 | v[0xc] = '`';
242 | v[0xd] = 'v';
243 | v[0xe] = 'r';
244 | v[0xf] = 'y';
245 | v[0x10] = 's';
246 | v[0x11] = 't';
247 | v[0x12] = 'l';
248 | v[0x13] = '6';
249 | v[0x14] = 'W';
250 | v[0x15] = '~';
251 | v[0x16] = 'q';
252 | v[0x17] = '\x7f';
253 | v[0x18] = '{';
254 | v[0x19] = 'm';
255 | v[0x1a] = '\x1b';
256 | v[0x1b] = 'h';
257 | v[0x1c] = 'y';
258 | v[0x1d] = 'o';
259 | v[0x1e] = 'N';
260 | v[0x1f] = 'D';
261 | v[0x20] = 'P';
262 | v[0x21] = 'F';
263 | v[0x22] = '\x07';
264 | v[0x23] = 'E';
265 | v[0x24] = 'K';
266 | v[0x25] = 'E';
267 | v[0x26] = 'K';
268 | v[0x27] = '\x02';
269 | v[0x28] = 'm';
270 | v[0x29] = 'C';
271 | v[0x2a] = 'Q';
272 | v[0x2b] = 'B';
273 | v[0x2c] = 'A';
274 | v[0x2d] = '\x08';
275 | v[0x2e] = 'x';
276 | v[0x2f] = '_';
277 | v[0x30] = 'W';
278 | v[0x31] = 'A';
279 | v[0x32] = 'Y';
280 | v[0x33] = '\x16';
281 | v[0x34] = 'V';
282 | v[0x35] = 'Z';
283 | v[0x36] = 'R';
284 | v[0x37] = 'Z';
285 | v[0x38] = '\x11';
286 | v[0x39] = '|';
287 | v[0x3a] = ',';
288 | v[0x3b] = ' ';
289 | v[0x3c] = '1';
290 | v[0x3d] = '0';
291 | v[0x3e] = '\x7f';
292 | v[0x3f] = '\t';
293 | v[0x40] = ',';
294 | v[0x41] = '&';
295 | v[0x42] = '>';
296 | v[0x43] = '(';
297 | v[0x44] = 'e';
298 | v[0x45] = '\'';
299 | v[0x46] = '-';
300 | v[0x47] = '#';
301 | v[0x48] = ')';
302 | v[0x49] = '`';
303 | v[0x4a] = '\x1f';
304 | v[0x4b] = '3';
305 | v[0x4c] = '8';
306 | v[0x4d] = '6';
307 | v[0x4e] = '7';
308 | v[0x4f] = '!';
309 | v[0x50] = 'm';
310 | v[0x51] = '\x0c';
311 | v[0x52] = '\x14';
312 | v[0x53] = '3';
313 | v[0x54] = ';';
314 | v[0x55] = '-';
315 | v[0x56] = '=';
316 | v[0x57] = 'r';
317 | v[0x58] = '2';
318 | v[0x59] = '>';
319 | v[0x5a] = '\x0e';
320 | v[0x5b] = '\x06';
321 | v[0x5c] = 'M';
322 | v[0x5d] = ',';
323 | v[0x5e] = '\x06';
324 | v[0x5f] = '\x0f';
325 | v[0x60] = '\x03';
326 | v[0x61] = '\x04';
327 | v[0x62] = '\x1c';
328 | v[0x63] = 'R';
329 | v[0x64] = 'C';
330 | v[0x65] = '\'';
331 | v[0x66] = '\x06';
332 | v[0x67] = '\x0c';
333 | v[0x68] = '\x18';
334 | v[0x69] = '\x0e';
335 | v[0x6a] = '_';
336 | v[0x6b] = 'l';
337 | v[0x6c] = '`';
338 | v[0x6d] = 'l';
339 | v[0x6e] = 'd';
340 | v[0x6f] = '+';
341 | v[0x70] = 'J';
342 | v[0x71] = 'd';
343 | v[0x72] = 'm';
344 | v[0x73] = 'm';
345 | v[0x74] = 'j';
346 | v[0x75] = '~';
347 | v[0x76] = '0';
348 | for (unsigned int i = 0; i < 0x77; ++i) {
349 | v[i] ^= ((i + 0x77) % m);
350 | }
351 | v[0x77] = '\0';
352 | }
353 |
354 | jclass findLoadedClass(JNIEnv *env, jobject classLoader, const char *name) {
355 | char v1[0x80], v2[0x80];
356 | jclass loadedClass = NULL;
357 |
358 | fill_java_lang_VMClassLoader(v1);
359 | jclass vmClassLoader = (*env)->FindClass(env, v1);
360 | if ((*env)->ExceptionCheck(env)) {
361 | (*env)->ExceptionClear(env);
362 | }
363 | if (vmClassLoader == NULL) {
364 | goto clean;
365 | }
366 |
367 | fill_findLoadedClass(v1);
368 | fill_findLoadedClass_signature(v2);
369 | jmethodID findLoadedClass = (*env)->GetStaticMethodID(env, vmClassLoader, v1, v2);
370 | if ((*env)->ExceptionCheck(env)) {
371 | (*env)->ExceptionClear(env);
372 | }
373 | if (findLoadedClass == NULL) {
374 | goto cleanVmClassLoader;
375 | }
376 |
377 | jstring string = (*env)->NewStringUTF(env, name);
378 | loadedClass = (jclass) (*env)->CallStaticObjectMethod(env,
379 | vmClassLoader,
380 | findLoadedClass,
381 | classLoader,
382 | string);
383 |
384 | if ((*env)->ExceptionCheck(env)) {
385 | (*env)->ExceptionClear(env);
386 | }
387 |
388 | (*env)->DeleteLocalRef(env, string);
389 | cleanVmClassLoader:
390 | (*env)->DeleteLocalRef(env, vmClassLoader);
391 | clean:
392 | return loadedClass;
393 | }
394 |
395 | jclass findXposedBridge(JNIEnv *env, jobject classLoader) {
396 | char v1[0x80];
397 | fill_de_robv_android_xposed_XposedBridge(v1);
398 | return findLoadedClass(env, classLoader, v1);
399 | }
400 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/anti-xposed.h:
--------------------------------------------------------------------------------
1 | #ifndef BREVENT_ANTI_XPOSED_H
2 | #define BREVENT_ANTI_XPOSED_H
3 |
4 | #ifdef __cplusplus
5 | extern "C" {
6 | #endif
7 |
8 | #include
9 |
10 | jclass findXposedBridge(JNIEnv *env, jobject classLoader);
11 |
12 | jclass findLoadedClass(JNIEnv *env, jobject classLoader, const char *name);
13 |
14 | #ifdef __cplusplus
15 | }
16 | #endif
17 |
18 | #endif //BREVENT_ANTI_XPOSED_H
19 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/art.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019-09-15.
3 | //
4 |
5 | #ifndef BREVENT_ART_H
6 | #define BREVENT_ART_H
7 |
8 | #include
9 |
10 | #ifndef NDEBUG
11 | #define ALWAYS_INLINE
12 | #else
13 | #define ALWAYS_INLINE __attribute__ ((always_inline))
14 | #endif
15 |
16 | #define DCHECK(...)
17 |
18 | #define OVERRIDE override
19 |
20 | #define ATTRIBUTE_UNUSED __attribute__((__unused__))
21 |
22 | #define REQUIRES_SHARED(...)
23 |
24 | #define MANAGED PACKED(4)
25 | #define PACKED(x) __attribute__ ((__aligned__(x), __packed__))
26 |
27 | namespace art {
28 |
29 | namespace mirror {
30 |
31 | class Object {
32 |
33 | };
34 |
35 | template
36 | class ObjPtr {
37 |
38 | public:
39 | MirrorType *Ptr() const {
40 | return nullptr;
41 | }
42 |
43 | };
44 |
45 | template
46 | class PtrCompression {
47 | public:
48 | // Compress reference to its bit representation.
49 | static uint32_t Compress(MirrorType *mirror_ptr) {
50 | uintptr_t as_bits = reinterpret_cast(mirror_ptr);
51 | return static_cast(kPoisonReferences ? -as_bits : as_bits);
52 | }
53 |
54 | // Uncompress an encoded reference from its bit representation.
55 | static MirrorType *Decompress(uint32_t ref) {
56 | uintptr_t as_bits = kPoisonReferences ? -ref : ref;
57 | return reinterpret_cast(as_bits);
58 | }
59 |
60 | // Convert an ObjPtr to a compressed reference.
61 | static uint32_t Compress(ObjPtr ptr) REQUIRES_SHARED(Locks::mutator_lock_) {
62 | return Compress(ptr.Ptr());
63 | }
64 | };
65 |
66 |
67 | // Value type representing a reference to a mirror::Object of type MirrorType.
68 | template
69 | class MANAGED ObjectReference {
70 | private:
71 | using Compression = PtrCompression;
72 |
73 | public:
74 | MirrorType *AsMirrorPtr() const {
75 | return Compression::Decompress(reference_);
76 | }
77 |
78 | void Assign(MirrorType *other) {
79 | reference_ = Compression::Compress(other);
80 | }
81 |
82 | void Assign(ObjPtr ptr) REQUIRES_SHARED(Locks::mutator_lock_);
83 |
84 | void Clear() {
85 | reference_ = 0;
86 | DCHECK(IsNull());
87 | }
88 |
89 | bool IsNull() const {
90 | return reference_ == 0;
91 | }
92 |
93 | uint32_t AsVRegValue() const {
94 | return reference_;
95 | }
96 |
97 | static ObjectReference
98 | FromMirrorPtr(MirrorType *mirror_ptr)
99 | REQUIRES_SHARED(Locks::mutator_lock_) {
100 | return ObjectReference(mirror_ptr);
101 | }
102 |
103 | protected:
104 | explicit ObjectReference(MirrorType *mirror_ptr) REQUIRES_SHARED(Locks::mutator_lock_)
105 | : reference_(Compression::Compress(mirror_ptr)) {
106 | }
107 |
108 | // The encoded reference to a mirror::Object.
109 | uint32_t reference_;
110 | };
111 |
112 | // Standard compressed reference used in the runtime. Used for StackReference and GC roots.
113 | template
114 | class MANAGED CompressedReference : public mirror::ObjectReference {
115 | public:
116 | CompressedReference() REQUIRES_SHARED(Locks::mutator_lock_)
117 | : mirror::ObjectReference(nullptr) {}
118 |
119 | static CompressedReference FromMirrorPtr(MirrorType *p)
120 | REQUIRES_SHARED(Locks::mutator_lock_) {
121 | return CompressedReference(p);
122 | }
123 |
124 | private:
125 | explicit CompressedReference(MirrorType *p) REQUIRES_SHARED(Locks::mutator_lock_)
126 | : mirror::ObjectReference(p) {}
127 | };
128 | }
129 |
130 | class RootInfo {
131 |
132 | };
133 |
134 | class RootVisitor {
135 | public:
136 | virtual ~RootVisitor() {}
137 |
138 | // Single root version, not overridable.
139 | ALWAYS_INLINE void VisitRoot(mirror::Object **root, const RootInfo &info)
140 | REQUIRES_SHARED(Locks::mutator_lock_) {
141 | VisitRoots(&root, 1, info);
142 | }
143 |
144 | // Single root version, not overridable.
145 | ALWAYS_INLINE void VisitRootIfNonNull(mirror::Object **root, const RootInfo &info)
146 | REQUIRES_SHARED(Locks::mutator_lock_) {
147 | if (*root != nullptr) {
148 | VisitRoot(root, info);
149 | }
150 | }
151 |
152 | virtual void VisitRoots(mirror::Object ***roots, size_t count, const RootInfo &info)
153 | REQUIRES_SHARED(Locks::mutator_lock_) = 0;
154 |
155 | virtual void VisitRoots(mirror::CompressedReference **roots, size_t count,
156 | const RootInfo &info)
157 | REQUIRES_SHARED(Locks::mutator_lock_) = 0;
158 | };
159 |
160 | // Only visits roots one at a time, doesn't handle updating roots. Used when performance isn't
161 | // critical.
162 | class SingleRootVisitor : public RootVisitor {
163 | private:
164 | void VisitRoots(mirror::Object ***roots, size_t count, const RootInfo &info) OVERRIDE
165 | REQUIRES_SHARED(Locks::mutator_lock_) {
166 | for (size_t i = 0; i < count; ++i) {
167 | VisitRoot(*roots[i], info);
168 | }
169 | }
170 |
171 | void VisitRoots(mirror::CompressedReference **roots, size_t count,
172 | const RootInfo &info) OVERRIDE
173 | REQUIRES_SHARED(Locks::mutator_lock_) {
174 | for (size_t i = 0; i < count; ++i) {
175 | VisitRoot(roots[i]->AsMirrorPtr(), info);
176 | }
177 | }
178 |
179 | virtual void VisitRoot(mirror::Object *root, const RootInfo &info) = 0;
180 | };
181 |
182 | class IsMarkedVisitor {
183 | public:
184 | virtual ~IsMarkedVisitor() {}
185 |
186 | // Return null if an object is not marked, otherwise returns the new address of that object.
187 | // May return the same address as the input if the object did not move.
188 | virtual mirror::Object *IsMarked(mirror::Object *obj) = 0;
189 | };
190 |
191 | }
192 |
193 | #endif //BREVENT_ART_H
194 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/check.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include "check.h"
9 | #include "classloader.h"
10 | #include "inline.h"
11 | #include "plt.h"
12 |
13 | static bool check_hook_function(void *handle, const char *name) {
14 | void *symbol = dlsym(handle, name);
15 | if (symbol != NULL && setRead(symbol) && isInlineHooked(symbol)) {
16 | return true;
17 | }
18 | return false;
19 | }
20 |
21 | #define HOOK_SYMBOL(x, y) check_hook_function(x, y)
22 |
23 | jboolean _inline(JNIEnv *env, jclass clazz, jstring jname) {
24 | const char *name = (*env)->GetStringUTFChars(env, jname, NULL);
25 | void *handle = dlopen("libc.so", RTLD_NOW);
26 | bool isInlineHooked = false;
27 | if (handle) isInlineHooked = HOOK_SYMBOL(handle, name);
28 | dlclose(handle);
29 | (*env)->ReleaseStringUTFChars(env, jname, name);
30 | return isInlineHooked;
31 | }
32 |
33 | jboolean FindClass_inline(JNIEnv *env, jclass clazz) {
34 | void *symbol = (*env)->FindClass;
35 | return isInlineHooked(symbol);
36 | }
37 |
38 | static inline void fill_ro_build_version_sdk(char v[]) {
39 | // ro.build.version.sdk
40 | static unsigned int m = 0;
41 |
42 | if (m == 0) {
43 | m = 19;
44 | } else if (m == 23) {
45 | m = 29;
46 | }
47 |
48 | v[0x0] = 's';
49 | v[0x1] = 'm';
50 | v[0x2] = '-';
51 | v[0x3] = 'f';
52 | v[0x4] = 'p';
53 | v[0x5] = 'o';
54 | v[0x6] = 'k';
55 | v[0x7] = 'l';
56 | v[0x8] = '\'';
57 | v[0x9] = '|';
58 | v[0xa] = 'n';
59 | v[0xb] = '~';
60 | v[0xc] = '~';
61 | v[0xd] = 'g';
62 | v[0xe] = '`';
63 | v[0xf] = '~';
64 | v[0x10] = '?';
65 | v[0x11] = 'a';
66 | v[0x12] = 'd';
67 | v[0x13] = 'j';
68 | for (unsigned int i = 0; i < 0x14; ++i) {
69 | v[i] ^= ((i + 0x14) % m);
70 | }
71 | v[0x14] = '\0';
72 | }
73 |
74 | bool xposed_status = false;
75 |
76 | jboolean findXposed(JNIEnv *env, jclass clazz) {
77 | static int sdk = 0;
78 | if (sdk == 0) {
79 | char v1[0x20];
80 | char prop[PROP_VALUE_MAX] = {0};
81 | fill_ro_build_version_sdk(v1);
82 | __system_property_get(v1, prop);
83 | sdk = (int) strtol(prop, NULL, 10);
84 | }
85 |
86 | checkClassLoader(env, sdk);
87 | return xposed_status;
88 | }
89 |
90 | jint _access(JNIEnv *env, jclass clazz, jstring jpath) {
91 | const char *path = (*env)->GetStringUTFChars(env, jpath, NULL);
92 | int i = access(path, F_OK);
93 | (*env)->ReleaseStringUTFChars(env, jpath, path);
94 | return i;
95 | }
96 |
97 | static inline char *getCannotOpen() {
98 | // cannot open /proc/self/maps
99 | char v[] = "gdhig}*d|h`/?a`|w:eemd,idvt";
100 | static unsigned int m = 0;
101 |
102 | if (m == 0) {
103 | m = 23;
104 | } else if (m == 29) {
105 | m = 31;
106 | }
107 |
108 | for (unsigned int i = 0; i < 0x1b; ++i) {
109 | v[i] ^= ((i + 0x1b) % m);
110 | }
111 | return strdup(v);
112 | }
113 |
114 | static inline char *getDataApp() {
115 | // /data/app
116 | char v[] = "-geqg/`rs";
117 | static unsigned int m = 0;
118 |
119 | if (m == 0) {
120 | m = 7;
121 | } else if (m == 11) {
122 | m = 13;
123 | }
124 |
125 | for (unsigned int i = 0; i < 0x9; ++i) {
126 | v[i] ^= ((i + 0x9) % m);
127 | }
128 | return strdup(v);
129 | }
130 |
131 | static inline char *getComGoogleAndroid() {
132 | // com.google.android
133 | char v[] = "fij&nedkld,bjatham";
134 | static unsigned int m = 0;
135 |
136 | if (m == 0) {
137 | m = 13;
138 | } else if (m == 17) {
139 | m = 19;
140 | }
141 |
142 | for (unsigned int i = 0; i < 0x12; ++i) {
143 | v[i] ^= ((i + 0x12) % m);
144 | }
145 | return strdup(v);
146 | }
147 |
148 | static inline char *getComBaiduTieba() {
149 | // com.baidu.tieba
150 | char v[] = "gjk)jhcdt,wm`df";
151 | static unsigned int m = 0;
152 |
153 | if (m == 0) {
154 | m = 11;
155 | } else if (m == 13) {
156 | m = 17;
157 | }
158 |
159 | for (unsigned int i = 0; i < 0xf; ++i) {
160 | v[i] ^= ((i + 0xf) % m);
161 | }
162 | return strdup(v);
163 | }
164 |
165 | jstring _fopen(JNIEnv *env, jclass clazz, jstring jpath) {
166 | const char *path = (*env)->GetStringUTFChars(env, jpath, NULL);
167 | FILE *f = fopen(path, "r");
168 | (*env)->ReleaseStringUTFChars(env, jpath, path);
169 | if (f == NULL) {
170 | return (*env)->NewStringUTF(env, getCannotOpen());
171 | }
172 |
173 | char result[PATH_MAX];
174 | strcpy(result, "");
175 | char line[PATH_MAX];
176 | while (fgets(line, PATH_MAX - 1, f) != NULL) {
177 | if (strstr(line, getDataApp()) != NULL
178 | && strstr(line, getComGoogleAndroid()) == NULL
179 | && strstr(line, getComBaiduTieba()) == NULL) {
180 | if (strlen(result) + strlen(line) > PATH_MAX) break;
181 | strcat(result, line);
182 | }
183 | }
184 | fclose(f);
185 | return (*env)->NewStringUTF(env, result);
186 | }
187 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/check.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #ifdef __cplusplus
4 | extern "C" {
5 | #endif
6 |
7 | jboolean _inline(JNIEnv *env, jclass clazz, jstring jname);
8 |
9 | jboolean FindClass_inline(JNIEnv *env, jclass clazz);
10 |
11 | jboolean findXposed(JNIEnv *env, jclass clazz);
12 |
13 | jint _access(JNIEnv *env, jclass clazz, jstring path);
14 |
15 | jstring _fopen(JNIEnv *env, jclass clazz, jstring jpath);
16 |
17 | #ifdef __cplusplus
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/classloader.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019/2/16.
3 | //
4 |
5 | #include
6 |
7 | #ifdef __cplusplus
8 | extern "C" {
9 | #endif
10 |
11 | void checkClassLoader(JNIEnv *env, int sdk);
12 |
13 | extern bool xposed_status;
14 |
15 | #ifdef __cplusplus
16 | }
17 | #endif
18 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/hash.c:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2020/9/6.
3 | //
4 |
5 | #include
6 | #include "hash.h"
7 |
8 | static int size;
9 | static int index;
10 | static intptr_t *container;
11 |
12 | bool add(intptr_t hash) {
13 | if (hash == 0) {
14 | return clear();
15 | }
16 | for (int i = 0; i < index; ++i) {
17 | if (container[i] == hash) {
18 | return false;
19 | }
20 | }
21 | if (index >= size) {
22 | size += 4;
23 | container = (intptr_t *) (realloc(container, size * sizeof(intptr_t)));
24 | }
25 | container[index++] = hash;
26 | return true;
27 | }
28 |
29 | bool clear() {
30 | if (container) {
31 | free(container);
32 | size = 0;
33 | index = 0;
34 | container = NULL;
35 | return true;
36 | } else {
37 | return false;
38 | }
39 | }
40 |
41 | #ifdef MAIN
42 | #include
43 | int main(int argc, char **argv) {
44 | for (int i = 1; i < argc; ++i) {
45 | printf("%s: %d\n", argv[i], add(atoi(argv[i])));
46 | }
47 | return 0;
48 | }
49 | #endif
50 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/hash.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2020/9/6.
3 | //
4 |
5 | #ifndef BREVENT_HASH_H
6 | #define BREVENT_HASH_H
7 |
8 | #include
9 | #include
10 |
11 | #ifdef __cplusplus
12 | extern "C" {
13 | #endif
14 |
15 | bool add(intptr_t hash);
16 |
17 | bool clear();
18 |
19 | #ifdef __cplusplus
20 | }
21 | #endif
22 |
23 | #endif //BREVENT_HASH_H
24 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/inline.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019/3/18.
3 | //
4 | #include
5 |
6 | #ifndef BREVENT_INLINE_H
7 | #define BREVENT_INLINE_H
8 |
9 | #ifndef APPLICATION_ID
10 | #define APPLICATION_ID "gm.tieba.tabswitch"
11 | #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_ERROR, APPLICATION_ID, __VA_ARGS__))
12 | #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_ERROR, APPLICATION_ID, __VA_ARGS__))
13 | #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, APPLICATION_ID, __VA_ARGS__))
14 | #endif
15 |
16 | #include
17 |
18 | #ifdef __cplusplus
19 | extern "C" {
20 | #endif
21 |
22 | bool setRead(void *symbol);
23 |
24 | bool isInlineHooked(void *symbol);
25 |
26 | #ifdef DEBUG_HOOK_SELF
27 | #if defined(__arm__) || defined(__aarch64__)
28 |
29 | #include "hookzz/hookzz.h"
30 |
31 | void check_inline_hook_hookzz();
32 |
33 | void check_inline_hook_hookzz_b();
34 |
35 | #endif
36 |
37 | void check_inline_hook_whale();
38 |
39 | #if defined(__arm__)
40 |
41 | void check_inline_hook_substrate();
42 |
43 | #endif
44 | #endif
45 |
46 | #ifdef __cplusplus
47 | }
48 | #endif
49 |
50 | #endif //BREVENT_INLINE_H
51 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/main.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "main.h"
4 | #include "check.h"
5 |
6 | jint JNI_OnLoad(JavaVM *jvm, void *v __unused) {
7 | JNIEnv *env;
8 | jclass clazz;
9 |
10 | if ((*jvm)->GetEnv(jvm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
11 | return JNI_ERR;
12 | }
13 |
14 | if ((clazz = (*env)->FindClass(env, "gm/tieba/tabswitch/hooker/extra/NativeCheck")) == NULL) {
15 | return JNI_ERR;
16 | }
17 |
18 | JNINativeMethod methods[] = {
19 | {"inline", "(Ljava/lang/String;)Z", _inline},
20 | {"isFindClassInline", "()Z", FindClass_inline},
21 | {"findXposed", "()Z", findXposed},
22 | {"access", "(Ljava/lang/String;)I", _access},
23 | {"fopen", "(Ljava/lang/String;)Ljava/lang/String;", _fopen},
24 | };
25 | if ((*env)->RegisterNatives(env, clazz, methods, NELEM(methods)) < 0) {
26 | return JNI_ERR;
27 | }
28 |
29 | return JNI_VERSION_1_6;
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/main.h:
--------------------------------------------------------------------------------
1 | #ifndef NELEM
2 | #define NELEM(x) (sizeof(x) / sizeof((x)[0]))
3 | #endif
4 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/path.c:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019-05-03.
3 | //
4 |
5 | #include "path.h"
6 | #include
7 |
8 | static inline bool isSystem(const char *str) {
9 | return str != NULL
10 | && *str == '/'
11 | && *++str == 's'
12 | && *++str == 'y'
13 | && *++str == 's'
14 | && *++str == 't'
15 | && *++str == 'e'
16 | && *++str == 'm'
17 | && *++str == '/';
18 | }
19 |
20 | static inline bool isVendor(const char *str) {
21 | return str != NULL
22 | && *str == '/'
23 | && *++str == 'v'
24 | && *++str == 'e'
25 | && *++str == 'n'
26 | && *++str == 'd'
27 | && *++str == 'o'
28 | && *++str == 'r'
29 | && *++str == '/';
30 | }
31 |
32 | static inline bool isOem(const char *str) {
33 | return str != NULL
34 | && *str == '/'
35 | && *++str == 'o'
36 | && *++str == 'e'
37 | && *++str == 'm'
38 | && *++str == '/';
39 | }
40 |
41 | bool isThirdParty(const char *str) {
42 | if (isSystem(str) || isVendor(str) || isOem(str)) {
43 | return false;
44 | } else {
45 | return true;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/path.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019-05-03.
3 | //
4 |
5 | #ifndef BREVENT_PATH_H
6 | #define BREVENT_PATH_H
7 |
8 | #include
9 |
10 | bool isThirdParty(const char *str);
11 |
12 | #endif //BREVENT_PATH_H
13 |
--------------------------------------------------------------------------------
/app/src/main/cpp/check/plt.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thom on 2019/2/16.
3 | //
4 |
5 | #ifndef BREVENT_PLT_H
6 | #define BREVENT_PLT_H
7 |
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | #ifdef __cplusplus
14 | extern "C" {
15 | #endif
16 |
17 | #define PLT_CHECK_PLT_APP ((unsigned short) 0x1u)
18 | #define PLT_CHECK_PLT_ALL ((unsigned short) 0x2u)
19 | #define PLT_CHECK_NAME ((unsigned short) 0x4u)
20 | #define PLT_CHECK_SYM_ONE ((unsigned short) 0x8u)
21 |
22 | typedef struct Symbol {
23 | unsigned short check;
24 | unsigned short size;
25 | size_t total;
26 | ElfW(Addr) *symbol_plt;
27 | ElfW(Addr) *symbol_sym;
28 | const char *symbol_name;
29 | char **names;
30 | } Symbol;
31 |
32 | int dl_iterate_phdr_symbol(Symbol *symbol);
33 |
34 | void *plt_dlsym(const char *name, size_t *total);
35 |
36 | bool isPltHooked(const char *name, bool all);
37 |
38 | #ifdef __cplusplus
39 | }
40 | #endif
41 |
42 | #endif //BREVENT_PLT_H
43 |
--------------------------------------------------------------------------------
/app/src/main/cpp/hide/hide.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "hide.h"
4 |
5 | static HookFunType hook_func = nullptr;
6 |
7 | jclass (*backup_FindClass)(JNIEnv *env, const char *name);
8 |
9 | jclass fake_FindClass(JNIEnv *env, const char *name) {
10 | if (!strcmp(name, "dalvik/system/BaseDexClassLoader")) return nullptr;
11 | return backup_FindClass(env, name);
12 | }
13 |
14 | void on_library_loaded(const char *name, void *handle) {
15 | // if (std::string(name).ends_with("libcheck.so")) {
16 | // // TODO
17 | // }
18 | }
19 |
20 | extern "C" [[gnu::visibility("default")]] [[gnu::used]]
21 | jint JNI_OnLoad(JavaVM *jvm, void *v __unused) {
22 | JNIEnv *env;
23 |
24 | if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
25 | return JNI_ERR;
26 | }
27 |
28 | hook_func((void *) env->functions->FindClass, (void *) fake_FindClass,
29 | (void **) &backup_FindClass);
30 | return JNI_VERSION_1_6;
31 | }
32 |
33 | extern "C" [[gnu::visibility("default")]] [[gnu::used]]
34 | NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
35 | hook_func = entries->hook_func;
36 | return on_library_loaded;
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/cpp/hide/hide.h:
--------------------------------------------------------------------------------
1 | #ifndef XPOSEDTEMPLATE_NATIVE_API_H
2 | #define XPOSEDTEMPLATE_NATIVE_API_H
3 |
4 | #include
5 | #include
6 |
7 | typedef int (*HookFunType)(void *func, void *replace, void **backup);
8 |
9 | typedef int (*UnhookFunType)(void *func);
10 |
11 | typedef void (*NativeOnModuleLoaded)(const char *name, void *handle);
12 |
13 | typedef struct {
14 | uint32_t version;
15 | HookFunType hook_func;
16 | UnhookFunType unhook_func;
17 | } NativeAPIEntries;
18 |
19 | typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries);
20 |
21 | #endif //XPOSEDTEMPLATE_NATIVE_API_H
22 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/Constants.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch
2 |
3 | object Constants {
4 | val strings = mapOf(
5 | "EULA" to "如果您对本协议的任何条款表示异议,您可以选择不使用本模块;使用本模块则意味着您已完全理解和同意遵守本协议。\n\n" +
6 | " ①本模块开源免费,所有版本均为自动构建,可确保构建版本与源代码一致。对本模块的任何异议都必须以源代码为依据。\n" +
7 | " ②本模块不会主动发起网络请求,不会上传任何用户数据,隐私泄露或者账号异常行为与本模块无关。\n" +
8 | " ③本模块主要用于学习和交流技术,任何人不得将本模块用于商业或非法用途。",
9 | "dev_tip" to "提示:您当前安装的是非正式版本,可能含有较多错误,如果您希望得到更稳定的使用体验,建议您安装正式版本。",
10 | "exception_rules_incomplete" to "请点击确定并重启应用以重新执行反混淆。若执行反混淆后仍出现此对话框则应尝试更新模块或向作者反馈。\n",
11 | "version_mismatch" to "当前贴吧版本不受支持。请使用受支持的贴吧版本(%s-%s)或尝试更新模块。\n",
12 | "exception_init_preference" to "初始化设置失败,请尝试更换贴吧版本。",
13 | "regex_hint" to "请输入正则表达式,如.*",
14 | "release_uri" to "https://github.com/GuhDoy/TiebaTS/releases",
15 | "ci_uri" to "https://github.com/GuhDoy/TiebaTS/actions",
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/XposedContext.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch
2 |
3 | import android.content.Context
4 | import android.content.res.AssetManager
5 | import android.os.Build
6 | import android.os.Handler
7 | import android.os.Looper
8 | import de.robv.android.xposed.XC_MethodHook
9 | import de.robv.android.xposed.XC_MethodReplacement
10 | import de.robv.android.xposed.XposedBridge
11 | import de.robv.android.xposed.XposedHelpers
12 | import de.robv.android.xposed.callbacks.XCallback
13 | import java.lang.ref.WeakReference
14 | import java.lang.reflect.Method
15 |
16 | abstract class XposedContext {
17 |
18 | companion object {
19 | val isModuleBetaVersion = BuildConfig.VERSION_NAME.contains("alpha") || BuildConfig.VERSION_NAME.contains("beta")
20 | val exceptions: MutableMap = HashMap(0)
21 |
22 | lateinit var sClassLoader: ClassLoader
23 | lateinit var sPath: String
24 | lateinit var sAssetManager: AssetManager
25 |
26 | private lateinit var sHandler: Handler
27 | private lateinit var sContextRef: WeakReference
28 |
29 | @JvmStatic
30 | fun getContext(): Context = checkNotNull(sContextRef.get()) { "ApplicationContext is null" }
31 |
32 | fun attachBaseContext(context: Context) {
33 | sContextRef = WeakReference(context.applicationContext)
34 | sHandler = Handler(Looper.getMainLooper())
35 | }
36 |
37 | @JvmStatic
38 | protected fun load(filename: String) {
39 | val soPaths = Build.SUPPORTED_ABIS.map { abi -> "$sPath!/lib/$abi/lib$filename.so" }
40 | val errors = mutableListOf()
41 |
42 | for (soPath in soPaths) {
43 | try {
44 | System.load(soPath)
45 | return
46 | } catch (e: UnsatisfiedLinkError) {
47 | errors.add(e)
48 | }
49 | }
50 |
51 | val linkError = UnsatisfiedLinkError("Failed to load native library: $filename")
52 | errors.forEach { linkError.addSuppressed(it) }
53 | XposedBridge.log(linkError)
54 | throw linkError
55 | }
56 |
57 | fun runOnUiThread(r: Runnable) {
58 | sHandler.post(r)
59 | }
60 |
61 | fun findClass(className: String): Class<*> = XposedHelpers.findClass(className, sClassLoader)
62 |
63 | inline fun hookBeforeMethod(
64 | className: String,
65 | methodName: String,
66 | vararg parameterTypes: Any?,
67 | crossinline beforeHook: (XC_MethodHook.MethodHookParam) -> Unit
68 | ): XC_MethodHook.Unhook {
69 | return XposedHelpers.findAndHookMethod(
70 | className, sClassLoader, methodName, *parameterTypes,
71 | object : XC_MethodHook() {
72 | override fun beforeHookedMethod(param: MethodHookParam) {
73 | beforeHook(param)
74 | }
75 | }
76 | )
77 | }
78 |
79 | inline fun hookAfterMethod(
80 | className: String,
81 | methodName: String,
82 | vararg parameterTypes: Any?,
83 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
84 | ): XC_MethodHook.Unhook {
85 | return XposedHelpers.findAndHookMethod(
86 | className, sClassLoader, methodName, *parameterTypes,
87 | object : XC_MethodHook() {
88 | override fun afterHookedMethod(param: MethodHookParam) {
89 | afterHook(param)
90 | }
91 | }
92 | )
93 | }
94 |
95 | inline fun hookAfterMethodPriority(
96 | className: String,
97 | methodName: String,
98 | vararg parameterTypes: Any?,
99 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
100 | ): XC_MethodHook.Unhook {
101 | return XposedHelpers.findAndHookMethod(
102 | className, sClassLoader, methodName, *parameterTypes,
103 | object : XC_MethodHook(XCallback.PRIORITY_LOWEST) {
104 | override fun afterHookedMethod(param: MethodHookParam) {
105 | afterHook(param)
106 | }
107 | }
108 | )
109 | }
110 |
111 | inline fun hookReplaceMethod(
112 | className: String,
113 | methodName: String,
114 | vararg parameterTypes: Any?,
115 | crossinline replaceHook: (XC_MethodHook.MethodHookParam) -> Any?
116 | ): XC_MethodHook.Unhook {
117 | return XposedHelpers.findAndHookMethod(
118 | className, sClassLoader, methodName, *parameterTypes,
119 | object : XC_MethodReplacement() {
120 | override fun replaceHookedMethod(param: MethodHookParam): Any? {
121 | return replaceHook(param)
122 | }
123 | }
124 | )
125 | }
126 |
127 | inline fun hookBeforeMethod(
128 | clazz: Class<*>,
129 | methodName: String,
130 | vararg parameterTypes: Any?,
131 | crossinline beforeHook: (XC_MethodHook.MethodHookParam) -> Unit
132 | ): XC_MethodHook.Unhook {
133 | return XposedHelpers.findAndHookMethod(
134 | clazz, methodName, *parameterTypes,
135 | object : XC_MethodHook() {
136 | override fun beforeHookedMethod(param: MethodHookParam) {
137 | beforeHook(param)
138 | }
139 | }
140 | )
141 | }
142 |
143 | inline fun hookAfterMethod(
144 | clazz: Class<*>,
145 | methodName: String,
146 | vararg parameterTypes: Any?,
147 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
148 | ): XC_MethodHook.Unhook {
149 | return XposedHelpers.findAndHookMethod(
150 | clazz, methodName, *parameterTypes,
151 | object : XC_MethodHook() {
152 | override fun afterHookedMethod(param: MethodHookParam) {
153 | afterHook(param)
154 | }
155 | }
156 | )
157 | }
158 |
159 | inline fun hookReplaceMethod(
160 | clazz: Class<*>,
161 | methodName: String,
162 | vararg parameterTypes: Any?,
163 | crossinline replaceHook: (XC_MethodHook.MethodHookParam) -> Any?
164 | ): XC_MethodHook.Unhook {
165 | return XposedHelpers.findAndHookMethod(
166 | clazz, methodName, *parameterTypes,
167 | object : XC_MethodReplacement() {
168 | override fun replaceHookedMethod(param: MethodHookParam): Any? {
169 | return replaceHook(param)
170 | }
171 | }
172 | )
173 | }
174 |
175 | inline fun hookBeforeMethod(
176 | method: Method,
177 | crossinline beforeHook: (XC_MethodHook.MethodHookParam) -> Unit
178 | ): XC_MethodHook.Unhook {
179 | return XposedBridge.hookMethod(
180 | method,
181 | object : XC_MethodHook() {
182 | override fun beforeHookedMethod(param: MethodHookParam) {
183 | beforeHook(param)
184 | }
185 | }
186 | )
187 | }
188 |
189 | inline fun hookAfterMethod(
190 | method: Method,
191 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
192 | ): XC_MethodHook.Unhook {
193 | return XposedBridge.hookMethod(
194 | method,
195 | object : XC_MethodHook() {
196 | override fun afterHookedMethod(param: MethodHookParam) {
197 | afterHook(param)
198 | }
199 | }
200 | )
201 | }
202 |
203 | inline fun hookReplaceMethod(
204 | method: Method,
205 | crossinline replaceHook: (XC_MethodHook.MethodHookParam) -> Any?
206 | ): XC_MethodHook.Unhook {
207 | return XposedBridge.hookMethod(
208 | method,
209 | object : XC_MethodReplacement() {
210 | override fun replaceHookedMethod(param: MethodHookParam): Any? {
211 | return replaceHook(param)
212 | }
213 | }
214 | )
215 | }
216 |
217 | inline fun hookBeforeConstructor(
218 | className: String,
219 | vararg parameterTypes: Any?,
220 | crossinline beforeHook: (XC_MethodHook.MethodHookParam) -> Unit
221 | ): XC_MethodHook.Unhook {
222 | return XposedHelpers.findAndHookConstructor(
223 | className, sClassLoader, *parameterTypes,
224 | object : XC_MethodHook() {
225 | override fun beforeHookedMethod(param: MethodHookParam) {
226 | beforeHook(param)
227 | }
228 | }
229 | )
230 | }
231 |
232 | inline fun hookAfterConstructor(
233 | className: String,
234 | vararg parameterTypes: Any?,
235 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
236 | ): XC_MethodHook.Unhook {
237 | return XposedHelpers.findAndHookConstructor(
238 | className, sClassLoader, *parameterTypes,
239 | object : XC_MethodHook() {
240 | override fun afterHookedMethod(param: MethodHookParam) {
241 | afterHook(param)
242 | }
243 | }
244 | )
245 | }
246 |
247 | inline fun hookBeforeConstructor(
248 | clazz: Class<*>,
249 | vararg parameterTypes: Any?,
250 | crossinline beforeHook: (XC_MethodHook.MethodHookParam) -> Unit
251 | ): XC_MethodHook.Unhook {
252 | return XposedHelpers.findAndHookConstructor(
253 | clazz, *parameterTypes,
254 | object : XC_MethodHook() {
255 | override fun beforeHookedMethod(param: MethodHookParam) {
256 | beforeHook(param)
257 | }
258 | }
259 | )
260 | }
261 |
262 | inline fun hookAfterConstructor(
263 | clazz: Class<*>,
264 | vararg parameterTypes: Any?,
265 | crossinline afterHook: (XC_MethodHook.MethodHookParam) -> Unit
266 | ): XC_MethodHook.Unhook {
267 | return XposedHelpers.findAndHookConstructor(
268 | clazz, *parameterTypes,
269 | object : XC_MethodHook() {
270 | override fun afterHookedMethod(param: MethodHookParam) {
271 | afterHook(param)
272 | }
273 | }
274 | )
275 | }
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/dao/AcRule.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.dao
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Dao
5 | import androidx.room.Database
6 | import androidx.room.Delete
7 | import androidx.room.Entity
8 | import androidx.room.Insert
9 | import androidx.room.PrimaryKey
10 | import androidx.room.Query
11 | import androidx.room.RoomDatabase
12 |
13 | @Entity
14 | data class AcRule(
15 | @PrimaryKey(autoGenerate = true) val id: Int,
16 | @ColumnInfo(name = "matcher") val matcher: String,
17 | @ColumnInfo(name = "class") val clazz: String,
18 | @ColumnInfo(name = "method") val method: String
19 | ) {
20 | companion object {
21 | fun create(matcher: String, clazz: String, method: String) =
22 | AcRule(0, matcher, clazz, method)
23 | }
24 | }
25 |
26 | @Dao
27 | interface AcRuleDao {
28 | @Query("SELECT * FROM AcRule")
29 | fun getAll(): List
30 |
31 | @Query("SELECT * FROM AcRule WHERE matcher IN (:matchers)")
32 | fun loadAllMatch(vararg matchers: String): List
33 |
34 | @Insert
35 | fun insertAll(vararg rules: AcRule)
36 |
37 | @Delete
38 | fun delete(rule: AcRule)
39 | }
40 |
41 | @Database(entities = [AcRule::class], version = 1, exportSchema = false)
42 | abstract class AcRuleDatabase : RoomDatabase() {
43 | abstract fun acRuleDao(): AcRuleDao
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/dao/AcRules.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.dao
2 |
3 | import android.content.Context
4 | import androidx.room.Room.databaseBuilder
5 | import gm.tieba.tabswitch.dao.AcRule.Companion.create
6 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
7 |
8 | object AcRules {
9 | private const val ACRULES_DATABASE_NAME = "Deobfs.db"
10 | private lateinit var ruleDao: AcRuleDao
11 | fun init(context: Context) {
12 | ruleDao = databaseBuilder(
13 | context.applicationContext, AcRuleDatabase::class.java, ACRULES_DATABASE_NAME
14 | )
15 | .allowMainThreadQueries()
16 | .build()
17 | .acRuleDao()
18 | }
19 |
20 | fun dropAllRules() {
21 | ruleDao.getAll().forEach { rule ->
22 | ruleDao.delete(rule)
23 | }
24 | }
25 |
26 | fun putRule(matcher: String, clazz: String, method: String) {
27 | ruleDao.insertAll(create(matcher, clazz, method))
28 | }
29 |
30 | fun findRule(matcher: Matcher, callback: (String, String, String) -> Unit) {
31 | ruleDao.loadAllMatch(matcher.toString()).forEach { rule ->
32 | callback(rule.matcher, rule.clazz, rule.method)
33 | }
34 | }
35 |
36 | fun findRule(matchers: List, callback: (String, String, String) -> Unit) {
37 | val matcherStrings = matchers.map { it.toString() }.toTypedArray()
38 | ruleDao.loadAllMatch(*matcherStrings).forEach { rule ->
39 | callback(rule.matcher, rule.clazz, rule.method)
40 | }
41 | }
42 |
43 | fun findRule(str: String, callback: (String, String, String) -> Unit) {
44 | ruleDao.loadAllMatch(str).forEach { rule ->
45 | callback(rule.matcher, rule.clazz, rule.method)
46 | }
47 | }
48 |
49 | fun isRuleFound(matcher: String): Boolean {
50 | return ruleDao.loadAllMatch(matcher).isNotEmpty()
51 | }
52 |
53 | fun isRuleFound(vararg matchers: String): Boolean {
54 | return matchers.all { matcher -> isRuleFound(matcher) }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/dao/Adp.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.dao
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.Preferences.putLikeForum
6 |
7 | object Adp : XposedContext() {
8 | var BDUSS: String? = null
9 | var tbs: String? = null
10 | var account: String? = null
11 |
12 | fun initializeAdp() {
13 | refreshAccountData()
14 | refreshCache()
15 | }
16 |
17 | private fun refreshAccountData() {
18 | hookAfterMethod("com.baidu.tbadk.core.data.AccountData", "getBDUSS") { param ->
19 | BDUSS = param.result as String
20 | }
21 | hookAfterMethod("com.baidu.tbadk.core.data.AccountData", "getTbs") { param ->
22 | tbs = param.result as String
23 | }
24 | hookAfterMethod("com.baidu.tbadk.core.data.AccountData", "getAccount") { param ->
25 | account = param.result as String
26 | }
27 | }
28 |
29 | private fun refreshCache() {
30 | hookBeforeMethod("tbclient.ForumRecommend.DataRes\$Builder",
31 | "build", Boolean::class.javaPrimitiveType) { param ->
32 | val forums: MutableSet = HashSet()
33 | val likeForumList = XposedHelpers.getObjectField(param.thisObject, "like_forum") as? List<*>
34 | likeForumList?.forEach { forums.add(XposedHelpers.getObjectField(it, "forum_name") as String) }
35 | putLikeForum(forums)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/dao/Preferences.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.dao
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import java.util.Calendar
7 |
8 | object Preferences {
9 | private lateinit var sTsPreferences: SharedPreferences
10 | private lateinit var sTsConfig: SharedPreferences
11 |
12 | fun init(context: Context) {
13 | sTsPreferences = context.getSharedPreferences("TS_preferences", Context.MODE_PRIVATE)
14 | sTsConfig = context.getSharedPreferences("TS_config", Context.MODE_PRIVATE)
15 | }
16 |
17 | fun getAll(): Map = sTsPreferences.all
18 |
19 | fun remove(key: String) {
20 | sTsPreferences.edit().remove(key).apply()
21 | }
22 |
23 | fun putBoolean(key: String, value: Boolean) {
24 | sTsPreferences.edit().putBoolean(key, value).apply()
25 | }
26 |
27 | @JvmStatic
28 | fun getBoolean(key: String): Boolean = sTsPreferences.getBoolean(key, false)
29 |
30 | fun putString(key: String, value: String) {
31 | sTsPreferences.edit().putString(key, value).apply()
32 | }
33 |
34 | fun getString(key: String): String? = sTsPreferences.getString(key, null)
35 |
36 | fun putStringSet(key: String, value: String, isContain: Boolean) {
37 | val set = getStringSet(key).toMutableSet()
38 | if (isContain) {
39 | set.add(value)
40 | } else {
41 | set.remove(value)
42 | }
43 | sTsPreferences.edit().putStringSet(key, set).apply()
44 | }
45 |
46 | fun getStringSet(key: String): Set = sTsPreferences.getStringSet(key, emptySet()) ?: emptySet()
47 |
48 | // Config
49 | fun putEULAAccepted() {
50 | sTsConfig.edit().putBoolean("EULA", true).apply()
51 | }
52 |
53 | fun getIsEULAAccepted(): Boolean = sTsConfig.getBoolean("EULA", false)
54 |
55 | fun putAutoSignEnabled() {
56 | sTsConfig.edit().putBoolean("auto_sign", true).apply()
57 | }
58 |
59 | fun getIsAutoSignEnabled(): Boolean = sTsConfig.getBoolean("auto_sign", false)
60 |
61 | @SuppressLint("ApplySharedPref")
62 | fun putPurgeEnabled() {
63 | sTsConfig.edit().putBoolean("ze", true).commit()
64 | }
65 |
66 | fun getIsPurgeEnabled(): Boolean = sTsConfig.getBoolean("ze", false)
67 |
68 | @SuppressLint("ApplySharedPref")
69 | fun putSignature(i: Int) {
70 | sTsConfig.edit().putInt("signature", i).commit()
71 | }
72 |
73 | fun getSignature(): Int = sTsConfig.getInt("signature", 0)
74 |
75 | fun putLikeForum(follow: Set?) {
76 | sTsConfig.edit().putStringSet("like_forum", follow).apply()
77 | }
78 |
79 | fun getLikeForum(): Set? = sTsConfig.getStringSet("like_forum", null)
80 |
81 | fun putSignDate() {
82 | sTsConfig.edit().putInt("sign_date", Calendar.getInstance()[Calendar.DAY_OF_YEAR]).apply()
83 | }
84 |
85 | fun getIsSigned(): Boolean =
86 | Calendar.getInstance()[Calendar.DAY_OF_YEAR] == sTsConfig.getInt("sign_date", 0)
87 |
88 | @SuppressLint("ApplySharedPref")
89 | fun commit() {
90 | sTsConfig.edit().commit()
91 | sTsPreferences.edit().commit()
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/IHooker.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker
2 |
3 | interface IHooker {
4 |
5 | fun key(): String
6 |
7 | @Throws(Throwable::class)
8 | fun hook()
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/Obfuscated.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker
2 |
3 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
4 |
5 | interface Obfuscated {
6 |
7 | fun matchers(): List
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/add/HistoryCache.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.add
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import android.graphics.Color
6 | import android.os.Bundle
7 | import android.text.Editable
8 | import android.text.InputType
9 | import android.text.TextWatcher
10 | import android.view.Gravity
11 | import android.view.KeyEvent
12 | import android.view.WindowManager
13 | import android.view.inputmethod.EditorInfo
14 | import android.widget.EditText
15 | import android.widget.LinearLayout
16 | import de.robv.android.xposed.XposedHelpers
17 | import gm.tieba.tabswitch.Constants.strings
18 | import gm.tieba.tabswitch.XposedContext
19 | import gm.tieba.tabswitch.hooker.IHooker
20 | import gm.tieba.tabswitch.util.dipToPx
21 | import gm.tieba.tabswitch.util.findFirstMethodByExactType
22 | import gm.tieba.tabswitch.util.fixAlertDialogWidth
23 | import gm.tieba.tabswitch.util.getCurrentActivity
24 | import gm.tieba.tabswitch.util.getDialogTheme
25 | import gm.tieba.tabswitch.util.getObjectField
26 | import gm.tieba.tabswitch.util.isLightMode
27 | import gm.tieba.tabswitch.widget.NavigationBar
28 | import gm.tieba.tabswitch.widget.TbToast
29 | import gm.tieba.tabswitch.widget.TbToast.Companion.showTbToast
30 | import java.util.regex.Pattern
31 | import java.util.regex.PatternSyntaxException
32 |
33 | class HistoryCache : XposedContext(), IHooker {
34 |
35 | private var mRegex = ""
36 |
37 | override fun key(): String {
38 | return "history_cache"
39 | }
40 |
41 | override fun hook() {
42 | hookAfterMethod(
43 | "com.baidu.tieba.myCollection.history.PbHistoryActivity",
44 | "onCreate", Bundle::class.java
45 | ) { param ->
46 | val activity = param.thisObject as Activity
47 | if (param.args[0] == null) {
48 | mRegex = ""
49 | }
50 | NavigationBar(param.thisObject).addTextButton("搜索") { showRegexDialog(activity) }
51 | }
52 |
53 | hookBeforeMethod(
54 | findFirstMethodByExactType("com.baidu.tieba.myCollection.history.PbHistoryActivity", MutableList::class.java)
55 | ) { param ->
56 | val historyList = param.args[0] as? MutableList<*>
57 | val pattern = Pattern.compile(mRegex, Pattern.CASE_INSENSITIVE)
58 |
59 | historyList?.removeIf { history ->
60 | val strings = try {
61 | arrayOf(
62 | XposedHelpers.getObjectField(history, "forumName") as String,
63 | XposedHelpers.getObjectField(history, "threadName") as String
64 | )
65 | } catch (e: NoSuchFieldError) {
66 | arrayOf(
67 | getObjectField(history, 3) as String,
68 | getObjectField(history, 2) as String
69 | )
70 | }
71 | strings.none { pattern.matcher(it).find() }
72 | }
73 | }
74 | }
75 |
76 | private fun showRegexDialog(activity: Activity) {
77 | val currentActivity = getCurrentActivity()
78 | val isLightMode = isLightMode(getContext())
79 |
80 | val editText = EditText(currentActivity).apply {
81 | setHint(strings["regex_hint"])
82 | setText(mRegex)
83 | if (!isLightMode) {
84 | setTextColor(Color.WHITE)
85 | setHintTextColor(Color.GRAY)
86 | }
87 | setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE)
88 | setFallbackLineSpacing(false)
89 | setLineSpacing(0f, 1.2f)
90 | addTextChangedListener(object : TextWatcher {
91 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
92 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
93 | override fun afterTextChanged(s: Editable) {
94 | mRegex = s.toString()
95 | }
96 | })
97 | }
98 |
99 | val linearLayout = LinearLayout(currentActivity).apply {
100 | gravity = Gravity.CENTER_HORIZONTAL
101 | }
102 | val layoutParams = LinearLayout.LayoutParams(
103 | LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
104 | ).apply {
105 | leftMargin = dipToPx(currentActivity, 20f)
106 | rightMargin = dipToPx(currentActivity, 20f)
107 | }
108 | editText.setLayoutParams(layoutParams)
109 | linearLayout.addView(editText)
110 |
111 | val currRegex = mRegex
112 | val alert = AlertDialog.Builder(
113 | currentActivity,
114 | getDialogTheme(isLightMode)
115 | )
116 | .setTitle("搜索")
117 | .setView(linearLayout)
118 | .setOnCancelListener { mRegex = currRegex }
119 | .setNegativeButton(activity.getString(android.R.string.cancel)) { _, _ -> mRegex = currRegex }
120 | .setPositiveButton(activity.getString(android.R.string.ok), null)
121 | .create()
122 | .apply {
123 | setOnShowListener {
124 | getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
125 | try {
126 | Pattern.compile(editText.getText().toString())
127 | dismiss()
128 | activity.recreate()
129 | } catch (e: PatternSyntaxException) {
130 | showTbToast(e.message, TbToast.LENGTH_SHORT)
131 | }
132 | }
133 | }
134 | show()
135 | fixAlertDialogWidth(this)
136 | }
137 |
138 | alert.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
139 |
140 | editText.apply {
141 | setSingleLine()
142 | setImeOptions(EditorInfo.IME_ACTION_SEARCH)
143 | setOnEditorActionListener { _, actionId, event ->
144 | when {
145 | actionId == EditorInfo.IME_ACTION_SEARCH || event?.keyCode == KeyEvent.KEYCODE_ENTER -> {
146 | alert.getButton(AlertDialog.BUTTON_POSITIVE).performClick()
147 | true
148 | }
149 | else -> false
150 | }
151 | }
152 | selectAll()
153 | requestFocus()
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/add/Ripple.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.add
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.graphics.drawable.Drawable
6 | import android.graphics.drawable.LayerDrawable
7 | import android.graphics.drawable.PaintDrawable
8 | import android.graphics.drawable.StateListDrawable
9 | import android.util.AttributeSet
10 | import android.util.SparseArray
11 | import android.view.View
12 | import android.widget.RelativeLayout
13 | import de.robv.android.xposed.XposedHelpers
14 | import gm.tieba.tabswitch.XposedContext
15 | import gm.tieba.tabswitch.hooker.IHooker
16 | import gm.tieba.tabswitch.util.dipToPx
17 | import gm.tieba.tabswitch.util.getColor
18 | import gm.tieba.tabswitch.util.getObjectField
19 | import java.lang.reflect.Method
20 |
21 | class Ripple : XposedContext(), IHooker {
22 |
23 | override fun key(): String {
24 | return "ripple"
25 | }
26 |
27 | override fun hook() {
28 |
29 | val subPbLayoutClass = try {
30 | findClass("com.baidu.tieba.pb.pb.sub.SubPbLayout")
31 | } catch (e: XposedHelpers.ClassNotFoundError) {
32 | findClass("com.baidu.tieba.pb.sub.view.SubPbLayout")
33 | }
34 |
35 | // 楼中楼
36 | val md: Method = try {
37 | subPbLayoutClass.declaredFields[4].type.getDeclaredMethod("createView")
38 | } catch (e: NoSuchMethodException) {
39 | subPbLayoutClass.declaredFields[4].type.getDeclaredMethod("b")
40 | }
41 |
42 | hookAfterMethod(md) { param ->
43 | val newSubPbListItem = param.result as View
44 | val tag = newSubPbListItem.tag as SparseArray<*>
45 | val b = tag.valueAt(0)
46 | // R.id.new_sub_pb_list_richText
47 | val view =
48 | getObjectField(b, "com.baidu.tbadk.widget.richText.TbRichTextView") as? View
49 | view?.background = createSubPbBackground(dipToPx(getContext(), 5f))
50 | }
51 |
52 | // 查看全部回复
53 | hookAfterConstructor(
54 | subPbLayoutClass,
55 | Context::class.java, AttributeSet::class.java,
56 | ) { param ->
57 | getObjectField(param.thisObject, RelativeLayout::class.java)
58 | ?.background = createSubPbBackground(dipToPx(getContext(), 3.5f))
59 | }
60 | }
61 |
62 | private fun createSubPbBackground(bottomInset: Int): StateListDrawable {
63 | val sld = StateListDrawable()
64 | val color = getColor("CAM_X0201")
65 |
66 | val bg = PaintDrawable(Color.argb(192, Color.red(color), Color.green(color), Color.blue(color)))
67 | bg.setCornerRadius(dipToPx(getContext(), 2f).toFloat())
68 |
69 | val layerBg = LayerDrawable(arrayOf(bg))
70 | layerBg.setLayerInset(0, 0, 0, 0, bottomInset)
71 |
72 | sld.addState(intArrayOf(android.R.attr.state_pressed), layerBg)
73 | return sld
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/add/SaveImages.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.add
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.os.Build
6 | import android.os.Environment
7 | import android.os.Handler
8 | import android.os.Looper
9 | import android.provider.MediaStore
10 | import android.view.View.OnLongClickListener
11 | import android.widget.ImageView
12 | import android.widget.LinearLayout
13 | import gm.tieba.tabswitch.XposedContext
14 | import gm.tieba.tabswitch.dao.AcRules.findRule
15 | import gm.tieba.tabswitch.hooker.IHooker
16 | import gm.tieba.tabswitch.hooker.Obfuscated
17 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
18 | import gm.tieba.tabswitch.hooker.deobfuscation.ReturnTypeMatcher
19 | import gm.tieba.tabswitch.util.copy
20 | import gm.tieba.tabswitch.util.findFirstMethodByExactType
21 | import gm.tieba.tabswitch.util.getExtension
22 | import gm.tieba.tabswitch.util.toByteBuffer
23 | import gm.tieba.tabswitch.widget.TbToast
24 | import gm.tieba.tabswitch.widget.TbToast.Companion.showTbToast
25 | import org.luckypray.dexkit.query.matchers.ClassMatcher
26 | import java.io.File
27 | import java.io.IOException
28 | import java.lang.reflect.Field
29 | import java.net.URL
30 | import java.text.SimpleDateFormat
31 | import java.util.Date
32 | import java.util.Locale
33 | import kotlin.concurrent.thread
34 |
35 | class SaveImages : XposedContext(), IHooker, Obfuscated {
36 |
37 | private var mDownloadImageViewField: Field? = null
38 | private lateinit var mList: ArrayList<*>
39 |
40 | override fun key(): String {
41 | return "save_images"
42 | }
43 |
44 | override fun matchers(): List {
45 | return listOf(
46 | ReturnTypeMatcher(LinearLayout::class.java, "save_images").apply {
47 | classMatcher = ClassMatcher.create().usingStrings("分享弹窗触发分享:分享成功")
48 | }
49 | )
50 | }
51 |
52 | override fun hook() {
53 | findRule("save_images") { _, clazz, method ->
54 | hookAfterMethod(clazz, method, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) { param ->
55 | val downloadIconView = param.result as LinearLayout
56 | downloadIconView.setOnLongClickListener(saveImageListener)
57 | }
58 | }
59 |
60 | hookBeforeMethod(
61 | findFirstMethodByExactType(
62 | "com.baidu.tbadk.coreExtra.view.ImagePagerAdapter",
63 | ArrayList::class.java
64 | )
65 | ) { param ->
66 | mList = ArrayList(param.args[0] as ArrayList<*>)
67 | mList.removeIf { (it as String).startsWith("####mLiveRoomPageProvider") }
68 | }
69 |
70 | val imageViewerBottomLayoutClass = findClass("com.baidu.tbadk.coreExtra.view.ImageViewerBottomLayout")
71 | val declaredFields = mutableListOf(*imageViewerBottomLayoutClass.declaredFields)
72 | declaredFields.removeIf { it.type != ImageView::class.java }
73 |
74 | mDownloadImageViewField = declaredFields[declaredFields.size - 1]
75 | mDownloadImageViewField?.let {
76 | hookAfterConstructor(
77 | "com.baidu.tbadk.coreExtra.view.ImageViewerBottomLayout",
78 | Context::class.java
79 | ) { param ->
80 | val imageView = it[param.thisObject] as ImageView
81 | imageView.setOnLongClickListener(saveImageListener)
82 | }
83 | }
84 | }
85 |
86 | private val saveImageListener = OnLongClickListener {
87 | showTbToast(
88 | "开始下载%d张图片".format(Locale.CHINA, mList.size),
89 | TbToast.LENGTH_SHORT
90 | )
91 |
92 | val baseTime = System.currentTimeMillis()
93 | val formattedTime = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date(baseTime))
94 |
95 | thread {
96 | try {
97 | mList.forEachIndexed { index, url ->
98 | val formattedUrl = (url as String).substringBeforeLast("*")
99 | saveImage(
100 | formattedUrl,
101 | "${formattedTime}_${"%02d".format(Locale.CHINA, index)}",
102 | getContext()
103 | )
104 | }
105 | Handler(Looper.getMainLooper()).post {
106 | showTbToast(
107 | "已保存%d张图片至手机相册".format(Locale.CHINA, mList.size),
108 | TbToast.LENGTH_SHORT
109 | )
110 | }
111 | } catch (e: IOException) {
112 | Handler(Looper.getMainLooper()).post {
113 | showTbToast("保存失败", TbToast.LENGTH_SHORT)
114 | }
115 | } catch (e: NullPointerException) {
116 | Handler(Looper.getMainLooper()).post {
117 | showTbToast("保存失败", TbToast.LENGTH_SHORT)
118 | }
119 | }
120 | }
121 | true
122 | }
123 |
124 | companion object {
125 | private fun saveImage(url: String, filename: String, context: Context) {
126 | URL(url).openStream().use { inputStream ->
127 | val byteBuffer = toByteBuffer(inputStream)
128 | val imageDetails = ContentValues().apply {
129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
130 | put(
131 | MediaStore.MediaColumns.RELATIVE_PATH,
132 | "${Environment.DIRECTORY_PICTURES}${File.separator}tieba"
133 | )
134 | } else {
135 | val path = File(
136 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
137 | "tieba"
138 | ).apply { mkdirs() }
139 | put(
140 | MediaStore.MediaColumns.DATA,
141 | "${path}${File.separator}$filename.${getExtension(byteBuffer)}"
142 | )
143 | }
144 | put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
145 | put(MediaStore.MediaColumns.MIME_TYPE, "image/${getExtension(byteBuffer)}")
146 | val currentTime = System.currentTimeMillis()
147 | put(MediaStore.MediaColumns.DATE_ADDED, currentTime / 1000)
148 | put(MediaStore.MediaColumns.DATE_MODIFIED, currentTime / 1000)
149 | }
150 |
151 | val resolver = context.contentResolver
152 | val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageDetails)
153 |
154 | imageUri?.let {
155 | resolver.openFileDescriptor(imageUri, "w")?.use { descriptor ->
156 | copy(byteBuffer, descriptor.fileDescriptor)
157 | } ?: throw IOException("Failed to open file descriptor")
158 | } ?: throw IOException("Failed to insert image into MediaStore")
159 | }
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/add/SelectClipboard.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.add
2 |
3 | import android.app.AlertDialog
4 | import android.content.ClipData
5 | import android.content.ClipboardManager
6 | import android.content.Context
7 | import android.widget.TextView
8 | import gm.tieba.tabswitch.XposedContext
9 | import gm.tieba.tabswitch.dao.AcRules.findRule
10 | import gm.tieba.tabswitch.hooker.IHooker
11 | import gm.tieba.tabswitch.hooker.Obfuscated
12 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
13 | import gm.tieba.tabswitch.hooker.deobfuscation.SmaliMatcher
14 | import gm.tieba.tabswitch.util.fixAlertDialogWidth
15 | import gm.tieba.tabswitch.util.getCurrentActivity
16 | import gm.tieba.tabswitch.util.getDialogTheme
17 | import gm.tieba.tabswitch.util.getObjectField
18 | import gm.tieba.tabswitch.util.getTbadkCoreApplicationInst
19 | import org.luckypray.dexkit.query.matchers.ClassMatcher
20 | import org.luckypray.dexkit.query.matchers.MethodMatcher
21 | import org.luckypray.dexkit.query.matchers.MethodsMatcher
22 |
23 | class SelectClipboard : XposedContext(), IHooker, Obfuscated {
24 |
25 | override fun key(): String {
26 | return "select_clipboard"
27 | }
28 |
29 | override fun matchers(): List {
30 | return listOf(
31 | SmaliMatcher("Landroid/text/ClipboardManager;->setText(Ljava/lang/CharSequence;)V").apply {
32 | classMatcher = ClassMatcher.create().methods(
33 | MethodsMatcher.create().add(
34 | MethodMatcher.create().addInvoke(
35 | MethodMatcher.create()
36 | .descriptor("Lcom/baidu/tbadk/core/data/SmallTailInfo;->()V")
37 | )
38 | )
39 | )
40 | }
41 | )
42 | }
43 |
44 | override fun hook() {
45 | findRule(matchers()) { matcher, clazz, method ->
46 | when (matcher) {
47 | "Landroid/text/ClipboardManager;->setText(Ljava/lang/CharSequence;)V" ->
48 | hookReplaceMethod(clazz, method) { param ->
49 |
50 | val tbRichText = getObjectField(
51 | param.thisObject,
52 | "com.baidu.tbadk.widget.richText.TbRichText"
53 | )
54 | val currentActivity = getCurrentActivity()
55 |
56 | AlertDialog.Builder(
57 | currentActivity,
58 | getDialogTheme(getContext())
59 | )
60 | .setTitle("自由复制")
61 | .setMessage(tbRichText.toString())
62 | .setNeutralButton("复制全部") { _, _ ->
63 | val clipboardManager =
64 | getTbadkCoreApplicationInst().getSystemService(
65 | Context.CLIPBOARD_SERVICE
66 | ) as ClipboardManager
67 | clipboardManager.setPrimaryClip(ClipData.newPlainText("tieba", tbRichText.toString()))
68 | }
69 | .setPositiveButton("完成", null)
70 | .create()
71 | .apply {
72 | show()
73 | findViewById(android.R.id.message)?.setTextIsSelectable(true)
74 | fixAlertDialogWidth(this)
75 | }
76 | null
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/AgreeNum.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 |
7 | class AgreeNum : XposedContext(), IHooker {
8 |
9 | override fun key(): String {
10 | return "agree_num"
11 | }
12 |
13 | override fun hook() {
14 | hookBeforeMethod(
15 | "tbclient.Agree\$Builder",
16 | "build", Boolean::class.javaPrimitiveType
17 | ) { param ->
18 | XposedHelpers.setObjectField(
19 | param.thisObject, "agree_num",
20 | XposedHelpers.getObjectField(param.thisObject, "diff_agree_num")
21 | )
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/AutoSign.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import android.os.Bundle
4 | import de.robv.android.xposed.XposedBridge
5 | import gm.tieba.tabswitch.XposedContext
6 | import gm.tieba.tabswitch.dao.Adp
7 | import gm.tieba.tabswitch.dao.Preferences.getIsSigned
8 | import gm.tieba.tabswitch.dao.Preferences.putLikeForum
9 | import gm.tieba.tabswitch.dao.Preferences.putSignDate
10 | import gm.tieba.tabswitch.hooker.IHooker
11 | import gm.tieba.tabswitch.widget.TbToast
12 | import gm.tieba.tabswitch.widget.TbToast.Companion.showTbToast
13 | import java.net.URLEncoder
14 | import kotlin.concurrent.thread
15 |
16 | class AutoSign : XposedContext(), IHooker {
17 |
18 | companion object {
19 | //获取用户所有关注贴吧
20 | private const val LIKE_URL = "https://tieba.baidu.com/mo/q/newmoindex"
21 |
22 | //获取用户的tbs
23 | private const val TBS_URL = "http://tieba.baidu.com/dc/common/tbs"
24 |
25 | //贴吧签到接口
26 | private const val SIGN_URL = "http://c.tieba.baidu.com/c/c/forum/sign"
27 | }
28 |
29 | private val mFollow = mutableListOf()
30 | private val mSuccess = mutableListOf()
31 | private var mTbs: String? = null
32 | private var mFollowNum = 201
33 |
34 | override fun key(): String {
35 | return "auto_sign"
36 | }
37 |
38 | override fun hook() {
39 | hookAfterMethod(
40 | "com.baidu.tieba.tblauncher.MainTabActivity",
41 | "onCreate", Bundle::class.java
42 | ) { _ ->
43 | if (!getIsSigned()) {
44 | thread {
45 | val result = main(Adp.BDUSS)
46 | if (result.endsWith("全部签到成功")) {
47 | putSignDate()
48 | putLikeForum(HashSet(mSuccess))
49 | }
50 | runOnUiThread { showTbToast(result, TbToast.LENGTH_SHORT) }
51 | }
52 | }
53 | }
54 | }
55 |
56 | private fun main(BDUSS: String?): String {
57 | if (BDUSS == null) return "暂未获取到 BDUSS"
58 |
59 | AutoSignHelper.setCookie(BDUSS)
60 | getTbs()
61 | getFollow()
62 | runSign()
63 |
64 | val failNum = mFollowNum - mSuccess.size
65 | val result = "共${mFollowNum}个吧 - 成功:${mSuccess.size} - 失败:$failNum"
66 | XposedBridge.log(result)
67 | return if (failNum == 0) "共${mFollowNum}个吧 - 全部签到成功" else result
68 | }
69 |
70 | private fun getTbs() {
71 | mTbs = Adp.tbs
72 | if (mTbs != null) return
73 | try {
74 | val jsonObject = AutoSignHelper.get(TBS_URL)
75 | if ("1" == jsonObject.getString("is_login")) {
76 | XposedBridge.log("获取tbs成功")
77 | mTbs = jsonObject.getString("tbs")
78 | } else XposedBridge.log("获取tbs失败 -- $jsonObject")
79 | } catch (e: Exception) {
80 | XposedBridge.log("获取tbs部分出现错误 -- $e")
81 | }
82 | }
83 |
84 | private fun getFollow() {
85 | try {
86 | val jsonObject = AutoSignHelper.get(LIKE_URL)
87 | XposedBridge.log("获取贴吧列表成功")
88 |
89 | val jsonArray = jsonObject.getJSONObject("data").getJSONArray("like_forum")
90 | mFollowNum = jsonArray.length()
91 |
92 | // 获取用户所有关注的贴吧
93 | for (i in 0 until jsonArray.length()) {
94 | val forumObject = jsonArray.optJSONObject(i)
95 | val forumName = forumObject?.getString("forum_name") ?: continue
96 |
97 | when (forumObject.getString("is_sign")) {
98 | "0" -> mFollow.add(forumName)
99 | else -> mSuccess.add(forumName)
100 | }
101 | }
102 | } catch (e: Exception) {
103 | XposedBridge.log("获取贴吧列表部分出现错误 -- $e")
104 | }
105 | }
106 |
107 | private fun runSign() {
108 | // 当执行 3 轮所有贴吧还未签到成功就结束操作
109 | var roundCount = 3
110 |
111 | try {
112 | while (mSuccess.size < mFollowNum && roundCount > 0) {
113 |
114 | mFollow.removeAll { forumName ->
115 | val encodedS = URLEncoder.encode(forumName, "UTF-8")
116 | val body = "kw=$encodedS&tbs=$mTbs&sign=${AutoSignHelper.enCodeMd5("kw=${forumName}tbs=${mTbs}tiebaclient!!!")}"
117 |
118 | val post = AutoSignHelper.post(SIGN_URL, body)
119 | when (post.getString("error_code")) {
120 | "0" -> {
121 | mSuccess.add(forumName)
122 | XposedBridge.log("$forumName: 签到成功")
123 | true
124 | }
125 | else -> {
126 | XposedBridge.log("$forumName: 签到失败")
127 | false
128 | }
129 | }
130 | }
131 |
132 | if (mSuccess.size != mFollowNum) {
133 | Thread.sleep(2500)
134 | getTbs()
135 | }
136 | roundCount--
137 | }
138 | } catch (e: Exception) {
139 | XposedBridge.log("签到部分出现错误 -- $e")
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/AutoSignHelper.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import de.robv.android.xposed.XposedBridge
4 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
5 | import okhttp3.OkHttpClient
6 | import okhttp3.Request
7 | import okhttp3.Request.Builder
8 | import okhttp3.RequestBody
9 | import okhttp3.RequestBody.Companion.toRequestBody
10 | import org.json.JSONObject
11 | import java.io.IOException
12 | import java.math.BigInteger
13 | import java.nio.charset.StandardCharsets
14 | import java.security.MessageDigest
15 |
16 | object AutoSignHelper {
17 |
18 | private lateinit var sCookie: String
19 |
20 | fun setCookie(BDUSS: String) {
21 | sCookie = "BDUSS=$BDUSS"
22 | }
23 |
24 | fun get(url: String): JSONObject {
25 | val okHttpClient = OkHttpClient()
26 | val request: Request = Builder()
27 | .url(url)
28 | .get()
29 | .addHeader("connection", "keep-alive")
30 | .addHeader("Content-Type", "application/x-www-form-urlencoded")
31 | .addHeader("charset", "UTF-8")
32 | .addHeader(
33 | "User-Agent",
34 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36"
35 | )
36 | .addHeader("Cookie", sCookie)
37 | .build()
38 |
39 | val respContent = try {
40 | val response = okHttpClient.newCall(request).execute()
41 | if (response.isSuccessful) {
42 | response.body?.string()
43 | } else {
44 | throw IOException("Response code: ${response.code}")
45 | }
46 | } catch (e: IOException) {
47 | XposedBridge.log("get请求错误 -- $e")
48 | null
49 | }
50 |
51 | return respContent?.let { JSONObject(it) } ?: JSONObject()
52 | }
53 |
54 | fun post(url: String, body: String): JSONObject {
55 | val mediaType = "text/x-markdown; charset=utf-8".toMediaTypeOrNull()
56 | val stringBody: RequestBody = body.toRequestBody(mediaType)
57 |
58 | val request: Request = Builder()
59 | .url(url)
60 | .post(stringBody)
61 | .addHeader("connection", "keep-alive")
62 | .addHeader("Host", "tieba.baidu.com")
63 | .addHeader("Content-Type", "application/x-www-form-urlencoded")
64 | .addHeader("charset", "UTF-8")
65 | .addHeader(
66 | "User-Agent",
67 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36"
68 | )
69 | .addHeader("Cookie", sCookie)
70 | .build()
71 |
72 | val respContent = try {
73 | val response = OkHttpClient().newCall(request).execute()
74 | if (response.isSuccessful) {
75 | response.body?.string()
76 | } else {
77 | throw IOException("Response code: ${response.code}")
78 | }
79 | } catch (e: IOException) {
80 | XposedBridge.log("post请求错误 -- $e")
81 | null
82 | }
83 |
84 | return respContent?.let { JSONObject(it) } ?: JSONObject()
85 | }
86 |
87 | fun enCodeMd5(str: String): String {
88 | return try {
89 | // 生成一个MD5加密计算摘要
90 | val md = MessageDigest.getInstance("MD5")
91 | // 计算md5函数
92 | md.update(str.toByteArray(StandardCharsets.UTF_8))
93 | // digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
94 | // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
95 | //一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)
96 | BigInteger(1, md.digest()).toString(16)
97 | } catch (e: Exception) {
98 | XposedBridge.log("字符串进行MD5加密错误 -- $e")
99 | ""
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/FrsTab.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.AcRules.findRule
6 | import gm.tieba.tabswitch.hooker.IHooker
7 | import gm.tieba.tabswitch.hooker.Obfuscated
8 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
9 | import gm.tieba.tabswitch.hooker.deobfuscation.StringMatcher
10 | import java.lang.reflect.Method
11 |
12 | class FrsTab : XposedContext(), IHooker, Obfuscated {
13 |
14 | override fun key(): String {
15 | return "frs_tab"
16 | }
17 |
18 | override fun matchers(): List {
19 | return listOf(
20 | StringMatcher("forum_tab_current_list"),
21 | StringMatcher("c/f/frs/page?cmd=301001&format=protobuf")
22 | )
23 | }
24 |
25 | private var mPosition = 0
26 |
27 | override fun hook() {
28 | hookBeforeMethod(
29 | "tbclient.FrsPage.DataRes\$Builder",
30 | "build", Boolean::class.javaPrimitiveType,
31 | ) { param ->
32 | val tabList = XposedHelpers.getObjectField(param.thisObject, "frs_main_tab_list") as? List<*>
33 | tabList?.forEachIndexed { index, tab ->
34 | if (XposedHelpers.getObjectField(tab, "tab_type") as Int == 14) {
35 | mPosition = index
36 | XposedHelpers.setObjectField(
37 | param.thisObject,
38 | "frs_tab_default",
39 | XposedHelpers.getObjectField(tab, "tab_id") as Int
40 | )
41 | return@hookBeforeMethod
42 | }
43 | }
44 | }
45 |
46 | findRule(matchers()) { matcher, clazz, method ->
47 | when (matcher) {
48 | "forum_tab_current_list" -> {
49 | if ("com.baidu.tieba.forum.controller.TopController" != clazz) return@findRule
50 | val targetMethod: Method = try {
51 | XposedHelpers.findMethodBestMatch(
52 | findClass(clazz),
53 | method,
54 | null,
55 | findClass(clazz)
56 | )
57 | } catch (e: NoSuchMethodError) { // 12.57+
58 | return@findRule
59 | }
60 |
61 | hookAfterMethod(targetMethod) { param ->
62 | val viewPager: Any? = try {
63 | XposedHelpers.findFirstFieldByExactType(
64 | param.args[1].javaClass,
65 | findClass("com.baidu.tbadk.widget.CustomViewPager")
66 | )[param.args[1]]
67 | } catch (e: NoSuchFieldError) { // 12.56+
68 | XposedHelpers.findFirstFieldByExactType(
69 | param.args[1].javaClass,
70 | findClass("androidx.viewpager.widget.ViewPager")
71 | )[param.args[1]]
72 | }
73 | XposedHelpers.callMethod(viewPager, "setCurrentItem", mPosition)
74 | }
75 | }
76 |
77 | "c/f/frs/page?cmd=301001&format=protobuf" -> {
78 | hookBeforeMethod(
79 | clazz, method, "com.baidu.tieba.forum.model.FrsPageRequestMessage"
80 | ) { param ->
81 | if (XposedHelpers.getObjectField(param.args[0], "sortType") as Int == -1) {
82 | val sharedPrefHelper = XposedHelpers.callStaticMethod(
83 | findClass("com.baidu.tbadk.core.sharedPref.SharedPrefHelper"),
84 | "getInstance"
85 | )
86 | val lastSortType = XposedHelpers.callMethod(sharedPrefHelper, "getInt", "key_forum_last_sort_type", 0) as Int
87 | XposedHelpers.setObjectField(param.args[0], "sortType", lastSortType)
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/MsgCenterTab.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import gm.tieba.tabswitch.XposedContext
4 | import gm.tieba.tabswitch.hooker.IHooker
5 | import gm.tieba.tabswitch.util.setObjectField
6 |
7 | class MsgCenterTab : XposedContext(), IHooker {
8 |
9 | override fun key(): String {
10 | return "msg_center_tab"
11 | }
12 |
13 | override fun hook() {
14 | findClass("com.baidu.tieba.immessagecenter.msgtab.ui.view.MsgCenterContainerView").declaredMethods.filter {
15 | it.parameterTypes.isEmpty() && it.returnType == Long::class.javaPrimitiveType
16 | }.forEach { method ->
17 | hookBeforeMethod(
18 | "com.baidu.tieba.immessagecenter.msgtab.ui.view.MsgCenterContainerView",
19 | method.name
20 | ) { param ->
21 | setObjectField(param.thisObject, Long::class.javaObjectType, -1L)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/NotificationDetect.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import gm.tieba.tabswitch.XposedContext
4 | import gm.tieba.tabswitch.hooker.IHooker
5 |
6 | class NotificationDetect : XposedContext(), IHooker {
7 |
8 | override fun key(): String {
9 | return "notification_detect"
10 | }
11 |
12 | override fun hook() {
13 | // 禁止检测通知开启状态
14 | hookReplaceMethod(
15 | "androidx.core.app.NotificationManagerCompat",
16 | "areNotificationsEnabled"
17 | ) { true }
18 | hookReplaceMethod(
19 | "com.baidu.tieba.push.PushSceneGroup",
20 | "getLimit"
21 | ) { 0 }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/OpenSign.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.View
7 | import gm.tieba.tabswitch.XposedContext
8 | import gm.tieba.tabswitch.dao.Preferences.getIsSigned
9 | import gm.tieba.tabswitch.dao.Preferences.putSignDate
10 | import gm.tieba.tabswitch.hooker.IHooker
11 | import java.util.Calendar
12 |
13 | class OpenSign : XposedContext(), IHooker {
14 |
15 | override fun key(): String {
16 | return "open_sign"
17 | }
18 |
19 | override fun hook() {
20 | hookAfterMethod(
21 | "com.baidu.tieba.tblauncher.MainTabActivity",
22 | "onCreate", Bundle::class.java
23 | ) { param ->
24 | val activity = param.thisObject as Activity
25 | if (!getIsSigned() && Calendar.getInstance()[Calendar.HOUR_OF_DAY] != 0) {
26 | val intent = Intent().setClassName(activity, "com.baidu.tieba.signall.SignAllForumActivity")
27 | activity.startActivity(intent)
28 | }
29 | }
30 | hookAfterMethod(
31 | "com.baidu.tieba.signall.SignAllForumActivity",
32 | "onClick", View::class.java
33 | ) { param ->
34 | val activity = param.thisObject as Activity
35 | if (!getIsSigned() && Calendar.getInstance()[Calendar.HOUR_OF_DAY] != 0) {
36 | putSignDate()
37 | activity.finish()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/OriginSrc.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import android.net.ConnectivityManager.NetworkCallback
7 | import android.net.Network
8 | import android.net.NetworkCapabilities
9 | import android.net.NetworkRequest
10 | import android.net.Uri
11 | import de.robv.android.xposed.XC_MethodHook
12 | import de.robv.android.xposed.XposedHelpers
13 | import gm.tieba.tabswitch.XposedContext
14 | import gm.tieba.tabswitch.dao.AcRules.findRule
15 | import gm.tieba.tabswitch.dao.Preferences.getBoolean
16 | import gm.tieba.tabswitch.hooker.IHooker
17 | import org.json.JSONException
18 | import org.json.JSONObject
19 |
20 | class OriginSrc : XposedContext(), IHooker {
21 |
22 | override fun key(): String {
23 | return "origin_src"
24 | }
25 |
26 | @SuppressLint("MissingPermission")
27 | override fun hook() {
28 | if (getBoolean("origin_src_only_wifi")) {
29 | val networkCallback = NetworkCallbackImpl()
30 | val builder = NetworkRequest.Builder()
31 | val request = builder.build()
32 | val connMgr = getContext().getSystemService(
33 | Context.CONNECTIVITY_SERVICE
34 | ) as ConnectivityManager
35 | connMgr.registerNetworkCallback(request, networkCallback)
36 | } else {
37 | doHook()
38 | }
39 | }
40 |
41 | private class NetworkCallbackImpl : NetworkCallback() {
42 | override fun onCapabilitiesChanged(
43 | network: Network,
44 | networkCapabilities: NetworkCapabilities
45 | ) {
46 | super.onCapabilitiesChanged(network, networkCapabilities)
47 | if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
48 | && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
49 | ) {
50 | doHook()
51 | } else {
52 | doUnHook()
53 | }
54 | }
55 | }
56 |
57 | companion object {
58 |
59 | private val unhookList = mutableListOf()
60 |
61 | private fun doHook() {
62 | if (unhookList.isNotEmpty()) return
63 |
64 | findRule("pic_amount") { _, clazz, method ->
65 | unhookList.add(hookBeforeMethod(
66 | clazz, method, JSONObject::class.java, Boolean::class.javaObjectType
67 | ) { param ->
68 | val jsonObject = param.args[0] as JSONObject
69 |
70 | jsonObject.optJSONArray("pic_list")?.let { picList ->
71 | for (i in 0 until picList.length()) {
72 | val pic = picList.optJSONObject(i)
73 | val img = pic.getJSONObject("img")
74 | val original = img.getJSONObject("original").apply {
75 | put("big_cdn_src", getString("original_src"))
76 | }
77 | img.put("original", original)
78 | pic.apply {
79 | put("img", img)
80 | put("show_original_btn", 0)
81 | }
82 | }
83 | jsonObject.put("pic_list", picList)
84 | }
85 | })
86 |
87 | unhookList.add(hookBeforeMethod(
88 | "tbclient.PbContent\$Builder",
89 | "build", Boolean::class.javaPrimitiveType,
90 | ) { param ->
91 | XposedHelpers.setObjectField(param.thisObject, "show_original_btn", 0)
92 | arrayOf("big_cdn_src", "cdn_src", "cdn_src_active").forEach { field ->
93 | XposedHelpers.setObjectField(
94 | param.thisObject,
95 | field,
96 | XposedHelpers.getObjectField(param.thisObject, "origin_src")
97 | )
98 | }
99 | })
100 |
101 | unhookList.add(hookBeforeMethod(
102 | "tbclient.Media\$Builder",
103 | "build", Boolean::class.javaPrimitiveType,
104 | ) { param ->
105 | XposedHelpers.setObjectField(param.thisObject, "show_original_btn", 0)
106 | arrayOf("small_pic", "water_pic").forEach { field ->
107 | XposedHelpers.setObjectField(
108 | param.thisObject,
109 | field,
110 | XposedHelpers.getObjectField(param.thisObject, "big_pic")
111 | )
112 | }
113 | })
114 |
115 | unhookList.add(hookBeforeMethod(
116 | "tbclient.PicInfo\$Builder",
117 | "build", Boolean::class.javaPrimitiveType,
118 | ) { param ->
119 | arrayOf("small_pic_url", "big_pic_url").forEach { field ->
120 | XposedHelpers.setObjectField(
121 | param.thisObject,
122 | field,
123 | XposedHelpers.getObjectField(param.thisObject, "origin_pic_url")
124 | )
125 | }
126 | })
127 |
128 | unhookList.add(hookBeforeMethod(
129 | "tbclient.FeedPicComponent\$Builder",
130 | "build", Boolean::class.javaPrimitiveType,
131 | ) { param ->
132 | val schema = XposedHelpers.getObjectField(param.thisObject, "schema") as? String
133 | val paramsJson = schema?.let { Uri.parse(it).getQueryParameter("params") }
134 |
135 | paramsJson?.let { schemaParams ->
136 | val jsonObject = JSONObject(schemaParams)
137 | try {
138 | val pageParams = jsonObject.getJSONObject("pageParams")
139 | val picDataList = pageParams.getJSONArray("pic_data_list")
140 | for (i in 0 until picDataList.length()) {
141 | val picData = picDataList.getJSONObject(i)
142 | val originPicUrl = picData.getString("origin_pic_url")
143 | picData.apply {
144 | put("big_pic_url", originPicUrl)
145 | put("small_pic_url", originPicUrl)
146 | put("is_show_origin_btn", 0)
147 | }
148 | }
149 | val modifiedUri = "tiebaapp://router/portal?params=$jsonObject"
150 | XposedHelpers.setObjectField(param.thisObject, "schema", modifiedUri)
151 | } catch (ignored: JSONException) {
152 | }
153 | }
154 | })
155 | }
156 | }
157 |
158 | private fun doUnHook() {
159 | if (unhookList.isEmpty()) return
160 | unhookList.forEach { it.unhook() }
161 | unhookList.clear()
162 | }
163 | }
164 | }
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/auto/TransitionAnimation.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.auto
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import de.robv.android.xposed.XposedHelpers
6 | import gm.tieba.tabswitch.XposedContext
7 | import gm.tieba.tabswitch.hooker.IHooker
8 | import gm.tieba.tabswitch.hooker.deobfuscation.DeobfuscationHelper.isTbSatisfyVersionRequirement
9 |
10 | @Suppress("DEPRECATION")
11 | class TransitionAnimation : XposedContext(), IHooker {
12 |
13 | private var CHAT_SQUARE_FADE_IN = 0
14 | private var CHAT_SQUARE_FADE_OUT = 0
15 | private var RES_BIG_IMAGE_IN_FROM_RIGHT = 0
16 | private var RES_BIG_IMAGE_OUT_TO_RIGHT = 0
17 | private var RES_CUSTOM_FADE_IN = 0
18 | private var RES_CUSTOM_FADE_OUT = 0
19 | private var RES_CUSTOM_IN_FROM_RIGHT = 0
20 | private var RES_CUSTOM_OUT_TO_RIGHT = 0
21 | private var RES_FADE_OUT = 0
22 | private var RES_NFADE_IN = 0
23 | private var RES_NORMAL_IN_FROM_BOTTOM = 0
24 | private var RES_NORMAL_IN_FROM_LEFT = 0
25 | private var RES_NORMAL_IN_FROM_RIGHT = 0
26 | private var RES_NORMAL_OUT_TO_BOTTOM = 0
27 | private var RES_NORMAL_OUT_TO_LEFT = 0
28 | private var RES_NORMAL_OUT_TO_RIGHT = 0
29 | private lateinit var activityPendingTransitionFactory: Class<*>
30 |
31 | override fun key(): String {
32 | return "transition_animation"
33 | }
34 |
35 | override fun hook() {
36 | if (!(Build.VERSION.SDK_INT >= 34 && isTbSatisfyVersionRequirement("12.58.2.1"))) {
37 | return
38 | }
39 |
40 | activityPendingTransitionFactory =
41 | findClass("com.baidu.tbadk.ActivityPendingTransitionFactory")
42 |
43 | CHAT_SQUARE_FADE_IN = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "CHAT_SQUARE_FADE_IN")
44 | CHAT_SQUARE_FADE_OUT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "CHAT_SQUARE_FADE_OUT")
45 | RES_BIG_IMAGE_IN_FROM_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_BIG_IMAGE_IN_FROM_RIGHT")
46 | RES_BIG_IMAGE_OUT_TO_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_BIG_IMAGE_OUT_TO_RIGHT")
47 | RES_CUSTOM_FADE_IN = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_CUSTOM_FADE_IN")
48 | RES_CUSTOM_FADE_OUT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_CUSTOM_FADE_OUT")
49 | RES_CUSTOM_IN_FROM_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_CUSTOM_IN_FROM_RIGHT")
50 | RES_CUSTOM_OUT_TO_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_CUSTOM_OUT_TO_RIGHT")
51 | RES_FADE_OUT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_FADE_OUT")
52 | RES_NFADE_IN = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NFADE_IN")
53 | RES_NORMAL_IN_FROM_BOTTOM = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_IN_FROM_BOTTOM")
54 | RES_NORMAL_IN_FROM_LEFT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_IN_FROM_LEFT")
55 | RES_NORMAL_IN_FROM_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_IN_FROM_RIGHT")
56 | RES_NORMAL_OUT_TO_BOTTOM = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_OUT_TO_BOTTOM")
57 | RES_NORMAL_OUT_TO_LEFT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_OUT_TO_LEFT")
58 | RES_NORMAL_OUT_TO_RIGHT = XposedHelpers.getStaticIntField(activityPendingTransitionFactory, "RES_NORMAL_OUT_TO_RIGHT")
59 |
60 | hookReplaceMethod(
61 | activityPendingTransitionFactory,
62 | "enterExitAnimation",
63 | "com.baidu.tbadk.TbPageContext", Int::class.javaPrimitiveType
64 | ) { param ->
65 | enterExitAnimation(param.args[0], param.args[1] as Int)
66 | }
67 |
68 | hookReplaceMethod(
69 | activityPendingTransitionFactory,
70 | "closeAnimation",
71 | "com.baidu.tbadk.TbPageContext", Int::class.javaPrimitiveType
72 | ) { param ->
73 | closeAnimation(param.args[0], param.args[1] as Int)
74 | }
75 | }
76 |
77 | private fun enterExitAnimation(tbPageContext: Any, i: Int) {
78 | var animationType = i
79 | val pageActivity = XposedHelpers.callMethod(tbPageContext, "getPageActivity") as Activity
80 | if (XposedHelpers.getStaticBooleanField(activityPendingTransitionFactory, "IS_CUSTOM_FROM_THIRD_PARTY")) {
81 | animationType = 3
82 | }
83 | when (animationType) {
84 | 0 -> pageActivity.overridePendingTransition(0, 0)
85 | 1 -> pageActivity.overridePendingTransition(RES_NORMAL_IN_FROM_RIGHT, RES_FADE_OUT)
86 | 2 -> pageActivity.overridePendingTransition(RES_BIG_IMAGE_IN_FROM_RIGHT, RES_FADE_OUT)
87 | 3 -> pageActivity.overridePendingTransition(RES_CUSTOM_IN_FROM_RIGHT, RES_CUSTOM_FADE_OUT)
88 | 4 -> pageActivity.overridePendingTransition(RES_NORMAL_IN_FROM_BOTTOM, RES_FADE_OUT)
89 | 5 -> pageActivity.overridePendingTransition(CHAT_SQUARE_FADE_IN, CHAT_SQUARE_FADE_OUT)
90 | 6 -> pageActivity.overridePendingTransition(RES_NORMAL_IN_FROM_LEFT, RES_FADE_OUT)
91 | else -> pageActivity.overridePendingTransition(RES_NORMAL_IN_FROM_RIGHT, RES_FADE_OUT)
92 | }
93 | }
94 |
95 | private fun closeAnimation(tbPageContext: Any, i: Int) {
96 | var animationType = i
97 | val pageActivity = XposedHelpers.callMethod(tbPageContext, "getPageActivity") as Activity
98 | if (XposedHelpers.getStaticBooleanField(activityPendingTransitionFactory, "IS_CUSTOM_FROM_THIRD_PARTY")) {
99 | animationType = 3
100 | }
101 | when (animationType) {
102 | 0 -> pageActivity.overridePendingTransition(0, 0)
103 | 1 -> pageActivity.overridePendingTransition(RES_NFADE_IN, RES_NORMAL_OUT_TO_RIGHT)
104 | 2 -> pageActivity.overridePendingTransition(RES_NFADE_IN, RES_BIG_IMAGE_OUT_TO_RIGHT)
105 | 3 -> pageActivity.overridePendingTransition(RES_CUSTOM_FADE_IN, RES_CUSTOM_OUT_TO_RIGHT)
106 | 4 -> pageActivity.overridePendingTransition(RES_NFADE_IN, RES_NORMAL_OUT_TO_BOTTOM)
107 | 5 -> pageActivity.overridePendingTransition(CHAT_SQUARE_FADE_IN, CHAT_SQUARE_FADE_OUT)
108 | 6 -> pageActivity.overridePendingTransition(RES_NFADE_IN, RES_NORMAL_OUT_TO_LEFT)
109 | else -> pageActivity.overridePendingTransition(RES_NFADE_IN, RES_NORMAL_OUT_TO_RIGHT)
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/deobfuscation/Deobfuscation.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.deobfuscation
2 |
3 | import android.content.Context
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.AcRules.putRule
6 | import gm.tieba.tabswitch.dao.Preferences.putSignature
7 | import org.luckypray.dexkit.DexKitBridge
8 | import org.luckypray.dexkit.query.FindClass
9 | import org.luckypray.dexkit.query.FindMethod
10 | import org.luckypray.dexkit.result.MethodDataList
11 | import java.util.Objects
12 | import java.util.zip.ZipFile
13 |
14 | class Deobfuscation : XposedContext() {
15 |
16 | private val matchers: MutableList = ArrayList()
17 | private lateinit var packageResource: String
18 |
19 | fun setMatchers(matchers: List) {
20 | this.matchers.clear()
21 | this.matchers.addAll(matchers)
22 | }
23 |
24 | private fun forEachProgressed(
25 | hooker: DeobfuscationHooker,
26 | collection: Collection,
27 | action: (T) -> Unit
28 | ) {
29 | val size = collection.size
30 | collection.forEachIndexed { index, item ->
31 | hooker.progress = (index + 1).toFloat() / size
32 | action(item)
33 | }
34 | }
35 |
36 | fun dexkit(context: Context, hooker: DeobfuscationHooker) {
37 | load("dexkit")
38 | packageResource = context.packageResourcePath
39 | val bridge = DexKitBridge.create(packageResource)
40 | Objects.requireNonNull(bridge)
41 |
42 | forEachProgressed(hooker, matchers) { matcher: Matcher ->
43 | val methodDataList = MethodDataList()
44 |
45 | matcher.classMatcher?.let { classMatcher ->
46 | bridge.findClass(FindClass.create().matcher(classMatcher))
47 | .flatMapTo(methodDataList) { classData ->
48 | findMethod(bridge, FindMethod.create().searchPackages(classData.name), matcher)
49 | }
50 | } ?: methodDataList.addAll(findMethod(bridge, FindMethod.create(), matcher))
51 |
52 | methodDataList.forEach { methodData ->
53 | putRule(matcher.toString(), methodData.className, methodData.name)
54 | }
55 | }
56 |
57 | bridge.close()
58 | }
59 |
60 | private fun findMethod(bridge: DexKitBridge, baseMethodQuery: FindMethod, matcher: Matcher): MethodDataList {
61 | return bridge.findMethod(baseMethodQuery.matcher(matcher.methodMatcher))
62 | }
63 |
64 | fun saveDexSignatureHashCode() {
65 | ZipFile(packageResource).use { apk ->
66 | apk.getInputStream(apk.getEntry("classes.dex")).use { inputStream ->
67 | val signatureHashCode = DeobfuscationHelper.calcSignature(inputStream).contentHashCode()
68 | putSignature(signatureHashCode)
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/deobfuscation/DeobfuscationHelper.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.deobfuscation
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.pm.PackageManager
8 | import android.os.Bundle
9 | import de.robv.android.xposed.XposedBridge
10 | import gm.tieba.tabswitch.XposedContext.Companion.hookBeforeMethod
11 | import gm.tieba.tabswitch.dao.Preferences.getSignature
12 | import gm.tieba.tabswitch.util.restart
13 | import java.io.File
14 | import java.io.IOException
15 | import java.io.InputStream
16 | import java.security.MessageDigest
17 | import java.security.NoSuchAlgorithmException
18 | import java.util.zip.ZipFile
19 | import kotlin.math.max
20 |
21 | object DeobfuscationHelper {
22 |
23 | private const val SIGNATURE_DATA_START_OFFSET = 32
24 | private const val SIGNATURE_SIZE = 20
25 | lateinit var sCurrentTbVersion: String
26 |
27 | fun calcSignature(dataStoreInput: InputStream): ByteArray {
28 | val md: MessageDigest = try {
29 | MessageDigest.getInstance("SHA-1")
30 | } catch (ex: NoSuchAlgorithmException) {
31 | throw RuntimeException(ex)
32 | }
33 |
34 | dataStoreInput.skip(SIGNATURE_DATA_START_OFFSET.toLong())
35 | val buffer = ByteArray(4 * 1024)
36 |
37 | dataStoreInput.use { input ->
38 | generateSequence { input.read(buffer).takeIf { it >= 0 } }
39 | .forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
40 | }
41 |
42 | return md.digest().also { signature ->
43 | check(signature.size == SIGNATURE_SIZE) { "unexpected digest write: ${signature.size} bytes" }
44 | }
45 | }
46 |
47 | fun isVersionChanged(context: Context): Boolean {
48 | val tsConfig = context.getSharedPreferences("TS_config", Context.MODE_PRIVATE)
49 | return tsConfig.getString("deobfs_version", "unknown") != getTbVersion(context)
50 | }
51 |
52 | fun isDexChanged(context: Context): Boolean {
53 | return try {
54 | ZipFile(File(context.packageResourcePath)).use { zipFile ->
55 | zipFile.getEntry("classes.dex")?.let { entry ->
56 | zipFile.getInputStream(entry).use { inputStream ->
57 | calcSignature(inputStream).contentHashCode() != getSignature()
58 | }
59 | } ?: false
60 | }
61 | } catch (e: IOException) {
62 | XposedBridge.log(e)
63 | false
64 | }
65 | }
66 |
67 | @Suppress("DEPRECATION")
68 | fun getTbVersion(context: Context): String {
69 | val pm = context.packageManager
70 | try {
71 | val applicationInfo = pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
72 | return when (applicationInfo.metaData["versionType"] as Int?) {
73 | 3 -> pm.getPackageInfo(context.packageName, 0).versionName
74 | 2 -> applicationInfo.metaData["grayVersion"].toString()
75 | 1 -> applicationInfo.metaData["subVersion"].toString()
76 | else -> throw PackageManager.NameNotFoundException("unknown tb version")
77 | }
78 | } catch (e: PackageManager.NameNotFoundException) {
79 | XposedBridge.log(e)
80 | return "unknown"
81 | }
82 | }
83 |
84 | @SuppressLint("ApplySharedPref")
85 | fun saveAndRestart(activity: Activity, version: String, trampoline: Class<*>?) {
86 | activity.getSharedPreferences("TS_config", Context.MODE_PRIVATE)
87 | .edit()
88 | .putString("deobfs_version", version)
89 | .commit()
90 |
91 | trampoline?.let { trampolineClass ->
92 | hookBeforeMethod(trampolineClass, "onCreate", Bundle::class.java) { param ->
93 | restart(param.thisObject as Activity)
94 | }
95 | activity.startActivity(
96 | Intent(activity, trampolineClass).apply {
97 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
98 | }
99 | )
100 | } ?: restart(activity)
101 | }
102 |
103 | // Adapted from https://stackoverflow.com/questions/198431/how-do-you-compare-two-version-strings-in-java
104 | fun isTbSatisfyVersionRequirement(requiredVersion: String): Boolean {
105 | val currParts = sCurrentTbVersion.split(".")
106 | val reqParts = requiredVersion.split(".")
107 |
108 | val length = max(currParts.size, reqParts.size)
109 | for (i in 0 until length) {
110 | try {
111 | val currPart = currParts.getOrNull(i)?.toInt() ?: 0
112 | val reqPart = reqParts.getOrNull(i)?.toInt() ?: 0
113 | if (currPart != reqPart) {
114 | return currPart > reqPart
115 | }
116 | } catch (e: NumberFormatException) {
117 | return false
118 | }
119 | }
120 | return true
121 | }
122 |
123 | // Inclusive of both ends
124 | fun isTbBetweenVersionRequirement(lower: String, upper: String): Boolean {
125 | return (isTbSatisfyVersionRequirement(lower)
126 | && (!isTbSatisfyVersionRequirement(upper) || sCurrentTbVersion == upper))
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/deobfuscation/DeobfuscationHooker.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.deobfuscation
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.app.Instrumentation
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.graphics.Color
9 | import android.os.Bundle
10 | import android.os.IBinder
11 | import android.util.Log
12 | import android.view.Gravity
13 | import android.view.View
14 | import android.widget.FrameLayout
15 | import android.widget.LinearLayout
16 | import android.widget.RelativeLayout
17 | import android.widget.TextView
18 | import de.robv.android.xposed.XC_MethodHook
19 | import de.robv.android.xposed.XposedBridge
20 | import gm.tieba.tabswitch.XposedContext
21 | import gm.tieba.tabswitch.dao.AcRules.dropAllRules
22 | import gm.tieba.tabswitch.dao.Preferences.getBoolean
23 | import gm.tieba.tabswitch.hooker.IHooker
24 | import gm.tieba.tabswitch.hooker.deobfuscation.DeobfuscationHelper.getTbVersion
25 | import gm.tieba.tabswitch.hooker.deobfuscation.DeobfuscationHelper.isDexChanged
26 | import gm.tieba.tabswitch.hooker.deobfuscation.DeobfuscationHelper.saveAndRestart
27 | import kotlin.concurrent.thread
28 | import kotlin.properties.Delegates
29 |
30 | class DeobfuscationHooker(private val mMatchers: List) : XposedContext(), IHooker {
31 |
32 | var progress : Float by Delegates.observable(0f) { _, _, new ->
33 | updateProgress(new)
34 | }
35 |
36 | private val deobfuscation = Deobfuscation()
37 | private lateinit var mActivity: Activity
38 | private lateinit var mProgress: View
39 | private lateinit var mMessage: TextView
40 | private lateinit var mProgressContainer: FrameLayout
41 | private lateinit var mContentView: LinearLayout
42 |
43 | override fun key(): String {
44 | return "deobfs"
45 | }
46 |
47 | @SuppressLint("ApplySharedPref", "CheckResult")
48 | override fun hook() {
49 | hookAfterMethod(
50 | "com.baidu.tieba.LogoActivity",
51 | "onCreate", Bundle::class.java
52 | ) { param ->
53 | val hooks = disableStartAndFinishActivity()
54 | mActivity = param.thisObject as Activity
55 | if (getBoolean("purge")) {
56 | mActivity.getSharedPreferences("settings", Context.MODE_PRIVATE)
57 | .edit()
58 | .putString("key_location_request_dialog_last_show_version", getTbVersion(mActivity))
59 | .commit()
60 | }
61 |
62 | if (isDexChanged(mActivity)) {
63 | dropAllRules()
64 | } else {
65 | hooks.forEach { it.unhook() }
66 | saveAndRestart(mActivity, getTbVersion(mActivity), findClass(TRAMPOLINE_ACTIVITY))
67 | return@hookAfterMethod
68 | }
69 |
70 | initProgressIndicator()
71 | mActivity.addContentView(
72 | mContentView, LinearLayout.LayoutParams(
73 | LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT
74 | )
75 | )
76 |
77 | thread {
78 | try {
79 | setMessage("搜索资源,字符串和方法调用")
80 | performDeobfuscation(mActivity, mMatchers)
81 |
82 | XposedBridge.log("Deobfuscation complete, current version: ${getTbVersion(mActivity)}")
83 | hooks.forEach { it.unhook() }
84 | saveAndRestart(
85 | mActivity,
86 | getTbVersion(mActivity),
87 | findClass(TRAMPOLINE_ACTIVITY)
88 | )
89 | } catch (e: Throwable) {
90 | XposedBridge.log(e)
91 | setMessage("处理失败\n${Log.getStackTraceString(e)}")
92 | }
93 | }
94 | }
95 | }
96 |
97 | private fun performDeobfuscation(context: Context, matchers: List) {
98 | deobfuscation.setMatchers(matchers)
99 | deobfuscation.dexkit(context, this)
100 | deobfuscation.saveDexSignatureHashCode()
101 | }
102 |
103 | private fun disableStartAndFinishActivity(): List {
104 | return listOf(
105 | hookReplaceMethod(Instrumentation::class.java, "execStartActivity",
106 | Context::class.java, IBinder::class.java, IBinder::class.java, Activity::class.java, Intent::class.java,
107 | Int::class.javaPrimitiveType, Bundle::class.java) { null },
108 | hookReplaceMethod(Activity::class.java, "finish",
109 | Int::class.javaPrimitiveType) { null },
110 | hookReplaceMethod(Activity::class.java, "finishActivity",
111 | Int::class.javaPrimitiveType) { null },
112 | hookReplaceMethod(Activity::class.java, "finishAffinity") { null }
113 | )
114 | }
115 |
116 | @SuppressLint("SetTextI18n")
117 | private fun initProgressIndicator() {
118 | val title = TextView(mActivity).apply {
119 | textSize = 16f
120 | setPaddingRelative(0, 0, 0, 8)
121 | textAlignment = View.TEXT_ALIGNMENT_CENTER
122 | setTextColor(Color.parseColor("#FF303030"))
123 | text = "贴吧TS正在定位被混淆的类和方法,请耐心等待"
124 | }
125 |
126 | mProgress = View(mActivity).apply {
127 | setBackgroundColor(Color.parseColor("#FFBEBEBE"))
128 | }
129 |
130 | mMessage = TextView(mActivity).apply {
131 | textAlignment = View.TEXT_ALIGNMENT_CENTER
132 | textSize = 16f
133 | setTextColor(Color.parseColor("#FF303030"))
134 | layoutParams = FrameLayout.LayoutParams(
135 | RelativeLayout.LayoutParams.MATCH_PARENT,
136 | FrameLayout.LayoutParams.WRAP_CONTENT
137 | )
138 | }
139 |
140 | mProgressContainer = FrameLayout(mActivity).apply {
141 | addView(mProgress)
142 | addView(mMessage)
143 | layoutParams = FrameLayout.LayoutParams(
144 | RelativeLayout.LayoutParams.MATCH_PARENT,
145 | FrameLayout.LayoutParams.WRAP_CONTENT
146 | )
147 | }
148 |
149 | val progressIndicator = LinearLayout(mActivity).apply {
150 | orientation = LinearLayout.VERTICAL
151 | setBackgroundColor(Color.WHITE)
152 | addView(title)
153 | addView(mProgressContainer)
154 | setPaddingRelative(0, 16, 0, 16)
155 | layoutParams = LinearLayout.LayoutParams(
156 | LinearLayout.LayoutParams.MATCH_PARENT,
157 | LinearLayout.LayoutParams.WRAP_CONTENT
158 | )
159 | }
160 |
161 | mContentView = LinearLayout(mActivity).apply {
162 | gravity = Gravity.CENTER
163 | addView(progressIndicator)
164 | }
165 | }
166 |
167 | private fun setMessage(message: String) {
168 | mActivity.runOnUiThread { mMessage.text = message }
169 | }
170 |
171 | private fun updateProgress(progress: Float) {
172 | mActivity.runOnUiThread {
173 | mProgress.layoutParams = mProgress.layoutParams.apply {
174 | height = mMessage.height
175 | width = Math.round(mProgressContainer.width * progress)
176 | }
177 | }
178 | }
179 |
180 | companion object {
181 | private const val TRAMPOLINE_ACTIVITY = "com.baidu.tieba.tblauncher.MainTabActivity"
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/deobfuscation/Matcher.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.deobfuscation
2 |
3 | import org.luckypray.dexkit.query.matchers.ClassMatcher
4 | import org.luckypray.dexkit.query.matchers.MethodMatcher
5 |
6 | abstract class Matcher(private val name: String) {
7 | var classMatcher: ClassMatcher? = null
8 | var requiredVersion: String? = null
9 | abstract val methodMatcher: MethodMatcher
10 |
11 | override fun toString(): String = name
12 | }
13 |
14 | class StringMatcher (str: String, name: String = str) : Matcher(name) {
15 | override val methodMatcher = MethodMatcher.create().usingStrings(str)
16 | }
17 |
18 | class SmaliMatcher (descriptor: String, name: String = descriptor) : Matcher(name) {
19 | override val methodMatcher = MethodMatcher.create().addInvoke(MethodMatcher.create().descriptor(descriptor))
20 | }
21 |
22 | class MethodNameMatcher(methodName: String, name: String) : Matcher(name) {
23 | override val methodMatcher = MethodMatcher.create().name(methodName)
24 | }
25 |
26 | class ReturnTypeMatcher(returnType: Class, name: String) : Matcher(name) {
27 | override val methodMatcher = MethodMatcher.create().returnType(returnType)
28 | }
29 |
30 | class ResMatcher(id: Long, name: String) : Matcher(name) {
31 | override val methodMatcher = MethodMatcher.create().usingNumbers(id)
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/ContentFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 | import gm.tieba.tabswitch.util.parsePbContent
7 |
8 | class ContentFilter : XposedContext(), IHooker, RegexFilter {
9 |
10 | override fun key(): String {
11 | return "content_filter"
12 | }
13 |
14 | override fun hook() {
15 | // 楼层
16 | hookBeforeMethod(
17 | "tbclient.PbPage.DataRes\$Builder",
18 | "build", Boolean::class.javaPrimitiveType
19 | ) { param ->
20 | val postList = XposedHelpers.getObjectField(param.thisObject, "post_list") as? MutableList<*>
21 | val pattern = getPattern() ?: return@hookBeforeMethod
22 | postList?.removeIf { post ->
23 | (XposedHelpers.getObjectField(post, "floor") as Int != 1
24 | && pattern.matcher(parsePbContent(post, "content")).find())
25 | }
26 | }
27 |
28 | // 楼中楼:[\u202e|\ud83c\udd10-\ud83c\udd89]
29 | hookBeforeMethod(
30 | "tbclient.SubPost\$Builder",
31 | "build", Boolean::class.javaPrimitiveType
32 | ) { param ->
33 | val subPostList = XposedHelpers.getObjectField(param.thisObject, "sub_post_list") as? MutableList<*>
34 | val pattern = getPattern() ?: return@hookBeforeMethod
35 | subPostList?.removeIf { subPost -> pattern.matcher(parsePbContent(subPost, "content")).find() }
36 | }
37 |
38 | // 楼层回复
39 | hookBeforeMethod(
40 | "tbclient.PbFloor.DataRes\$Builder",
41 | "build", Boolean::class.javaPrimitiveType
42 | ) { param ->
43 | val subPostList = XposedHelpers.getObjectField(param.thisObject, "subpost_list") as? MutableList<*>
44 | val pattern = getPattern() ?: return@hookBeforeMethod
45 | subPostList?.removeIf { subPost -> pattern.matcher(parsePbContent(subPost, "content")).find() }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/FoldTopCardView.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import gm.tieba.tabswitch.XposedContext
4 | import gm.tieba.tabswitch.hooker.IHooker
5 |
6 | class FoldTopCardView : XposedContext(), IHooker {
7 |
8 | override fun key(): String {
9 | return "fold_top_card_view"
10 | }
11 |
12 | override fun hook() {
13 | // 总是折叠置顶帖
14 | findClass("com.baidu.tieba.forum.view.TopCardView").declaredMethods.filter { method ->
15 | method.returnType == Boolean::class.javaPrimitiveType &&
16 | method.parameterTypes.size == 2 &&
17 | method.parameterTypes[0] == MutableList::class.java &&
18 | method.parameterTypes[1] == Boolean::class.javaPrimitiveType
19 | }.forEach { method ->
20 | hookReplaceMethod(method) { false }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/FollowFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.Preferences.getLikeForum
6 | import gm.tieba.tabswitch.hooker.IHooker
7 | import gm.tieba.tabswitch.widget.TbToast
8 | import gm.tieba.tabswitch.widget.TbToast.Companion.showTbToast
9 |
10 | class FollowFilter : XposedContext(), IHooker {
11 |
12 | override fun key(): String {
13 | return "follow_filter"
14 | }
15 |
16 | override fun hook() {
17 | hookBeforeMethod(
18 | "tbclient.Personalized.DataRes\$Builder",
19 | "build", Boolean::class.javaPrimitiveType
20 | ) { param ->
21 | val forums = getLikeForum() ?: run {
22 | runOnUiThread { showTbToast("暂未获取到关注列表", TbToast.LENGTH_LONG) }
23 | return@hookBeforeMethod
24 | }
25 | val threadList = XposedHelpers.getObjectField(param.thisObject, "thread_list") as? MutableList<*>
26 | threadList?.removeIf { thread -> !forums.contains(XposedHelpers.getObjectField(thread, "fname") as? String) }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/FragmentTab.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.AcRules.findRule
6 | import gm.tieba.tabswitch.dao.Preferences.getBoolean
7 | import gm.tieba.tabswitch.hooker.IHooker
8 | import gm.tieba.tabswitch.hooker.Obfuscated
9 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
10 | import gm.tieba.tabswitch.hooker.deobfuscation.SmaliMatcher
11 | import gm.tieba.tabswitch.hooker.deobfuscation.StringMatcher
12 | import gm.tieba.tabswitch.util.findFirstMethodByExactType
13 | import gm.tieba.tabswitch.util.setObjectField
14 | import org.luckypray.dexkit.query.matchers.ClassMatcher
15 |
16 | class FragmentTab : XposedContext(), IHooker, Obfuscated {
17 |
18 | override fun key(): String {
19 | return "fragment_tab"
20 | }
21 |
22 | override fun matchers(): List {
23 | return listOf(
24 | StringMatcher("has_show_message_tab_tips"),
25 | SmaliMatcher("Lcom/airbnb/lottie/LottieAnimationView;->setImageResource(I)V").apply {
26 | classMatcher = ClassMatcher.create().usingStrings("has_show_message_tab_tips")
27 | }
28 | )
29 | }
30 |
31 | override fun hook() {
32 | findRule("has_show_message_tab_tips") { _, clazz, _ ->
33 | val method = findFirstMethodByExactType(clazz, ArrayList::class.java)
34 | hookBeforeMethod(method) { param ->
35 | val tabsToRemove = HashSet().apply {
36 | if (getBoolean("home_recommend")) {
37 | add("com.baidu.tieba.homepage.framework.RecommendFrsDelegateStatic")
38 | }
39 | if (getBoolean("enter_forum")) {
40 | add("com.baidu.tieba.enterForum.home.EnterForumDelegateStatic")
41 | }
42 | if (getBoolean("write_thread")) {
43 | add("com.baidu.tieba.write.bottomButton.WriteThreadDelegateStatic")
44 | findRule("Lcom/airbnb/lottie/LottieAnimationView;->setImageResource(I)V") { _, clazz, method ->
45 | val md = XposedHelpers.findMethodExactIfExists(clazz, sClassLoader, method)
46 | md?.let {
47 | hookBeforeMethod(md) { param ->
48 | setObjectField(
49 | param.thisObject,
50 | "com.baidu.tbadk.widget.lottie.TBLottieAnimationView",
51 | null
52 | )
53 | param.setResult(null)
54 | }
55 | }
56 | }
57 | }
58 | if (getBoolean("im_message")) {
59 | add("com.baidu.tieba.imMessageCenter.im.chat.notify.ImMessageCenterDelegateStatic")
60 | add("com.baidu.tieba.immessagecenter.im.chat.notify.ImMessageCenterDelegateStatic")
61 | }
62 | }
63 |
64 | val tabList = param.args[0] as? ArrayList<*>
65 | tabList?.removeIf { tab -> tabsToRemove.contains(tab.javaClass.getName()) }
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/FrsPageFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 |
7 | class FrsPageFilter : XposedContext(), IHooker, RegexFilter {
8 |
9 | override fun key(): String {
10 | return "frs_page_filter"
11 | }
12 |
13 | override fun hook() {
14 | hookBeforeMethod(
15 | "tbclient.FrsPage.PageData\$Builder",
16 | "build", Boolean::class.javaPrimitiveType
17 | ) { param-> filterPageData(param.thisObject) }
18 | hookBeforeMethod(
19 | "tbclient.ThreadList.PageData\$Builder",
20 | "build", Boolean::class.javaPrimitiveType
21 | ) { param-> filterPageData(param.thisObject) }
22 | }
23 |
24 | private fun filterPageData(pageData: Any) {
25 | val feedList = XposedHelpers.getObjectField(pageData, "feed_list") as? MutableList<*>
26 | val pattern = getPattern() ?: return
27 |
28 | feedList?.removeIf { feedItem ->
29 | val currFeed = XposedHelpers.getObjectField(feedItem, "feed")
30 |
31 | currFeed?.let { feed ->
32 | val businessInfo = XposedHelpers.getObjectField(feed, "business_info") as? List<*>
33 |
34 | businessInfo?.any { feedKV ->
35 | val currKey = XposedHelpers.getObjectField(feedKV, "key").toString()
36 | when (currKey) {
37 | "title", "abstract" -> {
38 | val str = XposedHelpers.getObjectField(feedKV, "value").toString()
39 | pattern.matcher(str).find()
40 | }
41 | else -> false
42 | }
43 | } ?: false
44 | } ?: false
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/PersonalizedFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 | import gm.tieba.tabswitch.util.parsePbContent
7 |
8 | class PersonalizedFilter : XposedContext(), IHooker, RegexFilter {
9 |
10 | override fun key(): String {
11 | return "personalized_filter"
12 | }
13 |
14 | override fun hook() {
15 | hookBeforeMethod(
16 | "tbclient.Personalized.DataRes\$Builder",
17 | "build", Boolean::class.javaPrimitiveType
18 | ) { param ->
19 | val threadList = XposedHelpers.getObjectField(param.thisObject, "thread_list") as? MutableList<*>
20 | val pattern = getPattern() ?: return@hookBeforeMethod
21 | threadList?.removeIf { thread ->
22 | pattern.matcher(parsePbContent(thread, "first_post_content")).find() ||
23 | pattern.matcher(XposedHelpers.getObjectField(thread, "title") as? String ?: "").find()
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/PurgeEnter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import android.view.View
4 | import android.widget.LinearLayout
5 | import de.robv.android.xposed.XposedHelpers
6 | import de.robv.android.xposed.XposedHelpers.ClassNotFoundError
7 | import gm.tieba.tabswitch.XposedContext
8 | import gm.tieba.tabswitch.dao.AcRules.findRule
9 | import gm.tieba.tabswitch.hooker.IHooker
10 | import gm.tieba.tabswitch.hooker.Obfuscated
11 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
12 | import gm.tieba.tabswitch.hooker.deobfuscation.MethodNameMatcher
13 | import gm.tieba.tabswitch.hooker.deobfuscation.ResMatcher
14 | import gm.tieba.tabswitch.util.findFirstMethodByExactReturnType
15 | import gm.tieba.tabswitch.util.getDimen
16 | import gm.tieba.tabswitch.util.getObjectField
17 | import gm.tieba.tabswitch.util.getR
18 | import org.luckypray.dexkit.query.matchers.ClassMatcher
19 | import java.lang.reflect.Modifier
20 |
21 | class PurgeEnter : XposedContext(), IHooker, Obfuscated {
22 |
23 | private val mLayoutOffset = getDimen("tbds50").toInt()
24 | private var mInitLayoutHeight = -1
25 | private var mPbListViewInnerViewConstructorName: String? = null
26 | private lateinit var mRecForumClassName: String
27 | private lateinit var mRecForumSetNextPageMethodName: String
28 |
29 | override fun key(): String {
30 | return "purge_enter"
31 | }
32 |
33 | override fun matchers(): List {
34 | return listOf(
35 | ResMatcher(getR("dimen", "tbds400").toLong(), "dimen.tbds400").apply {
36 | classMatcher = ClassMatcher.create().usingStrings("enter_forum_login_tip")
37 | },
38 | MethodNameMatcher("onSuccess", "purge_enter_on_success").apply {
39 | classMatcher = ClassMatcher.create().usingStrings("enter_forum_login_tip")
40 | }
41 | )
42 | }
43 |
44 | override fun hook() {
45 | hookReplaceMethod(
46 | "com.baidu.tieba.enterForum.recforum.message.RecommendForumRespondedMessage",
47 | "getRecommendForumData"
48 | ) { null }
49 |
50 | mPbListViewInnerViewConstructorName = findClass("com.baidu.tbadk.core.view.PbListView").superclass.declaredMethods.find { method ->
51 | method.returnType.toString().endsWith("View") && !Modifier.isAbstract(method.modifiers)
52 | }?.name
53 |
54 | findRule(matchers()) { matcher, clazz, method ->
55 | when (matcher) {
56 | "dimen.tbds400" -> {
57 | mRecForumClassName = clazz
58 | mRecForumSetNextPageMethodName = method
59 | hookReplaceMethod(clazz, method) { param ->
60 | val pbListView = getObjectField(param.thisObject, "com.baidu.tbadk.core.view.PbListView")
61 | val pbListViewInnerView =
62 | XposedHelpers.callMethod(pbListView, mPbListViewInnerViewConstructorName) as? View
63 | val bdListView =
64 | getObjectField(param.thisObject, "com.baidu.adp.widget.ListView.BdListView")
65 | if (pbListViewInnerView?.parent == null) {
66 | XposedHelpers.callMethod(bdListView, "setNextPage", pbListView)
67 | XposedHelpers.callMethod(bdListView, "setOverScrollMode", View.OVER_SCROLL_ALWAYS)
68 | }
69 | val linearLayout = getObjectField(pbListView, "android.widget.LinearLayout") as LinearLayout
70 | val layoutParams = LinearLayout.LayoutParams(linearLayout.layoutParams)
71 | if (mInitLayoutHeight == -1) {
72 | mInitLayoutHeight = layoutParams.height + mLayoutOffset
73 | }
74 | layoutParams.height = mInitLayoutHeight
75 | linearLayout.setLayoutParams(layoutParams)
76 | XposedHelpers.callMethod(bdListView, "setExOnSrollToBottomListener", null as Any?)
77 | }
78 | }
79 |
80 | "purge_enter_on_success" ->
81 | hookReplaceMethod(
82 | clazz,
83 | method, Boolean::class.javaPrimitiveType
84 | ) { param ->
85 | val enterForumRec = getObjectField(param.thisObject, mRecForumClassName)
86 | XposedHelpers.callMethod(enterForumRec, mRecForumSetNextPageMethodName)
87 | }
88 | }
89 | }
90 |
91 | try { // 12.56.4.0+ 禁用WebView进吧页
92 | hookReplaceMethod(
93 | findFirstMethodByExactReturnType(
94 | "com.baidu.tieba.enterForum.helper.HybridEnterForumHelper",
95 | Boolean::class.javaPrimitiveType!!
96 | )
97 | ) { false }
98 | } catch (ignored: ClassNotFoundError) {
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/PurgeMy.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import de.robv.android.xposed.XposedBridge.log
6 | import de.robv.android.xposed.XposedHelpers
7 | import gm.tieba.tabswitch.BuildConfig
8 | import gm.tieba.tabswitch.XposedContext
9 | import gm.tieba.tabswitch.dao.AcRules.findRule
10 | import gm.tieba.tabswitch.dao.Preferences.getBoolean
11 | import gm.tieba.tabswitch.hooker.IHooker
12 | import gm.tieba.tabswitch.hooker.Obfuscated
13 | import gm.tieba.tabswitch.hooker.deobfuscation.DeobfuscationHelper
14 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
15 | import gm.tieba.tabswitch.hooker.deobfuscation.SmaliMatcher
16 | import gm.tieba.tabswitch.util.getDimen
17 | import gm.tieba.tabswitch.util.getObjectField
18 | import org.json.JSONArray
19 | import org.luckypray.dexkit.query.matchers.ClassMatcher
20 |
21 | class PurgeMy : XposedContext(), IHooker, Obfuscated {
22 |
23 | private val mGridTopPadding = getDimen("tbds25").toInt()
24 |
25 | override fun key(): String {
26 | return "purge_my"
27 | }
28 |
29 | override fun matchers(): List {
30 | return if (DeobfuscationHelper.isTbBetweenVersionRequirement(
31 | BuildConfig.MIN_VERSION,
32 | "12.67"
33 | )
34 | )
35 | listOf(
36 | SmaliMatcher("Lcom/baidu/tieba/personCenter/view/PersonOftenFuncItemView;->(Landroid/content/Context;)V"),
37 | SmaliMatcher(
38 | "Lcom/baidu/nadcore/download/basic/AdAppStateManager;->instance()Lcom/baidu/nadcore/download/basic/AdAppStateManager;"
39 | ).apply {
40 | classMatcher = ClassMatcher.create().usingStrings("隐私设置")
41 | }
42 | ) else emptyList()
43 | }
44 |
45 | override fun hook() {
46 | if (DeobfuscationHelper.isTbBetweenVersionRequirement(BuildConfig.MIN_VERSION, "12.67")) {
47 | hookBeforeMethod(
48 | "tbclient.Profile.DataRes\$Builder",
49 | "build", Boolean::class.javaPrimitiveType
50 | ) { param ->
51 |
52 | // 我的贴吧会员
53 | XposedHelpers.setObjectField(param.thisObject, "vip_banner", null)
54 |
55 | // 横幅广告
56 | XposedHelpers.setObjectField(param.thisObject, "banner", ArrayList())
57 |
58 | // 度小满 有钱花
59 | XposedHelpers.setObjectField(param.thisObject, "finance_tab", null)
60 |
61 | // 小程序
62 | XposedHelpers.setObjectField(param.thisObject, "recom_naws_list", ArrayList())
63 | }
64 |
65 | hookBeforeMethod(
66 | "tbclient.User\$Builder",
67 | "build", Boolean::class.javaPrimitiveType
68 | ) { param ->
69 | XposedHelpers.setObjectField(param.thisObject, "user_growth", null)
70 | }
71 |
72 | // Add padding to the top of 常用功能
73 | findRule(matchers()) { matcher, clazz, method ->
74 | when (matcher) {
75 | "Lcom/baidu/tieba/personCenter/view/PersonOftenFuncItemView;->(Landroid/content/Context;)V" ->
76 | hookAfterConstructor(
77 | clazz,
78 | "com.baidu.tbadk.TbPageContext"
79 | ) { param ->
80 | val mView = getObjectField(param.thisObject, View::class.java)
81 | mView?.setPadding(
82 | mView.getPaddingLeft(),
83 | mGridTopPadding,
84 | mView.getPaddingRight(),
85 | 0
86 | )
87 | }
88 |
89 | "Lcom/baidu/nadcore/download/basic/AdAppStateManager;->instance()Lcom/baidu/nadcore/download/basic/AdAppStateManager;" ->
90 | hookReplaceMethod(clazz, method) { null }
91 | }
92 | }
93 |
94 | // 12.56+
95 | val personCenterMemberCardViewClass = XposedHelpers.findClassIfExists(
96 | "com.baidu.tieba.personCenter.view.PersonCenterMemberCardView",
97 | sClassLoader
98 | )
99 | personCenterMemberCardViewClass?.let {
100 | hookAfterConstructor(
101 | it,
102 | View::class.java
103 | ) { param ->
104 | val mView = getObjectField(param.thisObject, View::class.java)
105 | (mView?.parent as? ViewGroup)?.removeView(mView)
106 | }
107 | }
108 |
109 | // Skip because we already disabled all AB tests in purge
110 | if (!getBoolean("purge")) {
111 | // 我的页面 AB test
112 | hookBeforeMethod(
113 | "com.baidu.tbadk.abtest.UbsABTestDataManager",
114 | "parseJSONArray",
115 | JSONArray::class.java
116 | ) { param ->
117 | val currentABTestJson = param.args[0] as JSONArray
118 | val newABTestJson = JSONArray()
119 | for (i in 0 until currentABTestJson.length()) {
120 | val currTest = currentABTestJson.getJSONObject(i)
121 | if (!currTest.getString("sid").startsWith("12_64_my_tab_new")) {
122 | newABTestJson.put(currTest)
123 | }
124 | }
125 | param.args[0] = newABTestJson
126 | }
127 | }
128 | } else {
129 | // 12.68.1.0+
130 | hookBeforeMethod(
131 | "tbclient.Profile.DataRes\$Builder",
132 | "build", Boolean::class.javaPrimitiveType
133 | ) { param ->
134 |
135 | val zoneInfo =
136 | XposedHelpers.getObjectField(param.thisObject, "zone_info")!! as List<*>
137 | XposedHelpers.setObjectField(param.thisObject, "zone_info", zoneInfo.filter {
138 | // 保留 常用功能,辅助功能
139 | XposedHelpers.getObjectField(it, "type") in listOf(
140 | "common_func",
141 | "auxiliary_func"
142 | )
143 | })
144 |
145 | // param.thisObject.javaClass.declaredFields.forEach {
146 | // XposedHelpers.getObjectField(param.thisObject, it.name)?.let { value ->
147 | // log("Field: ${it.name}, Value: $value")
148 | // }
149 | // }
150 |
151 | }
152 |
153 | hookBeforeMethod(
154 | "tbclient.Profile.CommonFunc\$Builder",
155 | "build", Boolean::class.javaPrimitiveType
156 | ) { param ->
157 |
158 | // 去除常用功能红点
159 | XposedHelpers.setObjectField(param.thisObject, "red_point_version", 0L)
160 | }
161 | }
162 |
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/PurgeVideo.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 |
7 | class PurgeVideo : XposedContext(), IHooker {
8 |
9 | override fun key(): String {
10 | return "purge_video"
11 | }
12 |
13 | override fun hook() {
14 | hookBeforeMethod(
15 | "tbclient.Personalized.DataRes\$Builder",
16 | "build", Boolean::class.javaPrimitiveType
17 | ) { param ->
18 | val threadList = XposedHelpers.getObjectField(param.thisObject, "thread_list") as? MutableList<*>
19 | threadList?.removeIf { thread -> XposedHelpers.getObjectField(thread, "video_info") != null }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/RedTip.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import gm.tieba.tabswitch.XposedContext
4 | import gm.tieba.tabswitch.hooker.IHooker
5 |
6 | class RedTip : XposedContext(), IHooker {
7 |
8 | override fun key(): String {
9 | return "red_tip"
10 | }
11 |
12 | override fun hook() {
13 | // hookReplaceMethod(
14 | // "com.baidu.tbadk.widget.tab.PagerSlidingTabBaseStrip",
15 | // "setShowConcernRedTip", Boolean::class.javaPrimitiveType
16 | // ) { null }
17 | hookReplaceMethod(
18 | "com.baidu.tieba.homepage.framework.indicator.PagerSlidingTabStrip",
19 | "setShowConcernRedTip", Boolean::class.javaPrimitiveType
20 | ) { null }
21 | hookReplaceMethod(
22 | "com.baidu.tieba.homepage.framework.indicator.PagerSlidingTabStrip",
23 | "setShowHotTopicRedTip", Boolean::class.javaPrimitiveType
24 | ) { null }
25 | hookReplaceMethod(
26 | "com.baidu.tieba.homepage.framework.indicator.ScrollFragmentTabHost",
27 | "setShowConcernRedTip", Boolean::class.javaPrimitiveType
28 | ) { null }
29 | hookReplaceMethod(
30 | "com.baidu.tieba.homepage.personalize.view.HomeTabBarView",
31 | "setShowConcernRedTip", Boolean::class.javaPrimitiveType
32 | ) { null }
33 |
34 | //底栏红点
35 | try {
36 | hookReplaceMethod(
37 | "com.baidu.tbadk.core.view.MessageRedDotView",
38 | "onChangeSkinType"
39 | ) { null }
40 | } catch (e: NoSuchMethodError) {
41 | hookReplaceMethod(
42 | "com.baidu.tbadk.core.view.MessageRedDotView",
43 | "e"
44 | ) { null }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/RegexFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import gm.tieba.tabswitch.dao.Preferences.getString
4 | import java.util.regex.Pattern
5 |
6 | internal interface RegexFilter {
7 |
8 | fun key(): String
9 |
10 | fun getPattern(): Pattern? {
11 | val _regex = getString(key()) ?: return null
12 | if (_regex != regex[0]) {
13 | regex[0] = _regex
14 | pattern[0] = Pattern.compile(_regex, Pattern.CASE_INSENSITIVE)
15 | }
16 | return pattern[0]
17 | }
18 |
19 | companion object {
20 | val regex = arrayOfNulls(1)
21 | val pattern = arrayOfNulls(1)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/RemoveUpdate.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import gm.tieba.tabswitch.XposedContext
4 | import gm.tieba.tabswitch.hooker.IHooker
5 | import org.json.JSONObject
6 |
7 | class RemoveUpdate : XposedContext(), IHooker {
8 |
9 | override fun key(): String {
10 | return "remove_update"
11 | }
12 |
13 | override fun hook() {
14 | //Lcom/baidu/tbadk/coreExtra/data/VersionData;->parserJson(Lorg/json/JSONObject;)V
15 | hookReplaceMethod(
16 | "com.baidu.tbadk.coreExtra.data.VersionData",
17 | "parserJson", JSONObject::class.java
18 | ) { null }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/eliminate/UserFilter.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.eliminate
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 | import java.util.regex.Pattern
7 |
8 | class UserFilter : XposedContext(), IHooker, RegexFilter {
9 |
10 | private val mIds: MutableSet = HashSet()
11 |
12 | override fun key(): String {
13 | return "user_filter"
14 | }
15 |
16 | override fun hook() {
17 | hookBeforeMethod("tbclient.Personalized.DataRes\$Builder",
18 | "build", Boolean::class.javaPrimitiveType) { param ->
19 | val threadList = XposedHelpers.getObjectField(param.thisObject, "thread_list") as? MutableList<*>
20 | val pattern = getPattern() ?: return@hookBeforeMethod
21 | threadList?.removeIf { thread ->
22 | val author = XposedHelpers.getObjectField(thread, "author")
23 | val authors = arrayOf(
24 | XposedHelpers.getObjectField(author, "name") as String,
25 | XposedHelpers.getObjectField(author, "name_show") as String
26 | )
27 | authors.any { pattern.matcher(it).find() }
28 | }
29 | }
30 |
31 | hookBeforeMethod(
32 | "tbclient.FrsPage.PageData\$Builder",
33 | "build", Boolean::class.javaPrimitiveType
34 | ) { param ->
35 | filterPageData(param.thisObject)
36 | }
37 |
38 | hookBeforeMethod(
39 | "tbclient.ThreadList.PageData\$Builder",
40 | "build", Boolean::class.javaPrimitiveType
41 | ) { param ->
42 | filterPageData(param.thisObject)
43 | }
44 |
45 | // 楼层
46 | hookBeforeMethod(
47 | "tbclient.PbPage.DataRes\$Builder",
48 | "build", Boolean::class.javaPrimitiveType
49 | ) { param ->
50 | val postList = XposedHelpers.getObjectField(param.thisObject, "post_list") as? MutableList<*>
51 | val pattern = getPattern() ?: return@hookBeforeMethod
52 | initIdList(param.thisObject, pattern)
53 | postList?.removeIf { post ->
54 | (XposedHelpers.getObjectField(post, "floor") as Int != 1
55 | && mIds.contains(XposedHelpers.getObjectField(post, "author_id")))
56 | }
57 | }
58 |
59 | // 楼中楼:[\u202e|\ud83c\udd10-\ud83c\udd89]
60 | hookBeforeMethod(
61 | "tbclient.SubPost\$Builder",
62 | "build", Boolean::class.javaPrimitiveType
63 | ) { param ->
64 | val subPostList = XposedHelpers.getObjectField(param.thisObject, "sub_post_list") as? MutableList<*>
65 | subPostList?.removeIf { subPost -> mIds.contains(XposedHelpers.getObjectField(subPost, "author_id")) }
66 | }
67 |
68 | // 楼层回复
69 | hookBeforeMethod(
70 | "tbclient.PbFloor.DataRes\$Builder",
71 | "build", Boolean::class.javaPrimitiveType
72 | ) { param ->
73 | val subpostList = XposedHelpers.getObjectField(param.thisObject, "subpost_list") as? MutableList<*>
74 | val pattern = getPattern() ?: return@hookBeforeMethod
75 | subpostList?.removeIf { subPost ->
76 | val author = XposedHelpers.getObjectField(subPost, "author")
77 | val authors = arrayOf(
78 | XposedHelpers.getObjectField(author, "name") as String,
79 | XposedHelpers.getObjectField(author, "name_show") as String
80 | )
81 | authors.any { pattern.matcher(it).find() }
82 | }
83 | }
84 | }
85 |
86 | private fun filterPageData(pageData: Any) {
87 | val feedList = XposedHelpers.getObjectField(pageData, "feed_list") as? MutableList<*>
88 | val pattern = getPattern() ?: return
89 |
90 | feedList?.removeIf { feed ->
91 | val currFeed = XposedHelpers.getObjectField(feed, "feed")
92 |
93 | currFeed?.let {
94 | val components = XposedHelpers.getObjectField(currFeed, "components") as? List<*>
95 |
96 | components?.firstOrNull { component ->
97 | XposedHelpers.getObjectField(component, "component").toString() == "feed_head"
98 | }?.let { feedHeadComponent ->
99 | val feedHead = XposedHelpers.getObjectField(feedHeadComponent, "feed_head")
100 | val mainData = XposedHelpers.getObjectField(feedHead, "main_data") as? List<*>
101 |
102 | mainData?.any { feedHeadSymbol ->
103 | val feedHeadText = XposedHelpers.getObjectField(feedHeadSymbol, "text")
104 | val username = feedHeadText?.let { XposedHelpers.getObjectField(it, "text") as? String }
105 | username?.let { pattern.matcher(it).find() } ?: false
106 | } ?: false
107 | } ?: false
108 | } ?: false
109 | }
110 | }
111 |
112 | private fun initIdList(thisObject: Any, pattern: Pattern) {
113 | val userList = XposedHelpers.getObjectField(thisObject, "user_list") as? List<*>
114 | userList?.forEach { user ->
115 | val authors = arrayOf(
116 | XposedHelpers.getObjectField(user, "name") as String,
117 | XposedHelpers.getObjectField(user, "name_show") as String
118 | )
119 | if (authors.any { name: String -> pattern.matcher(name).find() }) {
120 | mIds.add(XposedHelpers.getObjectField(user, "id"))
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/AutoRefresh.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra
2 |
3 | import de.robv.android.xposed.XposedHelpers
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.AcRules.findRule
6 | import gm.tieba.tabswitch.hooker.IHooker
7 | import gm.tieba.tabswitch.hooker.Obfuscated
8 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
9 | import gm.tieba.tabswitch.hooker.deobfuscation.StringMatcher
10 |
11 | class AutoRefresh : XposedContext(), IHooker, Obfuscated {
12 |
13 | override fun key(): String {
14 | return "auto_refresh"
15 | }
16 |
17 | override fun matchers(): List {
18 | return listOf(
19 | StringMatcher("recommend_frs_refresh_time")
20 | )
21 | }
22 |
23 | override fun hook() {
24 | findRule(matchers()) { _, clazz, method ->
25 | val md = XposedHelpers.findMethodExactIfExists(
26 | findClass(clazz),
27 | method,
28 | Boolean::class.javaPrimitiveType
29 | ) ?: XposedHelpers.findMethodExactIfExists(
30 | findClass(clazz),
31 | method
32 | )
33 | md?.let { hookReplaceMethod(it) { false } }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/ForbidGesture.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra
2 |
3 | import android.annotation.SuppressLint
4 | import android.graphics.Bitmap
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.MotionEvent
8 | import android.view.ViewGroup
9 | import de.robv.android.xposed.XposedHelpers
10 | import gm.tieba.tabswitch.XposedContext
11 | import gm.tieba.tabswitch.dao.AcRules.findRule
12 | import gm.tieba.tabswitch.hooker.IHooker
13 | import gm.tieba.tabswitch.hooker.Obfuscated
14 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
15 | import gm.tieba.tabswitch.hooker.deobfuscation.ResMatcher
16 | import gm.tieba.tabswitch.hooker.deobfuscation.SmaliMatcher
17 | import gm.tieba.tabswitch.util.getObjectField
18 | import gm.tieba.tabswitch.util.getR
19 | import org.luckypray.dexkit.query.matchers.ClassMatcher
20 |
21 | class ForbidGesture : XposedContext(), IHooker, Obfuscated {
22 |
23 | override fun key(): String {
24 | return "forbid_gesture"
25 | }
26 |
27 | override fun matchers(): List {
28 | return listOf(
29 | ResMatcher(getR("drawable", "icon_word_t_size").toLong(), "forbid_gesture"),
30 | SmaliMatcher("Ljava/lang/Math;->sqrt(D)D").apply {
31 | classMatcher = ClassMatcher.create().className("com.baidu.tbadk.widget.DragImageView")
32 | }
33 | )
34 | }
35 |
36 | @SuppressLint("ClickableViewAccessibility")
37 | override fun hook() {
38 | // 帖子字号
39 | findRule(matchers()) { matcher, clazz, method ->
40 | when (matcher) {
41 | "forbid_gesture" -> hookReplaceMethod(clazz, method) { null }
42 | "Ljava/lang/Math;->sqrt(D)D" -> hookAfterMethod(clazz, method, Bitmap::class.java) { param ->
43 | param.result = 3 * param.result as Float
44 | }
45 | }
46 | }
47 |
48 | // 视频帖字号
49 | hookAfterMethod("com.baidu.tieba.pb.videopb.fragment.DetailInfoAndReplyFragment",
50 | "onCreateView", LayoutInflater::class.java, ViewGroup::class.java, Bundle::class.java) { param ->
51 | val recyclerView = getObjectField(
52 | param.thisObject,
53 | "com.baidu.adp.widget.ListView.BdTypeRecyclerView"
54 | ) as? ViewGroup
55 | recyclerView?.setOnTouchListener { _, _ -> false }
56 | }
57 |
58 | // 帖子进吧
59 | hookBeforeMethod(
60 | "com.baidu.tieba.pb.pb.main.PbLandscapeListView",
61 | "dispatchTouchEvent", MotionEvent::class.java
62 | ) { param ->
63 | XposedHelpers.callMethod(param.thisObject, "setForbidDragListener", true)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/Hide.java:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | import de.robv.android.xposed.XC_MethodHook;
9 | import de.robv.android.xposed.XposedHelpers;
10 | import gm.tieba.tabswitch.BuildConfig;
11 | import gm.tieba.tabswitch.XposedContext;
12 | import gm.tieba.tabswitch.hooker.IHooker;
13 |
14 | public class Hide extends XposedContext implements IHooker {
15 |
16 | @NonNull
17 | @Override
18 | public String key() {
19 | return "hide";
20 | }
21 |
22 | /**
23 | * @deprecated hook VMStack_getThreadStackTrace instead.
24 | */
25 | @Deprecated
26 | @Override
27 | public void hook() throws Throwable {
28 | for (final Class> clazz : new Class>[]{Throwable.class, Thread.class}) {
29 | XposedHelpers.findAndHookMethod(clazz, "getStackTrace", new XC_MethodHook() {
30 | @Override
31 | protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
32 | final StackTraceElement[] stes = (StackTraceElement[]) param.getResult();
33 | final List filtered = new ArrayList<>();
34 | for (final StackTraceElement ste : stes) {
35 | final String name = ste.getClassName();
36 | if (!name.contains("posed") && !name.contains("Hooker")
37 | && !name.contains(BuildConfig.APPLICATION_ID)
38 | && !name.equals("java.lang.reflect.Method")) {
39 | filtered.add(ste);
40 | }
41 | }
42 |
43 | final StackTraceElement[] result = new StackTraceElement[filtered.size()];
44 | for (int i = 0; i < filtered.size(); i++) {
45 | result[i] = filtered.get(i);
46 | }
47 | param.setResult(result);
48 | }
49 | });
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/LogRedirect.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra
2 |
3 | import de.robv.android.xposed.XposedBridge
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.hooker.IHooker
6 |
7 | class LogRedirect : XposedContext(), IHooker {
8 |
9 | override fun key(): String {
10 | return "log_redirect"
11 | }
12 |
13 | override fun hook() {
14 | XposedBridge.log("TbLog redirect enabled")
15 | hookReplaceMethod("com.baidu.searchbox.config.AppConfig", "isDebug") { true }
16 | hookAfterMethod("com.baidu.tieba.log.TbLog",
17 | "d", String::class.java, String::class.java
18 | ) { param ->
19 | (param.args[0] as? String)?.let { tag ->
20 | (param.args[1] as? String)?.let { msg ->
21 | XposedBridge.log("[D] [$tag] $msg")
22 | }
23 | }
24 | }
25 | hookAfterMethod("com.baidu.tieba.log.TbLog",
26 | "e", String::class.java, String::class.java
27 | ) { param ->
28 | (param.args[0] as? String)?.let { tag ->
29 | (param.args[1] as? String)?.let { msg ->
30 | XposedBridge.log("[E] [$tag] $msg")
31 | }
32 | }
33 | }
34 | hookAfterMethod("com.baidu.tieba.log.TbLog",
35 | "i", String::class.java, String::class.java
36 | ) { param ->
37 | (param.args[0] as? String)?.let { tag ->
38 | (param.args[1] as? String)?.let { msg ->
39 | XposedBridge.log("[I] [$tag] $msg")
40 | }
41 | }
42 | }
43 | hookAfterMethod("com.baidu.tieba.log.TbLog",
44 | "v", String::class.java, String::class.java
45 | ) { param ->
46 | (param.args[0] as? String)?.let { tag ->
47 | (param.args[1] as? String)?.let { msg ->
48 | XposedBridge.log("[V] [$tag] $msg")
49 | }
50 | }
51 | }
52 | hookAfterMethod("com.baidu.tieba.log.TbLog",
53 | "w", String::class.java, String::class.java
54 | ) { param ->
55 | (param.args[0] as? String)?.let { tag ->
56 | (param.args[1] as? String)?.let { msg ->
57 | XposedBridge.log("[W] [$tag] $msg")
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/NativeCheck.java:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra;
2 |
3 | import android.annotation.SuppressLint;
4 |
5 | import gm.tieba.tabswitch.XposedContext;
6 |
7 | @SuppressLint("UnsafeDynamicallyLoadedCode")
8 | public class NativeCheck extends XposedContext {
9 | static {
10 | load("check");
11 | }
12 |
13 | public static native boolean inline(String name);
14 |
15 | public static native boolean isFindClassInline();
16 |
17 | public static native boolean findXposed();
18 |
19 | public static native int access(String path);
20 |
21 | public static native String fopen(String path);
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/StackTrace.java:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import java.lang.reflect.Method;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | import de.robv.android.xposed.XC_MethodHook;
10 | import de.robv.android.xposed.XposedBridge;
11 | import de.robv.android.xposed.XposedHelpers;
12 | import gm.tieba.tabswitch.XposedContext;
13 | import gm.tieba.tabswitch.hooker.IHooker;
14 |
15 | public class StackTrace extends XposedContext implements IHooker {
16 | public static List sStes = new ArrayList<>();
17 |
18 | @NonNull
19 | @Override
20 | public String key() {
21 | return "check_stack_trace";
22 | }
23 |
24 | @Override
25 | public void hook() throws Throwable {
26 | for (final Method method : XposedHelpers.findClass("com.baidu.tieba.LogoActivity", sClassLoader).getDeclaredMethods()) {
27 | XposedBridge.hookMethod(method, new XC_MethodHook() {
28 | @Override
29 | protected void beforeHookedMethod(final MethodHookParam param) throws Throwable {
30 | final List sts = new ArrayList<>();
31 | final StackTraceElement[] stes = Thread.currentThread().getStackTrace();
32 | boolean isXposedStackTrace = false;
33 | for (final StackTraceElement ste : stes) {
34 | final String name = ste.getClassName();
35 | if (name.contains("Activity")
36 | || name.equals("android.app.Instrumentation")) break;
37 | if (isXposedStackTrace) sts.add(name);
38 | if (name.equals("java.lang.Thread")) isXposedStackTrace = true;
39 | }
40 |
41 | for (final String st : sts) {
42 | if (!sStes.contains(st)) sStes.add(st);
43 | }
44 | }
45 | });
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/hooker/extra/TraceChecker.java:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.hooker.extra;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.pm.ApplicationInfo;
7 | import android.content.pm.PackageInfo;
8 | import android.content.pm.PackageManager;
9 | import android.content.pm.ResolveInfo;
10 | import android.os.Build;
11 | import android.os.Environment;
12 | import android.os.IBinder;
13 | import android.os.Process;
14 | import android.text.TextUtils;
15 |
16 | import java.io.BufferedReader;
17 | import java.io.File;
18 | import java.io.FileReader;
19 | import java.io.IOException;
20 | import java.lang.reflect.Field;
21 | import java.util.ArrayList;
22 | import java.util.List;
23 | import java.util.Locale;
24 | import java.util.Random;
25 |
26 | import dalvik.system.PathClassLoader;
27 | import de.robv.android.xposed.XposedBridge;
28 | import gm.tieba.tabswitch.BuildConfig;
29 | import gm.tieba.tabswitch.XposedContext;
30 | import gm.tieba.tabswitch.dao.Preferences;
31 | import gm.tieba.tabswitch.hooker.TSPreferenceHelper;
32 | import gm.tieba.tabswitch.util.FileUtils;
33 | import gm.tieba.tabswitch.widget.TbToast;
34 |
35 | public class TraceChecker extends XposedContext {
36 | public static int sChildCount;
37 | private final TSPreferenceHelper.PreferenceLayout mPreferenceLayout;
38 | private final String JAVA = "java";
39 | private final String C = "c";
40 | private final String S = "syscall";
41 | private final String FAKE = "fake";
42 | private int mTraceCount;
43 |
44 | public TraceChecker(final TSPreferenceHelper.PreferenceLayout preferenceLayout) {
45 | mPreferenceLayout = preferenceLayout;
46 | }
47 |
48 | public void checkAll() {
49 | mTraceCount = 0;
50 | while (mPreferenceLayout.getChildAt(sChildCount) != null) {
51 | mPreferenceLayout.removeViewAt(sChildCount);
52 | }
53 | getContext().getExternalFilesDir(null).mkdirs();
54 | if (Preferences.getBoolean("check_xposed")) {
55 | classloader();
56 | }
57 | if (Preferences.getBoolean("check_module")) {
58 | files();
59 | maps();
60 | mounts();
61 | pm();
62 | preferences();
63 | }
64 | if (Preferences.getBoolean("check_stack_trace")) stackTrace();
65 | TbToast.showTbToast(mTraceCount > 0 ? String.format(Locale.getDefault(), "%s\n检测出%d处痕迹",
66 | randomToast(), mTraceCount) : "未检测出痕迹", TbToast.LENGTH_SHORT);
67 | }
68 |
69 | private void classloader() {
70 | final ResultBuilder result = new ResultBuilder("类加载器");
71 | try {
72 | final String clazz = "de.robv.android.xposed.XposedBridge";
73 | PathClassLoader.getSystemClassLoader().loadClass(clazz);
74 | result.addTrace(JAVA, clazz);
75 | } catch (final ClassNotFoundException ignored) {
76 | }
77 |
78 | if (NativeCheck.findXposed()) result.addTrace(C, "de/robv/android/xposed/XposedBridge");
79 | if (NativeCheck.isFindClassInline()) result.addTrace(FAKE, "FindClass is inline hooked");
80 | result.show();
81 | }
82 |
83 | private void files() {
84 | final ResultBuilder result = new ResultBuilder("文件");
85 | for (final String symbol : new String[]{"access", "faccessat"}) {
86 | if (NativeCheck.inline(symbol)) result.addTrace(FAKE, symbol + " is inline hooked");
87 | }
88 |
89 | final String[] paths = new String[]{getContext().getFilesDir().getParent()
90 | .replace(getContext().getPackageName(), BuildConfig.APPLICATION_ID),
91 | getContext().getExternalFilesDir(null).getParent()
92 | .replace(getContext().getPackageName(), BuildConfig.APPLICATION_ID),
93 | getContext().getDatabasePath("Rules.db").getPath(),
94 | getContext().getFilesDir().getParent() + File.separator + "shared_prefs"
95 | + File.separator + "TS_preferences.xml",
96 | getContext().getFilesDir().getParent() + File.separator + "shared_prefs"
97 | + File.separator + "TS_config.xml",
98 | getContext().getFilesDir().getParent() + File.separator + "shared_prefs"
99 | + File.separator + "TS_notes.xml"};
100 | for (final String path : paths) {
101 | if (NativeCheck.access(path) == 0) result.addTrace(C, path);
102 | }
103 | result.show();
104 | }
105 |
106 | private void maps() {
107 | final ResultBuilder result = new ResultBuilder("内存映射");
108 | for (final String symbol : new String[]{"open", "open64", "openat", "openat64", "__openat",
109 | "fopen", "fdopen"}) {
110 | if (NativeCheck.inline(symbol)) result.addTrace(FAKE, symbol + " is inline hooked");
111 | }
112 |
113 | final String path = String.format(Locale.getDefault(), "/proc/%d/maps", Process.myPid());
114 | final String trace = NativeCheck.fopen(path);
115 | if (!TextUtils.isEmpty(trace)) {
116 | result.addTrace(C, trace.substring(0, trace.length() - 1));
117 | }
118 | result.show();
119 | }
120 |
121 | private void mounts() {
122 | final ResultBuilder result = new ResultBuilder("挂载");
123 | try {
124 | final BufferedReader br = new BufferedReader(new FileReader(String.format(Locale.getDefault(),
125 | "/proc/%d/mountinfo", Process.myPid())));
126 | final List paths = new ArrayList<>();
127 | String lastPath = getContext().getExternalFilesDir(null).getPath();
128 | while (!paths.contains(Environment.getExternalStorageDirectory().getPath())) {
129 | lastPath = FileUtils.getParent(lastPath);
130 | if (!lastPath.endsWith("/data")) paths.add(lastPath);
131 | }
132 |
133 | for (String line = br.readLine(); line != null; line = br.readLine()) {
134 | for (final String path : paths) {
135 | if (line.contains(String.format(" %s ", path))) {
136 | result.addTrace(FAKE, line);
137 | }
138 | }
139 | }
140 | } catch (final IOException e) {
141 | XposedBridge.log(e);
142 | result.addTrace(FAKE, e.getMessage());
143 | }
144 |
145 | if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
146 | try {
147 | for (final File f : getContext().getExternalFilesDir(null).getParentFile()
148 | .getParentFile().listFiles()) {
149 | result.addTrace(FAKE, f.getPath());
150 | }
151 | } catch (final NullPointerException ignored) {
152 | }
153 | }
154 | result.show();
155 | }
156 |
157 | @SuppressLint({"DiscouragedPrivateApi", "PrivateApi", "QueryPermissionsNeeded"})
158 | private void pm() {
159 | final ResultBuilder result = new ResultBuilder("包管理器");
160 | final List modules = new ArrayList<>();
161 | final PackageManager pm = getContext().getPackageManager();
162 | try {
163 | final IBinder service = (IBinder) Class.forName("android.os.ServiceManager")
164 | .getDeclaredMethod("getService", String.class).invoke(null, "package");
165 | final Object iPackageManager = Class.forName("android.content.pm.IPackageManager$Stub")
166 | .getDeclaredMethod("asInterface", IBinder.class).invoke(null, service);
167 | final Field field = pm.getClass().getDeclaredField("mPM");
168 | field.setAccessible(true);
169 | final Class> mPMClass = field.get(pm).getClass();
170 | if (!mPMClass.equals(iPackageManager.getClass())) {
171 | result.addTrace(FAKE, mPMClass.getName());
172 | }
173 | } catch (final Throwable e) {
174 | XposedBridge.log(e);
175 | result.addTrace(FAKE, e.getMessage());
176 | }
177 |
178 | for (final PackageInfo pkg : pm.getInstalledPackages(PackageManager.GET_META_DATA)) {
179 | final ApplicationInfo app = pkg.applicationInfo;
180 | if (app.metaData != null && app.metaData.containsKey("xposedmodule")) {
181 | modules.add(pm.getApplicationLabel(pkg.applicationInfo).toString());
182 | }
183 | }
184 |
185 | final Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
186 | intentToResolve.addCategory("de.robv.android.xposed.category.MODULE_SETTINGS");
187 | final List ris = pm.queryIntentActivities(intentToResolve, 0);
188 | for (final ResolveInfo ri : ris) {
189 | final String name = ri.loadLabel(pm).toString();
190 | if (!modules.contains(name)) modules.add(name);
191 | }
192 |
193 | if (modules.size() > 0) {
194 | result.addTrace(JAVA, modules.toString());
195 | }
196 | result.show();
197 | }
198 |
199 | private void preferences() {
200 | final ResultBuilder result = new ResultBuilder("偏好");
201 | for (final String sp : new String[]{"TS_preferences", "TS_config", "TS_notes"}) {
202 | if (getContext().getSharedPreferences(sp, Context.MODE_PRIVATE)
203 | .getAll().keySet().size() != 0) result.addTrace(JAVA, sp);
204 | }
205 | result.show();
206 | }
207 |
208 | private void stackTrace() {
209 | final ResultBuilder result = new ResultBuilder("堆栈");
210 | for (final String st : StackTrace.sStes) {
211 | result.addTrace(JAVA, st);
212 | }
213 | result.show();
214 | }
215 |
216 | private String randomToast() {
217 | switch (new Random().nextInt(9)) {
218 | case 0:
219 | return "没收尾巴球";
220 | case 1:
221 | return "尾巴捏捏";
222 | case 2:
223 | return "没收尾巴";
224 | case 3:
225 | return "点燃尾巴";
226 | case 4:
227 | return "捏尾巴";
228 | case 5:
229 | return "若要人不知,除非己莫为";
230 | case 6:
231 | return "哼!你满身都是破绽";
232 | case 7:
233 | return "checkmate";
234 | case 8:
235 | return "Xposed 无处可逃";
236 | default:
237 | return "";
238 | }
239 | }
240 |
241 | private class ResultBuilder {
242 | private static final String INDENT = " ";
243 | StringBuilder mResult;
244 |
245 | private ResultBuilder(final String text) {
246 | mResult = new StringBuilder("检测" + text + " -> ");
247 | }
248 |
249 | private void addTrace(final String tag, final String msg) {
250 | if (msg == null) return;
251 | mResult.append("\n").append(INDENT).append(tag).append(": ").append(msg);
252 | mTraceCount++;
253 | }
254 |
255 | private void show() {
256 | final String result = mResult.toString();
257 | XposedBridge.log(result);
258 | mPreferenceLayout.addView(TSPreferenceHelper.createTextView(result));
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/util/DisplayUtils.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("DisplayUtils")
2 |
3 | package gm.tieba.tabswitch.util
4 |
5 | import android.app.Activity
6 | import android.app.AlertDialog
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.res.Configuration
10 | import android.view.WindowManager
11 | import de.robv.android.xposed.XposedBridge
12 | import de.robv.android.xposed.XposedHelpers
13 | import gm.tieba.tabswitch.XposedContext
14 | import kotlin.math.roundToInt
15 | import kotlin.system.exitProcess
16 |
17 | fun isLightMode(context: Context): Boolean {
18 | return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_NO
19 | }
20 |
21 | fun restart(activity: Activity) {
22 | val intent = activity.packageManager.getLaunchIntentForPackage(activity.packageName)
23 | intent?.let {
24 | it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
25 | activity.startActivity(it)
26 | exitProcess(0)
27 | }
28 | }
29 |
30 | fun getTbSkin(context: Context): String {
31 | //Lcom/baidu/tbadk/core/TbadkCoreApplication;->getSkinType()I
32 | val skinType: Int = try {
33 | val instance = getTbadkCoreApplicationInst()
34 | XposedHelpers.callMethod(instance, "getSkinType") as Int
35 | } catch (e: Exception) {
36 | XposedBridge.log(e)
37 | val settings = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
38 | if (settings.getBoolean("key_is_follow_system_mode", false)) {
39 | return if (isLightMode(context)) "" else "_2"
40 | } else {
41 | val commonSettings = context.getSharedPreferences(
42 | "common_settings", Context.MODE_PRIVATE
43 | )
44 | commonSettings.getString("skin_", "0")?.toIntOrNull() ?: 0
45 | }
46 | }
47 | return when (skinType) {
48 | 1, 4 -> "_2"
49 | else -> ""
50 | }
51 | }
52 |
53 | fun dipToPx(context: Context, dipValue: Float): Int {
54 | val scale = context.resources.displayMetrics.density
55 | return (dipValue * scale).roundToInt()
56 | }
57 |
58 | fun pxToDip(context: Context, pxValue: Float): Int {
59 | val scale = context.resources.displayMetrics.density
60 | return (pxValue / scale).roundToInt()
61 | }
62 |
63 | fun getDisplayWidth(context: Context): Int? {
64 | return context.resources?.displayMetrics?.widthPixels
65 | }
66 |
67 | fun fixAlertDialogWidth(alert: AlertDialog) {
68 | alert.window?.let {
69 | val layoutParams = WindowManager.LayoutParams()
70 | layoutParams.copyFrom(it.attributes)
71 | getDisplayWidth(XposedContext.getContext())?.let { displayWidth ->
72 | layoutParams.width = displayWidth
73 | }
74 | it.attributes = layoutParams
75 | }
76 | }
77 |
78 | fun getDialogTheme(context: Context): Int =
79 | if (isLightMode(context)) android.R.style.Theme_DeviceDefault_Light_Dialog_Alert else android.R.style.Theme_DeviceDefault_Dialog_Alert
80 |
81 | fun getDialogTheme(isLightMode: Boolean): Int =
82 | if (isLightMode) android.R.style.Theme_DeviceDefault_Light_Dialog_Alert else android.R.style.Theme_DeviceDefault_Dialog_Alert
83 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/util/FileUtils.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("FileUtils")
2 |
3 | package gm.tieba.tabswitch.util
4 |
5 | import gm.tieba.tabswitch.XposedContext
6 | import java.io.File
7 | import java.io.FileDescriptor
8 | import java.io.FileInputStream
9 | import java.io.FileOutputStream
10 | import java.io.IOException
11 | import java.io.InputStream
12 | import java.io.OutputStream
13 | import java.nio.ByteBuffer
14 |
15 | fun copy(input: Any?, output: Any?) {
16 | val inputStream: InputStream = when (input) {
17 | is InputStream -> input
18 | is File -> FileInputStream(input)
19 | is FileDescriptor -> FileInputStream(input)
20 | is String -> FileInputStream(input)
21 | else -> throw IllegalArgumentException("unknown input type")
22 | }
23 |
24 | val outputStream: OutputStream = when (output) {
25 | is OutputStream -> output
26 | is File -> FileOutputStream(output)
27 | is FileDescriptor -> FileOutputStream(output)
28 | is String -> FileOutputStream(output)
29 | else -> throw IllegalArgumentException("unknown output type")
30 | }
31 |
32 | copy(inputStream, outputStream)
33 | }
34 |
35 | fun copy(inputStream: InputStream, outputStream: OutputStream) {
36 | inputStream.use { input ->
37 | outputStream.use { output ->
38 | input.copyTo(output)
39 | }
40 | }
41 | }
42 |
43 | fun copy(bb: ByteBuffer, output: Any?) {
44 | val outputStream: OutputStream = when (output) {
45 | is OutputStream -> output
46 | is File -> FileOutputStream(output)
47 | is FileDescriptor -> FileOutputStream(output)
48 | is String -> FileOutputStream(output)
49 | else -> throw IllegalArgumentException("unknown output type")
50 | }
51 | outputStream.use {
52 | it.write(bb.array())
53 | }
54 | }
55 |
56 | fun toByteBuffer(inputStream: InputStream): ByteBuffer {
57 | return ByteBuffer.wrap(inputStream.readBytes())
58 | }
59 |
60 | fun getExtension(bb: ByteBuffer): String {
61 | val chunk = String(bb.array(), 0, 6)
62 | return when {
63 | chunk.contains("GIF") -> "gif"
64 | chunk.contains("PNG") -> "png"
65 | else -> "jpeg"
66 | }.also {
67 | bb.rewind()
68 | }
69 | }
70 |
71 | fun getParent(path: String): String {
72 | return path.substring(0, path.lastIndexOf(File.separatorChar))
73 | }
74 |
75 | fun getAssetFileContent(filename: String?): String? {
76 | return try {
77 | filename?.let { name ->
78 | XposedContext.sAssetManager.open(name).use { inputStream ->
79 | inputStream.bufferedReader().use { reader ->
80 | reader.readText()
81 | }
82 | }
83 | }
84 | } catch (ignored: IOException) {
85 | null
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/util/Parser.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("Parser")
2 |
3 | package gm.tieba.tabswitch.util
4 |
5 | import de.robv.android.xposed.XposedHelpers
6 |
7 | fun parsePbContent(instance: Any?, fieldName: String?): String {
8 | val contents = XposedHelpers.getObjectField(instance, fieldName) as? List<*> ?: return ""
9 | return contents.mapNotNull { XposedHelpers.getObjectField(it, "text") as? String }.joinToString("")
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/util/ReflectUtils.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("ReflectUtils")
2 |
3 | package gm.tieba.tabswitch.util
4 |
5 | import android.app.Activity
6 | import android.app.Application
7 | import androidx.annotation.ColorInt
8 | import de.robv.android.xposed.XposedBridge
9 | import de.robv.android.xposed.XposedHelpers
10 | import gm.tieba.tabswitch.XposedContext
11 | import gm.tieba.tabswitch.XposedContext.Companion.findClass
12 | import java.lang.reflect.InvocationTargetException
13 | import java.lang.reflect.Method
14 |
15 | fun getR(innerClassName: String, fieldName: String): Int {
16 | return XposedContext.getContext().resources
17 | .getIdentifier(fieldName, innerClassName, XposedContext.getContext().packageName)
18 | }
19 |
20 | fun getId(fieldName: String): Int {
21 | return getR("id", fieldName)
22 | }
23 |
24 | @ColorInt
25 | fun getColor(fieldName: String): Int {
26 | return XposedContext.getContext().getColor(
27 | getR("color", fieldName + getTbSkin(XposedContext.getContext()))
28 | )
29 | }
30 |
31 | fun getDimen(fieldName: String): Float {
32 | when (fieldName) {
33 | "ds10" -> return dipToPx(XposedContext.getContext(), 5f).toFloat()
34 | "ds20" -> return dipToPx(XposedContext.getContext(), 10f).toFloat()
35 | "ds30" -> return dipToPx(XposedContext.getContext(), 15f).toFloat()
36 | "ds32" -> return dipToPx(XposedContext.getContext(), 16f).toFloat()
37 | "ds140" -> return dipToPx(XposedContext.getContext(), 70f).toFloat()
38 | }
39 | return XposedContext.getContext().resources.getDimension(getR("dimen", fieldName))
40 | }
41 |
42 | fun getDimenDip(fieldName: String): Float {
43 | when (fieldName) {
44 | "fontsize22" -> return 11f
45 | "fontsize28" -> return 14f
46 | "fontsize36" -> return 18f
47 | }
48 | return pxToDip(XposedContext.getContext(), getDimen(fieldName)).toFloat()
49 | }
50 |
51 | fun getDrawableId(fieldName: String): Int {
52 | return getR("drawable", fieldName)
53 | }
54 |
55 | /**
56 | * Returns the first field of the given type in a class.
57 | * Might be useful for Proguard'ed classes to identify fields with unique types.
58 | *
59 | * @param instance The class which either declares or inherits the field.
60 | * @param type The type of the field.
61 | * @return A reference to the first field of the given type.
62 | * @throws NoSuchFieldError In case no matching field was not found.
63 | */
64 | fun getObjectField(instance: Any?, type: Class): T? {
65 | return try {
66 | type.cast(XposedHelpers.findFirstFieldByExactType(instance?.javaClass, type)[instance])
67 | } catch (e: IllegalAccessException) {
68 | XposedBridge.log(e)
69 | throw IllegalAccessError(e.message)
70 | }
71 | }
72 |
73 | fun getObjectField(instance: Any?, className: String): Any? {
74 | return try {
75 | XposedHelpers.findFirstFieldByExactType(
76 | instance?.javaClass,
77 | findClass(className)
78 | )[instance]
79 | } catch (e: IllegalAccessException) {
80 | XposedBridge.log(e)
81 | throw IllegalAccessError(e.message)
82 | }
83 | }
84 |
85 | fun setObjectField(instance: Any?, type: Class<*>, value: Any?) {
86 | try {
87 | XposedHelpers.findFirstFieldByExactType(instance?.javaClass, type)[instance] = value
88 | } catch (e: IllegalAccessException) {
89 | XposedBridge.log(e)
90 | throw IllegalAccessError(e.message)
91 | }
92 | }
93 |
94 | fun setObjectField(instance: Any?, className: String, value: Any?) {
95 | try {
96 | XposedHelpers.findFirstFieldByExactType(
97 | instance?.javaClass,
98 | findClass(className)
99 | )[instance] = value
100 | } catch (e: IllegalAccessException) {
101 | XposedBridge.log(e)
102 | throw IllegalAccessError(e.message)
103 | }
104 | }
105 |
106 | /**
107 | * Returns the field at the given position in a class.
108 | * Might be useful for Proguard'ed classes to identify fields with fixed position.
109 | *
110 | * @param instance The class which either declares or inherits the field.
111 | * @param position The position of the field.
112 | * @return A reference to the first field of the given type.
113 | * @throws NoSuchFieldError In case no matching field was not found.
114 | */
115 | fun getObjectField(instance: Any?, position: Int): Any? {
116 | return try {
117 | val field = instance?.javaClass?.declaredFields?.get(position)
118 | field?.isAccessible = true
119 | field?.get(instance)
120 | } catch (e: IllegalAccessException) {
121 | XposedBridge.log(e)
122 | throw IllegalAccessError(e.message)
123 | }
124 | }
125 |
126 | fun setObjectField(instance: Any?, position: Int, value: Any?) {
127 | try {
128 | val field = instance?.javaClass?.declaredFields?.get(position)
129 | field?.isAccessible = true
130 | field?.set(instance, value)
131 | } catch (e: IllegalAccessException) {
132 | XposedBridge.log(e)
133 | throw IllegalAccessError(e.message)
134 | }
135 | }
136 |
137 | fun findFirstMethodByExactType(cls: Class<*>, vararg paramTypes: Class<*>): Method {
138 | return cls.declaredMethods.firstOrNull { method ->
139 | method.parameterTypes.contentEquals(paramTypes)
140 | } ?: throw NoSuchMethodError(paramTypes.contentToString())
141 | }
142 |
143 | fun findFirstMethodByExactType(className: String, vararg paramTypes: Class<*>): Method {
144 | return findFirstMethodByExactType(
145 | findClass(className),
146 | *paramTypes
147 | )
148 | }
149 |
150 | fun findFirstMethodByExactReturnType(cls: Class<*>, returnType: Class<*>): Method {
151 | return cls.declaredMethods.firstOrNull { method ->
152 | method.returnType == returnType
153 | } ?: throw NoSuchMethodError(returnType.toString())
154 | }
155 |
156 | fun findFirstMethodByExactReturnType(className: String, returnType: Class<*>): Method {
157 | return findFirstMethodByExactReturnType(
158 | findClass(className),
159 | returnType
160 | )
161 | }
162 |
163 | fun callMethod(method: Method, instance: Any?, vararg args: Any?): Any? {
164 | return try {
165 | method.isAccessible = true
166 | method.invoke(instance, *args)
167 | } catch (e: IllegalAccessException) {
168 | XposedBridge.log(e)
169 | throw IllegalArgumentException(e)
170 | } catch (e: InvocationTargetException) {
171 | XposedBridge.log(e)
172 | throw IllegalArgumentException(e)
173 | }
174 | }
175 |
176 | fun callStaticMethod(method: Method, vararg args: Any?): Any? {
177 | return callMethod(method, null, *args)
178 | }
179 |
180 | fun getTbadkCoreApplicationInst(): Application = XposedHelpers.callStaticMethod(
181 | findClass("com.baidu.tbadk.core.TbadkCoreApplication"),
182 | "getInst"
183 | ) as Application
184 |
185 | fun getCurrentActivity(): Activity = XposedHelpers.callMethod(
186 | getTbadkCoreApplicationInst(),
187 | "getCurrentActivity"
188 | ) as Activity
189 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/widget/NavigationBar.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.widget
2 |
3 | import android.view.View
4 | import android.widget.TextView
5 | import de.robv.android.xposed.XposedHelpers
6 | import gm.tieba.tabswitch.XposedContext
7 | import gm.tieba.tabswitch.util.getColor
8 | import gm.tieba.tabswitch.util.getObjectField
9 |
10 | class NavigationBar(thisObject: Any) : XposedContext() {
11 |
12 | private val mNavigationBar: Any? = getObjectField(
13 | thisObject,
14 | "com.baidu.tbadk.core.view.NavigationBar"
15 | )
16 |
17 | fun addTextButton(text: String?, l: View.OnClickListener?) {
18 | val controlAlignClass = findClass("com.baidu.tbadk.core.view.NavigationBar\$ControlAlign")
19 | val horizontalRight = controlAlignClass.enumConstants.find { it.toString() == "HORIZONTAL_RIGHT" }
20 | ?: throw IllegalStateException("HORIZONTAL_RIGHT enum constant not found")
21 | val textView = XposedHelpers.callMethod(
22 | mNavigationBar,
23 | "addTextButton", horizontalRight, text, l
24 | ) as TextView
25 | textView.setTextColor(getColor("CAM_X0105"))
26 | }
27 |
28 | fun setTitleText(title: String?) {
29 | title?.let {
30 | XposedHelpers.callMethod(mNavigationBar, "setTitleText", it)
31 | }
32 | }
33 |
34 | fun setCenterTextTitle(title: String?) {
35 | title?.let {
36 | XposedHelpers.callMethod(mNavigationBar, "setCenterTextTitle", it)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/widget/Switch.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.widget
2 |
3 | import android.os.Vibrator
4 | import android.view.View
5 | import de.robv.android.xposed.XposedHelpers
6 | import de.robv.android.xposed.XposedHelpers.ClassNotFoundError
7 | import gm.tieba.tabswitch.XposedContext
8 | import gm.tieba.tabswitch.util.callMethod
9 | import gm.tieba.tabswitch.util.getObjectField
10 | import java.lang.reflect.InvocationHandler
11 | import java.lang.reflect.Method
12 | import java.lang.reflect.Proxy
13 |
14 | class Switch : XposedContext() {
15 |
16 | var bdSwitch: View
17 | private var mMethods: Array
18 |
19 | init {
20 | val cls = findClass("com.baidu.adp.widget.BdSwitchView.BdSwitchView")
21 | bdSwitch = XposedHelpers.newInstance(cls, getContext()) as View
22 | mMethods = cls.declaredMethods
23 | }
24 |
25 | fun setOnSwitchStateChangeListener(l: InvocationHandler) {
26 | val clazz: Class<*> = try {
27 | findClass("com.baidu.adp.widget.BdSwitchView.BdSwitchView\$b")
28 | } catch (e: ClassNotFoundError) {
29 | findClass("com.baidu.adp.widget.BdSwitchView.BdSwitchView\$a")
30 | }
31 | val proxy = Proxy.newProxyInstance(sClassLoader, arrayOf(clazz), l)
32 | XposedHelpers.callMethod(bdSwitch, "setOnSwitchStateChangeListener", proxy)
33 | }
34 |
35 | fun isOn(): Boolean = try {
36 | XposedHelpers.callMethod(bdSwitch, "isOn") as Boolean
37 | } catch (e: NoSuchMethodError) {
38 | callMethod(mMethods[6], bdSwitch) as Boolean
39 | }
40 |
41 | fun changeState() {
42 | try {
43 | XposedHelpers.callMethod(bdSwitch, "changeState")
44 | } catch (e: NoSuchMethodError) {
45 | callMethod(mMethods[3], bdSwitch)
46 | }
47 | }
48 |
49 | fun turnOn() {
50 | try {
51 | XposedHelpers.callMethod(bdSwitch, "turnOn")
52 | } catch (e: NoSuchMethodError) {
53 | callMethod(mMethods[11], bdSwitch)
54 | }
55 | }
56 |
57 | fun turnOff() {
58 | try {
59 | XposedHelpers.callMethod(bdSwitch, "turnOff")
60 | } catch (e: NoSuchMethodError) {
61 | callMethod(mMethods[8], bdSwitch)
62 | }
63 | }
64 |
65 | fun getVibrator(): Vibrator? = getObjectField(bdSwitch, Vibrator::class.java)
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/gm/tieba/tabswitch/widget/TbToast.kt:
--------------------------------------------------------------------------------
1 | package gm.tieba.tabswitch.widget
2 |
3 | import androidx.annotation.MainThread
4 | import gm.tieba.tabswitch.XposedContext
5 | import gm.tieba.tabswitch.dao.AcRules
6 | import gm.tieba.tabswitch.hooker.Obfuscated
7 | import gm.tieba.tabswitch.hooker.deobfuscation.Matcher
8 | import gm.tieba.tabswitch.hooker.deobfuscation.StringMatcher
9 | import gm.tieba.tabswitch.util.callStaticMethod
10 | import gm.tieba.tabswitch.util.findFirstMethodByExactType
11 |
12 | class TbToast : XposedContext(), Obfuscated {
13 |
14 | override fun matchers(): List {
15 | // setToastString()
16 | return listOf(StringMatcher("can not be call not thread! trace = "))
17 | }
18 |
19 | companion object {
20 | @JvmField
21 | var LENGTH_SHORT = 2000
22 | @JvmField
23 | var LENGTH_LONG = 3500
24 |
25 | @JvmStatic
26 | @MainThread
27 | fun showTbToast(text: String?, duration: Int) {
28 | AcRules.findRule("can not be call not thread! trace = ") { _, clazz, _ ->
29 | val md = findFirstMethodByExactType(
30 | clazz,
31 | String::class.java,
32 | Int::class.javaPrimitiveType!!,
33 | Boolean::class.javaPrimitiveType!!
34 | )
35 | runOnUiThread { callStaticMethod(md, text, duration, true) }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - com.baidu.tieba
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 贴吧TS
3 | 提供修改百度贴吧底栏等个性化功能
4 |
5 |
--------------------------------------------------------------------------------
/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=-Xmx2048m -Dfile.encoding=UTF-8
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 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=false
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | # Enables namespacing of each library's R class so that its R class includes only the
23 | # resources declared in the library itself and none from the library's dependencies,
24 | # thereby reducing the size of the R class for that library
25 | android.nonTransitiveRClass=true
26 | android.experimental.enableNewResourceShrinker=true
27 | android.experimental.enableNewResourceShrinker.preciseShrinking=true
28 | android.enableR8.fullMode=true
29 | android.nonFinalResIds=false
30 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klxiaoniu/TiebaTS/51c17db849991535a669147a82ff906bfd3d44f5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 24 21:31:51 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | plugins {
8 | id("com.android.application") version "8.5.0"
9 | id("com.android.library") version "8.5.0"
10 | id("org.jetbrains.kotlin.android") version "1.9.0"
11 | id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven { url = uri("https://api.xposed.info/") }
20 | }
21 | }
22 | rootProject.name = "贴吧TS"
23 | include(":app")
24 |
--------------------------------------------------------------------------------