Skip to content

Commit 5f631ac

Browse files
committed
v4.4.0: Interface labels, DNS cache controls, expanded notification, top apps
- Connection log: human-readable interface labels (rmnet0->Mobile, wlan0->WiFi, tun0->VPN, bt->Bluetooth, eth->Ethernet, p2p->WiFi Direct, etc) - Settings: DNS cache management section with live stats (entries, hit rate, evictions) and clear cache button via DnsVpnService.clearCacheCallback - Notification: expanded with Pause 5m + Pause 30m + Stop (was single Pause 5m) - Home: Top Querying Apps mini-card showing top 3 most active apps with counts - DnsVpnService: clearCacheCallback companion field for UI-triggered cache clear
1 parent bf721ee commit 5f631ac

8 files changed

Lines changed: 113 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# HostShield
22

33
## Overview
4-
Modern, AMOLED-dark hosts-based ad blocker app for Android. Inspired by AdAway. v4.3.2.
4+
Modern, AMOLED-dark hosts-based ad blocker app for Android. Inspired by AdAway. v4.4.0.
55

66
## Tech Stack
77
- Kotlin, Jetpack Compose, Material 3
@@ -70,6 +70,7 @@ cd app
7070
- Secrets configured: `KEYSTORE_BASE64`, `KEY_ALIAS`, `KEY_PASSWORD`, `STORE_PASSWORD`
7171

7272
## Version History
73+
- v4.4.0: Connection log interface labels (rmnet0=Mobile, wlan0=WiFi, etc), DNS cache management in Settings (clear cache button + live stats), expanded notification (Pause 5m / Pause 30m / Stop), top querying apps mini-card on Home dashboard
7374
- v4.3.2: UI fixes — FlowRow wrapping for category chips (was smushed in single Row), larger text fields (search bar, custom DNS), FlowRow for DoH provider selector and feature pills, removed fixed heights that clipped text
7475
- v4.3.1: Bug audit — fix AutomationReceiver rate limiting (static companion state), GeoIpLookup atomic CAS window reset, SourceHealthWorker ensures alert channel exists, pauseResumeJob @Volatile, baselineRates synchronized list
7576
- v4.3.0: Notification pause/resume action (5-min pause from notification), CNAME CLOAK badge in log detail sheet, pretty upstream server labels (DoH:Cloudflare), source health DEAD notifications (push alert), alerts notification channel, pause state bypasses blocking

