├── .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 |
--------------------------------------------------------------------------------