Skip to content

Commit 4dd5abc

Browse files
committed
Release v1.1.0
1 parent a20f07b commit 4dd5abc

18 files changed

Lines changed: 941 additions & 105 deletions

File tree

app/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ plugins {
55

66
val whiteDnsVersionCode = providers.gradleProperty("WHITE_DNS_VERSION_CODE")
77
.map { it.toInt() }
8-
.orElse(6)
8+
.orElse(7)
99
val whiteDnsVersionName = providers.gradleProperty("WHITE_DNS_VERSION_NAME")
10-
.orElse("1.0.0")
10+
.orElse("1.1.0")
1111

1212
android {
1313
namespace = "shop.whitedns.client"
@@ -83,6 +83,7 @@ dependencies {
8383
implementation("androidx.compose.foundation:foundation")
8484
implementation("androidx.compose.material:material-icons-extended")
8585
implementation("androidx.compose.material3:material3")
86+
implementation("com.google.zxing:core:3.5.3")
8687

8788
testImplementation("junit:junit:4.13.2")
8889
testImplementation("org.json:json:20240303")

app/src/main/AndroidManifest.xml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@
2626

2727
<activity
2828
android:name=".MainActivity"
29-
android:exported="true">
29+
android:exported="true"
30+
android:launchMode="singleTop">
3031
<intent-filter>
3132
<action android:name="android.intent.action.MAIN" />
3233

3334
<category android:name="android.intent.category.LAUNCHER" />
3435
</intent-filter>
36+
<intent-filter>
37+
<action android:name="android.intent.action.VIEW" />
38+
39+
<category android:name="android.intent.category.DEFAULT" />
40+
<category android:name="android.intent.category.BROWSABLE" />
41+
42+
<data android:scheme="stormdns" />
43+
</intent-filter>
3544
</activity>
3645

3746
<service
@@ -52,6 +61,20 @@
5261
android:foregroundServiceType="dataSync"
5362
android:process=":proxy" />
5463

64+
<service
65+
android:name=".quicksettings.WhiteDnsTileService"
66+
android:exported="true"
67+
android:icon="@drawable/ic_notification"
68+
android:label="@string/app_name"
69+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
70+
<intent-filter>
71+
<action android:name="android.service.quicksettings.action.QS_TILE" />
72+
</intent-filter>
73+
<meta-data
74+
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
75+
android:value="true" />
76+
</service>
77+
5578
<provider
5679
android:name="androidx.core.content.FileProvider"
5780
android:authorities="${applicationId}.fileprovider"

app/src/main/java/shop/whitedns/client/MainActivity.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ class MainActivity : ComponentActivity() {
127127
)
128128
}
129129
}
130+
handleProfileLinkIntent(intent)
131+
}
132+
133+
override fun onNewIntent(intent: Intent) {
134+
super.onNewIntent(intent)
135+
setIntent(intent)
136+
handleProfileLinkIntent(intent)
130137
}
131138

132139
private fun openNotificationSettings() {
@@ -162,4 +169,21 @@ class MainActivity : ComponentActivity() {
162169
viewModel.refreshBatteryOptimizationStatusWithRetry()
163170
}
164171
}
172+
173+
private fun handleProfileLinkIntent(intent: Intent?) {
174+
if (intent?.action != Intent.ACTION_VIEW || intent.data?.scheme != StormDnsScheme) {
175+
return
176+
}
177+
if (intent.getBooleanExtra(ExtraProfileImportHandled, false)) {
178+
return
179+
}
180+
val link = intent.dataString?.takeIf(String::isNotBlank) ?: return
181+
intent.putExtra(ExtraProfileImportHandled, true)
182+
viewModel.importProfileLink(link)
183+
}
184+
185+
private companion object {
186+
const val StormDnsScheme = "stormdns"
187+
const val ExtraProfileImportHandled = "shop.whitedns.client.extra.PROFILE_IMPORT_HANDLED"
188+
}
165189
}