app/app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// HostShield v4.3.2
1+
// HostShield v4.4.0
22
plugins {
33
id("com.android.application")
44
id("org.jetbrains.kotlin.android")
@@ -15,8 +15,8 @@ android {
1515
applicationId = "com.hostshield"
1616
minSdk = 26
1717
targetSdk = 35
18-
versionCode = 45
19-
versionName = "4.3.2"
18+
versionCode = 46
19+
versionName = "4.4.0"
2020

2121
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2222

app/app/src/main/java/com/hostshield/service/DnsVpnService.kt

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ class DnsVpnService : VpnService() {
110110
@Volatile var currentDroppedQueries: Int = 0
111111
private set
112112

113+
/** Clear DNS cache from UI. Safe to call when VPN is not running (no-op). */
114+
@Volatile var clearCacheCallback: (() -> Unit)? = null
115+
private set
116+
113117
// VPN interface
114118
private const val VPN_ADDRESS = "10.120.0.1"
115119
private const val VPN_ADDRESS6 = "fd00::1"
@@ -495,6 +499,7 @@ class DnsVpnService : VpnService() {
495499
dnsAnswerCache.clear()
496500
droppedQueries.set(0)
497501
totalQueriesCount.set(0)
502+
clearCacheCallback = { dnsCache.clear() }
498503
serviceScope.launch { writeLoop() }
499504
serviceScope.launch { packetLoop() }
500505
startLogFlusher()
@@ -541,6 +546,7 @@ class DnsVpnService : VpnService() {
541546
ContextState.unregister(this)
542547
dnsAnswerCache.clear()
543548
dnsCache.clear()
549+
clearCacheCallback = null
544550
try { writeChannel.close() } catch (_: Exception) { }
545551
try { vpnInterface?.close() } catch (_: Exception) { }
546552
vpnInterface = null
@@ -1967,17 +1973,19 @@ class DnsVpnService : VpnService() {
19671973
}.let { nm.createNotificationChannel(it) }
19681974
}
19691975

1976+
private fun makePausePendingIntent(minutes: Int, requestCode: Int): PendingIntent =
1977+
PendingIntent.getService(this, requestCode,
1978+
Intent(this, DnsVpnService::class.java).apply {
1979+
action = ACTION_PAUSE; putExtra("pause_minutes", minutes)
1980+
},
1981+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
1982+
19701983
private fun buildNotification(blocked: Int): Notification {
19711984
val ci = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java),
19721985
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
19731986
val si = PendingIntent.getService(this, 1,
19741987
Intent(this, DnsVpnService::class.java).apply { action = ACTION_STOP },
19751988
PendingIntent.FLAG_IMMUTABLE)
1976-
val pauseIntent = PendingIntent.getService(this, 2,
1977-
Intent(this, DnsVpnService::class.java).apply {
1978-
action = ACTION_PAUSE; putExtra("pause_minutes", 5)
1979-
},
1980-
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
19811989

19821990
val title = if (isPaused) "HostShield Paused" else "HostShield Active"
19831991
val sub = buildString {
@@ -1996,15 +2004,11 @@ class DnsVpnService : VpnService() {
19962004
.setPriority(NotificationCompat.PRIORITY_LOW)
19972005

19982006
if (isPaused) {
1999-
// Show resume action when paused
2000-
val resumeIntent = PendingIntent.getService(this, 3,
2001-
Intent(this, DnsVpnService::class.java).apply {
2002-
action = ACTION_PAUSE; putExtra("pause_minutes", 0)
2003-
},
2004-
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
2005-
builder.addAction(android.R.drawable.ic_media_play, "Resume", resumeIntent)
2007+
builder.addAction(android.R.drawable.ic_media_play, "Resume", makePausePendingIntent(0, 5))
20062008
} else {
2007-
builder.addAction(android.R.drawable.ic_media_pause, "Pause 5m", pauseIntent)
2009+
// Max 3 actions: Pause 5m, Pause 30m, Stop
2010+
builder.addAction(android.R.drawable.ic_media_pause, "Pause 5m", makePausePendingIntent(5, 2))
2011+
builder.addAction(android.R.drawable.ic_media_pause, "Pause 30m", makePausePendingIntent(30, 3))
20082012
}
20092013
builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", si)
20102014

app/app/src/main/java/com/hostshield/ui/screens/home/HomeScreen.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,31 @@ fun HomeScreen(
641641
}
642642
}
643643

644+
// ── Top Querying Apps ────────────────────────────────
645+
if (state.topApps.isNotEmpty()) {
646+
Spacer(Modifier.height(10.dp))
647+
GlassCard(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp)) {
648+
Column(modifier = Modifier.padding(14.dp)) {
649+
Text("Top Querying Apps", color = TextDim, fontSize = 10.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.sp)
650+
Spacer(Modifier.height(8.dp))
651+
state.topApps.forEachIndexed { idx, (_, label, count) ->
652+
Row(
653+
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp),
654+
verticalAlignment = Alignment.CenterVertically
655+
) {
656+
val medalColor = when (idx) { 0 -> Teal; 1 -> Blue; else -> TextDim }
657+
Text("${idx + 1}", color = medalColor, fontSize = 12.sp, fontWeight = FontWeight.Bold,
658+
modifier = Modifier.width(18.dp))
659+
Text(label.ifBlank { "Unknown" }, color = TextPrimary, fontSize = 12.sp,
660+
maxLines = 1, modifier = Modifier.weight(1f),
661+
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis)
662+
Text("$count", color = TextDim, fontSize = 11.sp, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace)
663+
}
664+
}
665+
}
666+
}
667+
}
668+
644669
Spacer(Modifier.height(16.dp))
645670

646671
// ── Live DNS Activity Feed ──────────────────────────

app/app/src/main/java/com/hostshield/ui/screens/home/HomeViewModel.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ data class HomeUiState(
8383
/** DNS cache hit rate (from VPN). */
8484
val cacheHitRate: Float = 0f,
8585
/** Dropped queries from buffer overflow. */
86-
val droppedQueries: Int = 0
86+
val droppedQueries: Int = 0,
87+
/** Top querying apps (up to 3). */
88+
val topApps: List<Triple<String, String, Int>> = emptyList() // (package, label, count)
8789
)
8890

8991
@HiltViewModel
@@ -135,6 +137,17 @@ class HomeViewModel @Inject constructor(
135137
calculatePrivacyScore()
136138
trackQueryRate()
137139
observeCategoryCounts()
140+
observeTopApps()
141+
}
142+
143+
private fun observeTopApps() {
144+
viewModelScope.launch {
145+
dnsLogDao.getTopQueryApps(3).collect { apps ->
146+
_uiState.update { it.copy(
147+
topApps = apps.map { a -> Triple(a.appPackage, a.appLabel, a.cnt) }
148+
) }
149+
}
150+
}
138151
}
139152

140153
private fun observeCategoryCounts() {

app/app/src/main/java/com/hostshield/ui/screens/logs/ConnectionLogScreen.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ private fun ConnectionLogRow(entry: ConnectionLogEntry, timeFmt: SimpleDateForma
252252
maxLines = 1, overflow = TextOverflow.Ellipsis
253253
)
254254
if (entry.interfaceName.isNotBlank()) {
255-
Text(" via ${entry.interfaceName}", color = TextDim.copy(alpha = 0.5f), fontSize = 10.sp)
255+
val ifLabel = interfaceLabel(entry.interfaceName)
256+
Text(" via $ifLabel", color = TextDim.copy(alpha = 0.5f), fontSize = 10.sp)
256257
}
257258
}
258259
}
@@ -290,3 +291,17 @@ private fun TabPill(label: String, selected: Boolean, accent: Color, onClick: ()
290291
)
291292
}
292293
}
294+
295+
/** Map raw interface names to human-readable labels. */
296+
private fun interfaceLabel(iface: String): String = when {
297+
iface.startsWith("wlan") -> "WiFi"
298+
iface.startsWith("rmnet") || iface.startsWith("ccmni") -> "Mobile"
299+
iface.startsWith("tun") || iface.startsWith("vpn") -> "VPN"
300+
iface.startsWith("bt-") || iface.startsWith("bnep") -> "Bluetooth"
301+
iface.startsWith("eth") || iface.startsWith("usb") -> "Ethernet"
302+
iface.startsWith("lo") -> "Loopback"
303+
iface.startsWith("dummy") -> "Dummy"
304+
iface.startsWith("p2p") -> "WiFi Direct"
305+
iface.startsWith("swlan") -> "WiFi Hotspot"
306+
else -> iface
307+
}

app/app/src/main/java/com/hostshield/ui/screens/settings/SettingsScreen.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ fun SettingsScreen(
156156
)
157157
Spacer(Modifier.height(6.dp))
158158
BlockResponseSelector(state.blockResponseType) { viewModel.setBlockResponseType(it) }
159+
160+
// DNS Cache management
161+
Spacer(Modifier.height(12.dp))
162+
Text("DNS Cache", color = TextPrimary, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
163+
Spacer(Modifier.height(4.dp))
164+
val cacheStats = com.hostshield.service.DnsVpnService.currentCacheStats
165+
if (cacheStats != null) {
166+
Row(
167+
modifier = Modifier.fillMaxWidth(),
168+
horizontalArrangement = Arrangement.SpaceBetween
169+
) {
170+
Text("${cacheStats.size + cacheStats.negativeSize} entries", color = TextDim, fontSize = 11.sp)
171+
Text("${(cacheStats.hitRate * 100).toInt()}% hit rate", color = Green, fontSize = 11.sp)
172+
Text("${cacheStats.evictions} evictions", color = TextDim, fontSize = 11.sp)
173+
}
174+
Spacer(Modifier.height(6.dp))
175+
}
176+
Surface(
177+
onClick = { viewModel.clearDnsCache() },
178+
shape = RoundedCornerShape(8.dp),
179+
color = Surface2,
180+
modifier = Modifier.fillMaxWidth()
181+
) {
182+
Row(
183+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
184+
verticalAlignment = Alignment.CenterVertically
185+
) {
186+
Icon(Icons.Filled.Cached, null, tint = Blue, modifier = Modifier.size(16.dp))
187+
Spacer(Modifier.width(10.dp))
188+
Text("Clear DNS cache", color = TextPrimary, fontSize = 13.sp)
189+
}
190+
}
159191
}
160192

161193
// VPN Settings

app/app/src/main/java/com/hostshield/ui/screens/settings/SettingsViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ class SettingsViewModel @Inject constructor(
207207
fun setScheduleMode(mode: String) { viewModelScope.launch { prefs.setScheduleMode(mode) } }
208208
fun setCustomUpstreamDns(dns: String) { viewModelScope.launch { prefs.setCustomUpstreamDns(dns.trim()) } }
209209

210+
fun clearDnsCache() {
211+
com.hostshield.service.DnsVpnService.clearCacheCallback?.invoke()
212+
}
213+
210214
/** Export rules JSON directly to a SAF URI. */
211215
fun exportRulesToUri(uri: Uri) {
212216
viewModelScope.launch {

0 commit comments

Comments
 (0)