Skip to content

Commit 0fc8c2c

Browse files
committed
меню*
1 parent 82267a8 commit 0fc8c2c

25 files changed

Lines changed: 1442 additions & 675 deletions

.github/workflows/android-reusable.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
- uses: android-actions/setup-android@v3
2424
with:
25-
packages: "tools platform-tools ndk;25.2.9519653 platforms;android-34 build-tools;34.0.0"
25+
packages: "tools platform-tools ndk;25.2.9519653 platforms;android-35 build-tools;35.0.0"
2626

2727
- name: Install gomobile
2828
run: |

android/app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ val ciKeystorePass =
5252

5353
android {
5454
namespace = "dev.c0redev.volter"
55-
compileSdk = 34
55+
compileSdk = 35
5656

5757
defaultConfig {
5858
applicationId = "dev.c0redev.volter"
5959
minSdk = 33
60-
targetSdk = 34
60+
targetSdk = 35
6161

6262
versionCode = volterVersionCode()
6363
versionName = volterVersionName()
@@ -118,7 +118,7 @@ android {
118118
}
119119

120120
dependencies {
121-
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
121+
val composeBom = platform("androidx.compose:compose-bom:2025.04.01")
122122
implementation(composeBom)
123123

124124
implementation(files("libs/volter-core.aar"))

android/app/src/main/java/dev/c0redev/volter/MainActivity.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import androidx.activity.ComponentActivity
66
import androidx.activity.compose.setContent
77
import androidx.activity.enableEdgeToEdge
88
import androidx.activity.viewModels
9+
import androidx.compose.foundation.background
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.fillMaxSize
912
import androidx.compose.material3.MaterialTheme
10-
import androidx.compose.material3.Surface
1113
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.graphics.Brush
1215
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
1316
import dev.c0redev.volter.theme.VolterTheme
1417
import dev.c0redev.volter.ui.AppNavGraph
@@ -24,7 +27,19 @@ class MainActivity : ComponentActivity() {
2427
super.onCreate(savedInstanceState)
2528
setContent {
2629
VolterTheme {
27-
Surface(modifier = Modifier, color = MaterialTheme.colorScheme.background) {
30+
val scheme = MaterialTheme.colorScheme
31+
Box(
32+
modifier = Modifier
33+
.fillMaxSize()
34+
.background(
35+
Brush.verticalGradient(
36+
listOf(
37+
scheme.background,
38+
scheme.surfaceContainerLowest.copy(alpha = 0.94f),
39+
),
40+
),
41+
),
42+
) {
2843
AppNavGraph(vm = vm)
2944
}
3045
}

android/app/src/main/java/dev/c0redev/volter/VolterVpnService.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.core.app.ServiceCompat
1717
import dev.c0redev.volter.core.CoreBridge
1818
import dev.c0redev.volter.domain.model.ClientSettings
1919
import dev.c0redev.volter.domain.model.Config
20+
import dev.c0redev.volter.traffic.VpnTrafficRecorder
2021
import org.json.JSONObject
2122
import java.io.File
2223
import java.net.Inet4Address
@@ -35,6 +36,8 @@ class VolterVpnService : VpnService() {
3536
private var tunFd: Int = -1
3637
private var tunPfd: ParcelFileDescriptor? = null
3738
private val sessionAbort = AtomicBoolean(false)
39+
private var trafficWallStartMs: Long = 0L
40+
private var activeVpnMode: String? = null
3841

3942
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
4043
if (intent?.action == ACTION_STOP) {
@@ -152,6 +155,7 @@ class VolterVpnService : VpnService() {
152155
}
153156

154157
private fun stopActive() {
158+
flushTrafficSnapshot()
155159
CoreSocketProtect.clear()
156160
val handle = coreHandle
157161
VolterLog.i("stopActive prevCoreHandle=$handle tunFd=$tunFd")
@@ -171,6 +175,19 @@ class VolterVpnService : VpnService() {
171175
stopForeground(STOP_FOREGROUND_DETACH)
172176
}
173177

178+
private fun flushTrafficSnapshot() {
179+
val start = trafficWallStartMs
180+
if (start <= 0L) return
181+
val end = System.currentTimeMillis()
182+
val mode = activeVpnMode ?: "tun"
183+
trafficWallStartMs = 0L
184+
activeVpnMode = null
185+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
186+
runCatching { VpnTrafficRecorder.writePending(this, start, end, mode) }
187+
.onFailure { VolterLog.w("flushTrafficSnapshot failed: ${it.message}") }
188+
}
189+
}
190+
174191
private fun startProxyInternal(cfg: Config, settings: ClientSettings, configDir: String) {
175192
CoreSocketProtect.install(this)
176193
val cfgJson = cfg.toJson().toString()
@@ -188,6 +205,8 @@ class VolterVpnService : VpnService() {
188205
return
189206
}
190207
VolterLog.i("startProxy ok handle=$coreHandle")
208+
trafficWallStartMs = System.currentTimeMillis()
209+
activeVpnMode = "proxy"
191210
broadcastSession(coreHandle, "proxy")
192211
ensureForeground("proxy")
193212
}
@@ -294,6 +313,8 @@ class VolterVpnService : VpnService() {
294313
return
295314
}
296315
VolterLog.i("startTun core ok handle=$coreHandle")
316+
trafficWallStartMs = System.currentTimeMillis()
317+
activeVpnMode = "tun"
297318
broadcastSession(coreHandle, "tun")
298319
ensureForeground("connected")
299320
}

android/app/src/main/java/dev/c0redev/volter/domain/model/Metrics.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ import org.json.JSONArray
55
import org.json.JSONObject
66
import java.time.Instant
77

8+
data class AppTrafficEntry(
9+
val uid: Int,
10+
val rxBytes: Long,
11+
val txBytes: Long,
12+
val label: String,
13+
) {
14+
fun toJson(): JSONObject = JSONObject().apply {
15+
put("uid", uid)
16+
put("rx", rxBytes)
17+
put("tx", txBytes)
18+
put("label", label)
19+
}
20+
21+
companion object {
22+
fun fromJson(o: JSONObject): AppTrafficEntry = AppTrafficEntry(
23+
uid = o.optInt("uid"),
24+
rxBytes = o.optLong("rx", 0L),
25+
txBytes = o.optLong("tx", 0L),
26+
label = o.optString("label", "?"),
27+
)
28+
}
29+
}
30+
831
data class SessionRecord(
932
val start: Instant,
1033
val end: Instant? = null,
@@ -19,6 +42,12 @@ data class SessionRecord(
1942
val dnsOkBefore: Boolean,
2043
val dnsOkAfter: Boolean? = null,
2144
val probeOk: Boolean,
45+
val rxBytes: Long? = null,
46+
val txBytes: Long? = null,
47+
val byApp: List<AppTrafficEntry> = emptyList(),
48+
val trafficCollectError: String? = null,
49+
val routePrefixes: List<String> = emptyList(),
50+
val excludePrefixes: List<String> = emptyList(),
2251
) {
2352
fun toJson(): JSONObject {
2453
val j = JSONObject()
@@ -35,6 +64,18 @@ data class SessionRecord(
3564
j.put("dnsOKBefore", dnsOkBefore)
3665
dnsOkAfter?.let { j.put("dnsOKAfter", it) }
3766
j.put("probeOK", probeOk)
67+
rxBytes?.let { j.put("rxBytes", it) }
68+
txBytes?.let { j.put("txBytes", it) }
69+
trafficCollectError?.let { j.put("trafficCollectError", it) }
70+
if (byApp.isNotEmpty()) {
71+
j.put("byApp", JSONArray().apply { byApp.forEach { put(it.toJson()) } })
72+
}
73+
if (routePrefixes.isNotEmpty()) {
74+
j.put("routePrefixes", JSONArray().apply { routePrefixes.forEach { put(it) } })
75+
}
76+
if (excludePrefixes.isNotEmpty()) {
77+
j.put("excludePrefixes", JSONArray().apply { excludePrefixes.forEach { put(it) } })
78+
}
3879
return j
3980
}
4081

@@ -53,9 +94,25 @@ data class SessionRecord(
5394
dnsOkBefore = j.getBoolean("dnsOKBefore"),
5495
dnsOkAfter = if (j.has("dnsOKAfter") && !j.isNull("dnsOKAfter")) j.getBoolean("dnsOKAfter") else null,
5596
probeOk = j.getBoolean("probeOK"),
97+
rxBytes = if (j.has("rxBytes") && !j.isNull("rxBytes")) j.getLong("rxBytes") else null,
98+
txBytes = if (j.has("txBytes") && !j.isNull("txBytes")) j.getLong("txBytes") else null,
99+
byApp = parseAppList(j.optJSONArray("byApp")),
100+
trafficCollectError = j.optNullableString("trafficCollectError"),
101+
routePrefixes = parseStringList(j.optJSONArray("routePrefixes")),
102+
excludePrefixes = parseStringList(j.optJSONArray("excludePrefixes")),
56103
)
57104

58105
fun listFromJson(arr: JSONArray): List<SessionRecord> = List(arr.length()) { i -> fromJson(arr.getJSONObject(i)) }
106+
107+
private fun parseAppList(arr: JSONArray?): List<AppTrafficEntry> {
108+
if (arr == null) return emptyList()
109+
return List(arr.length()) { i -> AppTrafficEntry.fromJson(arr.getJSONObject(i)) }
110+
}
111+
112+
private fun parseStringList(arr: JSONArray?): List<String> {
113+
if (arr == null) return emptyList()
114+
return List(arr.length()) { i -> arr.getString(i) }
115+
}
59116
}
60117
}
61118

android/app/src/main/java/dev/c0redev/volter/theme/VolterSpacing.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@ package dev.c0redev.volter.theme
33
import androidx.compose.ui.unit.dp
44

55
object VolterSpacing {
6-
val screenHorizontal = 20.dp
7-
val screenVertical = 16.dp
8-
val cardInner = 20.dp
9-
val sectionGap = 16.dp
6+
val screenHorizontal = 18.dp
7+
val screenVertical = 14.dp
8+
val cardInner = 16.dp
9+
val sectionGap = 14.dp
1010
val minTouch = 48.dp
11+
val glassRadius = 20.dp
12+
val controlRadius = 12.dp
13+
val chipRadius = 16.dp
14+
val barPillRadius = 22.dp
15+
val bottomBarGlassRadius = 24.dp
16+
val skeletonLineRadius = 4.dp
17+
val skeletonBlockRadius = 8.dp
18+
val fullPill = 999.dp
1119
}

0 commit comments

Comments
 (0)