app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ data class WhiteDnsSettings(
105105
val downloadCompression: Int = 2,
106106
val baseEncodeData: Boolean = false,
107107
val minUploadMtu: String = "40",
108-
val minDownloadMtu: String = "100",
109-
val maxUploadMtu: String = "64",
110-
val maxDownloadMtu: String = "140",
108+
val minDownloadMtu: String = "300",
109+
val maxUploadMtu: String = "140",
110+
val maxDownloadMtu: String = "3000",
111111
val mtuTestRetriesResolvers: String = "3",
112112
val mtuTestTimeoutResolvers: String = "2.0",
113113
val mtuTestParallelismResolvers: String = "100",
@@ -124,6 +124,8 @@ data class WhiteDnsSettings(
124124
val streamQueueInitialCapacity: String = "128",
125125
val orphanQueueInitialCapacity: String = "32",
126126
val dnsResponseFragmentStoreCapacity: String = "256",
127+
val maxActiveStreams: String = "2048",
128+
val localHandshakeTimeoutSeconds: String = "5.0",
127129
val socksUdpAssociateReadTimeoutSeconds: String = "30.0",
128130
val clientTerminalStreamRetentionSeconds: String = "45.0",
129131
val clientCancelledSetupRetentionSeconds: String = "120.0",
@@ -182,6 +184,8 @@ data class ResolvedWhiteDnsSettings(
182184
val streamQueueInitialCapacity: Int,
183185
val orphanQueueInitialCapacity: Int,
184186
val dnsResponseFragmentStoreCapacity: Int,
187+
val maxActiveStreams: Int,
188+
val localHandshakeTimeoutSeconds: Double,
185189
val socksUdpAssociateReadTimeoutSeconds: Double,
186190
val clientTerminalStreamRetentionSeconds: Double,
187191
val clientCancelledSetupRetentionSeconds: Double,
@@ -247,6 +251,19 @@ data class ConnectionProgressState(
247251
}
248252
}
249253

254+
object ConnectionVerificationStatus {
255+
const val Idle = "idle"
256+
const val Checking = "checking"
257+
const val Verified = "verified"
258+
const val Failed = "failed"
259+
}
260+
261+
data class ConnectionVerificationState(
262+
val status: String = ConnectionVerificationStatus.Idle,
263+
val message: String = "",
264+
val checkedAtMillis: Long = 0,
265+
)
266+
250267
data class WhiteDnsUiState(
251268
val connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED,
252269
val settings: WhiteDnsSettings = WhiteDnsSettings(),
@@ -259,6 +276,7 @@ data class WhiteDnsUiState(
259276
val connectionStats: ConnectionStats = ConnectionStats(),
260277
val resolverRuntimeState: ResolverRuntimeState = ResolverRuntimeState(),
261278
val connectionProgress: ConnectionProgressState = ConnectionProgressState(),
279+
val connectionVerification: ConnectionVerificationState = ConnectionVerificationState(),
262280
)
263281

264282
object WhiteDnsRuntimeProxy {
@@ -694,6 +712,8 @@ fun WhiteDnsSettings.resetAdvancedSettings(): WhiteDnsSettings {
694712
streamQueueInitialCapacity = defaults.streamQueueInitialCapacity,
695713
orphanQueueInitialCapacity = defaults.orphanQueueInitialCapacity,
696714
dnsResponseFragmentStoreCapacity = defaults.dnsResponseFragmentStoreCapacity,
715+
maxActiveStreams = defaults.maxActiveStreams,
716+
localHandshakeTimeoutSeconds = defaults.localHandshakeTimeoutSeconds,
697717
socksUdpAssociateReadTimeoutSeconds = defaults.socksUdpAssociateReadTimeoutSeconds,
698718
clientTerminalStreamRetentionSeconds = defaults.clientTerminalStreamRetentionSeconds,
699719
clientCancelledSetupRetentionSeconds = defaults.clientCancelledSetupRetentionSeconds,
@@ -740,13 +760,29 @@ private fun normalizeResolverText(raw: String): String {
740760
}
741761

742762
private fun resolverTextTokens(raw: String): Sequence<String> {
743-
return raw
763+
val lines = raw
744764
.replace("\r\n", "\n")
745765
.replace('\r', '\n')
746766
.lineSequence()
747767
.map(String::trim)
748768
.filter { it.isNotEmpty() && !it.startsWith("#") }
749-
.flatMap { line -> line.split(',', ';').asSequence() }
769+
.toList()
770+
771+
val separators = if (detectResolverTextEntryType(lines) == ResolverTextEntryType.CommaSeparated) {
772+
charArrayOf(',', ';')
773+
} else {
774+
charArrayOf()
775+
}
776+
777+
return lines
778+
.asSequence()
779+
.flatMap { line ->
780+
if (separators.isEmpty()) {
781+
sequenceOf(line)
782+
} else {
783+
line.split(*separators).asSequence()
784+
}
785+
}
750786
.map(String::trim)
751787
.filter { it.isNotEmpty() && !it.startsWith("#") }
752788
}
@@ -757,6 +793,9 @@ private fun normalizeResolverEntry(entry: String): String? {
757793
val hostPort = splitResolverHostPort(entry) ?: return null
758794
val target = normalizeResolverTarget(hostPort.first) ?: return null
759795
val port = hostPort.second.toIntOrNull()?.takeIf { it in 1..65535 } ?: return null
796+
if (port == DefaultResolverPort) {
797+
return target
798+
}
760799
return if (resolverTargetNeedsBrackets(target)) {
761800
"[$target]:$port"
762801
} else {
@@ -853,6 +892,21 @@ private fun resolverTargetNeedsBrackets(target: String): Boolean {
853892
return target.substringBefore('/').contains(':')
854893
}
855894

895+
private fun detectResolverTextEntryType(lines: List<String>): ResolverTextEntryType {
896+
return if (lines.any { it.contains(',') || it.contains(';') }) {
897+
ResolverTextEntryType.CommaSeparated
898+
} else {
899+
ResolverTextEntryType.LineSeparated
900+
}
901+
}
902+
903+
private enum class ResolverTextEntryType {
904+
LineSeparated,
905+
CommaSeparated,
906+
}
907+
908+
private const val DefaultResolverPort = 53
909+
856910
private val ResolverIpv6Chars = Regex("^[0-9A-Fa-f:.]+$")
857911

858912
private fun normalizeSplitTunnelMode(raw: String): String {
@@ -896,12 +950,15 @@ fun WhiteDnsSettings.resolve(): ResolvedWhiteDnsSettings {
896950
return value.coerceIn(minValue, maxValue)
897951
}
898952

899-
val resolvers = resolverText
900-
.lineSequence()
901-
.map(String::trim)
902-
.filter(String::isNotEmpty)
903-
.distinct()
904-
.toList()
953+
fun boundedPositiveDouble(raw: String, defaultValue: Double, minValue: Double, maxValue: Double): Double {
954+
val value = raw.trim().toDoubleOrNull() ?: return defaultValue
955+
if (value <= 0.0) {
956+
return defaultValue
957+
}
958+
return value.coerceIn(minValue, maxValue)
959+
}
960+
961+
val resolvers = validateResolverText(resolverText).normalizedResolvers
905962

906963
val resolvedRxTxWorkers = boundedInt(rxTxWorkers, defaultValue = 4, minValue = 1, maxValue = 64)
907964
val resolvedTunnelProcessWorkers = boundedInt(
@@ -916,6 +973,12 @@ fun WhiteDnsSettings.resolve(): ResolvedWhiteDnsSettings {
916973
minValue = 0.1,
917974
maxValue = 60.0,
918975
)
976+
val resolvedMinUploadMtu = boundedInt(minUploadMtu, defaultValue = 40, minValue = 1, maxValue = 65535)
977+
val resolvedMinDownloadMtu = boundedInt(minDownloadMtu, defaultValue = 300, minValue = 1, maxValue = 65535)
978+
val resolvedMaxUploadMtu = boundedInt(maxUploadMtu, defaultValue = 140, minValue = 1, maxValue = 65535)
979+
.coerceAtLeast(resolvedMinUploadMtu)
980+
val resolvedMaxDownloadMtu = boundedInt(maxDownloadMtu, defaultValue = 3000, minValue = 1, maxValue = 65535)
981+
.coerceAtLeast(resolvedMinDownloadMtu)
919982

920983
return ResolvedWhiteDnsSettings(
921984
connectionMode = when (connectionMode) {
@@ -937,10 +1000,10 @@ fun WhiteDnsSettings.resolve(): ResolvedWhiteDnsSettings {
9371000
uploadCompression = uploadCompression.coerceIn(0, 3),
9381001
downloadCompression = downloadCompression.coerceIn(0, 3),
9391002
baseEncodeData = baseEncodeData,
940-
minUploadMtu = boundedInt(minUploadMtu, defaultValue = 40, minValue = 1, maxValue = 65535),
941-
minDownloadMtu = boundedInt(minDownloadMtu, defaultValue = 100, minValue = 1, maxValue = 65535),
942-
maxUploadMtu = boundedInt(maxUploadMtu, defaultValue = 64, minValue = 1, maxValue = 65535),
943-
maxDownloadMtu = boundedInt(maxDownloadMtu, defaultValue = 140, minValue = 1, maxValue = 65535),
1003+
minUploadMtu = resolvedMinUploadMtu,
1004+
minDownloadMtu = resolvedMinDownloadMtu,
1005+
maxUploadMtu = resolvedMaxUploadMtu,
1006+
maxDownloadMtu = resolvedMaxDownloadMtu,
9441007
mtuTestRetriesResolvers = boundedInt(mtuTestRetriesResolvers, defaultValue = 3, minValue = 1, maxValue = 100),
9451008
mtuTestTimeoutResolvers = positiveDouble(mtuTestTimeoutResolvers, defaultValue = 2.0),
9461009
mtuTestParallelismResolvers = boundedInt(mtuTestParallelismResolvers, defaultValue = 100, minValue = 1, maxValue = 1024),
@@ -987,6 +1050,18 @@ fun WhiteDnsSettings.resolve(): ResolvedWhiteDnsSettings {
9871050
minValue = 16,
9881051
maxValue = 16384,
9891052
),
1053+
maxActiveStreams = boundedInt(
1054+
maxActiveStreams,
1055+
defaultValue = 2048,
1056+
minValue = 1,
1057+
maxValue = 65535,
1058+
),
1059+
localHandshakeTimeoutSeconds = boundedPositiveDouble(
1060+
localHandshakeTimeoutSeconds,
1061+
defaultValue = 5.0,
1062+
minValue = 0.5,
1063+
maxValue = 60.0,
1064+
),
9901065
socksUdpAssociateReadTimeoutSeconds = boundedDouble(
9911066
socksUdpAssociateReadTimeoutSeconds,
9921067
defaultValue = 30.0,

app/src/main/java/shop/whitedns/client/model/WhiteDnsSettingsStore.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ class WhiteDnsSettingsStore(
115115
KeyDnsResponseFragmentStoreCapacity,
116116
defaults.dnsResponseFragmentStoreCapacity,
117117
) ?: defaults.dnsResponseFragmentStoreCapacity,
118+
maxActiveStreams = preferences.getString(KeyMaxActiveStreams, defaults.maxActiveStreams)
119+
?: defaults.maxActiveStreams,
120+
localHandshakeTimeoutSeconds = preferences.getString(
121+
KeyLocalHandshakeTimeoutSeconds,
122+
defaults.localHandshakeTimeoutSeconds,
123+
) ?: defaults.localHandshakeTimeoutSeconds,
118124
socksUdpAssociateReadTimeoutSeconds = preferences.getString(
119125
KeySocksUdpAssociateReadTimeoutSeconds,
120126
defaults.socksUdpAssociateReadTimeoutSeconds,
@@ -219,6 +225,8 @@ class WhiteDnsSettingsStore(
219225
.putString(KeyStreamQueueInitialCapacity, normalizedSettings.streamQueueInitialCapacity)
220226
.putString(KeyOrphanQueueInitialCapacity, normalizedSettings.orphanQueueInitialCapacity)
221227
.putString(KeyDnsResponseFragmentStoreCapacity, normalizedSettings.dnsResponseFragmentStoreCapacity)
228+
.putString(KeyMaxActiveStreams, normalizedSettings.maxActiveStreams)
229+
.putString(KeyLocalHandshakeTimeoutSeconds, normalizedSettings.localHandshakeTimeoutSeconds)
222230
.putString(KeySocksUdpAssociateReadTimeoutSeconds, normalizedSettings.socksUdpAssociateReadTimeoutSeconds)
223231
.putString(KeyClientTerminalStreamRetentionSeconds, normalizedSettings.clientTerminalStreamRetentionSeconds)
224232
.putString(KeyClientCancelledSetupRetentionSeconds, normalizedSettings.clientCancelledSetupRetentionSeconds)
@@ -363,12 +371,20 @@ class WhiteDnsSettingsStore(
363371
replaceOldDefault(KeyDnsResponseFragmentStoreCapacity, oldValue = "1024", newValue = "256")
364372
replaceOldDefault(KeyClientCancelledSetupRetentionSeconds, oldValue = "90.0", newValue = "120.0")
365373
replaceOldDefault(KeySessionInitRetryMaxSeconds, oldValue = "30.0", newValue = "60.0")
374+
replaceOldDefault(KeyMinUploadMtu, oldValue = "100", newValue = "40")
375+
replaceOldDefault(KeyMinDownloadMtu, oldValue = "100", newValue = "300")
376+
replaceOldDefault(KeyMinDownloadMtu, oldValue = "1000", newValue = "300")
377+
replaceOldDefault(KeyMaxUploadMtu, oldValue = "64", newValue = "140")
378+
replaceOldDefault(KeyMaxUploadMtu, oldValue = "200", newValue = "140")
379+
replaceOldDefault(KeyMaxDownloadMtu, oldValue = "140", newValue = "3000")
380+
replaceOldDefault(KeyMaxDownloadMtu, oldValue = "4000", newValue = "3000")
381+
replaceOldDefault(KeyStartupMode, oldValue = "logs", newValue = "resolvers")
366382
editor.putInt(KeyAdvancedDefaultsRevision, AdvancedDefaultsRevision).apply()
367383
}
368384

369385
private companion object {
370386
const val PreferencesName = "white_dns_settings"
371-
const val AdvancedDefaultsRevision = 1
387+
const val AdvancedDefaultsRevision = 4
372388
const val LegacyDefaultResolverText = "1.1.1.1\n8.8.8.8\n9.9.9.9"
373389
const val KeyAdvancedDefaultsRevision = "advanced_defaults_revision"
374390
const val KeySelectedConnectionProfileId = "selected_connection_profile_id"
@@ -415,6 +431,8 @@ class WhiteDnsSettingsStore(
415431
const val KeyStreamQueueInitialCapacity = "stream_queue_initial_capacity"
416432
const val KeyOrphanQueueInitialCapacity = "orphan_queue_initial_capacity"
417433
const val KeyDnsResponseFragmentStoreCapacity = "dns_response_fragment_store_capacity"
434+
const val KeyMaxActiveStreams = "max_active_streams"
435+
const val KeyLocalHandshakeTimeoutSeconds = "local_handshake_timeout_seconds"
418436
const val KeySocksUdpAssociateReadTimeoutSeconds = "socks_udp_associate_read_timeout_seconds"
419437
const val KeyClientTerminalStreamRetentionSeconds = "client_terminal_stream_retention_seconds"
420438
const val KeyClientCancelledSetupRetentionSeconds = "client_cancelled_setup_retention_seconds"

app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,19 @@ class WhiteDnsProxyService : Service() {
417417
openAppIntent,
418418
pendingIntentFlags,
419419
)
420+
val stopPendingIntent = PendingIntent.getService(
421+
this,
422+
1,
423+
Intent(this, WhiteDnsProxyService::class.java).setAction(ActionStop),
424+
pendingIntentFlags,
425+
)
420426

421427
return NotificationCompat.Builder(this, NotificationChannelId)
422428
.setSmallIcon(R.drawable.ic_notification)
423429
.setContentTitle("WhiteDNS Proxy")
424430
.setContentText(statusText)
425431
.setContentIntent(openAppPendingIntent)
432+
.addAction(R.drawable.ic_notification, "Disconnect", stopPendingIntent)
426433
.setCategory(NotificationCompat.CATEGORY_SERVICE)
427434
.setPriority(NotificationCompat.PRIORITY_LOW)
428435
.setOngoing(true)

0 commit comments

Comments
 (0)