├── .gitignore ├── DEVELOP.md ├── Jenkinsfile ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ └── top │ │ └── trangle │ │ └── mbga │ │ ├── Constants.kt │ │ ├── application │ │ └── DefaultApplication.kt │ │ ├── hook │ │ ├── HomeViewHooker.kt │ │ ├── HookEntry.kt │ │ ├── MainActivityHooker.kt │ │ ├── MineViewHooker.kt │ │ ├── SearchViewHooker.kt │ │ ├── SettingEntryHooker.kt │ │ ├── VideoCommentHooker.kt │ │ ├── VideoDetailHooker.kt │ │ └── VideoPlayerHooker.kt │ │ ├── utils │ │ ├── MyHooker.kt │ │ ├── Reflect.kt │ │ └── factory │ │ │ └── FunctionFactory.kt │ │ └── views │ │ ├── IntEditTextPreference.kt │ │ ├── KeywordListPreference.kt │ │ └── SettingsActivity.kt │ ├── res │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ ├── drawable │ │ ├── comment_view_empty_page.png │ │ ├── comment_view_follow.png │ │ ├── comment_view_operation.png │ │ ├── comment_view_qoe.png │ │ ├── comment_view_quick_reply.png │ │ ├── comment_view_search.png │ │ ├── comment_view_vote.png │ │ ├── home_no_feed_clean_top.png │ │ ├── ic_im_pink.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_search_pink.xml │ │ ├── mine_add_search_entry.png │ │ ├── search_no_default_words.png │ │ ├── video_detail_label.png │ │ ├── video_player_activity_meta.png │ │ ├── video_player_dm_click.png │ │ ├── video_player_online_count.png │ │ ├── video_player_portrait_btn.png │ │ ├── video_player_qoe.png │ │ └── video_player_segmented_section.jpg │ ├── layout │ │ ├── activity_settings.xml │ │ └── layout_pref_switch.xml │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_yukihookapi.png │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ ├── values-en │ │ └── strings.xml │ ├── values │ │ ├── array.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── xml │ │ └── preferences.xml │ └── resources │ └── META-INF │ └── yukihookapi_init ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # 一些开发时所需资源 2 | 3 | ## 方便开发使用的代码片段 4 | 5 | ### 输出调用栈 6 | 7 | ``` 8 | val e = RuntimeException("") 9 | e.fillInStackTrace() 10 | YLog.debug(": ", e) 11 | ``` 12 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node('android-34') { 2 | dir(path: '/build') { 3 | stage('Checkout') { 4 | checkout scm 5 | } 6 | stage('Prepare') { 7 | env.BUILD_ID = sh(returnStdout: true, script: 'curl https://jenkins.cled.top/id-gen/cledwynl/mbga').trim() 8 | currentBuild.displayName = "#${env.BUILD_ID}" 9 | } 10 | stage('Build') { 11 | def target = env.BRANCH_IS_PRIMARY ? 'Release' : 'Feature' 12 | withCredentials([ 13 | certificate( 14 | credentialsId: 'keystore', 15 | keystoreVariable: 'KEYSTORE', 16 | passwordVariable: 'STORE_PASSWORD', 17 | ), 18 | usernamePassword( 19 | credentialsId: 'key-alias', 20 | usernameVariable: 'KEY_ALIAS', 21 | passwordVariable: 'KEY_PASSWORD' 22 | ) 23 | ]) { 24 | sh "./gradlew assemble${target}" 25 | } 26 | archiveArtifacts(artifacts: 'app/build/outputs/apk/**') 27 | } 28 | stage('Check') { 29 | catchError(buildResult: env.BRANCH_IS_PRIMARY ? 'FAILURE' : 'UNSTABLE', stageResult: 'FAILURE') { 30 | sh './gradlew ktLint' 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Make Bilibili Great Again! 2 | 3 | ## 功能 4 | 5 | 目前只支持国际版(com.bilibili.app.in),兼容性见文末 6 | 7 | ### 设置 8 | 9 | * 打开B站->我的->设置->标题上有MBGA字样,点击打开设置界面 10 | 11 | ### 底部Tab 12 | 13 | * 允许干掉除了“我的”以外的所有Tab 14 | 15 | ### 首页 16 | 17 | * 干掉竖屏 18 | * 禁止切后台一段时间回来后自动刷新视频列表 19 | * 防止刷新推荐视频时之前的列表被自动清除 20 | ![home_no_feed_clean_top](./app/src/main/res/drawable/home_no_feed_clean_top.png) 21 | * 支持过滤非UGC,只展示用户上传的视频 22 | * 支持过滤过短的视频 23 | * 干净的视频卡片:干掉那些“竖屏”“1万点赞”“已关注”推荐理由,只给老子展示UP主名字 24 | * 支持强制视频卡片比例为16:9 25 | * 支持根据关键词过滤推荐视频 26 | 27 | ### 搜索页 28 | 29 | * 干掉热搜 30 | * 干掉搜索历史 31 | * 干掉搜索发现 32 | * 禁止在搜索框内推荐搜索词 33 | ![search_no_default_words](./app/src/main/res/drawable/search_no_default_words.png) 34 | 35 | ### 视频播放器 36 | 37 | * 干掉播放器内展示的“云视听小电视”等activityMeta(不确定未来这个字段里还会塞些什么乱七八糟的东西,目前要么没有要么就是这个云视听,直接全部干掉了) 38 | ![video_player_activity_meta](./app/src/main/res/drawable/video_player_activity_meta.png) 39 | * 干掉播放器内展示的关注弹窗、投票弹窗等(包括UP主弹幕) 40 | * 禁止播放器自动开启章节进度条 41 | ![video_player_segment_section](./app/src/main/res/drawable/video_player_segmented_section.jpg) 42 | * 允许在小窗、分屏模式下全屏播放(体验并不是非常好) 43 | * 禁止展示竖屏全屏按钮 44 | ![video_player_portrait_btn](./app/src/main/res/drawable/video_player_portrait_btn.png) 45 | * 禁止点击弹幕 46 | ![video_player_dm_click](./app/src/main/res/drawable/video_player_dm_click.png) 47 | * 禁止展示反馈弹窗 48 | ![video_player_qoe](./app/src/main/res/drawable/video_player_qoe.png) 49 | * ~全屏时禁止展示在线人数~ 该功能已移除,因为 50 | B站本来就支持,可在通过这个途径设置:B站设置->播放设置->其他(播放)设置->全屏播放画面展示同时在看人数 51 | ![video_player_online_count](./app/src/main/res/drawable/video_player_online_count.png) 52 | 53 | ### 视频详情页 54 | 55 | * 视频标题左边的“热门”等标签 56 | ![video_detail_label](./app/src/main/res/drawable/video_detail_label.png) 57 | * 更纯粹的链接分享 58 | 原来分享链接是`https://b23.tv/xxxxxxx`这样的短链,展开后有乱七八糟的参数,让它变成`https://b23.tv/av10492`这样干净的av号链接 59 | * 干掉下方相关视频 60 | * 隐藏系统状态栏,可从顶部下划临时展示状态栏 61 | 62 | ### 视频评论区 63 | 64 | * 完全隐藏评论区 65 | * 点击评论区文字不会自动弹出回复区 66 | ![comment_view_quick_reply](./app/src/main/res/drawable/comment_view_quick_reply.png) 67 | * 干掉评论区投票 68 | ![comment_view_vote](./app/src/main/res/drawable/comment_view_vote.png) 69 | * 干掉评论区的关注按钮(笔记类型的评论右上角) 70 | ![comment_view_follow](./app/src/main/res/drawable/comment_view_follow.png) 71 | * 干掉评论内的关键词高亮搜索 72 | ![comment_view_search](./app/src/main/res/drawable/comment_view_search.png) 73 | * 没有评论时禁止一键发送评论 74 | ![comment_view_empty_page](./app/src/main/res/drawable/comment_view_empty_page.png) 75 | * 禁止展示反馈问卷 76 | ![comment_view_qoe](./app/src/main/res/drawable/comment_view_qoe.png) 77 | * 禁止展示评论区顶部推广内容 78 | ![comment_view_qoe](./app/src/main/res/drawable/comment_view_operation.png) 79 | * 支持根据关键词过滤评论,[这里](https://github.com/cledwynl/mbga/issues/79)提供了一些常见的灌水内容 80 | 81 | ### 我的 82 | 83 | * 在“更多服务”部分添加搜索入口,配合隐藏“首页”tab食用 84 | ![mine_add_search_entry](./app/src/main/res/drawable/mine_add_search_entry.png) 85 | * 在“更多服务”部分添加消息入口,配合隐藏“首页”tab食用 86 | * 隐藏大会员入口 87 | 88 | ## 感谢 89 | 90 | > 本项目参考、使用了下列开源项目的部分或全部内容 91 | 92 | * [Yuki Hook API](https://github.com/HighCapable/YukiHookAPI) 93 | * [FuckBilibiliVote](https://github.com/zerorooot/FuckBilibiliVote) 94 | * [不要竖屏](https://github.com/WankkoRee/Portrait2Landscape) 95 | 96 | ## 兼容性 97 | 98 | 下面仅列出测试过的版本,相近版本大概率能兼容 99 | 100 | * MBGA v1.0.0 ~ v1.0.6 101 | * 兼容国际版 未知 ~ 3.18.2 102 | * MBGA v1.1.0 ~ 103 | * 兼容国际版 未知 ~ 3.18.2 ~ 3.19.2 104 | * MBGA v1.3.0 ~ 105 | * 兼容国际版 未知 ~ 3.19.0 ~ 3.19.2 106 | * MBGA v1.5.0 ~ 107 | * 兼容国际版 未知 ~ 3.19.0 ~ 3.20.1 108 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jmailen.gradle.kotlinter.tasks.FormatTask 2 | import org.jmailen.gradle.kotlinter.tasks.LintTask 3 | 4 | val projectName = "MBGA" 5 | val androidCompileSdk = 34 6 | val androidMinSdk = 27 7 | val androidTargetSdk = 34 8 | val appPackageName = "top.trangle.mbga" 9 | val appVersionName = "1.5.0" 10 | 11 | fun gitBranch(): String { 12 | val envBranch = System.getenv("BRANCH_NAME") 13 | if (envBranch != null) { 14 | return envBranch 15 | } 16 | val os = org.apache.commons.io.output.ByteArrayOutputStream() 17 | project.exec { 18 | commandLine = "git rev-parse --abbrev-ref HEAD".split(" ") 19 | standardOutput = os 20 | } 21 | return String(os.toByteArray()).trim() 22 | } 23 | 24 | repositories { 25 | gradlePluginPortal() 26 | google() 27 | mavenCentral() 28 | maven { url = uri("https://api.xposed.info/") } 29 | maven { url = uri("https://jitpack.io") } 30 | } 31 | plugins { 32 | id("com.android.application") version "8.6.1" 33 | id("org.jetbrains.kotlin.android") version "2.0.0" 34 | id("com.google.devtools.ksp") version "2.0.0-1.0.22" 35 | id("org.jmailen.kotlinter") version "4.2.0" 36 | } 37 | android { 38 | namespace = appPackageName 39 | compileSdk = androidCompileSdk 40 | androidResources.additionalParameters += listOf( 41 | "--allow-reserved-package-id", 42 | "--package-id", 43 | "0x51" 44 | ) 45 | 46 | defaultConfig { 47 | applicationId = appPackageName 48 | minSdk = androidMinSdk 49 | targetSdk = androidTargetSdk 50 | versionName = appVersionName 51 | versionCode = System.getenv("BUILD_ID")?.toIntOrNull() ?: 1 52 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 53 | } 54 | 55 | applicationVariants.all { 56 | applicationVariants.all { 57 | val variant = this 58 | variant.outputs 59 | .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } 60 | .forEach { output -> 61 | output.outputFileName = "$applicationId-v$versionCode($versionName).apk" 62 | } 63 | } 64 | } 65 | 66 | val keystorePath = System.getenv("KEYSTORE") 67 | if (keystorePath != null) { 68 | signingConfigs { 69 | create("config") { 70 | keyAlias = System.getenv("KEY_ALIAS") 71 | keyPassword = System.getenv("KEY_PASSWORD") 72 | storeFile = file(keystorePath) 73 | storePassword = System.getenv("STORE_PASSWORD") 74 | } 75 | } 76 | } 77 | buildTypes { 78 | debug { 79 | versionNameSuffix = "_debug" 80 | } 81 | release { 82 | isMinifyEnabled = true 83 | isShrinkResources = true 84 | proguardFiles( 85 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 86 | ) 87 | signingConfig = signingConfigs.findByName("config") 88 | } 89 | create("feature") { 90 | initWith(getByName("release")) 91 | versionNameSuffix = "_feature-" + gitBranch().split("/").last() 92 | } 93 | all { 94 | var name = "$projectName ${defaultConfig.versionName}" 95 | if (versionNameSuffix != null) { 96 | name += versionNameSuffix 97 | } 98 | resValue("string", "app_name_with_version", name) 99 | } 100 | } 101 | compileOptions { 102 | sourceCompatibility = JavaVersion.VERSION_17 103 | targetCompatibility = JavaVersion.VERSION_17 104 | } 105 | kotlinOptions { 106 | jvmTarget = "17" 107 | freeCompilerArgs = listOf( 108 | "-Xno-param-assertions", "-Xno-call-assertions", "-Xno-receiver-assertions" 109 | ) 110 | } 111 | buildFeatures { 112 | buildConfig = true 113 | viewBinding = true 114 | } 115 | lint { checkReleaseBuilds = false } 116 | } 117 | 118 | dependencies { 119 | implementation("androidx.preference:preference-ktx:1.2.1") 120 | compileOnly("de.robv.android.xposed:api:82") 121 | implementation("com.highcapable.yukihookapi:api:1.2.1") 122 | ksp("com.highcapable.yukihookapi:ksp-xposed:1.2.1") 123 | implementation("androidx.core:core-ktx:1.13.1") 124 | implementation("androidx.appcompat:appcompat:1.7.0") 125 | implementation("com.google.android.material:material:1.12.0") 126 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 127 | implementation("com.hendraanggrian.material:collapsingtoolbarlayout-subtitle:1.5.0") 128 | } 129 | 130 | tasks.register("ktLint") { 131 | group = "verification" 132 | source(files("src")) 133 | } 134 | 135 | tasks.register("ktFormat") { 136 | group = "formatting" 137 | source(files("src")) 138 | } 139 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # FreeReflection 24 | -keep class me.weishu.reflection.** {*;} 25 | 26 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 27 | public static *** throwUninitializedProperty(...); 28 | public static *** throwUninitializedPropertyAccessException(...); 29 | } 30 | 31 | -keepclassmembers class * implements androidx.viewbinding.ViewBinding { 32 | *** inflate(android.view.LayoutInflater); 33 | } 34 | 35 | -keep class * extends android.app.Activity 36 | -keep class * implements androidx.viewbinding.ViewBinding { 37 | (); 38 | *** inflate(android.view.LayoutInflater); 39 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 19 | 20 | 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | top.trangle.mbga.hook.HookEntry_YukiHookXposedInit -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/Constants.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga 2 | 3 | const val BILI_IN_PKG_ID = "com.bilibili.app.in" 4 | 5 | const val CHANNEL_MSG_REQ_HOME_BOTTOM_TABS = "request_home_bottom_tabs" 6 | const val CHANNEL_MSG_HOME_BOTTOM_TABS = "home_bottom_tabs" 7 | 8 | const val BILI_IN_VER_3_18_2 = 7420600L 9 | const val BILI_IN_VER_3_19_0 = 7750300L 10 | const val BILI_IN_VER_3_19_1 = 7750500L 11 | const val BILI_IN_VER_3_19_2 = 7750600L 12 | const val BILI_IN_VER_3_20_0 = 8230300L 13 | const val BILI_IN_VER_3_20_1 = 8230500L 14 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/application/DefaultApplication.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.application 2 | 3 | import com.google.android.material.color.DynamicColors 4 | import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication 5 | 6 | class DefaultApplication : ModuleApplication() { 7 | override fun onCreate() { 8 | super.onCreate() 9 | DynamicColors.applyToActivitiesIfAvailable(this) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/HomeViewHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import com.highcapable.yukihookapi.hook.factory.constructor 4 | import com.highcapable.yukihookapi.hook.factory.field 5 | import com.highcapable.yukihookapi.hook.factory.method 6 | import com.highcapable.yukihookapi.hook.log.YLog 7 | import com.highcapable.yukihookapi.hook.type.java.IntType 8 | import com.highcapable.yukihookapi.hook.type.java.LongType 9 | import top.trangle.mbga.CHANNEL_MSG_HOME_BOTTOM_TABS 10 | import top.trangle.mbga.CHANNEL_MSG_REQ_HOME_BOTTOM_TABS 11 | import top.trangle.mbga.utils.MyHooker 12 | import top.trangle.mbga.utils.reflectionToString 13 | 14 | data class BottomTab(val name: String, val scheme: String) : java.io.Serializable 15 | 16 | object HomeViewHooker : MyHooker() { 17 | private var bottomTabs: ArrayList? = null 18 | 19 | override fun onHook() { 20 | subHook(this::hookPortraitVideo) 21 | subHook(this::hookBottomTabs) 22 | subHook(this::hookAutoRefresh) 23 | subHook(this::hookFeedConfig) 24 | subHook(this::hookIndexFeedList) 25 | 26 | setupTabProvider() 27 | } 28 | 29 | private fun hookPortraitVideo() { 30 | val clzBasicIndexItem = "com.bilibili.pegasus.api.model.BasicIndexItem".toClass() 31 | val getUri = clzBasicIndexItem.method { name = "getUri" } 32 | 33 | getUri.hook { 34 | after { 35 | if (!prefs.getBoolean("home_disable_portrait")) { 36 | return@after 37 | } 38 | val uri = result as String? 39 | if (!uri.isNullOrEmpty()) { 40 | if (uri.startsWith("bilibili://story/")) { 41 | result = "bilibili://video/" + uri.substringAfter("bilibili://story/") 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | private fun hookBottomTabs() { 49 | "tv.danmaku.bili.ui.main2.MainFragment\$a".toClass().method { name = "a" }.hook { 50 | after { 51 | val newList = ArrayList() 52 | 53 | (result as ArrayList<*>).removeIf { tabItem -> 54 | val c = 55 | tabItem.javaClass.field { name = "c" }.get(tabItem).any() 56 | ?: return@removeIf false 57 | val tabName = c.javaClass.field { name = "b" }.get(c).string() 58 | val tabScheme = c.javaClass.field { name = "d" }.get(c).string() 59 | 60 | if (tabScheme != "bilibili://user_center/mine") { 61 | newList.add(BottomTab(name = tabName, scheme = tabScheme)) 62 | } 63 | 64 | if (prefs.getBoolean("dev_log_bottom_tabs")) { 65 | YLog.debug("首页底部Tab: ${reflectionToString(c)}") 66 | } 67 | 68 | prefs.getBoolean("tabs_disable#$tabScheme") 69 | } 70 | 71 | if (bottomTabs == null) { 72 | bottomTabs = newList 73 | } 74 | } 75 | } 76 | } 77 | 78 | private fun hookAutoRefresh() { 79 | val clzConfig = "com.bilibili.pegasus.api.modelv2.Config".toClass() 80 | 81 | arrayOf( 82 | "getAutoRefreshTimeByActiveInterval", 83 | "getAutoRefreshTimeByAppearInterval", 84 | "getAutoRefreshTimeByBehaviorInterval", 85 | ).forEach { methodName -> 86 | clzConfig.method { name = methodName } 87 | .hook { 88 | replaceAny { 89 | if (!prefs.getBoolean("home_disable_auto_refresh")) { 90 | callOriginal() 91 | } else { 92 | -1L 93 | } 94 | } 95 | } 96 | } 97 | 98 | val clzIndexFeedFragmentV2 = 99 | "com.bilibili.pegasus.promo.index.IndexFeedFragmentV2".toClass() 100 | clzIndexFeedFragmentV2.method { 101 | modifiers { isStatic } 102 | param { 103 | it.size == 6 && it[0] == clzIndexFeedFragmentV2 && it[1] == IntType && it[2] == LongType && it[4] == IntType 104 | } 105 | }.hook { 106 | replaceUnit { 107 | val isByOnResume = 108 | Throwable().stackTrace.any { 109 | it.methodName.contains("onResume") 110 | } 111 | if (!isByOnResume || !prefs.getBoolean("home_disable_auto_refresh")) { 112 | callOriginal() 113 | } 114 | } 115 | } 116 | } 117 | 118 | private fun hookFeedConfig() { 119 | val clzIndexFragmentV2 = "com.bilibili.pegasus.promo.index.IndexFeedFragmentV2".toClass() 120 | 121 | val clzConfig = "com.bilibili.pegasus.api.modelv2.Config".toClass() 122 | val fieldFeedTopClean = clzConfig.field { name = "feedTopClean" } 123 | val fieldRatio = clzConfig.field { name = "smallCoverWhRatio" } 124 | 125 | clzIndexFragmentV2.method { param(clzConfig) } 126 | .hook { 127 | before { 128 | val cfg = args[0] 129 | if (prefs.getBoolean("home_disable_feed_top_clean")) { 130 | fieldFeedTopClean.get(cfg).set(0) 131 | } 132 | if (prefs.getBoolean("home_dense_vid_card")) { 133 | fieldRatio.get(cfg).set(1.77777777f) 134 | } 135 | } 136 | } 137 | } 138 | 139 | private fun hookIndexFeedList() { 140 | val clzJsonArray = "com.alibaba.fastjson.JSONArray".toClass() 141 | 142 | val clzBasicIndexItem = "com.bilibili.pegasus.api.model.BasicIndexItem".toClass() 143 | val fieldCardGoto = clzBasicIndexItem.field { name = "cardGoto" } 144 | val fieldPlayerArgs = clzBasicIndexItem.field { name = "playerArgs" } 145 | val fieldArgs = clzBasicIndexItem.field { name = "args" } 146 | val fieldTitle = clzBasicIndexItem.field { name = "title" } 147 | 148 | val clzPlayerArgs = "com.bilibili.app.comm.list.common.api.model.PlayerArgs".toClass() 149 | val fieldDuration = clzPlayerArgs.field { name = "fakeDuration" } 150 | 151 | val clzArgs = "com.bilibili.pegasus.api.modelv2.Args".toClass() 152 | val fieldUpName = clzArgs.field { name = "upName" } 153 | val fieldUpId = clzArgs.field { name = "upId" } 154 | 155 | val clzSmallCoverV2Item = "com.bilibili.pegasus.api.modelv2.SmallCoverV2Item".toClass() 156 | val fieldRcmdReason = clzSmallCoverV2Item.field { name = "rcmdReason" } 157 | val fieldGotoIcon = clzSmallCoverV2Item.field { name = "storyCardIcon" } 158 | val fieldDescButton = clzSmallCoverV2Item.field { name = "descButton" } 159 | 160 | val clzDescButton = "com.bilibili.pegasus.api.modelv2.DescButton".toClass() 161 | val cntrDescButton = clzDescButton.constructor { paramCount(0) } 162 | val fieldText = clzDescButton.field { name = "text" } 163 | val fieldUri = clzDescButton.field { name = "uri" } 164 | 165 | "com.bilibili.pegasus.api.BaseTMApiParser".toClass() 166 | .method { 167 | param(clzJsonArray) 168 | returnType = ArrayList::class.java 169 | } 170 | .hook { 171 | after { 172 | val keepOnlyUgc = prefs.getBoolean("home_show_only_ugc") 173 | val durationMin = prefs.getInt("home_duration_min", 0) 174 | val filterKeywords = prefs.getStringSet("home_vid_filter_keyword") 175 | 176 | if (keepOnlyUgc || durationMin > 0 || filterKeywords.isNotEmpty()) { 177 | (result as ArrayList<*>).removeIf { 178 | if (keepOnlyUgc && fieldCardGoto.get(it).string() != "av") { 179 | if (prefs.getBoolean("dev_log_feed_removal")) { 180 | YLog.debug("feed item removed because it's not ugc: ${reflectionToString(it)}") 181 | } 182 | return@removeIf true 183 | } 184 | if (durationMin > 0) { 185 | val playerArgs = 186 | fieldPlayerArgs.get(it).any() 187 | ?: return@removeIf false 188 | val duration = 189 | fieldDuration.get(playerArgs).int() 190 | if (duration < durationMin) { 191 | if (prefs.getBoolean("dev_log_feed_removal")) { 192 | YLog.debug("feed item removed because it's too short: ${reflectionToString(it)}") 193 | } 194 | return@removeIf true 195 | } 196 | } 197 | if (filterKeywords.isNotEmpty()) { 198 | val title = fieldTitle.get(it).string() 199 | filterKeywords.forEach { keyword -> 200 | if (title.contains(keyword)) { 201 | if (prefs.getBoolean("dev_log_feed_removal")) { 202 | YLog.debug("feed item removed because keyword $keyword: ${reflectionToString(it)}") 203 | } 204 | return@removeIf true 205 | } 206 | } 207 | } 208 | false 209 | } 210 | } 211 | 212 | val pureVidCard = prefs.getBoolean("home_pure_vid_card") 213 | if (pureVidCard) { 214 | (result as List<*>).forEach { 215 | if (clzSmallCoverV2Item.isInstance(it)) { 216 | fieldRcmdReason.get(it).setNull() 217 | fieldGotoIcon.get(it).setNull() 218 | val args = fieldArgs.get(it).any() 219 | val upName = fieldUpName.get(args).string() 220 | val upId = fieldUpId.get(args).long() 221 | val descButton = fieldDescButton.get(it) 222 | if (descButton.any() == null) { 223 | val newBtn = cntrDescButton.give()?.newInstance() 224 | fieldText.get(newBtn).set(upName) 225 | fieldUri.get(newBtn).set("bilibili://space/$upId") 226 | descButton.set(newBtn) 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | 235 | private fun setupTabProvider() { 236 | dataChannel.wait(key = CHANNEL_MSG_REQ_HOME_BOTTOM_TABS) { 237 | sendTabToModule() 238 | } 239 | } 240 | 241 | private fun sendTabToModule() { 242 | val tabs = bottomTabs ?: return 243 | dataChannel.put( 244 | key = CHANNEL_MSG_HOME_BOTTOM_TABS, 245 | value = tabs, 246 | ) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/HookEntry.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed 4 | import com.highcapable.yukihookapi.hook.factory.configs 5 | import com.highcapable.yukihookapi.hook.factory.encase 6 | import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit 7 | import top.trangle.mbga.BILI_IN_PKG_ID 8 | import top.trangle.mbga.BuildConfig 9 | 10 | @InjectYukiHookWithXposed 11 | class HookEntry : IYukiHookXposedInit { 12 | override fun onInit() = 13 | configs { 14 | debugLog { 15 | tag = "MBGA" 16 | isEnable = true 17 | } 18 | isEnableHookSharedPreferences = true 19 | isEnableDataChannel = true 20 | isDebug = BuildConfig.BUILD_TYPE == "debug" 21 | } 22 | 23 | override fun onHook() = 24 | encase { 25 | loadApp(name = BILI_IN_PKG_ID) { 26 | onAppLifecycle { 27 | onCreate { 28 | loadHooker(MainActivityHooker) 29 | loadHooker(VideoPlayerHooker) 30 | loadHooker(VideoDetailHooker) 31 | loadHooker(SettingEntryHooker) 32 | loadHooker(VideoCommentHooker) 33 | loadHooker(HomeViewHooker) 34 | loadHooker(SearchViewHooker) 35 | loadHooker(MineViewHooker) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/MainActivityHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import android.app.Activity 4 | import android.content.ComponentName 5 | import android.content.Intent 6 | import com.highcapable.yukihookapi.hook.factory.injectModuleAppResources 7 | import com.highcapable.yukihookapi.hook.factory.method 8 | import top.trangle.mbga.utils.MyHooker 9 | 10 | object MainActivityHooker : MyHooker() { 11 | override fun onHook() { 12 | subHook(this::injectResources) 13 | subHook(this::hookMainCreate) 14 | } 15 | 16 | private fun injectResources() { 17 | val clzActivity = "android.app.Activity".toClass() 18 | clzActivity.method { name = "onConfigurationChanged" } 19 | .hook { 20 | before { 21 | val activity = instance as Activity 22 | activity.injectModuleAppResources() 23 | } 24 | } 25 | clzActivity.method { name = "onCreate" } 26 | .hook { 27 | before { 28 | val activity = instance as Activity 29 | activity.injectModuleAppResources() 30 | } 31 | } 32 | } 33 | 34 | private fun hookMainCreate() { 35 | "tv.danmaku.bili.MainActivityV2".toClass().method { name = "onCreate" } 36 | .hook { 37 | after { 38 | val activity = instance as Activity 39 | 40 | if (!prefs.isPreferencesAvailable) { 41 | val intent = 42 | Intent().apply { 43 | component = 44 | ComponentName( 45 | "top.trangle.mbga", 46 | "top.trangle.mbga.views.SettingsActivity", 47 | ) 48 | putExtra("show_first_launch_alert", true) 49 | } 50 | activity.startActivity(intent) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/MineViewHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import com.highcapable.yukihookapi.hook.factory.constructor 6 | import com.highcapable.yukihookapi.hook.factory.field 7 | import com.highcapable.yukihookapi.hook.factory.method 8 | import top.trangle.mbga.R 9 | import top.trangle.mbga.utils.MyHooker 10 | 11 | const val SEARCH_URI = "bilibili://search" 12 | const val IM_URI = "activity://link/im-home" 13 | 14 | object MineViewHooker : MyHooker() { 15 | override fun onHook() { 16 | subHook(this::hookMoreServiceMenu) 17 | subHook(this::hookFragmentResume) 18 | } 19 | 20 | private fun hookMoreServiceMenu() { 21 | val clzMenuGroup = "com.bilibili.lib.homepage.mine.MenuGroup".toClass() 22 | val fieldStyle = clzMenuGroup.field { name = "style" } 23 | val fieldItemList = clzMenuGroup.field { name = "itemList" } 24 | 25 | val clzMenuGroupItem = "com.bilibili.lib.homepage.mine.MenuGroup\$Item".toClass() 26 | val ctorMenuGroupItem = clzMenuGroupItem.constructor() 27 | val fieldIconResId = clzMenuGroupItem.field { name = "iconResId" } 28 | val fieldTitle = clzMenuGroupItem.field { name = "title" } 29 | val fieldUri = clzMenuGroupItem.field { name = "uri" } 30 | val fieldVisible = clzMenuGroupItem.field { name = "visible" } 31 | 32 | val clzAccountMine = "tv.danmaku.bili.ui.main2.api.AccountMine".toClass() 33 | 34 | "tv.danmaku.bili.ui.main2.mine.HomeUserCenterFragment".toClass().method { 35 | param(android.content.Context::class.java, java.util.List::class.java, clzAccountMine) 36 | } 37 | .hook { 38 | before { 39 | val addSearch = prefs.getBoolean("mine_add_search") 40 | val addIm = prefs.getBoolean("mine_add_im") 41 | if (!(addSearch || addIm)) { 42 | return@before 43 | } 44 | val ctx = args[0] as Context 45 | (args[1] as List<*>).forEach { menuGroup -> 46 | if (fieldStyle.get(menuGroup).int() != 2) { 47 | return@forEach 48 | } 49 | @Suppress("UNCHECKED_CAST") 50 | val list = 51 | fieldItemList.get(menuGroup).any() as? ArrayList ?: return@forEach 52 | val newList = ArrayList() 53 | if (addSearch) { 54 | newList.add( 55 | ctorMenuGroupItem.get().call().also { item -> 56 | fieldIconResId.get(item).set(R.drawable.ic_search_pink) 57 | fieldTitle.get(item) 58 | .set(ctx.resources.getString(R.string.common_search)) 59 | fieldUri.get(item).set(SEARCH_URI) 60 | fieldVisible.get(item).set(1) 61 | }, 62 | ) 63 | } 64 | if (addIm) { 65 | newList.add( 66 | ctorMenuGroupItem.get().call().also { item -> 67 | fieldIconResId.get(item).set(R.drawable.ic_im_pink) 68 | fieldTitle.get(item) 69 | .set(ctx.resources.getString(R.string.common_im)) 70 | fieldUri.get(item).set(IM_URI) 71 | fieldVisible.get(item).set(1) 72 | }, 73 | ) 74 | } 75 | 76 | newList.addAll( 77 | list, 78 | ) 79 | fieldItemList.get(menuGroup).set(newList) 80 | } 81 | } 82 | } 83 | } 84 | 85 | private fun hookFragmentResume() { 86 | val clzMineVipEntranceView = "tv.danmaku.bili.ui.main2.mine.widgets.MineVipEntranceView".toClass() 87 | 88 | val clzHomeUserCenterFragment = "tv.danmaku.bili.ui.main2.mine.HomeUserCenterFragment".toClass() 89 | val fieldMineVipEntranceView = clzHomeUserCenterFragment.field { type = clzMineVipEntranceView } 90 | 91 | clzHomeUserCenterFragment.method { 92 | name = "onResume" 93 | }.hook { 94 | before { 95 | if (prefs.getBoolean("mine_remove_vip")) { 96 | val vipView = fieldMineVipEntranceView.get(instance).any() as View 97 | vipView.visibility = 98 | if (prefs.getBoolean("mine_keep_vip_space")) { 99 | View.INVISIBLE 100 | } else { 101 | View.GONE 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/SearchViewHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import com.highcapable.yukihookapi.hook.factory.method 4 | import top.trangle.mbga.BILI_IN_VER_3_19_2 5 | import top.trangle.mbga.BILI_IN_VER_3_20_0 6 | import top.trangle.mbga.utils.MyHooker 7 | 8 | object SearchViewHooker : MyHooker() { 9 | override fun onHook() { 10 | subHook(this::hookSearchType) 11 | versionSpecifiedSubHook(this::hookDefaultSearchWordsV1, Long.MIN_VALUE..BILI_IN_VER_3_19_2) 12 | versionSpecifiedSubHook(this::hookDefaultSearchWordsV2, BILI_IN_VER_3_20_0..Long.MAX_VALUE) 13 | } 14 | 15 | private fun hookSearchType() { 16 | "com.bilibili.search2.api.SearchSquareType".toClass().method { name = "getType" }.hook { 17 | /* 18 | * 这个钩子基于这个方法的实现:com.bilibili.search2.discover.l$a.d7 19 | * 它会根据SearchSquareType.getType的返回值将数据展示到UI对应区域上, 20 | * 如果getType返回了它不认识的类型,会直接丢弃。所以这里遇到不想要的类型直接返回空就行 21 | */ 22 | 23 | after { 24 | if (prefs.getBoolean("search_disable_$result")) { 25 | resultNull() 26 | } 27 | } 28 | } 29 | } 30 | 31 | private fun hookDefaultSearchWordsV1() { 32 | "com.bapis.bilibili.app.interfaces.v1.SearchMoss".toClass() 33 | .method { name = "defaultWords" } 34 | .hook { 35 | replaceAny { 36 | if (!prefs.getBoolean("search_disable_default_words")) { 37 | callOriginal() 38 | } else { 39 | null 40 | } 41 | } 42 | } 43 | } 44 | 45 | private fun hookDefaultSearchWordsV2() { 46 | "com.bapis.bilibili.app.interfaces.v1.SearchMoss".toClass() 47 | .method { name = "executeDefaultWords" } 48 | .hook { 49 | replaceAny { 50 | if (!prefs.getBoolean("search_disable_default_words")) { 51 | callOriginal() 52 | } else { 53 | null 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/SettingEntryHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.view.Gravity 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import com.highcapable.yukihookapi.hook.factory.method 10 | import top.trangle.mbga.utils.MyHooker 11 | import top.trangle.mbga.utils.factory.dp 12 | 13 | object SettingEntryHooker : MyHooker() { 14 | override fun onHook() { 15 | subHook(this::hookPreferencesActivity) 16 | } 17 | 18 | private fun hookPreferencesActivity() { 19 | val clzBiliPreferencesActivity = 20 | "com.bilibili.app.preferences.BiliPreferencesActivity".toClass() 21 | val onCreate = clzBiliPreferencesActivity.method { name = "onCreate" } 22 | 23 | onCreate.hook { 24 | after { 25 | val activity = instance as Activity 26 | 27 | @SuppressLint("DiscouragedApi") 28 | val toolbar: ViewGroup = 29 | activity.findViewById( 30 | activity.resources 31 | .getIdentifier("nav_top_bar", "id", activity.packageName), 32 | ) 33 | val lp = 34 | ViewGroup.LayoutParams( 35 | ViewGroup.LayoutParams.WRAP_CONTENT, 36 | ViewGroup.LayoutParams.MATCH_PARENT, 37 | ) 38 | val tv = TextView(instance as Activity) 39 | 40 | @SuppressLint("SetTextI18n") 41 | tv.text = "MBGA!" 42 | tv.gravity = Gravity.CENTER 43 | tv.setPadding(16.dp(activity), 0, 16.dp(activity), 0) 44 | tv.setOnClickListener { 45 | val intent = Intent(Intent.ACTION_MAIN) 46 | intent.setClassName( 47 | "top.trangle.mbga", 48 | "top.trangle.mbga.views.SettingsActivity", 49 | ) 50 | activity.startActivity(intent) 51 | } 52 | toolbar.addView(tv, 2, lp) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/VideoCommentHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import com.highcapable.yukihookapi.hook.core.YukiMemberHookCreator 4 | import com.highcapable.yukihookapi.hook.core.api.priority.YukiHookPriority 5 | import com.highcapable.yukihookapi.hook.factory.constructor 6 | import com.highcapable.yukihookapi.hook.factory.field 7 | import com.highcapable.yukihookapi.hook.factory.method 8 | import com.highcapable.yukihookapi.hook.log.YLog 9 | import com.highcapable.yukihookapi.hook.type.java.ListClass 10 | import com.highcapable.yukihookapi.hook.type.java.UnitType 11 | import top.trangle.mbga.BILI_IN_VER_3_18_2 12 | import top.trangle.mbga.BILI_IN_VER_3_19_0 13 | import top.trangle.mbga.BILI_IN_VER_3_19_1 14 | import top.trangle.mbga.BILI_IN_VER_3_19_2 15 | import top.trangle.mbga.BILI_IN_VER_3_20_0 16 | import top.trangle.mbga.BILI_IN_VER_3_20_1 17 | import top.trangle.mbga.utils.MyHooker 18 | 19 | object VideoCommentHooker : MyHooker() { 20 | override fun onHook() { 21 | versionSpecifiedSubHook(this::hookCommentClickV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 22 | versionSpecifiedSubHook(this::hookCommentClickV2, BILI_IN_VER_3_19_0..BILI_IN_VER_3_19_0) 23 | versionSpecifiedSubHook(this::hookCommentClickV3, BILI_IN_VER_3_19_1..BILI_IN_VER_3_19_2) 24 | versionSpecifiedSubHook(this::hookCommentClickV4, BILI_IN_VER_3_20_0..BILI_IN_VER_3_20_0) 25 | versionSpecifiedSubHook(this::hookCommentClickV5, BILI_IN_VER_3_20_1..Long.MAX_VALUE) 26 | subHook(this::hookTopVote) 27 | versionSpecifiedSubHook(this::hookStandVoteV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 28 | versionSpecifiedSubHook(this::hookStandVoteV2, BILI_IN_VER_3_19_0..Long.MAX_VALUE) 29 | versionSpecifiedSubHook(this::hookFollowV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 30 | versionSpecifiedSubHook(this::hookFollowV2, BILI_IN_VER_3_19_0..Long.MAX_VALUE) 31 | subHook(this::hookUrls) 32 | versionSpecifiedSubHook(this::hookEmptyPageV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 33 | versionSpecifiedSubHook(this::hookEmptyPageV2, BILI_IN_VER_3_19_0..Long.MAX_VALUE) 34 | subHook(this::hookMainList) 35 | subHook(this::hookCommentProvider) 36 | subHook(this::hookGetReplies) 37 | } 38 | 39 | /** 3.18.2 可用 */ 40 | private fun hookCommentClickV1() { 41 | val clzCommentMessageWidget = 42 | "com.bilibili.app.comm.comment2.phoenix.view.CommentMessageWidget".toClass() 43 | val onClick = 44 | clzCommentMessageWidget.method { 45 | modifiers { isFinal } 46 | param { it.size == 2 && it[1] == android.view.View::class.java } 47 | } 48 | 49 | onClick.hook { 50 | replaceUnit { 51 | if (!prefs.getBoolean("vid_comment_no_quick_reply")) { 52 | callOriginal() 53 | } 54 | } 55 | } 56 | } 57 | 58 | /** 3.19.0 可用 */ 59 | private fun hookCommentClickV2() { 60 | "com.bilibili.app.comment3.viewmodel.CommentViewModel".toClass() 61 | .method { 62 | name = "N2" 63 | returnType = UnitType 64 | } // NOTE: 更新后容易失效的 65 | .hook { 66 | replaceUnit { 67 | if (!prefs.getBoolean("vid_comment_no_quick_reply")) { 68 | callOriginal() 69 | } else if (!args[0].toString().startsWith("ShowPublishDialog")) { 70 | callOriginal() 71 | } else { 72 | val allowClickFrom = 73 | arrayOf( 74 | "CommentActionBarHandler", 75 | "CommentViewModel\$dispatchAction\$1", 76 | "CommentMainLayer", 77 | "CommentDetailLayer", 78 | ) 79 | val isAllowed = 80 | Throwable().stackTrace.any { st -> 81 | allowClickFrom.any { fr -> 82 | st.className.contains(fr) 83 | } 84 | } 85 | if (isAllowed) { 86 | callOriginal() 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 3.19.1, 3.19.2 可用 */ 94 | private fun hookCommentClickV3() { 95 | "com.bilibili.app.comment3.viewmodel.CommentViewModel".toClass() 96 | .method { 97 | name = "M2" 98 | returnType = UnitType 99 | } // NOTE: 更新后容易失效的 100 | .hook { 101 | replaceUnit { 102 | if (!prefs.getBoolean("vid_comment_no_quick_reply")) { 103 | callOriginal() 104 | } else if (!args[0].toString().startsWith("ShowPublishDialog")) { 105 | callOriginal() 106 | } else { 107 | val allowClickFrom = 108 | arrayOf( 109 | "CommentActionBarHandler", 110 | "CommentViewModel\$dispatchAction\$1", 111 | "CommentMainLayer", 112 | "CommentDetailLayer", 113 | ) 114 | val isAllowed = 115 | Throwable().stackTrace.any { st -> 116 | allowClickFrom.any { fr -> 117 | st.className.contains(fr) 118 | } 119 | } 120 | if (isAllowed) { 121 | callOriginal() 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | /** 3.20.0 可用 */ 129 | private fun hookCommentClickV4() { 130 | "com.bilibili.app.comment3.viewmodel.CommentViewModel".toClass() 131 | .method { 132 | name = "l3" 133 | returnType = UnitType 134 | } // NOTE: 更新后容易失效的 135 | .hook { 136 | replaceUnit { 137 | if (!prefs.getBoolean("vid_comment_no_quick_reply")) { 138 | callOriginal() 139 | } else if (!args[0].toString().startsWith("ShowPublishDialog")) { 140 | callOriginal() 141 | } else { 142 | YLog.debug(Throwable().stackTraceToString()) 143 | val allowClickFrom = 144 | arrayOf( 145 | // 得放,不然同一个评论,第二次点击回复会没反应 146 | "CommentViewModel\$dispatchAction\$1", 147 | // 底部 148 | "CommentMainLayer", 149 | // 楼中楼底部 150 | "CommentDetailLayer", 151 | // 回复按钮 152 | "CommentContentHolder.p4", 153 | // 长按或点击点点点弹出选择回复 154 | "CommentMoreMenuDialog", 155 | ) 156 | val isAllowed = 157 | Throwable().stackTrace.any { st -> 158 | allowClickFrom.any { fr -> 159 | "${st.className}.${st.methodName}".contains(fr) 160 | } 161 | } 162 | if (isAllowed) { 163 | callOriginal() 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | /** 3.20.1 可用 */ 171 | private fun hookCommentClickV5() { 172 | "com.bilibili.app.comment3.viewmodel.CommentViewModel".toClass() 173 | .method { 174 | name = "k3" 175 | returnType = UnitType 176 | } // NOTE: 更新后容易失效的 177 | .hook { 178 | replaceUnit { 179 | if (!prefs.getBoolean("vid_comment_no_quick_reply")) { 180 | callOriginal() 181 | } else if (!args[0].toString().startsWith("ShowPublishDialog")) { 182 | callOriginal() 183 | } else { 184 | YLog.debug(Throwable().stackTraceToString()) 185 | val allowClickFrom = 186 | arrayOf( 187 | // 得放,不然同一个评论,第二次点击回复会没反应 188 | "CommentViewModel\$dispatchAction\$1", 189 | // 底部 190 | "CommentMainLayer", 191 | // 楼中楼底部 192 | "CommentDetailLayer", 193 | // 回复按钮 194 | "CommentContentHolder.p4", 195 | // 长按或点击点点点弹出选择回复 196 | "CommentMoreMenuDialog", 197 | ) 198 | val isAllowed = 199 | Throwable().stackTrace.any { st -> 200 | allowClickFrom.any { fr -> 201 | "${st.className}.${st.methodName}".contains(fr) 202 | } 203 | } 204 | if (isAllowed) { 205 | callOriginal() 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | private val voteReplaceHook: (YukiMemberHookCreator.MemberHookCreator) -> Unit = { 213 | it.replaceUnit { 214 | if (!prefs.getBoolean("vid_comment_no_vote")) { 215 | callOriginal() 216 | } 217 | } 218 | } 219 | 220 | /** 评论区顶部的投票,3.18.2和3.19.0都可用 */ 221 | private fun hookTopVote() { 222 | "com.bilibili.app.comment.ext.widgets.CmtVoteWidget".toClass() 223 | .method { 224 | modifiers { isFinal } 225 | param { it[0].name.startsWith("com.bilibili.app.comment.ext.model.") } 226 | }.hook(YukiHookPriority.DEFAULT, voteReplaceHook) 227 | } 228 | 229 | /** 评论内容中站队信息,3.18.2 可用 */ 230 | private fun hookStandVoteV1() { 231 | "com.bilibili.app.comm.comment2.phoenix.view.CommentMountWidget".toClass().method { 232 | param { it.size == 1 && it[0].name.startsWith("com.bilibili.app.comm.comment2.comments.vvmadapter.") } 233 | }.hook(YukiHookPriority.DEFAULT, voteReplaceHook) 234 | } 235 | 236 | /** 评论内容中站队信息,3.19.0, 3.19.1 可用 */ 237 | private fun hookStandVoteV2() { 238 | "com.bilibili.app.comment.ext.widgets.CmtMountWidget".toClass() 239 | .method { 240 | modifiers { isFinal } 241 | param { it.size == 2 && it[0].name.startsWith("com.bilibili.app.comment.ext.model.") } 242 | }.hook(YukiHookPriority.DEFAULT, voteReplaceHook) 243 | } 244 | 245 | /** 3.18.2 可用 */ 246 | private fun hookFollowV1() { 247 | "com.bilibili.app.comm.comment2.phoenix.view.CommentFollowWidget".toClass() 248 | .method { 249 | param { it.size == 1 && it[0].name.startsWith("com.bilibili.app.comm.comment2.comments.vvmadapter.") } 250 | }.hook { 251 | replaceUnit { 252 | if (!prefs.getBoolean("vid_comment_no_follow")) { 253 | callOriginal() 254 | } 255 | } 256 | } 257 | } 258 | 259 | /** 3.19.0, 3.19.1, 3.19.2 可用 */ 260 | private fun hookFollowV2() { 261 | "com.bilibili.app.comment3.ui.widget.CommentHeaderDecorativeView".toClass().method { 262 | param { it.size == 2 && it[0] == ListClass } 263 | } 264 | .hook { 265 | before { 266 | if (!prefs.getBoolean("vid_comment_no_follow")) { 267 | return@before 268 | } 269 | (args[0] as List<*>).forEach { cmtIt -> 270 | cmtIt?.javaClass?.declaredFields?.forEach { field -> 271 | if (field.type.name.startsWith("com.bilibili.app.comment3.data.model.CommentItem")) { 272 | field.isAccessible = true 273 | if (field.get(cmtIt)?.toString()?.startsWith("Follow(") == true) { 274 | field.set(cmtIt, null) 275 | } 276 | } 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | private fun hookUrls() { 284 | val clzMapFieldLite = "com.google.protobuf.MapFieldLite".toClass() 285 | val methodMutableCopy = clzMapFieldLite.method { name = "mutableCopy" } 286 | 287 | val clzUrl = "com.bapis.bilibili.main.community.reply.v1.Url".toClass() 288 | val fieldAppUrlSchema = clzUrl.field { name = "appUrlSchema_" } 289 | 290 | "com.bapis.bilibili.main.community.reply.v1.Content".toClass() 291 | .method { name = "internalGetUrls" } 292 | .hook { 293 | after { 294 | if (!prefs.getBoolean("vid_comment_no_search")) { 295 | return@after 296 | } 297 | val map = methodMutableCopy.get(result).call() as LinkedHashMap<*, *> 298 | map.entries.removeIf { (_, value) -> 299 | fieldAppUrlSchema.get(value).string().startsWith("bilibili://search") 300 | } 301 | result = map 302 | } 303 | } 304 | } 305 | 306 | private fun hookEmptyPageV1() { 307 | val clzEmptyPage = "com.bapis.bilibili.main.community.reply.v1.EmptyPage".toClass() 308 | val defaultEmptyPage = clzEmptyPage.field { name = "DEFAULT_INSTANCE" } 309 | 310 | "com.bapis.bilibili.main.community.reply.v1.SubjectControl".toClass() 311 | .method { 312 | name = "getEmptyPage" 313 | }.hook { 314 | after { 315 | if (prefs.getBoolean("vid_comment_no_empty_page")) { 316 | result = defaultEmptyPage.get().any() 317 | } 318 | } 319 | } 320 | } 321 | 322 | private fun hookEmptyPageV2() { 323 | val clzEmptyPage = "com.bapis.bilibili.main.community.reply.v2.EmptyPage".toClass() 324 | val defaultEmptyPage = clzEmptyPage.field { name = "DEFAULT_INSTANCE" } 325 | 326 | "com.bapis.bilibili.main.community.reply.v2.SubjectDescriptionReply".toClass() 327 | .method { 328 | name = "getEmptyPage" 329 | }.hook { 330 | after { 331 | if (prefs.getBoolean("vid_comment_no_empty_page")) { 332 | result = defaultEmptyPage.get().any() 333 | } 334 | } 335 | } 336 | } 337 | 338 | private fun hookMainList() { 339 | val clzDmViewReply = "com.bapis.bilibili.main.community.reply.v1.MainListReply".toClass() 340 | val clearQoe = clzDmViewReply.method { name = "clearQoe" } 341 | val clearOperation = clzDmViewReply.method { name = "clearOperation" } 342 | val clearOperationV2 = clzDmViewReply.method { name = "clearOperationV2" } 343 | 344 | "com.bapis.bilibili.main.community.reply.v1.ReplyMossKtxKt\$suspendMainList\$\$inlined\$suspendCall\$1" 345 | .toClass() 346 | .method { name = "onNext" } 347 | .hook { 348 | before { 349 | if (prefs.getBoolean("dev_log_main_list")) { 350 | YLog.debug(args[0].toString()) 351 | } 352 | if (prefs.getBoolean("vid_comment_no_qoe")) { 353 | clearQoe.get(args[0]).call() 354 | } 355 | if (prefs.getBoolean("vid_comment_no_operation")) { 356 | clearOperation.get(args[0]).call() 357 | clearOperationV2.get(args[0]).call() 358 | } 359 | } 360 | } 361 | } 362 | 363 | private fun hookCommentProvider() { 364 | "com.bilibili.ship.theseus.united.page.tab.TheseusTabPagerService".toClass() 365 | .constructor() 366 | .give()?.let { ctor -> 367 | ctor.parameters[6].type.constructor().hook { 368 | before { 369 | if (!prefs.getBoolean("vid_comment_disable")) { 370 | return@before 371 | } 372 | args[0] = 373 | (args[0] as List<*>).filter { listItem -> 374 | listItem != null && 375 | !listItem.javaClass.typeName.contains("CommentTabPageProvider") 376 | } 377 | } 378 | } 379 | } 380 | } 381 | 382 | private fun hookGetReplies() { 383 | val clzReplyInfo = "com.bapis.bilibili.main.community.reply.v1.ReplyInfo".toClass() 384 | val fieldContent = clzReplyInfo.field { name = "content_" } 385 | 386 | val clzContent = "com.bapis.bilibili.main.community.reply.v1.Content".toClass() 387 | val fieldMessage = clzContent.field { name = "message_" } 388 | 389 | "com.bapis.bilibili.main.community.reply.v1.MainListReply".toClass() 390 | .method { name = "getRepliesList" } 391 | .hook { 392 | after { 393 | val keywords = prefs.getStringSet("vid_comment_filter_keyword") 394 | if (keywords.isNotEmpty()) { 395 | val replies = arrayListOf(*(result as List<*>).toTypedArray()) 396 | replies.removeIf { reply -> 397 | val content = fieldContent.get(reply).any() 398 | val message = fieldMessage.get(content).string() 399 | keywords.any { keyword -> 400 | message.contains(keyword) 401 | } 402 | } 403 | result = replies 404 | } 405 | } 406 | } 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/VideoDetailHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import android.app.Activity 4 | import android.view.WindowInsets 5 | import android.view.WindowInsetsController 6 | import com.highcapable.yukihookapi.hook.core.YukiMemberHookCreator 7 | import com.highcapable.yukihookapi.hook.core.api.priority.YukiHookPriority 8 | import com.highcapable.yukihookapi.hook.core.finder.members.FieldFinder 9 | import com.highcapable.yukihookapi.hook.core.finder.members.MethodFinder 10 | import com.highcapable.yukihookapi.hook.factory.field 11 | import com.highcapable.yukihookapi.hook.factory.method 12 | import com.highcapable.yukihookapi.hook.log.YLog 13 | import top.trangle.mbga.BILI_IN_VER_3_18_2 14 | import top.trangle.mbga.BILI_IN_VER_3_19_0 15 | import top.trangle.mbga.BILI_IN_VER_3_19_1 16 | import top.trangle.mbga.BILI_IN_VER_3_19_2 17 | import top.trangle.mbga.BILI_IN_VER_3_20_0 18 | import top.trangle.mbga.BILI_IN_VER_3_20_1 19 | import top.trangle.mbga.utils.MyHooker 20 | 21 | object VideoDetailHooker : MyHooker() { 22 | override fun onHook() { 23 | versionSpecifiedSubHook(this::hookLabelV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 24 | versionSpecifiedSubHook(this::hookLabelV2, BILI_IN_VER_3_19_0..Long.MAX_VALUE) 25 | versionSpecifiedSubHook(this::hookShareLinkV1, Long.MIN_VALUE..BILI_IN_VER_3_18_2) 26 | versionSpecifiedSubHook(this::hookShareLinkV2, BILI_IN_VER_3_19_0..BILI_IN_VER_3_19_0) 27 | versionSpecifiedSubHook(this::hookShareLinkV3, BILI_IN_VER_3_19_1..BILI_IN_VER_3_19_2) 28 | versionSpecifiedSubHook(this::hookShareLinkV4, BILI_IN_VER_3_20_0..BILI_IN_VER_3_20_0) 29 | versionSpecifiedSubHook(this::hookShareLinkV5, BILI_IN_VER_3_20_1..Long.MAX_VALUE) 30 | subHook(this::hookRelates) 31 | subHook(this::hookUnitedBizDetailActivity) 32 | } 33 | 34 | /** 3.18.2 可用 */ 35 | private fun hookLabelV1() { 36 | val clzLabel = "tv.danmaku.bili.videopage.data.view.model.BiliVideoDetail\$Label".toClass() 37 | val clzDescSection = "tv.danmaku.bili.ui.video.section.info.DescSection".toClass() 38 | val getLabelFromDesc = 39 | clzDescSection.method { 40 | emptyParam() 41 | returnType = clzLabel 42 | } 43 | 44 | getLabelFromDesc.hook { 45 | after { 46 | if (prefs.getBoolean("vid_detail_disable_label")) { 47 | resultNull() 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 3.19.0, 3.19.1, 3.19.2 可用 */ 54 | private fun hookLabelV2() { 55 | val clzLabel = "com.bapis.bilibili.app.viewunite.common.Label".toClass() 56 | val defaultLabel = clzLabel.field { name = "DEFAULT_INSTANCE" } 57 | 58 | val clzHeadline = "com.bapis.bilibili.app.viewunite.common.Headline".toClass() 59 | clzHeadline.method { name = "getLabel" } 60 | .hook { 61 | after { 62 | if (prefs.getBoolean("vid_detail_disable_label")) { 63 | result = defaultLabel.get().any() 64 | } 65 | } 66 | } 67 | } 68 | 69 | private fun generateShareLinkHook( 70 | clzShareResult: Class<*>, 71 | contentField: FieldFinder.Result, 72 | shareTaskCallback: MethodFinder.Result, 73 | ): (YukiMemberHookCreator.MemberHookCreator) -> Unit = 74 | { 75 | it.replaceUnit { 76 | if (!prefs.getBoolean("vid_detail_disable_short_link")) { 77 | callOriginal() 78 | return@replaceUnit 79 | } 80 | val result = clzShareResult.getDeclaredConstructor().newInstance() 81 | val shareId = args[1] as String 82 | val shareObjId = args[2] as String 83 | val shareChannel = args[5] as String 84 | if (shareChannel != "COPY") { 85 | callOriginal() 86 | return@replaceUnit 87 | } 88 | val link = 89 | when { 90 | shareId.startsWith("main.") -> "https://b23.tv/av$shareObjId" 91 | shareId.startsWith("dt.") -> "https://m.bilibili.com/opus/$shareObjId" 92 | shareId.startsWith("live.") -> "https://live.bilibili.com/$shareObjId" 93 | else -> { 94 | YLog.info("shareId not supported: $shareId") 95 | callOriginal() 96 | return@replaceUnit 97 | } 98 | } 99 | contentField.get(result).set(link) 100 | shareTaskCallback.get(args[17]).invoke(result) 101 | } 102 | } 103 | 104 | /** 3.18.2 可用 */ 105 | private fun hookShareLinkV1() { 106 | val clzShareResult = "com.bilibili.lib.sharewrapper.online.api.ShareClickResult".toClass() 107 | val contentField = clzShareResult.field { name = "content" } 108 | 109 | val clzShareTargetTask = 110 | "com.bilibili.app.comm.supermenu.share.v2.ShareTargetTask\$f".toClass() 111 | val shareTaskCallback = clzShareTargetTask.method { name = "l" } 112 | 113 | ("ah1.c".toClassOrNull() ?: return).method { name = "c" }.hook( 114 | YukiHookPriority.DEFAULT, 115 | generateShareLinkHook( 116 | clzShareResult, 117 | contentField, 118 | shareTaskCallback, 119 | ), 120 | ) 121 | } 122 | 123 | /** 3.19.0 可用 */ 124 | private fun hookShareLinkV2() { 125 | val clzShareResult = "com.bilibili.lib.sharewrapper.online.api.ShareClickResult".toClass() 126 | val contentField = clzShareResult.field { name = "content" } 127 | 128 | // NOTE: 更新后容易失效的 129 | val clzShareTargetTask = 130 | "com.bilibili.app.comm.supermenu.share.v2.ShareTargetTask\$f".toClass() 131 | val shareTaskCallback = clzShareTargetTask.method { name = "l" } 132 | 133 | ("com.bilibili.lib.sharewrapper.online.api.b".toClassOrNull() ?: return).method { 134 | name = "g" 135 | }.hook( 136 | YukiHookPriority.DEFAULT, 137 | generateShareLinkHook( 138 | clzShareResult, 139 | contentField, 140 | shareTaskCallback, 141 | ), 142 | ) 143 | } 144 | 145 | /** 3.19.1, 3.19.2 可用 */ 146 | private fun hookShareLinkV3() { 147 | val clzShareResult = "com.bilibili.lib.sharewrapper.online.api.ShareClickResult".toClass() 148 | val contentField = clzShareResult.field { name = "content" } 149 | 150 | // NOTE: 更新后容易失效的 151 | val clzShareTargetTask = 152 | "com.bilibili.app.comm.supermenu.share.v2.ShareTargetTask\$f".toClass() 153 | val shareTaskCallback = clzShareTargetTask.method { name = "l" } 154 | 155 | ("com.bilibili.lib.sharewrapper.online.api.b".toClassOrNull() ?: return).method { 156 | name = "d" 157 | }.hook( 158 | YukiHookPriority.DEFAULT, 159 | generateShareLinkHook( 160 | clzShareResult, 161 | contentField, 162 | shareTaskCallback, 163 | ), 164 | ) 165 | } 166 | 167 | /** 3.20.0 可用 */ 168 | private fun hookShareLinkV4() { 169 | val clzShareResult = "com.bilibili.lib.sharewrapper.online.api.ShareClickResult".toClass() 170 | val contentField = clzShareResult.field { name = "content" } 171 | 172 | // NOTE: 更新后容易失效的 173 | val clzShareTargetTask = 174 | "com.bilibili.app.comm.supermenu.share.v2.ShareTargetTask\$f".toClass() 175 | val shareTaskCallback = clzShareTargetTask.method { name = "l" } 176 | 177 | ("im1.d".toClassOrNull() ?: return).method { 178 | name = "f" 179 | }.hook( 180 | YukiHookPriority.DEFAULT, 181 | generateShareLinkHook( 182 | clzShareResult, 183 | contentField, 184 | shareTaskCallback, 185 | ), 186 | ) 187 | } 188 | 189 | /** 3.20.1 可用 */ 190 | private fun hookShareLinkV5() { 191 | val clzShareResult = "com.bilibili.lib.sharewrapper.online.api.ShareClickResult".toClass() 192 | val contentField = clzShareResult.field { name = "content" } 193 | 194 | // NOTE: 更新后容易失效的 195 | val clzShareTargetTask = 196 | "com.bilibili.app.comm.supermenu.share.v2.ShareTargetTask\$f".toClass() 197 | val shareTaskCallback = clzShareTargetTask.method { name = "l" } 198 | 199 | ("hm1.d".toClassOrNull() ?: return).method { 200 | name = "b" 201 | }.hook( 202 | YukiHookPriority.DEFAULT, 203 | generateShareLinkHook( 204 | clzShareResult, 205 | contentField, 206 | shareTaskCallback, 207 | ), 208 | ) 209 | } 210 | 211 | private fun hookRelates() { 212 | val clzRelates = "com.bapis.bilibili.app.viewunite.common.Relates".toClass() 213 | val defaultRelates = clzRelates.field { name = "DEFAULT_INSTANCE" } 214 | 215 | "com.bapis.bilibili.app.viewunite.common.Module".toClass().method { name = "getRelates" } 216 | .hook { 217 | after { 218 | if (prefs.getBoolean("vid_detail_no_relates")) { 219 | result = defaultRelates.get().any() 220 | } 221 | } 222 | } 223 | } 224 | 225 | private val statusBarHook: (YukiMemberHookCreator.MemberHookCreator) -> Unit = { 226 | it.after { 227 | if (!prefs.getBoolean("vid_detail_hide_status_bar")) { 228 | return@after 229 | } 230 | val activity = instance as Activity 231 | 232 | val controller = activity.window.insetsController 233 | controller?.hide(WindowInsets.Type.statusBars()) 234 | controller?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 235 | } 236 | } 237 | 238 | private fun hookUnitedBizDetailActivity() { 239 | val clzUnitedBizDetailsActivity = "com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity".toClass() 240 | 241 | clzUnitedBizDetailsActivity.method { 242 | name = "onWindowFocusChanged" 243 | }.hook(YukiHookPriority.DEFAULT, statusBarHook) 244 | 245 | clzUnitedBizDetailsActivity.method { 246 | name = "onBackPressed" 247 | }.hook(YukiHookPriority.DEFAULT, statusBarHook) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/hook/VideoPlayerHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.hook 2 | 3 | import com.highcapable.yukihookapi.hook.core.YukiMemberHookCreator 4 | import com.highcapable.yukihookapi.hook.core.api.priority.YukiHookPriority 5 | import com.highcapable.yukihookapi.hook.factory.field 6 | import com.highcapable.yukihookapi.hook.factory.hasMethod 7 | import com.highcapable.yukihookapi.hook.factory.method 8 | import com.highcapable.yukihookapi.hook.log.YLog 9 | import top.trangle.mbga.utils.MyHooker 10 | 11 | object VideoPlayerHooker : MyHooker() { 12 | override fun onHook() { 13 | subHook(this::hookOldVersion) 14 | subHook(this::hookNewVersion) 15 | subHook(this::hookDmReply) 16 | subHook(this::hookSegmentedSection) 17 | subHook(this::hookMultiWindowFullscreen) 18 | subHook(this::hookPortraitVideo) 19 | subHook(this::hookDmClick) 20 | } 21 | 22 | private fun hookOldVersion() { 23 | val clzViewProgressReply = "com.bapis.bilibili.app.view.v1.ViewProgressReply".toClass() 24 | val getVideoGuide = clzViewProgressReply.method { name = "getVideoGuide" } 25 | 26 | val clzVideoGuideType = "com.bapis.bilibili.app.view.v1.VideoGuide".toClass() 27 | val clearAttention = clzVideoGuideType.method { name = "clearAttention" } 28 | val clearCardsSecond = clzVideoGuideType.method { name = "clearCardsSecond" } 29 | val clearCommandDms = clzVideoGuideType.method { name = "clearCommandDms" } 30 | val clearContractCard = clzVideoGuideType.method { name = "clearContractCard" } 31 | val clearOperationCard = clzVideoGuideType.method { name = "clearOperationCard" } 32 | val clearOperationCardNew = clzVideoGuideType.method { name = "clearOperationCardNew" } 33 | 34 | getVideoGuide.hook { 35 | after { 36 | if (!prefs.getBoolean("vid_player_disable_command_dms")) { 37 | return@after 38 | } 39 | clearAttention.get(result).call() 40 | clearCardsSecond.get(result).call() 41 | clearCommandDms.get(result).call() 42 | clearContractCard.get(result).call() 43 | clearOperationCard.get(result).call() 44 | clearOperationCardNew.get(result).call() 45 | } 46 | } 47 | } 48 | 49 | private fun hookNewVersion() { 50 | val clzDmViewReply = "com.bapis.bilibili.community.service.dm.v1.DmViewReply".toClass() 51 | val getCommand = clzDmViewReply.method { name = "getCommand" } 52 | 53 | val clzCommand = "com.bapis.bilibili.community.service.dm.v1.Command".toClass() 54 | val clearCommandDms = clzCommand.method { name = "clearCommandDms" } 55 | 56 | getCommand.hook { 57 | after { 58 | if (!prefs.getBoolean("vid_player_disable_command_dms")) { 59 | return@after 60 | } 61 | clearCommandDms.get(result).call() 62 | } 63 | } 64 | } 65 | 66 | private fun hookDmReply() { 67 | val clzDmViewReply = "com.bapis.bilibili.community.service.dm.v1.DmViewReply".toClass() 68 | val clearActivityMeta = clzDmViewReply.method { name = "clearActivityMeta" } 69 | val clearQoe = 70 | if (clzDmViewReply.hasMethod { name = "clearQoe" }) { 71 | clzDmViewReply.method { name = "clearQoe" } 72 | } else { 73 | null 74 | } 75 | 76 | "com.bapis.bilibili.community.service.dm.v1.DmMossKtxKt\$suspendDmView\$\$inlined\$suspendCall\$1" 77 | .toClass() 78 | .method { name = "onNext" } 79 | .hook { 80 | before { 81 | if (prefs.getBoolean("dev_log_dm_view")) { 82 | YLog.debug(args[0].toString()) 83 | } 84 | if (prefs.getBoolean("vid_player_disable_activity_meta")) { 85 | clearActivityMeta.get(args[0]).call() 86 | } 87 | if (prefs.getBoolean("vid_player_disable_qoe")) { 88 | clearQoe?.get(args[0])?.call() 89 | } 90 | } 91 | } 92 | } 93 | 94 | private fun hookSegmentedSection() { 95 | val clzSpecificPlayConfig = 96 | "com.bapis.bilibili.app.distribution.setting.play.SpecificPlayConfig".toClass() 97 | 98 | val getEnableSegmentedSection = 99 | clzSpecificPlayConfig.method { name = "getEnableSegmentedSection" } 100 | val fieldEnableSegmentedSection = 101 | clzSpecificPlayConfig.field { name = "enableSegmentedSection_" } 102 | 103 | getEnableSegmentedSection.hook { 104 | before { 105 | if (!prefs.getBoolean("vid_player_disable_segmented_section")) { 106 | return@before 107 | } 108 | fieldEnableSegmentedSection.get(instance).set(null) 109 | } 110 | } 111 | } 112 | 113 | private fun hookMultiWindowFullscreen() { 114 | "android.app.Activity".toClass().method { name = "isInMultiWindowMode" }.hook { 115 | after { 116 | if (!prefs.getBoolean("vid_player_fullscreen_when_multi_window")) { 117 | return@after 118 | } 119 | val callStack = Throwable().stackTrace 120 | if (callStack.any { it.className.contains("GeminiPlayerFullscreenWidget") }) { 121 | resultFalse() 122 | } else if (callStack.any { it.className.contains("PlayerFullscreenWidget") }) { 123 | resultFalse() 124 | } 125 | } 126 | } 127 | } 128 | 129 | private fun hookPortraitVideo() { 130 | val clzStoryEntrance = "com.bapis.bilibili.app.viewunite.v1.StoryEntrance".toClass() 131 | val methods = 132 | arrayOf( 133 | "getPlayStory", 134 | "getArcPlayStory", 135 | "getArcLandscapeStory", 136 | ) 137 | 138 | methods.forEach { 139 | clzStoryEntrance.method { name = it }.hook { 140 | after { 141 | if (prefs.getBoolean("vid_player_disable_portrait")) { 142 | resultFalse() 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | private val dmClickHook: (YukiMemberHookCreator.MemberHookCreator) -> Unit = { 150 | it.before { 151 | if (prefs.getBoolean("vid_player_disable_dm_click")) { 152 | args[0] = floatArrayOf(-1f, -1f) 153 | } 154 | } 155 | } 156 | 157 | private fun hookDmClick() { 158 | "tv.danmaku.biliplayerv2.service.interact.biz.chronos.chronosrpc.methods.send.GestureEventReceived\$Request" 159 | .toClass() 160 | .method { name = "setLocation" } 161 | .hook(YukiHookPriority.DEFAULT, dmClickHook) 162 | 163 | "tv.danmaku.biliplayerv2.service.interact.biz.chronos.chronosrpc.methods.send.TouchEventReceive\$Request" 164 | .toClass() 165 | .method { name = "setLocation" } 166 | .hook(YukiHookPriority.DEFAULT, dmClickHook) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/utils/MyHooker.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.utils 2 | 3 | import androidx.core.content.pm.PackageInfoCompat 4 | import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker 5 | import com.highcapable.yukihookapi.hook.log.YLog 6 | 7 | abstract class MyHooker : YukiBaseHooker() { 8 | fun subHook(block: () -> Unit) { 9 | runCatching(block).onFailure { e -> YLog.error(e.toString()) } 10 | } 11 | 12 | fun versionSpecifiedSubHook( 13 | block: () -> Unit, 14 | versionRange: LongRange, 15 | ) { 16 | val pkgInfo = 17 | appContext?.packageManager?.getPackageInfo(packageName, 0) 18 | ?: return YLog.error("Unable to get host info, versionSpecifiedSubHook not hooked") 19 | val version = PackageInfoCompat.getLongVersionCode(pkgInfo) 20 | if (version in versionRange) { 21 | YLog.debug("loading version specified hook: $block") 22 | runCatching(block).onFailure { e -> YLog.error(e.toString()) } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/utils/Reflect.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.utils 2 | 3 | import java.lang.reflect.Modifier 4 | import java.util.LinkedList 5 | 6 | fun reflectionToString( 7 | obj: Any?, 8 | indent: Int, 9 | ): String { 10 | if (obj == null) { 11 | return "null" 12 | } 13 | val os = obj.toString() 14 | if (!(os.startsWith("#") || os.startsWith("tv") || os.startsWith("com.bapis") || os.startsWith("com.bili"))) { 15 | return os 16 | } 17 | val s = LinkedList() 18 | var clz: Class? = obj.javaClass 19 | while (clz != null) { 20 | for (prop in clz.declaredFields.filterNot { Modifier.isStatic(it.modifiers) }) { 21 | prop.isAccessible = true 22 | var str = " ".repeat(indent + 1) + "${prop.name} -> " 23 | val v = prop.get(obj)?.toString()?.trim() ?: "null" 24 | str += 25 | if (!( 26 | v.startsWith("#") || v.startsWith("tv") || v.startsWith("com.bapis") || 27 | os.startsWith( 28 | "com.bili", 29 | ) 30 | ) 31 | ) { 32 | v 33 | } else { 34 | reflectionToString(prop.get(obj), indent + 1) 35 | } 36 | s += str 37 | } 38 | clz = clz.superclass 39 | } 40 | return "${obj.javaClass.simpleName} {\n${s.joinToString(",\n")}\n${" ".repeat(indent)}}" 41 | } 42 | 43 | fun reflectionToString(obj: Any?): String { 44 | return reflectionToString(obj, 0) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/utils/factory/FunctionFactory.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package top.trangle.mbga.utils.factory 4 | 5 | import android.content.Context 6 | import android.content.res.Configuration 7 | 8 | /** 9 | * System dark mode is enabled or not 10 | * 11 | * 系统深色模式是否开启 12 | * @return [Boolean] Whether to enable / 是否开启 13 | */ 14 | val Context.isSystemInDarkMode 15 | get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES 16 | 17 | /** 18 | * System dark mode is disable or not 19 | * 20 | * 系统深色模式是否没开启 21 | * @return [Boolean] Whether to enable / 是否开启 22 | */ 23 | inline val Context.isNotSystemInDarkMode 24 | get() = isSystemInDarkMode.not() 25 | 26 | /** 27 | * dp to pxInt 28 | * 29 | * dp 转换为 pxInt 30 | * @param context using instance / 使用的实例 31 | * @return [Int] 32 | */ 33 | fun Number.dp(context: Context) = dpFloat(context).toInt() 34 | 35 | /** 36 | * dp to pxFloat 37 | * 38 | * dp 转换为 pxFloat 39 | * @param context using instance / 使用的实例 40 | * @return [Float] 41 | */ 42 | fun Number.dpFloat(context: Context) = toFloat() * context.resources.displayMetrics.density 43 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/views/IntEditTextPreference.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.views 2 | 3 | import android.content.Context 4 | import android.text.InputType 5 | import android.util.AttributeSet 6 | import androidx.preference.EditTextPreference 7 | 8 | class IntEditTextPreference : EditTextPreference { 9 | constructor(context: Context) : super(context) { 10 | setup() 11 | } 12 | 13 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 14 | setup() 15 | } 16 | 17 | constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : 18 | super(context, attrs, defStyle) { 19 | setup() 20 | } 21 | 22 | private fun setup() { 23 | setOnBindEditTextListener { 24 | it.inputType = InputType.TYPE_CLASS_NUMBER 25 | } 26 | } 27 | 28 | override fun getPersistedString(defaultReturnValue: String?): String { 29 | return runCatching { getPersistedInt(0).toString() }.getOrElse { "0" } 30 | } 31 | 32 | override fun persistString(value: String?): Boolean { 33 | return persistInt(runCatching { Integer.valueOf(value!!) }.getOrElse { 0 }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/views/KeywordListPreference.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.preference.EditTextPreference 6 | 7 | class KeywordListPreference : EditTextPreference { 8 | constructor(context: Context) : super(context) { 9 | setup() 10 | } 11 | 12 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 13 | setup() 14 | } 15 | 16 | constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : 17 | super(context, attrs, defStyle) { 18 | setup() 19 | } 20 | 21 | private fun setup() { 22 | setOnBindEditTextListener { 23 | it.isSingleLine = false 24 | } 25 | } 26 | 27 | override fun getPersistedString(defaultReturnValue: String?): String { 28 | return runCatching { getPersistedStringSet(HashSet()).joinToString("\n") }.getOrElse { 29 | defaultReturnValue ?: "" 30 | } 31 | } 32 | 33 | override fun persistString(value: String?): Boolean { 34 | return persistStringSet( 35 | runCatching { 36 | value?.split("\n")?.filter { 37 | it != "" 38 | }?.toSet() 39 | }.getOrElse { HashSet() }, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/top/trangle/mbga/views/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package top.trangle.mbga.views 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.os.Bundle 6 | import android.view.View 7 | import androidx.activity.addCallback 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.preference.PreferenceCategory 10 | import androidx.preference.SwitchPreferenceCompat 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 12 | import com.highcapable.yukihookapi.hook.factory.dataChannel 13 | import com.highcapable.yukihookapi.hook.xposed.prefs.ui.ModulePreferenceFragment 14 | import top.trangle.mbga.BILI_IN_PKG_ID 15 | import top.trangle.mbga.CHANNEL_MSG_HOME_BOTTOM_TABS 16 | import top.trangle.mbga.CHANNEL_MSG_REQ_HOME_BOTTOM_TABS 17 | import top.trangle.mbga.R 18 | import top.trangle.mbga.databinding.ActivitySettingsBinding 19 | import top.trangle.mbga.hook.BottomTab 20 | 21 | val PREFS_NEED_RESTART = 22 | arrayOf( 23 | "mine_add_search", 24 | "mine_add_im", 25 | "home_dense_vid_card", 26 | ) 27 | 28 | class SettingsActivity : AppCompatActivity() { 29 | private lateinit var binding: ActivitySettingsBinding 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | 34 | if (intent.extras?.getBoolean("show_first_launch_alert") == true) { 35 | MaterialAlertDialogBuilder(this) 36 | .setMessage(R.string.app_first_launch_message) 37 | .setNegativeButton(R.string.app_first_launch_goback) { _, _ -> this.finish() } 38 | .setPositiveButton(R.string.app_first_launch_stay) { dialog, _ -> 39 | dialog.dismiss() 40 | } 41 | .create() 42 | .show() 43 | } 44 | binding = ActivitySettingsBinding.inflate(layoutInflater) 45 | setContentView(binding.root) 46 | 47 | setSupportActionBar(binding.toolbar) 48 | supportFragmentManager 49 | .beginTransaction() 50 | .replace(R.id.settings_content, MySettingsFragment()) 51 | .commit() 52 | } 53 | 54 | class MySettingsFragment : ModulePreferenceFragment() { 55 | private var needRestart = false 56 | 57 | override fun onCreatePreferences( 58 | savedInstanceState: Bundle?, 59 | rootKey: String?, 60 | ) { 61 | // FIXME: 临时解决方案,待Yukihook更新后删除 62 | @Suppress("DEPRECATION", "WorldReadableFiles") 63 | requireActivity().getSharedPreferences( 64 | "${activity?.packageName}_preferences", 65 | Context.MODE_WORLD_READABLE, 66 | ) 67 | super.onCreatePreferences(savedInstanceState, rootKey) 68 | } 69 | 70 | override fun onCreatePreferencesInModuleApp( 71 | savedInstanceState: Bundle?, 72 | rootKey: String?, 73 | ) { 74 | setPreferencesFromResource(R.xml.preferences, rootKey) 75 | } 76 | 77 | override fun onViewCreated( 78 | view: View, 79 | savedInstanceState: Bundle?, 80 | ) { 81 | super.onViewCreated(view, savedInstanceState) 82 | addBottomTabPrefs() 83 | val onBackPressedDispatcher = activity?.onBackPressedDispatcher ?: return 84 | onBackPressedDispatcher.addCallback { 85 | val quit: () -> Unit = { 86 | isEnabled = false 87 | onBackPressedDispatcher.onBackPressed() 88 | } 89 | if (!needRestart) { 90 | quit() 91 | } else { 92 | MaterialAlertDialogBuilder(view.context) 93 | .setMessage(R.string.bili_need_restart_message) 94 | .setPositiveButton(R.string.common_got_it) { _, _ -> 95 | quit() 96 | } 97 | .create() 98 | .show() 99 | } 100 | } 101 | } 102 | 103 | override fun onSharedPreferenceChanged( 104 | sharedPreferences: SharedPreferences?, 105 | key: String?, 106 | ) { 107 | super.onSharedPreferenceChanged(sharedPreferences, key) 108 | if ( 109 | !needRestart && 110 | (key?.startsWith("tabs_disable#") == true || PREFS_NEED_RESTART.contains(key)) 111 | ) { 112 | needRestart = true 113 | } 114 | } 115 | 116 | private fun addBottomTabPrefs() { 117 | val bottomTabsCategory = 118 | preferenceScreen.findPreference("bottom_tabs_category") 119 | ?: return 120 | 121 | activity?.application?.dataChannel(packageName = BILI_IN_PKG_ID)?.with { 122 | wait>(key = CHANNEL_MSG_HOME_BOTTOM_TABS) { value -> 123 | value.forEach { 124 | bottomTabsCategory.addPreference( 125 | SwitchPreferenceCompat(bottomTabsCategory.context).apply { 126 | title = resources.getString(R.string.setting_tabs_hide, it.name) 127 | key = "tabs_disable#${it.scheme}" 128 | isIconSpaceReserved = false 129 | }, 130 | ) 131 | } 132 | } 133 | 134 | put(key = CHANNEL_MSG_REQ_HOME_BOTTOM_TABS) 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_empty_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_empty_page.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_follow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_follow.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_operation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_operation.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_qoe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_qoe.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_quick_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_quick_reply.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/comment_view_vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/comment_view_vote.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_no_feed_clean_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/home_no_feed_clean_top.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_im_pink.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_pink.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mine_add_search_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/mine_add_search_entry.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_no_default_words.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/search_no_default_words.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_detail_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_detail_label.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_activity_meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_activity_meta.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_dm_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_dm_click.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_online_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_online_count.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_portrait_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_portrait_btn.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_qoe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_qoe.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_player_segmented_section.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/drawable/video_player_segmented_section.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 25 | 26 | 33 | 34 | 35 | 36 | 40 | 41 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_pref_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_yukihookapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xxhdpi/ic_yukihookapi.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MBGA 4 | Make Bilibili Great Again! 5 | This module is made by YukiHookAPI. \nLearn more https://github.com/HighCapable/YuKiHookAPI 6 | Settings 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/array.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.bilibili.app.in 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6750A4 4 | #FFFFFF 5 | #EADDFF 6 | #21005D 7 | #B3261E 8 | #FFFFFF 9 | #F9DEDC 10 | #410E0B 11 | #FFFBFE 12 | #1C1B1F 13 | #E7E0EC 14 | #D0BCFF 15 | #381E72 16 | #4F378B 17 | #EADDFF 18 | #CCC2DC 19 | #332D41 20 | #4A4458 21 | #E6E1E5 22 | #49454F 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MBGA 4 | Make Bilibili Great Again! 5 | 此模块使用 YukiHookAPI 构建。\n了解更多 https://github.com/HighCapable/YuKiHookAPI 6 | 设置 7 | 模块第一次启动,已加载默认设置,要回到B站开始浏览还是留在设置页面再看看?如果你反复看到这个弹窗,请提issue 8 | 回到B站 9 | 留在这 10 | 11 | 您修改的设置中有需要重启B站才能生效的,请重启B站 12 | 13 | 底部Tab 14 | 隐藏“%1$s” 15 | 16 | 首页 17 | 禁止竖屏播放器 18 | 打开竖屏视频时使用传统播放器 19 | 打开竖屏视频时使用新播放器(单击暂停的) 20 | 禁止自动刷新 21 | 切后台过一段时间回来保持之前看到的地方 22 | 切后台过一段时间回来会自动刷新 23 | 禁止刷新时清空之前的内容 24 | 刷新时会保留之前的内容(仅最上方一页) 25 | 点击Tab刷新时,可能会清空之前列表中的内容 26 | 只显示UGC 27 | 只展示用户上传的作品 28 | 首页推荐视频列表原封不动 29 | 内容最短时长 30 | 时长过短的内容会被过滤掉 31 | 单位为秒,时长达不到的视频会被过滤掉,不填或填0禁用 32 | 干净的视频卡片 33 | 只展示视频标题和UP名称 34 | 展示“竖屏”“1万点赞”“已关注”这些 35 | 强制视频封面比例为16:9 36 | 推荐视频关键词过滤 37 | 每行一个关键词,出现了关键词的推荐视频会被过滤。 38 | 39 | 搜索页 40 | 隐藏热搜 41 | 隐藏搜索发现 42 | 隐藏搜索历史 43 | 禁止在搜索框内推荐搜索词 44 | 45 | 播放器 46 | 干掉云视听小电视 47 | 干掉投票、关注等弹窗(待细分) 48 | 禁止默认开启章节进度条 49 | 允许在小窗/分屏模式下全屏播放 50 | 干掉竖屏全屏按钮 51 | 只展示传统全屏按钮 52 | 展示3.19.0新出的竖屏全屏按钮 53 | 不让点击弹幕 54 | 点击弹幕时什么事也不会发生,但同时“空降”弹幕也会无法点击(投票、关注这些应该也都点不了了) 55 | 点击弹幕时会弹出“点赞、复制、举报”浮窗 56 | 禁止展示反馈弹窗 57 | 禁止展示“对音量均衡功能满意吗”此类的弹窗 58 | 59 | 视频详情页 60 | 干掉标题左侧标签 61 | 常见标签有:热门、活动,点击会打开新页面 62 | 让分享链接更纯粹 63 | https://b23.tv/av10492 64 | 原始B站分享链接,可能有追踪参数 65 | 干掉相关视频 66 | 隐藏系统状态栏 67 | 可从顶部下划临时展示状态栏 68 | 69 | 视频评论区 70 | 完全隐藏评论区 71 | 禁止快速回复 72 | 点击评论文字什么也不会发生 73 | 点击评论文字会弹出键盘进行回复 74 | 禁止展示投票 75 | 评论区不会展示投票 76 | 有些视频的评论区会有投票 77 | 禁止展示关注 78 | 评论区不会展示关注按钮 79 | 有些评论区会有关注按钮 80 | 禁止展示评论内关键词搜索 81 | 评论内没有关键词高亮搜索(视频链接不算) 82 | 评论内关键词会高亮,点击会进入搜索页 83 | 禁止一键发送评论 84 | 视频没有评论时,什么都不展示 85 | 视频没有评论时,展示“发一条xxx吧”“说点别的”“一键发送” 86 | 禁止展示反馈问卷 87 | 禁止展示评论区顶部推广内容 88 | 如“大家都在搜”、“看看UP主写了什么” 89 | 关键词过滤 90 | 仅过滤评论,不过滤评论下方的回复 91 | 每行一个关键词,出现了关键词的评论会被过滤。 92 | 93 | 我的 94 | 增加搜索入口 95 | 在“更多服务”顶部添加一个搜索入口 96 | 增加消息入口 97 | 在“更多服务”顶部添加一个消息入口 98 | 隐藏大会员入口 99 | 保留大会员入口的空白区域 100 | 隐藏大会员入口后下面的内容位置保持不变 101 | 隐藏大会员入口后下面的内容会上移 102 | 103 | 开发者选项(会些微影响性能) 104 | 记录DmViewReply 105 | 记录MainListReply 106 | 记录底部Tab日志 107 | 记录首页信息流过滤日志 108 | 109 | 搜索 110 | 知道了 111 | 消息 112 | 113 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 31 | 32 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 18 | 25 | 32 | 39 | 45 | 52 | 58 | 62 | 67 | 68 | 69 | 74 | 79 | 84 | 89 | 94 | 95 | 96 | 101 | 108 | 115 | 122 | 129 | 135 | 141 | 146 | 147 | 148 | 153 | 161 | 170 | 174 | 179 | 180 | 181 | 186 | 191 | 198 | 205 | 212 | 219 | 226 | 231 | 237 | 243 | 244 | 245 | 250 | 256 | 262 | 267 | 275 | 276 | 277 | 282 | 287 | 292 | 297 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/yukihookapi_init: -------------------------------------------------------------------------------- 1 | top.trangle.mbga.hook.HookEntry -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/build.gradle.kts -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Compiler Configuration 2 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 3 | android.useAndroidX=true 4 | android.nonTransitiveRClass=true 5 | kotlin.code.style=official 6 | kotlin.incremental.useClasspathSnapshot=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cledwynl/mbga/27fcddef4eebef7af13bc2fb2a4aa0175fd0a8a5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jan 22 12:43:52 CST 2024 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 -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = "MBGA" 9 | include(":app") --------------------------------------------------------------------------------