Skip to content

Commit 55ffbcc

Browse files
committed
.
1 parent a110650 commit 55ffbcc

7 files changed

Lines changed: 246 additions & 15 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
23
<uses-permission android:name="android.permission.INTERNET" />
34
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
45
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
56
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
67
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
78
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
89
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
10+
<uses-permission
11+
android:name="android.permission.PACKAGE_USAGE_STATS"
12+
tools:ignore="ProtectedPermissions" />
913

1014
<queries>
1115
<intent>

android/app/src/main/java/dev/c0redev/volter/traffic/VpnTrafficRecorder.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package dev.c0redev.volter.traffic
22

3+
import android.app.AppOpsManager
34
import android.app.usage.NetworkStats
45
import android.app.usage.NetworkStatsManager
56
import android.content.Context
67
import android.net.ConnectivityManager
78
import android.os.Build
9+
import android.os.Process
810
import androidx.annotation.RequiresApi
911
import dev.c0redev.volter.VolterLog
1012
import org.json.JSONArray
@@ -30,6 +32,26 @@ object VpnTrafficRecorder {
3032

3133
private fun pendingFile(ctx: Context): File = File(ctx.cacheDir, FILE_NAME)
3234

35+
@JvmStatic
36+
fun hasUsageAccess(ctx: Context): Boolean {
37+
val ops = ctx.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager ?: return false
38+
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
39+
ops.unsafeCheckOpNoThrow(
40+
AppOpsManager.OPSTR_GET_USAGE_STATS,
41+
Process.myUid(),
42+
ctx.packageName,
43+
)
44+
} else {
45+
@Suppress("DEPRECATION")
46+
ops.checkOpNoThrow(
47+
AppOpsManager.OPSTR_GET_USAGE_STATS,
48+
Process.myUid(),
49+
ctx.packageName,
50+
)
51+
}
52+
return mode == AppOpsManager.MODE_ALLOWED
53+
}
54+
3355
@JvmStatic
3456
fun writePending(ctx: Context, startMs: Long, endMs: Long, mode: String) {
3557
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
@@ -114,6 +136,16 @@ object VpnTrafficRecorder {
114136
) to msg
115137
}
116138

139+
if (!hasUsageAccess(ctx)) {
140+
val msg = "Usage Access not granted"
141+
return@runCatching TrafficPending(
142+
rxBytes = null,
143+
txBytes = null,
144+
byApp = emptyList(),
145+
collectError = msg,
146+
) to msg
147+
}
148+
117149
var rxTotal = 0L
118150
var txTotal = 0L
119151
val byUid = mutableMapOf<Int, LongArray>()

