Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,24 @@ data class WhiteDnsScanState(
)
}

fun completeResumeWithoutRemainingResolvers(nowMillis: Long): WhiteDnsScanState {
val completedCount = totalResolvers.takeIf { it > 0 }
?: maxOf(completedResolvers, validResolvers + rejectedResolvers)
val duration = if (startedAtMillis > 0L) {
(nowMillis - startedAtMillis).coerceAtLeast(0L)
} else {
durationMillis
}
return copy(
status = WhiteDnsScanStatus.Completed,
completedResolvers = completedCount,
updatedAtMillis = nowMillis,
durationMillis = duration,
message = "No remaining resolvers to resume; Scanner result is up to date",
workerFailures = emptyList(),
)
}

val fraction: Float
get() = if (totalResolvers > 0) {
completedResolvers.coerceIn(0, totalResolvers).toFloat() / totalResolvers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ sealed class WhiteDnsProxyEvent {
data class Log(val sessionId: String, val message: String) : WhiteDnsProxyEvent()
data class Ready(val sessionId: String, val message: String) : WhiteDnsProxyEvent()
data class Failed(val sessionId: String, val message: String) : WhiteDnsProxyEvent()
data class Stopped(val sessionId: String, val message: String) : WhiteDnsProxyEvent()
}

object WhiteDnsProxyEvents {
Expand All @@ -31,6 +32,10 @@ object WhiteDnsProxyEvents {
emit(WhiteDnsProxyEvent.Failed(sessionId, message))
}

fun stopped(sessionId: String, message: String) {
emit(WhiteDnsProxyEvent.Stopped(sessionId, message))
}

private fun emit(event: WhiteDnsProxyEvent) {
listeners.forEach { listener ->
runCatching { listener(event) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,7 @@ class WhiteDnsProxyService : Service() {
stopProxyRuntime()
runtimeReady = false
lastTrafficNotificationUpdateMillis = 0L
WhiteDnsRuntimeStateStore.markStopped(
context = applicationContext,
mode = WhiteDnsRuntimeStateStore.ModeProxy,
sessionId = currentSessionId,
message = "Proxy service stopped",
)
reportStopped("Proxy service stopped")
exitForeground()
stopSelf()
START_NOT_STICKY
Expand All @@ -103,12 +98,7 @@ class WhiteDnsProxyService : Service() {
stopProxyRuntime()
runtimeReady = false
lastTrafficNotificationUpdateMillis = 0L
WhiteDnsRuntimeStateStore.markStopped(
context = applicationContext,
mode = WhiteDnsRuntimeStateStore.ModeProxy,
sessionId = currentSessionId,
message = "Proxy service stopped",
)
reportStopped("Proxy service stopped")
exitForeground()
serviceScope.cancel()
super.onDestroy()
Expand Down Expand Up @@ -479,6 +469,17 @@ class WhiteDnsProxyService : Service() {
sendProxyEvent(BroadcastTypeFailed, message)
}

private fun reportStopped(message: String) {
WhiteDnsRuntimeStateStore.markStopped(
context = applicationContext,
mode = WhiteDnsRuntimeStateStore.ModeProxy,
sessionId = currentSessionId,
message = message,
)
WhiteDnsProxyEvents.stopped(currentSessionId, message)
sendProxyEvent(BroadcastTypeStopped, message)
}

private fun reportReady(message: String) {
Log.i(Tag, message)
WhiteDnsProxyEvents.ready(currentSessionId, message)
Expand All @@ -504,6 +505,7 @@ class WhiteDnsProxyService : Service() {
const val BroadcastTypeLog = "log"
const val BroadcastTypeReady = "ready"
const val BroadcastTypeFailed = "failed"
const val BroadcastTypeStopped = "stopped"
private const val ActionStart = "shop.whitedns.client.proxy.START"
private const val ActionStop = "shop.whitedns.client.proxy.STOP"
private const val ExtraSessionId = "shop.whitedns.client.proxy.extra.SESSION_ID"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.json.JSONArray
Expand All @@ -42,6 +43,14 @@ import shop.whitedns.client.runtime.parseStormDnsConnectionProgressLine
import shop.whitedns.client.storm.StormDnsBinaryInstaller
import shop.whitedns.client.storm.StormDnsConfigRenderer

internal fun shouldPublishScanHeartbeat(
nowMillis: Long,
lastHeartbeatMillis: Long,
heartbeatIntervalMillis: Long,
): Boolean {
return heartbeatIntervalMillis > 0L && nowMillis - lastHeartbeatMillis >= heartbeatIntervalMillis
}

class WhiteDnsScanService : Service() {

private var foregroundStarted = false
Expand Down Expand Up @@ -164,6 +173,7 @@ class WhiteDnsScanService : Service() {
val totalResolverCount = maxOf(requestedTotalResolvers, pendingResolverCount + initialProcessedCount)
val startedAtMillis = System.currentTimeMillis()
var lastAggregatePublishMillis = 0L
var lastHeartbeatPublishMillis = startedAtMillis

fun aggregateState(status: String, message: String): WhiteDnsScanState {
val completed = (initialProcessedCount + workerStats.sumOf { it.completed })
Expand Down Expand Up @@ -261,7 +271,22 @@ class WhiteDnsScanService : Service() {
}
}

val results = jobs.awaitAll()
val heartbeatJob = launch(Dispatchers.IO) {
while (isActive && !stopRequested) {
delay(ScanHeartbeatIntervalMillis)
val now = System.currentTimeMillis()
if (shouldPublishScanHeartbeat(now, lastHeartbeatPublishMillis, ScanHeartbeatIntervalMillis)) {
lastHeartbeatPublishMillis = now
publishAggregate(WhiteDnsScanStatus.Running, "Scanning", forcePublish = true)
}
}
}

val results = try {
jobs.awaitAll()
} finally {
heartbeatJob.cancelAndJoin()
}
val successfulWorkers = results.count { it }
val finalStatus = if (successfulWorkers == workerInputs.size && workerFailures.isEmpty()) {
WhiteDnsScanStatus.Completed
Expand Down Expand Up @@ -748,6 +773,7 @@ class WhiteDnsScanService : Service() {
private const val MaxWorkerFailureOutputLines = 6
private const val MaxWorkerFailureOutputChars = 180
private const val ScanUiPublishMinIntervalMillis = 750L
private const val ScanHeartbeatIntervalMillis = 10_000L
private val AnsiEscapeRegex = Regex("${27.toChar()}\\[[;?0-9]*[ -/]*[@-~]")

fun start(
Expand Down
90 changes: 58 additions & 32 deletions app/src/main/java/shop/whitedns/client/ui/WhiteDnsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
Expand Down Expand Up @@ -1429,24 +1430,33 @@ private fun SettingProfileGuideButton(
onClick: () -> Unit,
) {
val haptic = rememberHapticFeedback()
val buttonDescription = WhiteDnsL10n.cdSettingGuide
Box(
modifier = Modifier
.size(36.dp)
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clip(CircleShape)
.background(WhiteDnsPalette.AccentSurface)
.border(1.5.dp, WhiteDnsPalette.Accent.copy(alpha = 0.26f), CircleShape)
.clickable {
.clickable(role = Role.Button) {
haptic.performLight()
onClick()
},
}
.semantics { this.contentDescription = buttonDescription },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.HelpOutline,
contentDescription = WhiteDnsL10n.cdSettingGuide,
tint = WhiteDnsPalette.AccentText,
modifier = Modifier.size(20.dp),
)
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(WhiteDnsPalette.AccentSurface)
.border(1.5.dp, WhiteDnsPalette.Accent.copy(alpha = 0.26f), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.HelpOutline,
contentDescription = null,
tint = WhiteDnsPalette.AccentText,
modifier = Modifier.size(20.dp),
)
}
}
}

Expand Down Expand Up @@ -1886,6 +1896,7 @@ private fun ScanInfoNotice(
onDismiss: () -> Unit,
) {
val haptic = rememberHapticFeedback()
val dismissDescription = WhiteDnsL10n.cdDismissScannerInfo

Row(
modifier = Modifier
Expand Down Expand Up @@ -1934,17 +1945,18 @@ private fun ScanInfoNotice(
}
Box(
modifier = Modifier
.size(28.dp)
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clip(CircleShape)
.clickable {
.clickable(role = Role.Button) {
haptic.performLight()
onDismiss()
},
}
.semantics { this.contentDescription = dismissDescription },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = WhiteDnsL10n.cdDismissScannerInfo,
contentDescription = null,
tint = WhiteDnsPalette.AccentText,
modifier = Modifier.size(16.dp),
)
Expand Down Expand Up @@ -2638,7 +2650,7 @@ private fun HomeSelectorCard(
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = stringResource(R.string.cd_dropdown_advanced_settings),
contentDescription = null,
tint = if (enabled) WhiteDnsPalette.Muted else WhiteDnsPalette.Disabled,
modifier = Modifier.size(18.dp),
)
Expand Down Expand Up @@ -5845,6 +5857,7 @@ private fun ProfileIconButton(
modifier: Modifier = Modifier,
) {
val haptic = rememberHapticFeedback()
val buttonDescription = contentDescription
val background = when {
!enabled -> WhiteDnsPalette.SurfaceAlt
emphasized -> WhiteDnsPalette.Accent
Expand All @@ -5863,22 +5876,30 @@ private fun ProfileIconButton(

Box(
modifier = modifier
.size(28.dp)
.clip(RoundedCornerShape(8.dp))
.background(background)
.border(1.5.dp, border, RoundedCornerShape(8.dp))
.clickable(enabled = enabled) {
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clip(RoundedCornerShape(12.dp))
.clickable(enabled = enabled, role = Role.Button) {
haptic.performMedium()
onClick()
},
}
.semantics { this.contentDescription = buttonDescription },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconColor,
modifier = Modifier.size(16.dp),
)
Box(
modifier = Modifier
.size(28.dp)
.clip(RoundedCornerShape(8.dp))
.background(background)
.border(1.5.dp, border, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconColor,
modifier = Modifier.size(16.dp),
)
}
}
}

Expand Down Expand Up @@ -5914,10 +5935,11 @@ private fun CompactActionButton(

Box(
modifier = modifier
.heightIn(min = 44.dp)
.clip(RoundedCornerShape(9.dp))
.background(background)
.border(1.5.dp, border, RoundedCornerShape(9.dp))
.clickable(enabled = enabled) {
.clickable(enabled = enabled, role = Role.Button) {
haptic.performMedium()
onClick()
}
Expand All @@ -5927,11 +5949,14 @@ private fun CompactActionButton(
Text(
text = label,
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 8.sp,
fontSize = 10.sp,
lineHeight = 12.sp,
color = textColor,
fontWeight = FontWeight.Medium,
letterSpacing = 0.9.sp,
letterSpacing = 0.sp,
),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
Expand Down Expand Up @@ -9438,6 +9463,7 @@ private fun WhiteDnsTextField(
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.semantics { contentDescription = label }
.onFocusChanged {
focused = it.isFocused
onFocusChange(it.isFocused)
Expand Down Expand Up @@ -9597,7 +9623,7 @@ private fun <T> WhiteDnsDropdownField(
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = stringResource(R.string.cd_dropdown_advanced_settings),
contentDescription = null,
tint = when {
!enabled -> WhiteDnsPalette.Disabled
expanded -> WhiteDnsPalette.Accent
Expand Down
Loading