├── .gitignore ├── .gitmodules ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── wirebare.jks │ ├── ic_wirebare-playstore.png │ ├── java │ └── top │ │ └── sankokomi │ │ └── wirebare │ │ └── ui │ │ ├── accesscontrol │ │ ├── AccessControlCompose.kt │ │ └── AccessControlUI.kt │ │ ├── app │ │ └── WireBareUIApp.kt │ │ ├── datastore │ │ ├── AbsDataStore.kt │ │ ├── AccessControlDataStore.kt │ │ ├── DataStoreProperty.kt │ │ └── ProxyPolicyDataStore.kt │ │ ├── launcher │ │ ├── LauncherCompose.kt │ │ ├── LauncherModel.kt │ │ └── LauncherUI.kt │ │ ├── record │ │ ├── ConcurrentFileWriter.kt │ │ ├── HttpDecoder.kt │ │ └── HttpRecorder.kt │ │ ├── resources │ │ ├── Color.kt │ │ ├── ComposeUI.kt │ │ ├── Theme.kt │ │ └── Type.kt │ │ ├── util │ │ ├── AppUtil.kt │ │ ├── BarUtil.kt │ │ ├── ClipBoardUtil.kt │ │ ├── ComposeUtil.kt │ │ ├── Global.kt │ │ ├── NetUtil.kt │ │ └── ToastUtil.kt │ │ └── wireinfo │ │ ├── WireBareHttpInterceptor.kt │ │ ├── WireDetailCompose.kt │ │ ├── WireDetailUI.kt │ │ ├── WireInfoCompose.kt │ │ └── WireInfoUI.kt │ └── res │ ├── drawable │ ├── ic_clear.xml │ ├── ic_more.xml │ ├── ic_request.xml │ ├── ic_response.xml │ ├── ic_wirebare.xml │ └── ic_wirebare_foreground.xml │ ├── mipmap-anydpi-v26 │ ├── ic_wirebare.xml │ └── ic_wirebare_round.xml │ ├── mipmap-hdpi │ ├── ic_wirebare.webp │ └── ic_wirebare_round.webp │ ├── mipmap-mdpi │ ├── ic_wirebare.webp │ └── ic_wirebare_round.webp │ ├── mipmap-xhdpi │ ├── ic_wirebare.webp │ └── ic_wirebare_round.webp │ ├── mipmap-xxhdpi │ ├── ic_wirebare.webp │ └── ic_wirebare_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_wirebare.webp │ └── ic_wirebare_round.webp │ ├── values-night │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── ic_wirebare_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── certificate ├── 318facc2.0 ├── wirebare.crt ├── wirebare.jks ├── wirebare.key ├── wirebare.p12 ├── wirebare.pem ├── wirebare_ca_installer.zip └── 使用说明.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wirebare-kernel"] 2 | path = wirebare-kernel 3 | url = https://github.com/Kokomi7QAQ/wirebare-kernel 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WireBare 2 | 3 | WireBare 是一个基于 Android VPN Service 开发的 Android 抓包框架 4 | 5 | 整个项目是一个完整的 Android 应用程序,其中的 [wirebare-kernel](https://github.com/Kokomi7QAQ/wirebare-kernel) 模块为核心的抓包模块,app 模块则提供了一些拓展功能和简单的用户界面 6 | 7 | 在高版本的 Android 系统中的 HTTPS 的拦截抓包功能需要先安装代理服务器根证书到 Android 系统的根证书目录下 8 | 9 | 证书相关文件和使用说明在项目根目录的 certificate 目录下 10 | 11 | 12 | ### 功能概览 13 | 14 | #### 网际层 15 | 16 | - 支持 IPv4 和 IPv6 的代理抓包 17 | - 支持 IP 协议解析 18 | 19 | #### 传输层 20 | 21 | - 支持 TCP 透明代理、拦截抓包 22 | - 支持 UDP 透明代理 23 | 24 | #### 应用层 25 | 26 | - 支持 HTTP 协议解析 27 | - 支持 HTTPS 加解密(基于 TLSv1.2,需要先为 Android 安装代理服务器根证书) 28 | 29 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | id("kotlin-parcelize") 6 | } 7 | 8 | android { 9 | namespace = "top.sankokomi.wirebare.ui" 10 | compileSdk = libs.versions.targetSdk.get().toInt() 11 | 12 | defaultConfig { 13 | applicationId = "top.sankokomi.wirebare.ui" 14 | minSdk = libs.versions.minSdk.get().toInt() 15 | targetSdk = libs.versions.targetSdk.get().toInt() 16 | versionCode = 1 17 | versionName = "0.1-snapshot-dev" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = false 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_17 36 | targetCompatibility = JavaVersion.VERSION_17 37 | } 38 | kotlinOptions { 39 | jvmTarget = "17" 40 | } 41 | buildFeatures { 42 | compose = true 43 | } 44 | composeOptions { 45 | kotlinCompilerExtensionVersion = libs.versions.jetpackCompose.get() 46 | } 47 | packaging { 48 | resources { 49 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 50 | } 51 | } 52 | } 53 | 54 | dependencies { 55 | implementation(project(":wirebare-kernel")) 56 | 57 | implementation(libs.androidx.core.ktx) 58 | implementation(libs.androidx.appcompat) 59 | implementation(libs.android.material) 60 | implementation(libs.androidx.lifecycle) 61 | 62 | implementation(libs.activity.compose) 63 | implementation(platform(libs.compose.bom)) 64 | implementation(libs.compose.ui) 65 | implementation(libs.compose.ui.graphics) 66 | implementation(libs.compose.ui.tooling.preview) 67 | implementation(libs.compose.material3) 68 | implementation(libs.compose.ui.tooling) 69 | implementation(libs.compose.ui.test.manifest) 70 | 71 | implementation(libs.datastore.preference) 72 | implementation(libs.coil.compose.kt) 73 | } -------------------------------------------------------------------------------- /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. 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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/assets/wirebare.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/assets/wirebare.jks -------------------------------------------------------------------------------- /app/src/main/ic_wirebare-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/ic_wirebare-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/accesscontrol/AccessControlCompose.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.accesscontrol 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.Checkbox 12 | import androidx.compose.material3.LinearProgressIndicator 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableIntStateOf 17 | import androidx.compose.runtime.mutableStateListOf 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.unit.dp 27 | import kotlinx.coroutines.Dispatchers 28 | import kotlinx.coroutines.launch 29 | import kotlinx.coroutines.sync.Mutex 30 | import kotlinx.coroutines.withContext 31 | import top.sankokomi.wirebare.ui.datastore.AccessControlDataStore 32 | import top.sankokomi.wirebare.ui.datastore.ProxyPolicyDataStore 33 | import top.sankokomi.wirebare.ui.resources.AppCheckBoxItemMenuPopup 34 | import top.sankokomi.wirebare.ui.resources.AppTitleBar 35 | import top.sankokomi.wirebare.ui.resources.Purple80 36 | import top.sankokomi.wirebare.ui.resources.RealColumn 37 | import top.sankokomi.wirebare.ui.resources.SmallColorfulText 38 | import top.sankokomi.wirebare.ui.util.AppData 39 | import top.sankokomi.wirebare.ui.util.Global 40 | import top.sankokomi.wirebare.ui.util.requireAppDataList 41 | 42 | @Composable 43 | fun AccessControlUI.AccessControlUIPage() { 44 | val appList = remember { mutableStateListOf() } 45 | val accessControlList = remember { mutableStateListOf() } 46 | var accessCount by remember { mutableIntStateOf(0) } 47 | val showSystemAppItem = remember { 48 | mutableStateOf("显示系统应用") 49 | } to remember { 50 | mutableStateOf(ProxyPolicyDataStore.showSystemApp.value) 51 | } 52 | val selectAllAppItem = remember { 53 | mutableStateOf("全选") 54 | } to remember { 55 | mutableStateOf(false) 56 | } 57 | val listOperateMutex = remember { Mutex(false) } 58 | val rememberScope = rememberCoroutineScope() 59 | LaunchedEffect(selectAllAppItem.second.value) { 60 | listOperateMutex.lock() 61 | // 当全选选项被修改时 62 | val isSelectAllApp = selectAllAppItem.second.value 63 | if (isSelectAllApp && accessCount < accessControlList.size) { 64 | // 若新选项是全选且当前没有全选 65 | withContext(Dispatchers.IO) { 66 | AccessControlDataStore.emitAll( 67 | appList.map { 68 | it.packageName to true 69 | } 70 | ) 71 | } 72 | accessControlList.replaceAll { true } 73 | accessCount = accessControlList.size 74 | } else if (!isSelectAllApp && accessCount >= accessControlList.size) { 75 | // 若新选项是全不选且当前不是全不选 76 | withContext(Dispatchers.IO) { 77 | AccessControlDataStore.emitAll( 78 | appList.map { 79 | it.packageName to false 80 | } 81 | ) 82 | } 83 | accessControlList.replaceAll { false } 84 | accessCount = 0 85 | } 86 | listOperateMutex.unlock() 87 | } 88 | LaunchedEffect(showSystemAppItem.second.value) { 89 | listOperateMutex.lock() 90 | // 当是否显示系统应用选项被修改时 91 | val showSystemApp = showSystemAppItem.second.value 92 | // 持久化当前是否显示系统应用 93 | ProxyPolicyDataStore.showSystemApp.value = showSystemApp 94 | accessCount = 0 95 | appList.clear() 96 | accessControlList.clear() 97 | val aList = withContext(Dispatchers.Default) { 98 | requireAppDataList { 99 | if (it.packageName == Global.appContext.packageName) { 100 | false 101 | } else if (!showSystemApp) { 102 | !it.isSystemApp 103 | } else { 104 | true 105 | } 106 | } 107 | } 108 | val acList = withContext(Dispatchers.IO) { 109 | AccessControlDataStore.collectAll( 110 | aList.map { it.packageName } 111 | ) 112 | } 113 | var count = 0 114 | acList.onEach { 115 | if (it) count++ 116 | } 117 | accessCount = count 118 | appList.addAll(aList) 119 | accessControlList.addAll(acList) 120 | listOperateMutex.unlock() 121 | } 122 | LaunchedEffect(accessCount) { 123 | listOperateMutex.lock() 124 | selectAllAppItem.second.value = accessCount == accessControlList.size 125 | listOperateMutex.unlock() 126 | } 127 | RealColumn { 128 | AppTitleBar( 129 | text = "访问控制" 130 | ) { 131 | AppCheckBoxItemMenuPopup( 132 | itemList = listOf( 133 | showSystemAppItem, 134 | selectAllAppItem 135 | ) 136 | ) 137 | } 138 | if (accessControlList.isEmpty()) { 139 | LinearProgressIndicator( 140 | modifier = Modifier.fillMaxWidth(), 141 | color = Purple80, 142 | trackColor = Color.Transparent 143 | ) 144 | } 145 | LazyColumn( 146 | modifier = Modifier.fillMaxWidth() 147 | ) { 148 | item { 149 | Spacer(modifier = Modifier.height(4.dp)) 150 | } 151 | items(accessControlList.size) { index -> 152 | val appData = appList[index] 153 | val accessControl = accessControlList[index] 154 | Box( 155 | modifier = Modifier 156 | .fillMaxWidth() 157 | .padding(horizontal = 16.dp, vertical = 4.dp) 158 | .clip(RoundedCornerShape(6.dp)) 159 | .clickable { 160 | rememberScope.launch(Dispatchers.IO) { 161 | listOperateMutex.lock() 162 | AccessControlDataStore.emit(appData.packageName to !accessControl) 163 | withContext(Dispatchers.Main) { 164 | accessControlList[index] = !accessControl 165 | if (!accessControl) accessCount++ else accessCount-- 166 | } 167 | listOperateMutex.unlock() 168 | } 169 | } 170 | ) { 171 | SmallColorfulText( 172 | mainText = appData.appName, 173 | subText = appData.packageName, 174 | backgroundColor = Purple80, 175 | textColor = Color.Black 176 | ) 177 | Checkbox( 178 | checked = accessControl, 179 | onCheckedChange = null, 180 | modifier = Modifier 181 | .align(Alignment.CenterEnd) 182 | .padding(end = 16.dp) 183 | ) 184 | } 185 | } 186 | item { 187 | Spacer(modifier = Modifier.height(4.dp)) 188 | } 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/accesscontrol/AccessControlUI.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.accesscontrol 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import top.sankokomi.wirebare.ui.resources.WirebareUITheme 12 | 13 | class AccessControlUI: ComponentActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | enableEdgeToEdge() 18 | setContent { 19 | WirebareUITheme( 20 | isShowNavigationBar = false 21 | ) { 22 | Surface( 23 | modifier = Modifier.fillMaxSize(), 24 | color = MaterialTheme.colorScheme.background 25 | ) { 26 | AccessControlUIPage() 27 | } 28 | } 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/app/WireBareUIApp.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.app 2 | 3 | import android.app.Application 4 | import top.sankokomi.wirebare.ui.util.Global 5 | 6 | class WireBareUIApp: Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | Global.attach(applicationContext) 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/datastore/AbsDataStore.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.datastore 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.preferencesDataStore 8 | import kotlinx.coroutines.flow.first 9 | import top.sankokomi.wirebare.ui.util.Global 10 | 11 | /** 12 | * @param VT 键值对的值类型 13 | * */ 14 | abstract class AbsDataStore( 15 | private val context: Context = Global.appContext, 16 | dataStoreName: String, 17 | private val default: VT 18 | ) { 19 | 20 | protected abstract fun String.pref(): Preferences.Key 21 | 22 | private val Context.accessControlDataStore: DataStore 23 | by preferencesDataStore(dataStoreName) 24 | 25 | open suspend fun emit(keyValue: Pair) { 26 | val (key, value) = keyValue 27 | context.accessControlDataStore.edit { 28 | it[key.pref()] = value 29 | } 30 | } 31 | 32 | open suspend fun emitAll(keyValues: List>) { 33 | context.accessControlDataStore.edit { 34 | for ((key, value) in keyValues) { 35 | it[key.pref()] = value 36 | } 37 | } 38 | } 39 | 40 | open suspend fun collect(key: String): VT { 41 | return context.accessControlDataStore 42 | .data.first()[key.pref()] ?: default 43 | } 44 | 45 | open suspend fun collectAll(keys: List): List { 46 | val result = mutableListOf() 47 | context.accessControlDataStore.data.first().let { 48 | for (key in keys) { 49 | result.add(it[key.pref()] ?: default) 50 | } 51 | } 52 | return result 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/datastore/AccessControlDataStore.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.datastore 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | 6 | @Suppress("StaticFieldLeak") 7 | object AccessControlDataStore : AbsDataStore( 8 | dataStoreName = "access_control", 9 | default = false 10 | ) { 11 | override fun String.pref(): Preferences.Key { 12 | return booleanPreferencesKey(this) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/datastore/DataStoreProperty.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.datastore 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.MutablePreferences 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.floatPreferencesKey 9 | import androidx.datastore.preferences.core.intPreferencesKey 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.DelicateCoroutinesApi 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.GlobalScope 16 | import kotlinx.coroutines.flow.MutableStateFlow 17 | import kotlinx.coroutines.flow.first 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.runBlocking 20 | import top.sankokomi.wirebare.ui.util.Global 21 | import kotlin.properties.ReadOnlyProperty 22 | import kotlin.reflect.KProperty 23 | 24 | abstract class AppDataStore( 25 | name: String, 26 | @OptIn(DelicateCoroutinesApi::class) val coroutineScope: CoroutineScope = GlobalScope 27 | ) { 28 | private val Context.dataStore by preferencesDataStore(name) 29 | val dataStoreFlow get() = Global.appContext.dataStore.data 30 | suspend fun edit(transform: suspend (MutablePreferences) -> Unit) { 31 | Global.appContext.dataStore.edit(transform) 32 | } 33 | } 34 | 35 | class AppFloatPref( 36 | keyName: String, 37 | default: Float = 0f 38 | ) : AppPreferenceProperty(default) { 39 | override val prefKey: Preferences.Key = floatPreferencesKey(keyName) 40 | } 41 | 42 | class AppIntPref( 43 | keyName: String, 44 | default: Int = 0 45 | ) : AppPreferenceProperty(default) { 46 | override val prefKey: Preferences.Key = intPreferencesKey(keyName) 47 | } 48 | 49 | class AppBooleanPref( 50 | keyName: String, 51 | default: Boolean = false 52 | ) : AppPreferenceProperty(default) { 53 | override val prefKey: Preferences.Key = booleanPreferencesKey(keyName) 54 | } 55 | 56 | class AppStringPref( 57 | keyName: String, 58 | default: String = "" 59 | ) : AppPreferenceProperty(default) { 60 | override val prefKey: Preferences.Key = stringPreferencesKey(keyName) 61 | } 62 | 63 | abstract class AppPreferenceProperty( 64 | private val default: T 65 | ) : ReadOnlyProperty> { 66 | abstract val prefKey: Preferences.Key 67 | private var _keyFlow: MutableStateFlow? = null 68 | private fun keyFlow(dataStore: AppDataStore): MutableStateFlow { 69 | _keyFlow?.let { return@keyFlow _keyFlow!! } 70 | return synchronized(Unit) { 71 | _keyFlow?.let { return@keyFlow _keyFlow!! } 72 | _keyFlow = runBlocking { 73 | MutableStateFlow( 74 | dataStore.dataStoreFlow.first()[prefKey] ?: default 75 | ) 76 | } 77 | dataStore.coroutineScope.launch(Dispatchers.IO) { 78 | dataStore.dataStoreFlow.collect { 79 | _keyFlow!!.value = it[prefKey] ?: default 80 | } 81 | } 82 | dataStore.coroutineScope.launch(Dispatchers.IO) { 83 | _keyFlow!!.collect { value -> 84 | dataStore.edit { 85 | it[prefKey] = value 86 | } 87 | } 88 | } 89 | _keyFlow!! 90 | } 91 | } 92 | 93 | override fun getValue( 94 | thisRef: AppDataStore, 95 | property: KProperty<*> 96 | ): MutableStateFlow { 97 | return keyFlow(thisRef) 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/datastore/ProxyPolicyDataStore.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.datastore 2 | 3 | object ProxyPolicyDataStore : AppDataStore("proxy_policy") { 4 | 5 | /** 6 | * true:禁用自动过滤,false:启用自动过滤 7 | * */ 8 | val banAutoFilter by AppBooleanPref("ban_auto_filter") 9 | 10 | /** 11 | * true:启用 SSL,false:禁用 SSL 12 | * */ 13 | val enableSSL by AppBooleanPref("enable_ssl") 14 | 15 | /** 16 | * true:启用 ipv6,false:禁用 ipv6 17 | * */ 18 | val enableIpv6 by AppBooleanPref("enable_ipv6") 19 | 20 | /** 21 | * true:显示系统应用,false:不显示系统应用 22 | * */ 23 | val showSystemApp by AppBooleanPref("ban_system_app") 24 | 25 | /** 26 | * 模拟丢包概率 27 | * 0 表示不丢包,100 表示全丢 28 | * */ 29 | val mockPacketLossProbability by AppIntPref("mock_packet_loss_probability") 30 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/launcher/LauncherCompose.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.launcher 2 | 3 | import android.content.Intent 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.animation.AnimatedVisibility 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.animation.togetherWith 9 | import androidx.compose.foundation.ExperimentalFoundationApi 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.clickable 12 | import androidx.compose.foundation.layout.Box 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.Spacer 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import androidx.compose.foundation.layout.height 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.foundation.lazy.LazyColumn 20 | import androidx.compose.foundation.pager.HorizontalPager 21 | import androidx.compose.foundation.pager.rememberPagerState 22 | import androidx.compose.foundation.rememberScrollState 23 | import androidx.compose.foundation.shape.RoundedCornerShape 24 | import androidx.compose.foundation.verticalScroll 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateListOf 31 | import androidx.compose.runtime.mutableStateOf 32 | import androidx.compose.runtime.remember 33 | import androidx.compose.runtime.setValue 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.draw.clip 37 | import androidx.compose.ui.draw.shadow 38 | import androidx.compose.ui.graphics.Color 39 | import androidx.compose.ui.res.painterResource 40 | import androidx.compose.ui.unit.dp 41 | import androidx.compose.ui.unit.sp 42 | import top.sankokomi.wirebare.kernel.common.EventSynopsis 43 | import top.sankokomi.wirebare.kernel.common.ProxyStatus 44 | import top.sankokomi.wirebare.kernel.common.WireBare 45 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 46 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 47 | import top.sankokomi.wirebare.ui.R 48 | import top.sankokomi.wirebare.ui.accesscontrol.AccessControlUI 49 | import top.sankokomi.wirebare.ui.datastore.ProxyPolicyDataStore 50 | import top.sankokomi.wirebare.ui.record.HttpRecorder 51 | import top.sankokomi.wirebare.ui.record.id 52 | import top.sankokomi.wirebare.ui.resources.AppNavigationBar 53 | import top.sankokomi.wirebare.ui.resources.AppTitleBar 54 | import top.sankokomi.wirebare.ui.resources.CornerSlideBar 55 | import top.sankokomi.wirebare.ui.resources.DeepPureRed 56 | import top.sankokomi.wirebare.ui.resources.ImageButton 57 | import top.sankokomi.wirebare.ui.resources.LargeColorfulText 58 | import top.sankokomi.wirebare.ui.resources.Pink80 59 | import top.sankokomi.wirebare.ui.resources.Purple40 60 | import top.sankokomi.wirebare.ui.resources.Purple80 61 | import top.sankokomi.wirebare.ui.resources.Purple80ToPurpleGrey40 62 | import top.sankokomi.wirebare.ui.resources.PurpleGrey40 63 | import top.sankokomi.wirebare.ui.resources.SmallColorfulText 64 | import top.sankokomi.wirebare.ui.wireinfo.WireInfoUI 65 | import kotlin.math.roundToInt 66 | 67 | @OptIn(ExperimentalFoundationApi::class) 68 | @Composable 69 | fun LauncherUI.WireBareUIPage() { 70 | val pagerState = rememberPagerState { 3 } 71 | val painterControlRes = painterResource(R.drawable.ic_wirebare) 72 | val painterRequestRes = painterResource(R.drawable.ic_request) 73 | val painterResponseRes = painterResource(R.drawable.ic_response) 74 | Column { 75 | AppTitleBar() 76 | HorizontalPager( 77 | state = pagerState, 78 | beyondViewportPageCount = 3, 79 | modifier = Modifier.weight(1F) 80 | ) { 81 | when (it) { 82 | 0 -> PageControlCenter() 83 | 1 -> PageProxyRequestResult() 84 | 2 -> PageProxyResponseResult() 85 | } 86 | } 87 | AppNavigationBar( 88 | pagerState = pagerState, 89 | navigationItems = listOf( 90 | (painterControlRes to "控制中心") to (painterControlRes to "控制中心"), 91 | (painterRequestRes to " 请求 ") to (painterRequestRes to " 请求 "), 92 | (painterResponseRes to " 响应 ") to (painterResponseRes to " 响应 ") 93 | ) 94 | ) 95 | } 96 | } 97 | 98 | @Composable 99 | private fun LauncherUI.PageControlCenter() { 100 | var wireBareStatus by remember { mutableStateOf(ProxyStatus.DEAD) } 101 | val isBanFilter by ProxyPolicyDataStore.banAutoFilter.collectAsState() 102 | val enableIpv6 by ProxyPolicyDataStore.enableIpv6.collectAsState() 103 | val enableSSL by ProxyPolicyDataStore.enableSSL.collectAsState() 104 | val mockPacketLossProbability by ProxyPolicyDataStore.mockPacketLossProbability.collectAsState() 105 | var maybeUnsupportedIpv6 by remember { mutableStateOf(false) } 106 | LaunchedEffect(Unit) { 107 | proxyStatusFlow.collect { 108 | maybeUnsupportedIpv6 = false 109 | wireBareStatus = it 110 | } 111 | } 112 | LaunchedEffect(Unit) { 113 | eventFlow.collect { event -> 114 | when (event.synopsis) { 115 | EventSynopsis.IPV6_UNREACHABLE -> { 116 | maybeUnsupportedIpv6 = true 117 | } 118 | 119 | else -> {} 120 | } 121 | } 122 | } 123 | Box( 124 | modifier = Modifier.fillMaxSize() 125 | ) { 126 | Column( 127 | modifier = Modifier 128 | .fillMaxWidth() 129 | .verticalScroll(rememberScrollState()) 130 | .clip(RoundedCornerShape(6.dp)) 131 | .padding(horizontal = 24.dp, vertical = 8.dp) 132 | ) { 133 | Box( 134 | modifier = Modifier 135 | .clip(RoundedCornerShape(6.dp)) 136 | ) { 137 | AnimatedContent( 138 | targetState = wireBareStatus, 139 | transitionSpec = { 140 | fadeIn().togetherWith(fadeOut()) 141 | }, 142 | label = "WireBareStatus" 143 | ) { status -> 144 | val mainText: String 145 | val subText: String 146 | val backgroundColor: Color 147 | val textColor: Color 148 | val onClick: () -> Unit 149 | when (status) { 150 | ProxyStatus.DEAD -> { 151 | mainText = "已停止" 152 | subText = "点此启动" 153 | backgroundColor = PurpleGrey40 154 | textColor = Color.White 155 | onClick = ::startProxy 156 | } 157 | 158 | ProxyStatus.STARTING -> { 159 | mainText = "正在启动" 160 | subText = "请稍后" 161 | backgroundColor = Purple80ToPurpleGrey40 162 | textColor = Color.White 163 | onClick = ::stopProxy 164 | } 165 | 166 | ProxyStatus.ACTIVE -> { 167 | mainText = "已启动" 168 | subText = "点此停止" 169 | backgroundColor = Purple80 170 | textColor = Color.Black 171 | onClick = ::stopProxy 172 | } 173 | 174 | ProxyStatus.DYING -> { 175 | mainText = "正在停止" 176 | subText = "请稍后" 177 | backgroundColor = Purple80ToPurpleGrey40 178 | textColor = Color.White 179 | onClick = ::stopProxy 180 | } 181 | } 182 | LargeColorfulText( 183 | mainText = mainText, 184 | subText = subText, 185 | backgroundColor = backgroundColor, 186 | textColor = textColor, 187 | onClick = onClick 188 | ) 189 | } 190 | 191 | } 192 | Spacer(modifier = Modifier.height(4.dp)) 193 | AnimatedVisibility( 194 | visible = wireBareStatus == ProxyStatus.ACTIVE || wireBareStatus == ProxyStatus.STARTING 195 | ) { 196 | Text( 197 | modifier = Modifier 198 | .fillMaxWidth() 199 | .padding(start = 20.dp, end = 20.dp, top = 8.dp), 200 | text = "下面的配置修改后需要重启服务生效", 201 | fontSize = 14.sp, 202 | color = Color.Black 203 | ) 204 | } 205 | Spacer(modifier = Modifier.height(12.dp)) 206 | Box( 207 | modifier = Modifier 208 | .clip(RoundedCornerShape(6.dp)) 209 | ) { 210 | LargeColorfulText( 211 | mainText = "访问控制", 212 | subText = "配置代理应用", 213 | backgroundColor = Purple80, 214 | textColor = Color.Black, 215 | onClick = { 216 | startActivity( 217 | Intent( 218 | this@PageControlCenter, 219 | AccessControlUI::class.java 220 | ) 221 | ) 222 | } 223 | ) 224 | } 225 | Spacer(modifier = Modifier.height(16.dp)) 226 | val afMainText: String 227 | val afSubText: String 228 | val afBackgroundColor: Color 229 | val afTextColor: Color 230 | if (isBanFilter) { 231 | afMainText = "自动过滤已停用" 232 | afSubText = "将显示代理到的所有请求" 233 | afBackgroundColor = PurpleGrey40 234 | afTextColor = Color.White 235 | } else { 236 | afMainText = "自动过滤已启用" 237 | afSubText = "将会自动过滤无法解析的请求" 238 | afBackgroundColor = Purple80 239 | afTextColor = Color.Black 240 | } 241 | Box( 242 | modifier = Modifier 243 | .clip(RoundedCornerShape(6.dp)) 244 | ) { 245 | LargeColorfulText( 246 | mainText = afMainText, 247 | subText = afSubText, 248 | backgroundColor = afBackgroundColor, 249 | textColor = afTextColor, 250 | onClick = { 251 | ProxyPolicyDataStore.banAutoFilter.value = !isBanFilter 252 | } 253 | ) 254 | } 255 | Spacer(modifier = Modifier.height(16.dp)) 256 | Box( 257 | modifier = Modifier 258 | .clip(RoundedCornerShape(6.dp)) 259 | ) { 260 | AnimatedContent( 261 | targetState = enableSSL, 262 | transitionSpec = { 263 | fadeIn().togetherWith(fadeOut()) 264 | }, 265 | label = "SslTlsStatus" 266 | ) { enable -> 267 | val sslMainText: String 268 | val sslSubText: String 269 | val sslBackgroundColor: Color 270 | val sslTextColor: Color 271 | if (enable) { 272 | sslMainText = "SSL/TLS 已启用" 273 | sslSubText = "进行 SSL/TLS 握手并解密 HTTPS" 274 | sslBackgroundColor = Purple80 275 | sslTextColor = Color.Black 276 | } else { 277 | sslMainText = "SSL/TLS 已禁用" 278 | sslSubText = "仅对 HTTPS 透明代理" 279 | sslBackgroundColor = PurpleGrey40 280 | sslTextColor = Color.White 281 | } 282 | LargeColorfulText( 283 | mainText = sslMainText, 284 | subText = sslSubText, 285 | backgroundColor = sslBackgroundColor, 286 | textColor = sslTextColor, 287 | onClick = { 288 | ProxyPolicyDataStore.enableSSL.value = !enableSSL 289 | } 290 | ) 291 | } 292 | } 293 | Spacer(modifier = Modifier.height(4.dp)) 294 | AnimatedVisibility( 295 | visible = enableSSL 296 | ) { 297 | Text( 298 | modifier = Modifier 299 | .fillMaxWidth() 300 | .padding(start = 20.dp, end = 20.dp, top = 8.dp), 301 | text = "可能需要您事先安装好代理服务器根证书", 302 | fontSize = 14.sp, 303 | color = Color.Black 304 | ) 305 | } 306 | Spacer(modifier = Modifier.height(12.dp)) 307 | Box( 308 | modifier = Modifier 309 | .clip(RoundedCornerShape(6.dp)) 310 | ) { 311 | AnimatedContent( 312 | targetState = enableIpv6, 313 | transitionSpec = { 314 | fadeIn().togetherWith(fadeOut()) 315 | }, 316 | label = "Ipv6Status" 317 | ) { enable -> 318 | val i6MainText: String 319 | val i6SubText: String 320 | val i6BackgroundColor: Color 321 | val i6TextColor: Color 322 | if (enable) { 323 | i6MainText = "IPv6 代理已启用" 324 | i6SubText = "代理 IPv4 和 IPv6 数据包" 325 | i6BackgroundColor = Purple80 326 | i6TextColor = Color.Black 327 | } else { 328 | i6MainText = "IPv6 代理已禁用" 329 | i6SubText = "仅代理 IPv4 数据包" 330 | i6BackgroundColor = PurpleGrey40 331 | i6TextColor = Color.White 332 | } 333 | LargeColorfulText( 334 | mainText = i6MainText, 335 | subText = i6SubText, 336 | backgroundColor = i6BackgroundColor, 337 | textColor = i6TextColor, 338 | onClick = { 339 | ProxyPolicyDataStore.enableIpv6.value = !enableIpv6 340 | } 341 | ) 342 | } 343 | } 344 | Box( 345 | modifier = Modifier 346 | .clip(RoundedCornerShape(6.dp)) 347 | ) { 348 | CornerSlideBar( 349 | mainText = "随机丢包概率", 350 | subText = "调整后立即生效", 351 | backgroundColor = Color.Transparent, 352 | textColor = Color.Black, 353 | barColor = Purple40, 354 | barBackgroundColor = Pink80, 355 | value = mockPacketLossProbability / 100f, 356 | valueRange = 0f..1f, 357 | onValueChange = { 358 | val probability = (it * 100).roundToInt() 359 | ProxyPolicyDataStore.mockPacketLossProbability.value = probability 360 | WireBare.dynamicConfiguration.mockPacketLossProbability = probability 361 | } 362 | ) 363 | } 364 | Spacer(modifier = Modifier.height(16.dp)) 365 | AnimatedVisibility( 366 | visible = maybeUnsupportedIpv6 && wireBareStatus == ProxyStatus.ACTIVE 367 | ) { 368 | Box( 369 | modifier = Modifier 370 | .clip(RoundedCornerShape(6.dp)) 371 | ) { 372 | LargeColorfulText( 373 | mainText = "注意", 374 | subText = "当前网络疑似不支持 IPv6", 375 | backgroundColor = DeepPureRed, 376 | textColor = Color.White, 377 | onClick = { 378 | ProxyPolicyDataStore.enableIpv6.value = !enableIpv6 379 | } 380 | ) 381 | } 382 | Spacer(modifier = Modifier.height(16.dp)) 383 | } 384 | } 385 | } 386 | } 387 | 388 | @Composable 389 | private fun LauncherUI.PageProxyRequestResult() { 390 | val isBanFilter by ProxyPolicyDataStore.banAutoFilter.collectAsState() 391 | val requestList = remember { mutableStateListOf() } 392 | LaunchedEffect(Unit) { 393 | requestFlow.collect { 394 | if (!isBanFilter) { 395 | if (it.url == null) return@collect 396 | // if (it.httpVersion?.startsWith("HTTP") != true) return@collect 397 | } 398 | requestList.add(it) 399 | } 400 | } 401 | Box( 402 | modifier = Modifier.fillMaxSize() 403 | ) { 404 | LazyColumn( 405 | modifier = Modifier.fillMaxSize() 406 | ) { 407 | item { 408 | Spacer(modifier = Modifier.height(4.dp)) 409 | } 410 | items(requestList.size) { i -> 411 | val index = requestList.size - i - 1 412 | val request = requestList[index] 413 | Box( 414 | modifier = Modifier 415 | .fillMaxWidth() 416 | .padding(horizontal = 16.dp, vertical = 4.dp) 417 | .clip(RoundedCornerShape(6.dp)) 418 | .clickable { 419 | startActivity( 420 | Intent( 421 | this@PageProxyRequestResult, 422 | WireInfoUI::class.java 423 | ).apply { 424 | putExtra("request", request) 425 | putExtra("session_id", request.id) 426 | } 427 | ) 428 | } 429 | ) { 430 | SmallColorfulText( 431 | mainText = request.url ?: request.destinationAddress ?: "", 432 | subText = request.formatHead?.getOrNull(0) ?: "", 433 | backgroundColor = Purple80, 434 | textColor = Color.Black 435 | ) 436 | } 437 | } 438 | item { 439 | Spacer(modifier = Modifier.height(4.dp)) 440 | } 441 | } 442 | Box( 443 | contentAlignment = Alignment.Center, 444 | modifier = Modifier 445 | .padding(24.dp) 446 | .align(Alignment.BottomEnd) 447 | .shadow(1.dp, RoundedCornerShape(6.dp), true) 448 | .background(Purple80) 449 | .clickable { 450 | requestList.clear() 451 | } 452 | .padding(horizontal = 12.dp, vertical = 4.dp) 453 | ) { 454 | ImageButton( 455 | painter = painterResource(id = R.drawable.ic_clear), 456 | str = "清空" 457 | ) 458 | } 459 | } 460 | } 461 | 462 | @Composable 463 | private fun LauncherUI.PageProxyResponseResult() { 464 | val isBanFilter by ProxyPolicyDataStore.banAutoFilter.collectAsState() 465 | val responseList = remember { mutableStateListOf() } 466 | LaunchedEffect(Unit) { 467 | responseFlow.collect { 468 | if (!isBanFilter) { 469 | if (it.url == null) return@collect 470 | // if (it.httpVersion?.startsWith("HTTP") != true) return@collect 471 | } 472 | responseList.add(it) 473 | } 474 | } 475 | Box( 476 | modifier = Modifier.fillMaxSize() 477 | ) { 478 | LazyColumn( 479 | modifier = Modifier.fillMaxSize() 480 | ) { 481 | item { 482 | Spacer(modifier = Modifier.height(4.dp)) 483 | } 484 | items(responseList.size) { i -> 485 | val index = responseList.size - i - 1 486 | val response = responseList[index] 487 | Box( 488 | modifier = Modifier 489 | .fillMaxWidth() 490 | .padding(horizontal = 16.dp, vertical = 4.dp) 491 | .clip(RoundedCornerShape(6.dp)) 492 | .clickable { 493 | startActivity( 494 | Intent( 495 | this@PageProxyResponseResult, 496 | WireInfoUI::class.java 497 | ).apply { 498 | putExtra("response", response) 499 | putExtra("session_id", response.id) 500 | } 501 | ) 502 | } 503 | ) { 504 | SmallColorfulText( 505 | mainText = response.url ?: response.destinationAddress ?: "", 506 | subText = (response.formatHead?.getOrNull(0) ?: "") + 507 | System.lineSeparator() + 508 | (response.contentType ?: "") + 509 | System.lineSeparator() + 510 | (response.contentEncoding ?: "identity"), 511 | backgroundColor = Purple80, 512 | textColor = Color.Black 513 | ) 514 | } 515 | } 516 | item { 517 | Spacer(modifier = Modifier.height(4.dp)) 518 | } 519 | } 520 | Box( 521 | contentAlignment = Alignment.Center, 522 | modifier = Modifier 523 | .padding(24.dp) 524 | .align(Alignment.BottomEnd) 525 | .shadow(1.dp, RoundedCornerShape(6.dp), true) 526 | .background(Purple80) 527 | .clickable { 528 | HttpRecorder.clearRewardsAsync() 529 | responseList.clear() 530 | } 531 | .padding(horizontal = 12.dp, vertical = 4.dp) 532 | ) { 533 | ImageButton( 534 | painter = painterResource(id = R.drawable.ic_clear), 535 | str = "清空" 536 | ) 537 | } 538 | } 539 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/launcher/LauncherModel.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.launcher 2 | 3 | import android.content.Context 4 | import top.sankokomi.wirebare.kernel.common.WireBare 5 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 6 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 7 | import top.sankokomi.wirebare.kernel.ssl.JKS 8 | import top.sankokomi.wirebare.kernel.util.Level 9 | import top.sankokomi.wirebare.ui.datastore.ProxyPolicyDataStore 10 | import top.sankokomi.wirebare.ui.wireinfo.WireBareHttpInterceptor 11 | 12 | object LauncherModel { 13 | 14 | fun startProxy( 15 | context: Context, 16 | targetPackageNameArray: Array, 17 | onRequest: (HttpRequest) -> Unit, 18 | onResponse: (HttpResponse) -> Unit 19 | ) { 20 | WireBare.logLevel = Level.VERBOSE 21 | WireBare.startProxy { 22 | if (ProxyPolicyDataStore.enableSSL.value) { 23 | jks = JKS( 24 | { context.assets.open("wirebare.jks") }, 25 | "wirebare", 26 | "wirebare".toCharArray(), 27 | "PKCS12", 28 | "WB", 29 | "WB" 30 | ) 31 | } 32 | mtu = 10 * 1024 33 | tcpProxyServerCount = 5 34 | ipv4ProxyAddress = "10.1.10.1" to 32 35 | enableIpv6 = ProxyPolicyDataStore.enableIpv6.value 36 | ipv6ProxyAddress = "a:a:1:1:a:a:1:1" to 128 37 | addRoutes("0.0.0.0" to 0, "::" to 0) 38 | addAllowedApplications(*targetPackageNameArray) 39 | addAsyncHttpInterceptor( 40 | listOf( 41 | WireBareHttpInterceptor.Factory(onRequest, onResponse) 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/launcher/LauncherUI.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.launcher 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.ui.Modifier 10 | import androidx.lifecycle.lifecycleScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.channels.BufferOverflow 13 | import kotlinx.coroutines.flow.MutableSharedFlow 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.asSharedFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.withContext 19 | import top.sankokomi.wirebare.kernel.common.IImportantEventListener 20 | import top.sankokomi.wirebare.kernel.common.IProxyStatusListener 21 | import top.sankokomi.wirebare.kernel.common.ImportantEvent 22 | import top.sankokomi.wirebare.kernel.common.ProxyStatus 23 | import top.sankokomi.wirebare.kernel.common.VpnPrepareActivity 24 | import top.sankokomi.wirebare.kernel.common.WireBare 25 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 26 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 27 | import top.sankokomi.wirebare.ui.datastore.AccessControlDataStore 28 | import top.sankokomi.wirebare.ui.datastore.ProxyPolicyDataStore 29 | import top.sankokomi.wirebare.ui.record.HttpRecorder 30 | import top.sankokomi.wirebare.ui.resources.WirebareUITheme 31 | import top.sankokomi.wirebare.ui.util.requireAppDataList 32 | 33 | class LauncherUI : VpnPrepareActivity() { 34 | 35 | private val _proxyStatusFlow = MutableStateFlow(ProxyStatus.DEAD) 36 | 37 | private val _eventFlow = MutableSharedFlow( 38 | 0, 1, BufferOverflow.SUSPEND 39 | ) 40 | 41 | private val _requestFlow = MutableSharedFlow() 42 | 43 | private val _responseFlow = MutableSharedFlow() 44 | 45 | val proxyStatusFlow = _proxyStatusFlow.asStateFlow() 46 | 47 | val eventFlow = _eventFlow.asSharedFlow() 48 | 49 | val requestFlow = _requestFlow.asSharedFlow() 50 | 51 | val responseFlow = _responseFlow.asSharedFlow() 52 | 53 | fun startProxy() { 54 | prepareProxy() 55 | } 56 | 57 | fun stopProxy() { 58 | WireBare.stopProxy() 59 | } 60 | 61 | override fun onPrepareSuccess() { 62 | lifecycleScope.launch(Dispatchers.IO) { 63 | // 提前设定状态为正在启动 64 | _proxyStatusFlow.value = ProxyStatus.STARTING 65 | val showSystemApp = ProxyPolicyDataStore.showSystemApp.value 66 | val appList = requireAppDataList { 67 | if (!showSystemApp) { 68 | !it.isSystemApp 69 | } else { 70 | true 71 | } 72 | } 73 | val accessList = AccessControlDataStore.collectAll( 74 | appList.map { app -> app.packageName } 75 | ).mapIndexedNotNull { index, b -> if (b) appList[index].packageName else null } 76 | withContext(Dispatchers.Main) { 77 | LauncherModel.startProxy( 78 | this@LauncherUI, 79 | accessList.toTypedArray(), 80 | onRequest = { 81 | lifecycleScope.launch { 82 | _requestFlow.emit(it) 83 | } 84 | }, 85 | onResponse = { 86 | lifecycleScope.launch { 87 | _responseFlow.emit(it) 88 | } 89 | } 90 | ) 91 | } 92 | } 93 | } 94 | 95 | private val wireBareStatusListener = object : IProxyStatusListener { 96 | override fun onVpnStatusChanged(oldStatus: ProxyStatus, newStatus: ProxyStatus): Boolean { 97 | _proxyStatusFlow.value = newStatus 98 | return false 99 | } 100 | } 101 | 102 | private val wireBareEventListener = object : IImportantEventListener { 103 | override fun onPost(event: ImportantEvent) { 104 | lifecycleScope.launch { 105 | _eventFlow.emit(event) 106 | } 107 | } 108 | } 109 | 110 | override fun onCreate(savedInstanceState: Bundle?) { 111 | super.onCreate(savedInstanceState) 112 | // 添加 WireBare 状态监听器 113 | WireBare.addVpnProxyStatusListener(wireBareStatusListener) 114 | WireBare.addImportantEventListener(wireBareEventListener) 115 | enableEdgeToEdge() 116 | setContent { 117 | WirebareUITheme { 118 | Surface( 119 | modifier = Modifier.fillMaxSize(), 120 | color = MaterialTheme.colorScheme.background 121 | ) { 122 | WireBareUIPage() 123 | } 124 | } 125 | } 126 | lifecycleScope.launch { 127 | HttpRecorder.clearRewards() 128 | } 129 | } 130 | 131 | override fun onDestroy() { 132 | // 解除监听,防止内存泄露 133 | WireBare.removeImportantEventListener(wireBareEventListener) 134 | WireBare.removeVpnProxyStatusListener(wireBareStatusListener) 135 | super.onDestroy() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/record/ConcurrentFileWriter.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.record 2 | 3 | import android.util.Log 4 | import java.io.BufferedOutputStream 5 | import java.io.Closeable 6 | import java.io.File 7 | import java.io.FileOutputStream 8 | import java.nio.ByteBuffer 9 | 10 | class ConcurrentFileWriter( 11 | private val file: File 12 | ) : Closeable { 13 | 14 | companion object { 15 | private const val TAG = "ConcurrentFileWriter" 16 | } 17 | 18 | private val output by lazy(LazyThreadSafetyMode.NONE) { 19 | BufferedOutputStream( 20 | FileOutputStream(file, true) 21 | ) 22 | } 23 | 24 | fun writeBytes(buffer: ByteBuffer) { 25 | output.write( 26 | buffer.array(), 27 | buffer.position(), 28 | buffer.remaining() 29 | ) 30 | output.flush() 31 | } 32 | 33 | override fun close() { 34 | runCatching { 35 | output.close() 36 | }.onFailure { 37 | Log.e(TAG, "close FAILED", it) 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/record/HttpDecoder.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.record 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.util.Log 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import top.sankokomi.wirebare.kernel.util.unzipBrotli 9 | import top.sankokomi.wirebare.kernel.util.unzipGzip 10 | 11 | private const val TAG = "HttpDecoder" 12 | 13 | suspend fun decodeHttpBody(id: String): ByteArray? { 14 | return withContext(Dispatchers.IO) { 15 | runCatching { 16 | val origin = getHttpRecordFileById(id) 17 | if (!origin.exists()) { 18 | return@withContext null 19 | } else { 20 | return@withContext origin.readBytes().httpBody() 21 | } 22 | }.onFailure { 23 | Log.e(TAG, "decodeHttpBody FAILED", it) 24 | return@withContext null 25 | } 26 | return@withContext null 27 | } 28 | } 29 | 30 | suspend fun decodeGzipHttpBody(id: String): ByteArray? { 31 | return withContext(Dispatchers.IO) { 32 | runCatching { 33 | return@withContext decodeHttpBody(id)?.unzipGzip() 34 | }.onFailure { 35 | Log.e(TAG, "decodeGzipHttpBody FAILED", it) 36 | return@withContext null 37 | } 38 | return@withContext null 39 | } 40 | } 41 | 42 | suspend fun decodeBrotliHttpBody(id: String): ByteArray? { 43 | return withContext(Dispatchers.IO) { 44 | runCatching { 45 | return@withContext decodeHttpBody(id)?.unzipBrotli() 46 | }.onFailure { 47 | Log.e(TAG, "decodeBrotliHttpBody FAILED", it) 48 | return@withContext null 49 | } 50 | return@withContext null 51 | } 52 | } 53 | 54 | suspend fun decodeBitmap(id: String): Bitmap? { 55 | return withContext(Dispatchers.IO) { 56 | runCatching { 57 | val body = decodeHttpBody(id) ?: return@withContext null 58 | return@withContext BitmapFactory.decodeByteArray(body, 0, body.size) 59 | }.onFailure { 60 | Log.e(TAG, "decodeBitmap FAILED", it) 61 | return@withContext null 62 | } 63 | return@withContext null 64 | } 65 | } 66 | 67 | suspend fun decodeGzipBitmap(id: String): Bitmap? { 68 | return withContext(Dispatchers.IO) { 69 | runCatching { 70 | val body = decodeGzipHttpBody(id) ?: return@withContext null 71 | return@withContext BitmapFactory.decodeByteArray(body, 0, body.size) 72 | }.onFailure { 73 | Log.e(TAG, "decodeGzipBitmap FAILED", it) 74 | return@withContext null 75 | } 76 | return@withContext null 77 | } 78 | } 79 | 80 | suspend fun decodeBrotliBitmap(id: String): Bitmap? { 81 | return withContext(Dispatchers.IO) { 82 | runCatching { 83 | val body = decodeBrotliHttpBody(id) ?: return@withContext null 84 | return@withContext BitmapFactory.decodeByteArray(body, 0, body.size) 85 | }.onFailure { 86 | Log.e(TAG, "decodeBrotliBitmap FAILED", it) 87 | return@withContext null 88 | } 89 | return@withContext null 90 | } 91 | } 92 | 93 | private fun ByteArray.httpBody(): ByteArray? { 94 | runCatching { 95 | val bytes = this 96 | var i = -1 97 | for (index in 0..bytes.size - 4) { 98 | if ( 99 | bytes[index] == '\r'.code.toByte() && 100 | bytes[index + 1] == '\n'.code.toByte() && 101 | bytes[index + 2] == '\r'.code.toByte() && 102 | bytes[index + 3] == '\n'.code.toByte() 103 | ) { 104 | i = index + 4 105 | break 106 | } 107 | } 108 | return@httpBody bytes.copyOfRange(i, bytes.size) 109 | }.onFailure { 110 | return null 111 | } 112 | return null 113 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/record/HttpRecorder.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.record 2 | 3 | import android.util.Log 4 | import kotlinx.coroutines.DelicateCoroutinesApi 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.GlobalScope 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withContext 9 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 10 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 11 | import top.sankokomi.wirebare.ui.util.Global 12 | import java.io.File 13 | import java.nio.ByteBuffer 14 | import java.util.concurrent.ConcurrentHashMap 15 | 16 | private const val TAG = "HttpRecorder" 17 | 18 | private val recordDir by lazy { 19 | File("${Global.appContext.externalCacheDir!!.absolutePath}${File.separator}http_record").also { 20 | if (!it.exists()) it.mkdirs() 21 | } 22 | } 23 | 24 | val HttpRequest.id: String get() = "req_${requestTime}_${hashCode()}" 25 | 26 | val HttpResponse.id: String get() = "rsp_${requestTime}_${hashCode()}" 27 | 28 | fun getHttpRecordFileById(id: String): File = File(recordDir, id) 29 | 30 | object HttpRecorder { 31 | 32 | private val writers = ConcurrentHashMap() 33 | 34 | fun parseRequestRecordFile(request: HttpRequest): File { 35 | val dest = File(recordDir, request.id) 36 | if (!dest.exists()) { 37 | dest.createNewFile() 38 | } 39 | return dest 40 | } 41 | 42 | fun parseResponseRecordFile(response: HttpResponse): File { 43 | val dest = File(recordDir, response.id) 44 | if (!dest.exists()) { 45 | dest.createNewFile() 46 | } 47 | return dest 48 | } 49 | 50 | suspend fun addRequestRecord(request: HttpRequest, buffer: ByteBuffer?) { 51 | withContext(Dispatchers.IO) { 52 | runCatching { 53 | val id = request.id 54 | if (buffer == null) { 55 | writers.remove(id)?.close() 56 | return@withContext 57 | } 58 | writers.computeIfAbsent(id) { 59 | ConcurrentFileWriter(parseRequestRecordFile(request)) 60 | }.writeBytes(buffer) 61 | }.onFailure { 62 | Log.e(TAG, "addHttpRequestReward FAILED", it) 63 | } 64 | } 65 | } 66 | 67 | suspend fun addResponseRecord(response: HttpResponse, buffer: ByteBuffer?) { 68 | withContext(Dispatchers.IO) { 69 | runCatching { 70 | val id = response.id 71 | if (buffer == null) { 72 | writers.remove(id)?.close() 73 | return@withContext 74 | } 75 | writers.computeIfAbsent(id) { 76 | ConcurrentFileWriter(parseResponseRecordFile(response)) 77 | }.writeBytes(buffer) 78 | }.onFailure { 79 | Log.e(TAG, "addHttpResponseReward FAILED", it) 80 | } 81 | } 82 | } 83 | 84 | suspend fun clearRewards() { 85 | withContext(Dispatchers.IO) { 86 | runCatching { 87 | recordDir.listFiles()?.forEach(File::delete) 88 | }.onFailure { 89 | Log.e(TAG, "clearHttpRewards FAILED", it) 90 | } 91 | } 92 | } 93 | 94 | @OptIn(DelicateCoroutinesApi::class) 95 | fun clearRewardsAsync() { 96 | GlobalScope.launch(Dispatchers.IO) { 97 | runCatching { 98 | recordDir.listFiles()?.forEach(File::delete) 99 | }.onFailure { 100 | Log.e(TAG, "clearHttpRewards FAILED", it) 101 | } 102 | } 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/resources/Color.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.resources 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple80ToPurpleGrey40 = Color(0xFF998CB8) 10 | 11 | val Purple40 = Color(0xFF6650A4) 12 | val PurpleGrey40 = Color(0xFF625B71) 13 | val Pink40 = Color(0xFF7D5260) 14 | 15 | val DeepPureRed = Color(0xFFC80000) -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/resources/ComposeUI.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.resources 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.AnimatedVisibilityScope 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.foundation.Image 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.combinedClickable 12 | import androidx.compose.foundation.layout.Arrangement 13 | import androidx.compose.foundation.layout.Box 14 | import androidx.compose.foundation.layout.BoxScope 15 | import androidx.compose.foundation.layout.Column 16 | import androidx.compose.foundation.layout.ColumnScope 17 | import androidx.compose.foundation.layout.Row 18 | import androidx.compose.foundation.layout.RowScope 19 | import androidx.compose.foundation.layout.Spacer 20 | import androidx.compose.foundation.layout.fillMaxWidth 21 | import androidx.compose.foundation.layout.height 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.foundation.layout.size 24 | import androidx.compose.foundation.layout.width 25 | import androidx.compose.foundation.pager.PagerState 26 | import androidx.compose.foundation.shape.RoundedCornerShape 27 | import androidx.compose.material3.Checkbox 28 | import androidx.compose.material3.DropdownMenu 29 | import androidx.compose.material3.Slider 30 | import androidx.compose.material3.Text 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.MutableState 33 | import androidx.compose.runtime.State 34 | import androidx.compose.runtime.getValue 35 | import androidx.compose.runtime.mutableStateOf 36 | import androidx.compose.runtime.remember 37 | import androidx.compose.runtime.rememberCoroutineScope 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.draw.alpha 42 | import androidx.compose.ui.draw.clip 43 | import androidx.compose.ui.draw.shadow 44 | import androidx.compose.ui.graphics.Color 45 | import androidx.compose.ui.graphics.painter.Painter 46 | import androidx.compose.ui.res.painterResource 47 | import androidx.compose.ui.res.stringResource 48 | import androidx.compose.ui.text.font.FontWeight 49 | import androidx.compose.ui.text.style.TextOverflow 50 | import androidx.compose.ui.unit.dp 51 | import androidx.compose.ui.unit.sp 52 | import coil.compose.AsyncImage 53 | import kotlinx.coroutines.launch 54 | import top.sankokomi.wirebare.ui.R 55 | import top.sankokomi.wirebare.ui.util.statusBarHeightDp 56 | 57 | @Composable 58 | fun AppStatusBar(color: Color = Color.Transparent) { 59 | Spacer( 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .height(statusBarHeightDp) 63 | .background(color) 64 | ) 65 | } 66 | 67 | @Composable 68 | fun AppTitleBar( 69 | icon: Any = R.mipmap.ic_wirebare, 70 | text: String = stringResource(id = R.string.app_name), 71 | endContent: @Composable BoxScope.() -> Unit = {} 72 | ) { 73 | RealColumn( 74 | modifier = Modifier.shadow(2.dp) 75 | ) { 76 | AppStatusBar(Color.White) 77 | RealBox( 78 | modifier = Modifier 79 | .fillMaxWidth() 80 | .background(Color.White) 81 | .padding(horizontal = 24.dp, vertical = 8.dp), 82 | contentAlignment = Alignment.CenterStart 83 | ) { 84 | RealRow( 85 | verticalAlignment = Alignment.CenterVertically 86 | ) { 87 | AsyncImage( 88 | model = icon, 89 | modifier = Modifier 90 | .size(36.dp) 91 | .clip(RoundedCornerShape(12.dp)), 92 | contentDescription = null 93 | ) 94 | Spacer(modifier = Modifier.width(8.dp)) 95 | Text(text = text, fontSize = 20.sp, fontWeight = FontWeight.Bold) 96 | } 97 | RealBox( 98 | modifier = Modifier.align(Alignment.CenterEnd), 99 | content = endContent 100 | ) 101 | } 102 | } 103 | } 104 | 105 | @Composable 106 | fun AppCheckBoxItemMenuPopup( 107 | itemList: List, MutableState>>, 108 | size: Int = itemList.size 109 | ) { 110 | var isMenuExpanded by remember { mutableStateOf(false) } 111 | Box { 112 | Image( 113 | painter = painterResource(id = R.drawable.ic_more), 114 | modifier = Modifier 115 | .size(32.dp) 116 | .clip(RoundedCornerShape(6.dp)) 117 | .clickable { 118 | isMenuExpanded = true 119 | }, 120 | contentDescription = null 121 | ) 122 | DropdownMenu( 123 | expanded = isMenuExpanded, 124 | modifier = Modifier 125 | .background(Purple80) 126 | .padding(vertical = 2.dp, horizontal = 10.dp), 127 | onDismissRequest = { 128 | isMenuExpanded = false 129 | } 130 | ) { 131 | for (i in 0 until size) { 132 | val item = itemList[i].first 133 | val checked = itemList[i].second 134 | RealRow( 135 | modifier = Modifier 136 | .clip(RoundedCornerShape(4.dp)) 137 | .clickable { 138 | checked.value = !checked.value 139 | isMenuExpanded = false 140 | } 141 | .padding(vertical = 2.dp) 142 | .padding(end = 8.dp), 143 | verticalAlignment = Alignment.CenterVertically 144 | ) { 145 | Checkbox( 146 | checked = checked.value, 147 | onCheckedChange = null 148 | ) 149 | Spacer(modifier = Modifier.width(8.dp)) 150 | Text( 151 | text = item.value, 152 | modifier = Modifier.fillMaxWidth(), 153 | color = Color.Black 154 | ) 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | @OptIn(ExperimentalFoundationApi::class) 162 | @Composable 163 | fun AppNavigationBar( 164 | pagerState: PagerState, 165 | navigationItems: List, Pair>> 166 | ) { 167 | val rememberScope = rememberCoroutineScope() 168 | RealRow( 169 | modifier = Modifier 170 | .fillMaxWidth() 171 | .shadow(4.dp) 172 | .background(Purple80) 173 | .padding(vertical = 2.dp) 174 | ) { 175 | for (index in navigationItems.indices) { 176 | val item = navigationItems[index] 177 | RealBox( 178 | contentAlignment = Alignment.Center, 179 | modifier = Modifier.weight(1F) 180 | ) { 181 | val (painter, str) = if (pagerState.currentPage != index) { 182 | item.first 183 | } else { 184 | item.second 185 | } 186 | Box( 187 | modifier = Modifier 188 | .padding(2.dp) 189 | .clip(RoundedCornerShape(6.dp)) 190 | .clickable { 191 | rememberScope.launch { 192 | pagerState.animateScrollToPage(index) 193 | } 194 | } 195 | ) { 196 | ImageButton( 197 | painter = painter, 198 | str = str 199 | ) 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | @Composable 207 | fun ImageButton( 208 | painter: Painter, 209 | str: String 210 | ) { 211 | RealColumn( 212 | horizontalAlignment = Alignment.CenterHorizontally 213 | ) { 214 | Image( 215 | painter = painter, 216 | modifier = Modifier.size(32.dp), 217 | contentDescription = null 218 | ) 219 | Spacer(modifier = Modifier.height(2.dp)) 220 | Text( 221 | text = str, 222 | fontSize = 14.sp, 223 | color = Color.Black 224 | ) 225 | } 226 | } 227 | 228 | @Composable 229 | fun VisibleFadeInFadeOutAnimation( 230 | visible: Boolean = true, 231 | content: @Composable (AnimatedVisibilityScope.() -> Unit) 232 | ) { 233 | AnimatedVisibility( 234 | visible = visible, 235 | enter = fadeIn(), 236 | exit = fadeOut(), 237 | content = content 238 | ) 239 | } 240 | 241 | @Composable 242 | fun SmallColorfulText( 243 | mainText: String, 244 | subText: String, 245 | backgroundColor: Color, 246 | textColor: Color 247 | ) { 248 | RealColumn( 249 | modifier = Modifier 250 | .fillMaxWidth() 251 | .background(backgroundColor) 252 | .clip(RoundedCornerShape(6.dp)) 253 | .padding(horizontal = 16.dp, vertical = 6.dp) 254 | ) { 255 | Text( 256 | text = mainText, 257 | modifier = Modifier 258 | .fillMaxWidth(), 259 | color = textColor, 260 | fontSize = 16.sp, 261 | fontWeight = FontWeight.Medium, 262 | lineHeight = 15.sp, 263 | overflow = TextOverflow.Ellipsis, 264 | maxLines = 2 265 | ) 266 | Spacer(modifier = Modifier.height(2.dp)) 267 | Text( 268 | text = subText, 269 | modifier = Modifier 270 | .fillMaxWidth(), 271 | color = textColor, 272 | fontSize = 12.sp, 273 | lineHeight = 13.sp, 274 | overflow = TextOverflow.Ellipsis, 275 | maxLines = 3 276 | ) 277 | } 278 | } 279 | 280 | @OptIn(ExperimentalFoundationApi::class) 281 | @Composable 282 | fun LargeColorfulText( 283 | mainText: String, 284 | subText: String, 285 | backgroundColor: Color, 286 | textColor: Color, 287 | onClick: () -> Unit = {}, 288 | onLongClick: () -> Unit = {} 289 | ) { 290 | RealColumn( 291 | modifier = Modifier 292 | .fillMaxWidth() 293 | .background(backgroundColor) 294 | .clip(RoundedCornerShape(6.dp)) 295 | .combinedClickable(onLongClick = onLongClick, onClick = onClick) 296 | .padding(horizontal = 20.dp, vertical = 12.dp) 297 | ) { 298 | Text( 299 | text = mainText, 300 | color = textColor, 301 | fontSize = 18.sp, 302 | fontWeight = FontWeight.Bold, 303 | lineHeight = 20.sp 304 | ) 305 | Spacer(modifier = Modifier.height(4.dp)) 306 | Text(text = subText, color = textColor, fontSize = 14.sp, lineHeight = 16.sp) 307 | } 308 | } 309 | 310 | @Composable 311 | fun CornerSlideBar( 312 | mainText: String, 313 | subText: String, 314 | backgroundColor: Color, 315 | textColor: Color, 316 | barColor: Color, 317 | barBackgroundColor: Color, 318 | value: Float, 319 | valueRange: ClosedFloatingPointRange, 320 | onValueChange: (Float) -> Unit = {}, 321 | valueText: (Float) -> String = { "${(it * 100).toInt()}%" } 322 | ) { 323 | RealColumn( 324 | modifier = Modifier 325 | .fillMaxWidth() 326 | .background(backgroundColor) 327 | .clip(RoundedCornerShape(6.dp)) 328 | .padding(vertical = 12.dp) 329 | ) { 330 | Text( 331 | modifier = Modifier.padding(horizontal = 20.dp), 332 | text = mainText, 333 | color = textColor, 334 | fontSize = 18.sp, 335 | fontWeight = FontWeight.Bold, 336 | lineHeight = 20.sp 337 | ) 338 | RealRow { 339 | Text( 340 | modifier = Modifier 341 | .padding(horizontal = 20.dp) 342 | .align(Alignment.Bottom), 343 | text = subText, 344 | color = textColor, 345 | fontSize = 14.sp, 346 | lineHeight = 16.sp 347 | ) 348 | Spacer(modifier = Modifier.weight(1f)) 349 | Text( 350 | modifier = Modifier.padding(horizontal = 20.dp), 351 | text = valueText(value), 352 | color = textColor, 353 | fontSize = 18.sp, 354 | fontWeight = FontWeight.Medium, 355 | lineHeight = 16.sp 356 | ) 357 | } 358 | Spacer(modifier = Modifier.height(12.dp)) 359 | RealBox { 360 | Slider( 361 | modifier = Modifier 362 | .align(Alignment.Center) 363 | .fillMaxWidth() 364 | .height(32.dp) 365 | .alpha(0f), 366 | value = value, 367 | valueRange = valueRange, 368 | onValueChange = onValueChange 369 | ) 370 | Box( 371 | modifier = Modifier 372 | .align(Alignment.Center) 373 | .fillMaxWidth() 374 | .height(32.dp) 375 | .clip(RoundedCornerShape(16.dp)) 376 | .background(barBackgroundColor) 377 | ) 378 | Row { 379 | val percentage = value / (valueRange.endInclusive - valueRange.start) 380 | if (percentage > 0f) { 381 | Spacer(modifier = Modifier.weight(percentage)) 382 | } 383 | Box( 384 | modifier = Modifier 385 | .size(32.dp) 386 | .clip(RoundedCornerShape(16.dp)) 387 | .background(barColor) 388 | ) 389 | if (percentage < 1f) { 390 | Spacer(modifier = Modifier.weight(1 - percentage)) 391 | } 392 | } 393 | } 394 | } 395 | } 396 | 397 | @Composable 398 | fun RealBox( 399 | modifier: Modifier = Modifier, 400 | contentAlignment: Alignment = Alignment.TopStart, 401 | propagateMinConstraints: Boolean = false, 402 | content: @Composable BoxScope.() -> Unit 403 | ) { 404 | Box(modifier, contentAlignment, propagateMinConstraints, content) 405 | } 406 | 407 | @Composable 408 | fun RealColumn( 409 | modifier: Modifier = Modifier, 410 | verticalArrangement: Arrangement.Vertical = Arrangement.Top, 411 | horizontalAlignment: Alignment.Horizontal = Alignment.Start, 412 | content: @Composable ColumnScope.() -> Unit 413 | ) { 414 | Column(modifier, verticalArrangement, horizontalAlignment, content) 415 | } 416 | 417 | @Composable 418 | fun RealRow( 419 | modifier: Modifier = Modifier, 420 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 421 | verticalAlignment: Alignment.Vertical = Alignment.Top, 422 | content: @Composable RowScope.() -> Unit 423 | ) { 424 | Row(modifier, horizontalArrangement, verticalAlignment, content) 425 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/resources/Theme.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.resources 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | import top.sankokomi.wirebare.ui.util.hideNavigationBar 17 | import top.sankokomi.wirebare.ui.util.hideStatusBar 18 | import top.sankokomi.wirebare.ui.util.showNavigationBar 19 | import top.sankokomi.wirebare.ui.util.showStatusBar 20 | 21 | private val DarkColorScheme = darkColorScheme( 22 | primary = Purple80, 23 | secondary = PurpleGrey80, 24 | tertiary = Pink80 25 | ) 26 | 27 | private val LightColorScheme = lightColorScheme( 28 | primary = Purple40, 29 | secondary = PurpleGrey40, 30 | tertiary = Pink40 31 | 32 | /* Other default colors to override 33 | background = Color(0xFFFFFBFE), 34 | surface = Color(0xFFFFFBFE), 35 | onPrimary = Color.White, 36 | onSecondary = Color.White, 37 | onTertiary = Color.White, 38 | onBackground = Color(0xFF1C1B1F), 39 | onSurface = Color(0xFF1C1B1F), 40 | */ 41 | ) 42 | 43 | @Composable 44 | fun WirebareUITheme( 45 | darkTheme: Boolean = isSystemInDarkTheme(), 46 | // Dynamic color is available on Android 12+ 47 | dynamicColor: Boolean = true, 48 | isShowStatusBar: Boolean = false, 49 | isShowNavigationBar: Boolean = true, 50 | content: @Composable () -> Unit 51 | ) { 52 | val colorScheme = when { 53 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 54 | val context = LocalContext.current 55 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 56 | } 57 | 58 | darkTheme -> DarkColorScheme 59 | else -> LightColorScheme 60 | } 61 | val view = LocalView.current 62 | if (!view.isInEditMode) { 63 | SideEffect { 64 | val window = (view.context as Activity).window 65 | WindowCompat.getInsetsController(window, view).apply { 66 | isAppearanceLightStatusBars = !darkTheme 67 | } 68 | if (!isShowStatusBar) window.hideStatusBar() else window.showStatusBar() 69 | if (!isShowNavigationBar) window.hideNavigationBar() else window.showNavigationBar() 70 | } 71 | } 72 | 73 | MaterialTheme( 74 | colorScheme = colorScheme, 75 | typography = Typography, 76 | content = content 77 | ) 78 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/resources/Type.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.resources 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/AppUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.content.pm.PackageManager 5 | 6 | data class AppData( 7 | val appName: String, 8 | val packageName: String, 9 | val isSystemApp: Boolean 10 | ) 11 | 12 | fun requireAppDataList(filter: (AppData) -> Boolean = { true }): List { 13 | return Global.appContext.packageManager.getInstalledApplications( 14 | PackageManager.MATCH_UNINSTALLED_PACKAGES 15 | ).mapNotNull { 16 | val appData = AppData( 17 | Global.appContext.packageManager.getApplicationLabel(it).toString(), 18 | it.packageName, 19 | it.flags and ApplicationInfo.FLAG_SYSTEM != 0 20 | ) 21 | if (filter(appData)) { 22 | appData 23 | } else { 24 | null 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/BarUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import android.view.View 4 | import android.view.Window 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | 8 | val statusBarHeightDp: Dp 9 | @Suppress("InternalInsetResource", "DiscouragedApi") 10 | get() = Global.appContext.resources?.run { 11 | val id = getIdentifier( 12 | "status_bar_height", "dimen", "android" 13 | ) 14 | (getDimensionPixelSize(id) / displayMetrics.density + 0.5F).dp 15 | } ?: 0.dp 16 | 17 | /** 18 | * 隐藏状态栏 19 | * */ 20 | @Suppress("DEPRECATION") 21 | fun Window.hideStatusBar() { 22 | decorView.systemUiVisibility = 23 | decorView.systemUiVisibility and 24 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 25 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 26 | } 27 | 28 | /** 29 | * 显示状态栏 30 | * */ 31 | @Suppress("DEPRECATION") 32 | fun Window.showStatusBar() { 33 | decorView.systemUiVisibility = 34 | decorView.systemUiVisibility and ( 35 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 36 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 37 | ).inv() 38 | } 39 | 40 | /** 41 | * 隐藏导航栏 42 | * */ 43 | @Suppress("DEPRECATION") 44 | fun Window.hideNavigationBar() { 45 | decorView.systemUiVisibility = 46 | decorView.systemUiVisibility and 47 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 48 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 49 | } 50 | 51 | /** 52 | * 显示导航栏 53 | * */ 54 | @Suppress("DEPRECATION") 55 | fun Window.showNavigationBar() { 56 | decorView.systemUiVisibility = 57 | decorView.systemUiVisibility and ( 58 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or 59 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 60 | ).inv() 61 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/ClipBoardUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import androidx.core.content.getSystemService 6 | 7 | fun copyTextToClipBoard(s: String): Boolean { 8 | // 获取系统剪贴板 9 | val clipboard = Global.appContext.getSystemService() 10 | // 创建一个剪贴数据集,包含一个普通文本数据条目(需要复制的数据) 11 | val clipData = ClipData.newPlainText(null, s) 12 | // 把数据集设置(复制)到剪贴板 13 | return clipboard?.let { 14 | it.setPrimaryClip(clipData) 15 | true 16 | } ?: false 17 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/ComposeUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.toArgb 7 | import kotlin.random.Random 8 | 9 | /** 10 | * 用于测试 Composable 函数的重组情况,若发生了重组,背景颜色将会被改变 11 | * */ 12 | fun Modifier.test(): Modifier = 13 | this.then(background(Color(Random.nextInt()))) 14 | 15 | /** 16 | * 将 compose 颜色转换为 Int 17 | * */ 18 | val Color.androidColor: Int get() = toArgb() 19 | 20 | /** 21 | * 将 Int 转换为 compose 颜色 22 | * */ 23 | val Int.composeColor: Color get() = Color(this) -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/Global.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import android.content.Context 4 | 5 | object Global { 6 | 7 | @Suppress("StaticFieldLeak") 8 | lateinit var appContext: Context 9 | private set 10 | 11 | fun attach(context: Context) { 12 | appContext = context 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/NetUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/util/ToastUtil.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.util 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.widget.Toast 6 | import java.lang.ref.WeakReference 7 | 8 | private var toastRef: WeakReference? = null 9 | 10 | fun showToast(msg: String, time: Int = Toast.LENGTH_SHORT) { 11 | toastRef?.get()?.cancel() 12 | if (Looper.getMainLooper() == Looper.myLooper()) { 13 | val toast = Toast.makeText(Global.appContext, msg, time) 14 | toastRef = WeakReference(toast) 15 | toast.show() 16 | } else { 17 | Handler(Looper.getMainLooper()).post { 18 | val toast = Toast.makeText(Global.appContext, msg, time) 19 | toastRef = WeakReference(toast) 20 | toast.show() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/wireinfo/WireBareHttpInterceptor.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.wireinfo 2 | 3 | import top.sankokomi.wirebare.kernel.interceptor.http.async.AsyncHttpIndexedInterceptor 4 | import top.sankokomi.wirebare.kernel.interceptor.http.async.AsyncHttpInterceptChain 5 | import top.sankokomi.wirebare.kernel.interceptor.http.async.AsyncHttpInterceptor 6 | import top.sankokomi.wirebare.kernel.interceptor.http.async.AsyncHttpInterceptorFactory 7 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 8 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 9 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpSession 10 | import top.sankokomi.wirebare.ui.record.HttpRecorder 11 | import java.nio.ByteBuffer 12 | 13 | class WireBareHttpInterceptor( 14 | private val onRequest: (HttpRequest) -> Unit, 15 | private val onResponse: (HttpResponse) -> Unit 16 | ) : AsyncHttpIndexedInterceptor() { 17 | 18 | class Factory( 19 | private val onRequest: (HttpRequest) -> Unit, 20 | private val onResponse: (HttpResponse) -> Unit 21 | ) : AsyncHttpInterceptorFactory { 22 | override fun create(): AsyncHttpInterceptor { 23 | return WireBareHttpInterceptor(onRequest, onResponse) 24 | } 25 | } 26 | 27 | override suspend fun onRequest( 28 | chain: AsyncHttpInterceptChain, 29 | buffer: ByteBuffer, 30 | session: HttpSession, 31 | index: Int 32 | ) { 33 | if (index == 0) { 34 | onRequest(session.request) 35 | } 36 | HttpRecorder.addRequestRecord(session.request, buffer) 37 | super.onRequest(chain, buffer, session, index) 38 | } 39 | 40 | override suspend fun onRequestFinished( 41 | chain: AsyncHttpInterceptChain, 42 | session: HttpSession, 43 | index: Int 44 | ) { 45 | HttpRecorder.addRequestRecord(session.request, null) 46 | super.onRequestFinished(chain, session, index) 47 | } 48 | 49 | override suspend fun onResponse( 50 | chain: AsyncHttpInterceptChain, 51 | buffer: ByteBuffer, 52 | session: HttpSession, 53 | index: Int 54 | ) { 55 | if (index == 0) { 56 | onResponse(session.response) 57 | } 58 | HttpRecorder.addResponseRecord(session.response, buffer) 59 | super.onResponse(chain, buffer, session, index) 60 | } 61 | 62 | override suspend fun onResponseFinished( 63 | chain: AsyncHttpInterceptChain, 64 | session: HttpSession, 65 | index: Int 66 | ) { 67 | HttpRecorder.addResponseRecord(session.response, null) 68 | super.onResponseFinished(chain, session, index) 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/wireinfo/WireDetailCompose.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.wireinfo 2 | 3 | import android.graphics.Bitmap 4 | import android.webkit.WebView 5 | import android.webkit.WebViewClient 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.asImageBitmap 17 | import androidx.compose.ui.viewinterop.AndroidView 18 | import top.sankokomi.wirebare.ui.record.decodeBitmap 19 | import top.sankokomi.wirebare.ui.record.decodeBrotliBitmap 20 | import top.sankokomi.wirebare.ui.record.decodeBrotliHttpBody 21 | import top.sankokomi.wirebare.ui.record.decodeGzipBitmap 22 | import top.sankokomi.wirebare.ui.record.decodeGzipHttpBody 23 | import top.sankokomi.wirebare.ui.record.decodeHttpBody 24 | 25 | enum class DetailMode { 26 | DirectHtml, 27 | GzipHtml, 28 | BrotliHtml, 29 | DirectImage, 30 | GzipImage, 31 | BrotliImage 32 | } 33 | 34 | @Composable 35 | fun WireDetailUI.LoadDetail( 36 | sessionId: String, 37 | mode: Int 38 | ) { 39 | Box(modifier = Modifier.fillMaxSize()) { 40 | when (mode) { 41 | DetailMode.DirectHtml.ordinal -> { 42 | DirectHtml(sessionId) 43 | } 44 | 45 | DetailMode.GzipHtml.ordinal -> { 46 | GzipHtml(sessionId) 47 | } 48 | 49 | DetailMode.BrotliHtml.ordinal -> { 50 | BrotliHtml(sessionId) 51 | } 52 | 53 | DetailMode.DirectImage.ordinal -> { 54 | DirectImage(sessionId) 55 | } 56 | 57 | DetailMode.GzipImage.ordinal -> { 58 | GzipImage(sessionId) 59 | } 60 | 61 | DetailMode.BrotliImage.ordinal -> { 62 | BrotliImage(sessionId) 63 | } 64 | } 65 | } 66 | } 67 | 68 | @Composable 69 | fun WireDetailUI.DirectHtml(sessionId: String) { 70 | var html by remember { mutableStateOf("") } 71 | LaunchedEffect(Unit) { 72 | val bytes = decodeHttpBody(sessionId) ?: return@LaunchedEffect 73 | html = String(bytes, 0, bytes.size) 74 | } 75 | val text = html 76 | if (text.isNotBlank()) { 77 | AndroidView( 78 | factory = { 79 | WebView(it) 80 | }, 81 | modifier = Modifier 82 | .fillMaxSize(), 83 | update = { web -> 84 | web.webViewClient = WebViewClient() 85 | web.loadDataWithBaseURL(null, text, "text/html", "UTF-8", null) 86 | } 87 | ) 88 | } 89 | } 90 | 91 | @Composable 92 | fun WireDetailUI.GzipHtml(sessionId: String) { 93 | var html by remember { mutableStateOf("") } 94 | LaunchedEffect(Unit) { 95 | val bytes = decodeGzipHttpBody(sessionId) ?: return@LaunchedEffect 96 | html = String(bytes, 0, bytes.size) 97 | } 98 | val text = html 99 | if (text.isNotBlank()) { 100 | AndroidView( 101 | factory = { 102 | WebView(it) 103 | }, 104 | modifier = Modifier 105 | .fillMaxSize(), 106 | update = { web -> 107 | web.webViewClient = WebViewClient() 108 | web.loadDataWithBaseURL(null, text, "text/html", "UTF-8", null) 109 | } 110 | ) 111 | } 112 | } 113 | 114 | @Composable 115 | fun WireDetailUI.BrotliHtml(sessionId: String) { 116 | var html by remember { mutableStateOf("") } 117 | LaunchedEffect(Unit) { 118 | val bytes = decodeBrotliHttpBody(sessionId) ?: return@LaunchedEffect 119 | html = String(bytes, 0, bytes.size) 120 | } 121 | val text = html 122 | if (text.isNotBlank()) { 123 | AndroidView( 124 | factory = { 125 | WebView(it) 126 | }, 127 | modifier = Modifier 128 | .fillMaxSize(), 129 | update = { web -> 130 | web.webViewClient = WebViewClient() 131 | web.loadDataWithBaseURL(null, text, "text/html", "UTF-8", null) 132 | } 133 | ) 134 | } 135 | } 136 | 137 | @Composable 138 | fun WireDetailUI.DirectImage(sessionId: String) { 139 | var bitmap: Bitmap? by remember { mutableStateOf(null) } 140 | LaunchedEffect(Unit) { 141 | bitmap = decodeBitmap(sessionId) 142 | } 143 | val b = bitmap 144 | if (b != null) { 145 | Image( 146 | bitmap = b.asImageBitmap(), 147 | modifier = Modifier 148 | .fillMaxSize(), 149 | contentDescription = null 150 | ) 151 | } 152 | } 153 | 154 | @Composable 155 | fun WireDetailUI.GzipImage(sessionId: String) { 156 | var bitmap: Bitmap? by remember { mutableStateOf(null) } 157 | LaunchedEffect(Unit) { 158 | bitmap = decodeGzipBitmap(sessionId) 159 | } 160 | val b = bitmap 161 | if (b != null) { 162 | Image( 163 | bitmap = b.asImageBitmap(), 164 | modifier = Modifier 165 | .fillMaxSize(), 166 | contentDescription = null 167 | ) 168 | } 169 | } 170 | 171 | @Composable 172 | fun WireDetailUI.BrotliImage(sessionId: String) { 173 | var bitmap: Bitmap? by remember { mutableStateOf(null) } 174 | LaunchedEffect(Unit) { 175 | bitmap = decodeBrotliBitmap(sessionId) 176 | } 177 | val b = bitmap 178 | if (b != null) { 179 | Image( 180 | bitmap = b.asImageBitmap(), 181 | modifier = Modifier 182 | .fillMaxSize(), 183 | contentDescription = null 184 | ) 185 | } 186 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/wireinfo/WireDetailUI.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.wireinfo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import top.sankokomi.wirebare.ui.resources.WirebareUITheme 12 | 13 | class WireDetailUI : ComponentActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | val sessionId = intent.getStringExtra("session_id") ?: "" 18 | val detailMode = intent.getIntExtra("detail_mode", DetailMode.DirectHtml.ordinal) 19 | enableEdgeToEdge() 20 | setContent { 21 | WirebareUITheme( 22 | isShowStatusBar = true, 23 | isShowNavigationBar = false 24 | ) { 25 | Surface( 26 | modifier = Modifier.fillMaxSize(), 27 | color = MaterialTheme.colorScheme.background 28 | ) { 29 | LoadDetail(sessionId = sessionId, mode = detailMode) 30 | } 31 | } 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/wireinfo/WireInfoCompose.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.wireinfo 2 | 3 | import android.content.Intent 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 19 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 20 | import top.sankokomi.wirebare.ui.resources.AppStatusBar 21 | import top.sankokomi.wirebare.ui.resources.LargeColorfulText 22 | import top.sankokomi.wirebare.ui.resources.Purple80 23 | import top.sankokomi.wirebare.ui.util.copyTextToClipBoard 24 | import top.sankokomi.wirebare.ui.util.showToast 25 | 26 | @Composable 27 | fun WireInfoUI.WireInfoUIPage( 28 | request: HttpRequest, 29 | sessionId: String 30 | ) { 31 | Box { 32 | Column( 33 | modifier = Modifier 34 | .fillMaxWidth() 35 | .verticalScroll(rememberScrollState()) 36 | .padding(horizontal = 24.dp, vertical = 4.dp) 37 | ) { 38 | AppStatusBar() 39 | Box( 40 | modifier = Modifier 41 | .clip(RoundedCornerShape(6.dp)) 42 | ) { 43 | LargeColorfulText( 44 | mainText = "目的 IP 地址", 45 | subText = request.destinationAddress ?: "", 46 | backgroundColor = Purple80, 47 | textColor = Color.Black 48 | ) 49 | } 50 | Spacer(modifier = Modifier.height(16.dp)) 51 | Box( 52 | modifier = Modifier 53 | .clip(RoundedCornerShape(6.dp)) 54 | ) { 55 | LargeColorfulText( 56 | mainText = "来源端口号", 57 | subText = request.sourcePort?.toUShort()?.toString() ?: "", 58 | backgroundColor = Purple80, 59 | textColor = Color.Black 60 | ) 61 | } 62 | Spacer(modifier = Modifier.height(16.dp)) 63 | Box( 64 | modifier = Modifier 65 | .clip(RoundedCornerShape(6.dp)) 66 | ) { 67 | LargeColorfulText( 68 | mainText = "目的端口号", 69 | subText = request.destinationPort?.toUShort()?.toString() ?: "", 70 | backgroundColor = Purple80, 71 | textColor = Color.Black 72 | ) 73 | } 74 | Spacer(modifier = Modifier.height(16.dp)) 75 | Box( 76 | modifier = Modifier 77 | .clip(RoundedCornerShape(6.dp)) 78 | ) { 79 | LargeColorfulText( 80 | mainText = "URL 链接", 81 | subText = request.url ?: "", 82 | backgroundColor = Purple80, 83 | textColor = Color.Black, 84 | onLongClick = { 85 | val url = request.url 86 | if (!url.isNullOrBlank()) { 87 | copyTextToClipBoard(url) 88 | showToast("已复制 URL") 89 | } 90 | } 91 | ) 92 | } 93 | Spacer(modifier = Modifier.height(16.dp)) 94 | Box( 95 | modifier = Modifier 96 | .clip(RoundedCornerShape(6.dp)) 97 | ) { 98 | LargeColorfulText( 99 | mainText = "HTTP 请求方法", 100 | subText = request.method ?: "", 101 | backgroundColor = Purple80, 102 | textColor = Color.Black 103 | ) 104 | } 105 | Spacer(modifier = Modifier.height(16.dp)) 106 | Box( 107 | modifier = Modifier 108 | .clip(RoundedCornerShape(6.dp)) 109 | ) { 110 | LargeColorfulText( 111 | mainText = "HTTP 版本", 112 | subText = request.httpVersion ?: "", 113 | backgroundColor = Purple80, 114 | textColor = Color.Black 115 | ) 116 | } 117 | Spacer(modifier = Modifier.height(16.dp)) 118 | Box( 119 | modifier = Modifier 120 | .clip(RoundedCornerShape(6.dp)) 121 | ) { 122 | LargeColorfulText( 123 | mainText = "INTERNAL SESSION KEY", 124 | subText = sessionId, 125 | backgroundColor = Purple80, 126 | textColor = Color.Black 127 | ) 128 | } 129 | Spacer(modifier = Modifier.height(16.dp)) 130 | Box( 131 | modifier = Modifier 132 | .clip(RoundedCornerShape(6.dp)) 133 | ) { 134 | LargeColorfulText( 135 | mainText = "HTTP 请求头", 136 | subText = request.formatHead?.joinToString("\n\n") ?: "", 137 | backgroundColor = Purple80, 138 | textColor = Color.Black 139 | ) 140 | } 141 | DataViewer(sessionId) 142 | } 143 | } 144 | } 145 | 146 | @Composable 147 | fun WireInfoUI.WireInfoUIPage( 148 | response: HttpResponse, 149 | sessionId: String 150 | ) { 151 | Box { 152 | Column( 153 | modifier = Modifier 154 | .fillMaxWidth() 155 | .verticalScroll(rememberScrollState()) 156 | .padding(horizontal = 24.dp, vertical = 4.dp) 157 | ) { 158 | AppStatusBar() 159 | Box( 160 | modifier = Modifier 161 | .clip(RoundedCornerShape(6.dp)) 162 | ) { 163 | LargeColorfulText( 164 | mainText = "目的 IP 地址", 165 | subText = response.destinationAddress ?: "", 166 | backgroundColor = Purple80, 167 | textColor = Color.Black 168 | ) 169 | } 170 | Spacer(modifier = Modifier.height(16.dp)) 171 | Box( 172 | modifier = Modifier 173 | .clip(RoundedCornerShape(6.dp)) 174 | ) { 175 | LargeColorfulText( 176 | mainText = "来源端口", 177 | subText = response.sourcePort?.toUShort()?.toString() ?: "", 178 | backgroundColor = Purple80, 179 | textColor = Color.Black 180 | ) 181 | } 182 | Spacer(modifier = Modifier.height(16.dp)) 183 | Box( 184 | modifier = Modifier 185 | .clip(RoundedCornerShape(6.dp)) 186 | ) { 187 | LargeColorfulText( 188 | mainText = "目的端口", 189 | subText = response.destinationPort?.toUShort()?.toString() ?: "", 190 | backgroundColor = Purple80, 191 | textColor = Color.Black 192 | ) 193 | } 194 | Spacer(modifier = Modifier.height(16.dp)) 195 | Box( 196 | modifier = Modifier 197 | .clip(RoundedCornerShape(6.dp)) 198 | ) { 199 | LargeColorfulText( 200 | mainText = "URL 链接", 201 | subText = response.url ?: "", 202 | backgroundColor = Purple80, 203 | textColor = Color.Black, 204 | onLongClick = { 205 | val url = response.url 206 | if (!url.isNullOrBlank()) { 207 | copyTextToClipBoard(url) 208 | showToast("已复制 URL") 209 | } 210 | } 211 | ) 212 | } 213 | Spacer(modifier = Modifier.height(16.dp)) 214 | Box( 215 | modifier = Modifier 216 | .clip(RoundedCornerShape(6.dp)) 217 | ) { 218 | LargeColorfulText( 219 | mainText = "HTTP 版本", 220 | subText = response.httpVersion ?: "", 221 | backgroundColor = Purple80, 222 | textColor = Color.Black 223 | ) 224 | } 225 | Spacer(modifier = Modifier.height(16.dp)) 226 | Box( 227 | modifier = Modifier 228 | .clip(RoundedCornerShape(6.dp)) 229 | ) { 230 | LargeColorfulText( 231 | mainText = "HTTP 响应状态码", 232 | subText = response.rspStatus ?: "", 233 | backgroundColor = Purple80, 234 | textColor = Color.Black 235 | ) 236 | } 237 | Spacer(modifier = Modifier.height(16.dp)) 238 | Box( 239 | modifier = Modifier 240 | .clip(RoundedCornerShape(6.dp)) 241 | ) { 242 | LargeColorfulText( 243 | mainText = "INTERNAL SESSION ID", 244 | subText = sessionId, 245 | backgroundColor = Purple80, 246 | textColor = Color.Black 247 | ) 248 | } 249 | Spacer(modifier = Modifier.height(16.dp)) 250 | Box( 251 | modifier = Modifier 252 | .clip(RoundedCornerShape(6.dp)) 253 | ) { 254 | LargeColorfulText( 255 | mainText = "HTTP 响应头", 256 | subText = response.formatHead?.joinToString("\n\n") ?: "", 257 | backgroundColor = Purple80, 258 | textColor = Color.Black 259 | ) 260 | } 261 | DataViewer(sessionId) 262 | } 263 | } 264 | } 265 | 266 | @Composable 267 | private fun WireInfoUI.DataViewer(sessionId: String) { 268 | Spacer(modifier = Modifier.height(16.dp)) 269 | Column( 270 | modifier = Modifier 271 | .clip(RoundedCornerShape(6.dp)) 272 | ) { 273 | LargeColorfulText( 274 | mainText = "解析为 HTML", 275 | subText = "将报文作为 HTML 文本进行解析", 276 | backgroundColor = Purple80, 277 | textColor = Color.Black, 278 | onClick = { 279 | startActivity( 280 | Intent( 281 | this@DataViewer, 282 | WireDetailUI::class.java 283 | ).apply { 284 | putExtra("detail_mode", DetailMode.DirectHtml.ordinal) 285 | putExtra("session_id", sessionId) 286 | } 287 | ) 288 | } 289 | ) 290 | } 291 | Spacer(modifier = Modifier.height(16.dp)) 292 | Box( 293 | modifier = Modifier 294 | .clip(RoundedCornerShape(6.dp)) 295 | ) { 296 | LargeColorfulText( 297 | mainText = "gzip 解压缩并解析为 HTML(TESTING)", 298 | subText = "将报文作为被 gzip 压缩的 HTML 文本进行解析", 299 | backgroundColor = Purple80, 300 | textColor = Color.Black, 301 | onClick = { 302 | startActivity( 303 | Intent( 304 | this@DataViewer, 305 | WireDetailUI::class.java 306 | ).apply { 307 | putExtra("detail_mode", DetailMode.GzipHtml.ordinal) 308 | putExtra("session_id", sessionId) 309 | } 310 | ) 311 | } 312 | ) 313 | } 314 | Spacer(modifier = Modifier.height(16.dp)) 315 | Box( 316 | modifier = Modifier 317 | .clip(RoundedCornerShape(6.dp)) 318 | ) { 319 | LargeColorfulText( 320 | mainText = "brotli 解压缩并解析为 HTML(TESTING)", 321 | subText = "将报文作为被 brotli 压缩的 HTML 文本进行解析", 322 | backgroundColor = Purple80, 323 | textColor = Color.Black, 324 | onClick = { 325 | startActivity( 326 | Intent( 327 | this@DataViewer, 328 | WireDetailUI::class.java 329 | ).apply { 330 | putExtra("detail_mode", DetailMode.BrotliHtml.ordinal) 331 | putExtra("session_id", sessionId) 332 | } 333 | ) 334 | } 335 | ) 336 | } 337 | Spacer(modifier = Modifier.height(16.dp)) 338 | Column( 339 | modifier = Modifier 340 | .clip(RoundedCornerShape(6.dp)) 341 | ) { 342 | LargeColorfulText( 343 | mainText = "解析为图片", 344 | subText = "将报文作为图片数据进行解析", 345 | backgroundColor = Purple80, 346 | textColor = Color.Black, 347 | onClick = { 348 | startActivity( 349 | Intent( 350 | this@DataViewer, 351 | WireDetailUI::class.java 352 | ).apply { 353 | putExtra("detail_mode", DetailMode.DirectImage.ordinal) 354 | putExtra("session_id", sessionId) 355 | } 356 | ) 357 | } 358 | ) 359 | } 360 | Spacer(modifier = Modifier.height(16.dp)) 361 | Column( 362 | modifier = Modifier 363 | .clip(RoundedCornerShape(6.dp)) 364 | ) { 365 | LargeColorfulText( 366 | mainText = "gzip 解压缩并解析为图片(TESTING)", 367 | subText = "将报文作为被 gzip 压缩的图片数据进行解析", 368 | backgroundColor = Purple80, 369 | textColor = Color.Black, 370 | onClick = { 371 | startActivity( 372 | Intent( 373 | this@DataViewer, 374 | WireDetailUI::class.java 375 | ).apply { 376 | putExtra("detail_mode", DetailMode.GzipImage.ordinal) 377 | putExtra("session_id", sessionId) 378 | } 379 | ) 380 | } 381 | ) 382 | } 383 | Spacer(modifier = Modifier.height(16.dp)) 384 | Column( 385 | modifier = Modifier 386 | .clip(RoundedCornerShape(6.dp)) 387 | ) { 388 | LargeColorfulText( 389 | mainText = "brotli 解压缩并解析为图片(TESTING)", 390 | subText = "将报文作为被 brotli 压缩的图片数据进行解析(测试中)", 391 | backgroundColor = Purple80, 392 | textColor = Color.Black, 393 | onClick = { 394 | startActivity( 395 | Intent( 396 | this@DataViewer, 397 | WireDetailUI::class.java 398 | ).apply { 399 | putExtra("detail_mode", DetailMode.BrotliImage.ordinal) 400 | putExtra("session_id", sessionId) 401 | } 402 | ) 403 | } 404 | ) 405 | } 406 | Spacer(modifier = Modifier.height(16.dp)) 407 | } -------------------------------------------------------------------------------- /app/src/main/java/top/sankokomi/wirebare/ui/wireinfo/WireInfoUI.kt: -------------------------------------------------------------------------------- 1 | package top.sankokomi.wirebare.ui.wireinfo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.ui.Modifier 11 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpRequest 12 | import top.sankokomi.wirebare.kernel.interceptor.http.HttpResponse 13 | import top.sankokomi.wirebare.ui.resources.WirebareUITheme 14 | 15 | class WireInfoUI : ComponentActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | val request = intent.getSerializableExtra("request") as? HttpRequest 20 | val response = intent.getSerializableExtra("response") as? HttpResponse 21 | val sessionId = intent.getStringExtra("session_id") 22 | enableEdgeToEdge() 23 | setContent { 24 | WirebareUITheme( 25 | isShowNavigationBar = false 26 | ) { 27 | Surface( 28 | modifier = Modifier.fillMaxSize(), 29 | color = MaterialTheme.colorScheme.background 30 | ) { 31 | if (request != null) { 32 | WireInfoUIPage(request = request, sessionId = sessionId ?: "") 33 | } else if (response != null) { 34 | WireInfoUIPage(response = response, sessionId = sessionId ?: "") 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_request.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_response.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wirebare.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wirebare_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_wirebare.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_wirebare_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_wirebare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-hdpi/ic_wirebare.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_wirebare_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-hdpi/ic_wirebare_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_wirebare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-mdpi/ic_wirebare.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_wirebare_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-mdpi/ic_wirebare_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_wirebare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xhdpi/ic_wirebare.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_wirebare_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xhdpi/ic_wirebare_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_wirebare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xxhdpi/ic_wirebare.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_wirebare_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xxhdpi/ic_wirebare_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_wirebare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xxxhdpi/ic_wirebare.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_wirebare_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/app/src/main/res/mipmap-xxxhdpi/ic_wirebare_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_wirebare_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #D8EBBD 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WireBare 3 | WireBareUI 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.jetbrains.kotlin.android) apply false 5 | alias(libs.plugins.compose.compiler) apply false 6 | } -------------------------------------------------------------------------------- /certificate/318facc2.0: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfzCCAmegAwIBAgIUTvAeETWYtSQsb5goM9JZ2AkpZxUwDQYJKoZIhvcNAQEL 3 | BQAwTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldCMQswCQYDVQQHDAJXQjELMAkG 4 | A1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQDDAJXQjAgFw0yNDA3MTcwODIy 5 | NTBaGA8yMTI0MDcxODA4MjI1MFowTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldC 6 | MQswCQYDVQQHDAJXQjELMAkGA1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQD 7 | DAJXQjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ1dZHRjzrjVZkhO 8 | lOl/qdcea1xWDaOQVcZTQaHAWyYoSbEZN8Y5l9XT+NSb5KfHNPDgBhwT1dVVekl4 9 | eitY2Adj5o3mUhwhHo5ZHXMoOV5XnC64jHlC6kDdSNJFucpa+jmHkkJDmMyow2Th 10 | by0DWhHeg1Kxx9voRr2xlCYS5twGFeVdXe0PDgjr3NpO+XCbezdt05wGLT4SV4Xe 11 | TppF31AhgCzsR6INJ4MspMTgPAfPcfCJV4bDuFEnR+Uj/mcnrOE8OsDOsyl1RAKF 12 | ZDmeQF46EekK9GxOI0/9ONf/Da27+wtH+cR1m9qcDOSP0uWU18SRRhFieJKbhCIn 13 | AL5BpcECAwEAAaNTMFEwHQYDVR0OBBYEFM7p8tM0+t34NTOp19DLSqDltL/OMB8G 14 | A1UdIwQYMBaAFM7p8tM0+t34NTOp19DLSqDltL/OMA8GA1UdEwEB/wQFMAMBAf8w 15 | DQYJKoZIhvcNAQELBQADggEBADBWDi8R9yAZo0ZwKmYIiWTfLpACVZlnwrmYvovK 16 | S5mID31AAKCOMfg9jB0xApyiPOOyduYofJ1S9W1YzSixSSW/1qlHBwmT/DVxNHKj 17 | Hjlqme1qPrkNWlPfCE7baztwdJiNi1EKdcf7RolKoY74TIel/nb7FbFpYTaCJr+b 18 | ukIGTngLwJDRG2U9Fws11pg2zJ+qJ6V7BbziHbvSesnic5HK+GQX/l3hzsgf8n80 19 | Lq0PRC46fNN/0fdb+5BarSkhFfNka1n0mC4cf/b9ZYapwWGijUdOZpZ2rC89hzj3 20 | Q882gBtoKgJ4i8LnhDYBwGB0Iuet/ErGj2B2ptbtGgIXF9g= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /certificate/wirebare.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfzCCAmegAwIBAgIUTvAeETWYtSQsb5goM9JZ2AkpZxUwDQYJKoZIhvcNAQEL 3 | BQAwTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldCMQswCQYDVQQHDAJXQjELMAkG 4 | A1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQDDAJXQjAgFw0yNDA3MTcwODIy 5 | NTBaGA8yMTI0MDcxODA4MjI1MFowTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldC 6 | MQswCQYDVQQHDAJXQjELMAkGA1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQD 7 | DAJXQjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ1dZHRjzrjVZkhO 8 | lOl/qdcea1xWDaOQVcZTQaHAWyYoSbEZN8Y5l9XT+NSb5KfHNPDgBhwT1dVVekl4 9 | eitY2Adj5o3mUhwhHo5ZHXMoOV5XnC64jHlC6kDdSNJFucpa+jmHkkJDmMyow2Th 10 | by0DWhHeg1Kxx9voRr2xlCYS5twGFeVdXe0PDgjr3NpO+XCbezdt05wGLT4SV4Xe 11 | TppF31AhgCzsR6INJ4MspMTgPAfPcfCJV4bDuFEnR+Uj/mcnrOE8OsDOsyl1RAKF 12 | ZDmeQF46EekK9GxOI0/9ONf/Da27+wtH+cR1m9qcDOSP0uWU18SRRhFieJKbhCIn 13 | AL5BpcECAwEAAaNTMFEwHQYDVR0OBBYEFM7p8tM0+t34NTOp19DLSqDltL/OMB8G 14 | A1UdIwQYMBaAFM7p8tM0+t34NTOp19DLSqDltL/OMA8GA1UdEwEB/wQFMAMBAf8w 15 | DQYJKoZIhvcNAQELBQADggEBADBWDi8R9yAZo0ZwKmYIiWTfLpACVZlnwrmYvovK 16 | S5mID31AAKCOMfg9jB0xApyiPOOyduYofJ1S9W1YzSixSSW/1qlHBwmT/DVxNHKj 17 | Hjlqme1qPrkNWlPfCE7baztwdJiNi1EKdcf7RolKoY74TIel/nb7FbFpYTaCJr+b 18 | ukIGTngLwJDRG2U9Fws11pg2zJ+qJ6V7BbziHbvSesnic5HK+GQX/l3hzsgf8n80 19 | Lq0PRC46fNN/0fdb+5BarSkhFfNka1n0mC4cf/b9ZYapwWGijUdOZpZ2rC89hzj3 20 | Q882gBtoKgJ4i8LnhDYBwGB0Iuet/ErGj2B2ptbtGgIXF9g= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /certificate/wirebare.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/certificate/wirebare.jks -------------------------------------------------------------------------------- /certificate/wirebare.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIFJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQOfLWLL3NID5yzy5U 3 | o19gfAICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIXhyDlu4yGQ4EggTI 4 | 1Gd5wFrsbm7tcCsbP4LFlKEfsksLXWnkHm3H9ASk2EJ+Xk3ySRq92IDpIf7GDyLs 5 | AXt/+bY3Mg3kfyPffGgTJ++1QHTR4VIJkzsNql8ZQuYBckiiIODkqxLIYqUbY9xk 6 | QCIrprq/F16LP47VIQMGXPKfDYF2rZaDsPKsma+ssCuDwcWKeaH4aWHoOco2435Z 7 | uiG8HiUje11gMUjUI2cvKYP5neKHED0u99m1Enwsn78IytzGrz7TthvpRiVLRYxD 8 | FwCFbJvsCbvI56pSqlYeXWsy00foIspb4zECde4iQGF4b/+26s4KZfIx11in1vVF 9 | LaCnESsvfYR3bfxfeNEbziww5Xce6JTKm1Rxdh1XsAQ7RyDqLZ0tiai1fi7JPpwp 10 | 4y1RKp/JV4nSNYDNr8oNsGlc/e58LdXe5Qp49oElLQs3zdkqgk1roZ6hs4Wxn3G0 11 | bBK3BfLTpWzkOyxE5DtuUu45X3iaagCy+tYpH5NyILXyJ1g7FKLU27a0mnpUJjCy 12 | jmYkgNjeNE0arQSDYgoCfOYiXoF+FPEshjyphyzraCF++KFNvRHb5zM5v7MxgFAU 13 | cvilnMSsXRnL0qLqdVVFnlBwinNep3LqHFqy5E074ulX6cQV0e4Kv4HdC2gOzPk3 14 | KcPKlRLJskKCG2URMHmO+TukyvWow4DapqYidSEeavIxR27sOCMA6lzPHYLojrdK 15 | kAhSrvMv8JUlb0vGIn+9clEaBhkwi/zoovbyc4Also+qc0cPtYSckfBwoM2DS3Um 16 | ZLgXxboESX/XeUc4JuS9PHcBUF1xw/pt3CT9y7nNLayr8+RIYemTPwaV8N9G+xFs 17 | wrg8Wk6EmLQ33CLgEMAok17AzvGhOUtXXJCNMU5NrrGUb/sFCvoSvTKTyOAOjGy3 18 | FccQDE757YSCA5Rq+GsK6v7vYxCzDX1aEEp3vQyVeYFI63Y0O6c20xbe+JbLKawn 19 | oy2OjH+iSVQujQMsfq3FuSgN4Y08bBBgg8/fa0k0nDIOegjnsPL4VTxBGSgDf6/k 20 | a6yGXEgLewAxskJPUW1aQEGRbKD3+CZascoOC+Jr3PJfWUd7qEEqKrDt4XkQfZV1 21 | C5t52W6dBMzHFP1F8xql+DpCPXxxwHtFxt5VvwaZ9NPeAIapqze8gJIaGnonJz+z 22 | k9xqRcKh0bM/Z4EhVMdIQTeUnvWNcmRS76RjXC8sCjgG1Fe6js7TuKrdxd/ppbhi 23 | EPJMb0nO7/X5Q+9JoEjvlO2ET5WnE65IsNK6p/JZX3yJ7CIirJ3qg2PneCaK+Ond 24 | +Hn4YE+x+jVMvz4C87D2nAvxMcV6XM4sJ5cAZlmqxVbxjB+62RPecdLXD2ZmDPxe 25 | Qw4SRgM+k0g8tyZ8JX3XF3KYX95H5Mg+TSYfXkYj8TgkJiISiT2r2kJyW0Awzw4W 26 | OJ78W93Yx2Fx8mj2FS0k3wuUXi0DRsTF46h9Uzg6Qx0drsVRuI+e9oCWHaM5lCNl 27 | CCky2+mIobii1SCIMRE9XrTh5uEIqLVKIvnK+0FvtRi7yJWkynC80gX+xvvZOhlr 28 | 5FD2sJQGjn9zOh2hiLfHBdC0VkCDyXrEIjunmMJWb5ab3D5f0pAFebkWtNxHRNEZ 29 | IRWP5yvMAhS+WZ4+HJneeo8bM/y0YNZ8 30 | -----END ENCRYPTED PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /certificate/wirebare.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/certificate/wirebare.p12 -------------------------------------------------------------------------------- /certificate/wirebare.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfzCCAmegAwIBAgIUTvAeETWYtSQsb5goM9JZ2AkpZxUwDQYJKoZIhvcNAQEL 3 | BQAwTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldCMQswCQYDVQQHDAJXQjELMAkG 4 | A1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQDDAJXQjAgFw0yNDA3MTcwODIy 5 | NTBaGA8yMTI0MDcxODA4MjI1MFowTjELMAkGA1UEBhMCV0IxCzAJBgNVBAgMAldC 6 | MQswCQYDVQQHDAJXQjELMAkGA1UECgwCV0IxCzAJBgNVBAsMAldCMQswCQYDVQQD 7 | DAJXQjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ1dZHRjzrjVZkhO 8 | lOl/qdcea1xWDaOQVcZTQaHAWyYoSbEZN8Y5l9XT+NSb5KfHNPDgBhwT1dVVekl4 9 | eitY2Adj5o3mUhwhHo5ZHXMoOV5XnC64jHlC6kDdSNJFucpa+jmHkkJDmMyow2Th 10 | by0DWhHeg1Kxx9voRr2xlCYS5twGFeVdXe0PDgjr3NpO+XCbezdt05wGLT4SV4Xe 11 | TppF31AhgCzsR6INJ4MspMTgPAfPcfCJV4bDuFEnR+Uj/mcnrOE8OsDOsyl1RAKF 12 | ZDmeQF46EekK9GxOI0/9ONf/Da27+wtH+cR1m9qcDOSP0uWU18SRRhFieJKbhCIn 13 | AL5BpcECAwEAAaNTMFEwHQYDVR0OBBYEFM7p8tM0+t34NTOp19DLSqDltL/OMB8G 14 | A1UdIwQYMBaAFM7p8tM0+t34NTOp19DLSqDltL/OMA8GA1UdEwEB/wQFMAMBAf8w 15 | DQYJKoZIhvcNAQELBQADggEBADBWDi8R9yAZo0ZwKmYIiWTfLpACVZlnwrmYvovK 16 | S5mID31AAKCOMfg9jB0xApyiPOOyduYofJ1S9W1YzSixSSW/1qlHBwmT/DVxNHKj 17 | Hjlqme1qPrkNWlPfCE7baztwdJiNi1EKdcf7RolKoY74TIel/nb7FbFpYTaCJr+b 18 | ukIGTngLwJDRG2U9Fws11pg2zJ+qJ6V7BbziHbvSesnic5HK+GQX/l3hzsgf8n80 19 | Lq0PRC46fNN/0fdb+5BarSkhFfNka1n0mC4cf/b9ZYapwWGijUdOZpZ2rC89hzj3 20 | Q882gBtoKgJ4i8LnhDYBwGB0Iuet/ErGj2B2ptbtGgIXF9g= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /certificate/wirebare_ca_installer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/certificate/wirebare_ca_installer.zip -------------------------------------------------------------------------------- /certificate/使用说明.md: -------------------------------------------------------------------------------- 1 | 安装证书时一般会用到的证书文件是 318facc2.0 或 wirebare.crt 2 | 3 | 需要将此证书安装到Android系统的系统根证书目录下 4 | 5 | 提供三种安装方式: 6 | 7 | #### 手动安装到用户信任的凭证 8 | 9 | (此方法一般仅适用于 Android 7.0 以下,高版本系统一般都不再信任用户安装的凭证) 10 | 11 | 需要从手机设置中安装凭证文件(即 wirebare.crt)到用户信任凭证中 12 | 13 | #### 手动安装到系统目录 14 | 15 | (一般只有较低版本的 Android 系统支持在开机时修改此分区的文件,较高版本可以采用第二种方式) 16 | 17 | 需要将此文件(即 318facc2.0 )手动保存到以下系统目录下:`system/etc/security/cacerts/` 18 | 19 | 20 | #### 通过 Magisk 安装 21 | 22 | 如果Android设备的ROOT权限由Magisk管理,则可以通过直接安装已经制作好的Magisk模块文件(即 wirebare_ca_installer.zip )来安装证书文件 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | minSdk = "24" 3 | targetSdk = "35" 4 | 5 | androidxCoreKtx = "1.15.0" 6 | androidxAppcompat = "1.7.0" 7 | androidMeterial = "1.12.0" 8 | androidxLifecycleRuntimeKtx = "2.8.7" 9 | jetpackCompose = "1.7.8" 10 | activityCompose = "1.10.1" 11 | composeBom = "2025.02.00" 12 | datastore = "1.1.3" 13 | coilComposeKt = "2.5.0" 14 | brotliDec = "0.1.2" 15 | kotlinxCoroutines = "1.9.0" 16 | bouncycastle = "1.67" 17 | 18 | agp = "8.5.2" 19 | kotlin = "2.1.10" 20 | 21 | 22 | [libraries] 23 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" } 24 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" } 25 | android-material = { group = "com.google.android.material", name = "material", version.ref = "androidMeterial" } 26 | androidx-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycleRuntimeKtx" } 27 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 28 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 29 | compose-ui = { group = "androidx.compose.ui", name = "ui" } 30 | compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 31 | compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 32 | compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 33 | compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 34 | compose-material3 = { group = "androidx.compose.material3", name = "material3" } 35 | datastore-preference = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } 36 | coil-compose-kt = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilComposeKt" } 37 | ktx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 38 | bouncycastle-bcpkix = { group = "org.bouncycastle", name = "bcpkix-jdk15on", version.ref = "bouncycastle" } 39 | bouncycastle-bcprov = { group = "org.bouncycastle", name = "bcprov-jdk15on", version.ref = "bouncycastle" } 40 | brotli-dec = { group = "org.brotli", name = "dec", version.ref = "brotliDec" } 41 | 42 | 43 | [plugins] 44 | android-application = { id = "com.android.application", version.ref = "agp" } 45 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 46 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 47 | 48 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kokomi7QAQ/wirebare-android/f3a0358c703f91166a9e177163cebc61a9a6a780/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 27 20:28:12 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /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" "$@" 186 | -------------------------------------------------------------------------------- /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 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "wirebare-android" 17 | include(":app") 18 | include(":wirebare-kernel") 19 | --------------------------------------------------------------------------------