├── .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 | [![Android CI](https://github.com/GuhDoy/TiebaTS/workflows/Android%20CI/badge.svg)](https://github.com/GuhDoy/TiebaTS/actions) 6 | [![Chat](https://img.shields.io/badge/Telegram-Chat-blue.svg?logo=telegram)](https://t.me/TabSwitch) 7 | [![Stars](https://img.shields.io/github/stars/GuhDoy/TiebaTS?label=Stars)](https://github.com/GuhDoy/TiebaTS) 8 | [![Download](https://img.shields.io/github/v/release/GuhDoy/TiebaTS?label=Download)](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 | --------------------------------------------------------------------------------