android/app/src/main/java/dev/c0redev/volter/ui/components/C0reWidgets.kt

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,34 +112,62 @@ fun Sparkline(
112112
lineColor: Color = MaterialTheme.colorScheme.primary,
113113
) {
114114
val scheme = MaterialTheme.colorScheme
115+
val grid = scheme.outlineVariant
115116
Canvas(
116117
modifier = modifier
117118
.fillMaxWidth()
118-
.height(40.dp)
119+
.height(48.dp)
119120
.background(scheme.background)
120-
.border(width = 1.dp, color = scheme.outlineVariant, shape = RectangleShape)
121-
.padding(4.dp),
121+
.border(width = 1.dp, color = scheme.outline, shape = RectangleShape)
122+
.padding(1.dp),
122123
) {
124+
val w = size.width
125+
val h = size.height
126+
// сетка 4x3 ы
127+
val cols = 4
128+
val rows = 3
129+
for (i in 1 until cols) {
130+
val x = w / cols * i
131+
drawLine(grid, Offset(x, 0f), Offset(x, h), strokeWidth = 1f)
132+
}
133+
for (i in 1 until rows) {
134+
val y = h / rows * i
135+
drawLine(grid, Offset(0f, y), Offset(w, y), strokeWidth = 1f)
136+
}
123137
if (values.size < 2) return@Canvas
124138
val maxV = values.max()
125139
val minV = values.min()
126140
val span = (maxV - minV).takeIf { it > 0f } ?: 1f
127-
val w = size.width
128-
val h = size.height
129141
val stepX = w / (values.size - 1)
130-
val path = Path()
142+
fun py(v: Float) = h - ((v - minV) / span) * (h - 2f) - 1f
143+
val line = Path()
144+
val fill = Path()
131145
values.forEachIndexed { i, v ->
132146
val x = stepX * i
133-
val y = h - ((v - minV) / span) * h
134-
if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
147+
val y = py(v)
148+
if (i == 0) {
149+
line.moveTo(x, y)
150+
fill.moveTo(x, h)
151+
fill.lineTo(x, y)
152+
} else {
153+
line.lineTo(x, y)
154+
fill.lineTo(x, y)
155+
}
135156
}
157+
fill.lineTo(stepX * (values.size - 1), h)
158+
fill.close()
159+
drawPath(path = fill, color = lineColor.copy(alpha = 0.12f))
136160
drawPath(
137-
path = path,
161+
path = line,
138162
color = lineColor,
139-
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f),
163+
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.5f),
140164
)
141165
val lastX = stepX * (values.size - 1)
142-
val lastY = h - ((values.last() - minV) / span) * h
143-
drawCircle(color = lineColor, radius = 3f, center = Offset(lastX, lastY))
166+
val lastY = py(values.last())
167+
drawRect(
168+
color = lineColor,
169+
topLeft = Offset(lastX - 2f, lastY - 2f),
170+
size = androidx.compose.ui.geometry.Size(4f, 4f),
171+
)
144172
}
145173
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package dev.c0redev.volter.ui.components
2+
3+
import android.graphics.RuntimeShader
4+
import androidx.compose.animation.core.LinearEasing
5+
import androidx.compose.animation.core.animateFloat
6+
import androidx.compose.animation.core.infiniteRepeatable
7+
import androidx.compose.animation.core.rememberInfiniteTransition
8+
import androidx.compose.animation.core.tween
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.remember
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.composed
13+
import androidx.compose.ui.draw.drawWithCache
14+
import androidx.compose.ui.graphics.ShaderBrush
15+
16+
// бегущий зелёный курсор по периметру рамки, stable-индикатор активного соединения
17+
private const val RUNNING_BORDER_SRC = """
18+
uniform float2 u_res;
19+
uniform float u_time;
20+
21+
const half3 u_color = half3(0.435, 0.682, 0.435);
22+
23+
half4 main(float2 frag) {
24+
float px = 1.5;
25+
float bx = px / u_res.x;
26+
float by = px / u_res.y;
27+
float2 uv = frag / u_res;
28+
29+
float w = u_res.x;
30+
float h = u_res.y;
31+
float perim = 2.0 * (w + h);
32+
float x = frag.x;
33+
float y = frag.y;
34+
35+
bool onBorder = uv.x < bx || uv.x > 1.0 - bx ||
36+
uv.y < by || uv.y > 1.0 - by;
37+
38+
float t = 0.0;
39+
if (onBorder) {
40+
if (y < px) t = x / perim;
41+
else if (x > w - px) t = (w + y) / perim;
42+
else if (y > h - px) t = (w + h + (w - x)) / perim;
43+
else t = (2.0 * w + h + (h - y)) / perim;
44+
}
45+
46+
float cursor = mod(u_time * 0.5, 1.0);
47+
float dist = mod(cursor - t + 1.0, 1.0);
48+
float tail = (1.0 - smoothstep(0.0, 0.28, dist)) * step(dist, 0.28);
49+
float head = 1.0 - smoothstep(0.0, 0.02, dist);
50+
float bright = max(tail, head);
51+
52+
float hp = cursor * perim;
53+
float2 headPos;
54+
if (hp < w) headPos = float2(hp, 0.75);
55+
else if (hp < w + h) headPos = float2(w - 0.75, hp - w);
56+
else if (hp < 2.0*w + h) headPos = float2(w - (hp - w - h), h - 0.75);
57+
else headPos = float2(0.75, h - (hp - 2.0*w - h));
58+
59+
float bd = distance(float2(x, y), headPos);
60+
float blm = exp(-(bd * bd) / 30.0) * 0.55;
61+
62+
if (!onBorder && blm < 0.008) return half4(0.0);
63+
if (!onBorder) return half4(u_color * blm, blm);
64+
65+
float alpha = 0.18 + bright * 0.78;
66+
return half4(u_color * alpha, alpha);
67+
}
68+
"""
69+
70+
// рисует анимированную рамку поверх контента, пока active == true
71+
fun Modifier.runningBorder(active: Boolean): Modifier = composed {
72+
if (!active) return@composed this
73+
74+
val shader = remember { RuntimeShader(RUNNING_BORDER_SRC) }
75+
val brush = remember { ShaderBrush(shader) }
76+
val transition = rememberInfiniteTransition(label = "runningBorder")
77+
// время в секундах, период кратен 2с (циклу курсора), шов незаметен
78+
val time by transition.animateFloat(
79+
initialValue = 0f,
80+
targetValue = 120f,
81+
animationSpec = infiniteRepeatable(tween(120_000, easing = LinearEasing)),
82+
label = "time",
83+
)
84+
85+
drawWithCache {
86+
shader.setFloatUniform("u_res", size.width, size.height)
87+
onDrawWithContent {
88+
drawContent()
89+
shader.setFloatUniform("u_time", time)
90+
drawRect(brush)
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)