mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a27f48a2 | |||
| 1f978cdf96 | |||
| 4f816fa175 | |||
| ee4ac4e968 | |||
| ff53454966 | |||
| 22c17ef66b | |||
| 7a60b90d2b | |||
| 5fd3f89a59 | |||
| 9510f43252 | |||
| 064aa6aa74 | |||
| 0c09add0e4 | |||
| fd0fd33f71 | |||
| aaeb251bbf | |||
| e563608e49 | |||
| 584f0386b6 | |||
| cf49c34bff | |||
| a0f89d40f5 |
@@ -1,3 +1,4 @@
|
|||||||
ko_fi: zaneschepke
|
ko_fi: zaneschepke
|
||||||
liberapay: zaneschepke
|
liberapay: zaneschepke
|
||||||
github: zaneschepke
|
github: zaneschepke
|
||||||
|
custom: ["https://wgtunnel.com/donate/"]
|
||||||
|
|||||||
@@ -32,14 +32,6 @@ on:
|
|||||||
description: "Tag name for release"
|
description: "Tag name for release"
|
||||||
required: false
|
required: false
|
||||||
default: 1.1.1
|
default: 1.1.1
|
||||||
flavor:
|
|
||||||
type: choice
|
|
||||||
description: "Product flavor"
|
|
||||||
required: true
|
|
||||||
default: standalone
|
|
||||||
options:
|
|
||||||
- fdroid
|
|
||||||
- standalone
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
flavor:
|
flavor:
|
||||||
@@ -51,7 +43,11 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-fdroid:
|
build-fdroid:
|
||||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
inputs.release_type != 'none'
|
||||||
|
}}
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
@@ -59,16 +55,26 @@ jobs:
|
|||||||
flavor: fdroid
|
flavor: fdroid
|
||||||
|
|
||||||
build-standalone:
|
build-standalone:
|
||||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
inputs.release_type != 'none'
|
||||||
|
}}
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
|
build_type: ${{ github.event_name == 'push' && 'release' || inputs.release_type }}
|
||||||
flavor: standalone
|
flavor: standalone
|
||||||
|
|
||||||
publish:
|
publish-github:
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
inputs.release_type != 'none'
|
||||||
|
}}
|
||||||
needs:
|
needs:
|
||||||
- build-standalone
|
- build-fdroid
|
||||||
|
- build-standalone
|
||||||
name: publish-github
|
name: publish-github
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -118,7 +124,7 @@ jobs:
|
|||||||
- name: Set version release notes
|
- name: Set version release notes
|
||||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
||||||
run: |
|
run: |
|
||||||
VERSION_CODE=$(grep "const val VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}')
|
VERSION_CODE=$(sed -nE 's/.*const val VERSION_CODE[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' buildSrc/src/main/kotlin/Constants.kt)
|
||||||
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
|
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_CODE}.txt || echo "No changelog found for ${VERSION_CODE}")"
|
||||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||||
@@ -166,9 +172,13 @@ jobs:
|
|||||||
|
|
||||||
publish-fdroid-public:
|
publish-fdroid-public:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
inputs.release_type != 'none'
|
||||||
|
}}
|
||||||
needs:
|
needs:
|
||||||
- build-fdroid
|
- publish-github
|
||||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Dispatch update for fdroid repo
|
- name: Dispatch update for fdroid repo
|
||||||
uses: peter-evans/repository-dispatch@v4
|
uses: peter-evans/repository-dispatch@v4
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ android {
|
|||||||
licensee {
|
licensee {
|
||||||
allowedLicenses().forEach { allow(it) }
|
allowedLicenses().forEach { allow(it) }
|
||||||
allowedLicenseUrls().forEach { allowUrl(it) }
|
allowedLicenseUrls().forEach { allowUrl(it) }
|
||||||
|
// foss, but missing license
|
||||||
|
ignoreDependencies("com.github.T8RIN.QuickieExtended")
|
||||||
}
|
}
|
||||||
|
|
||||||
android.applicationVariants.all {
|
android.applicationVariants.all {
|
||||||
|
|||||||
@@ -59,10 +59,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.App.Start"
|
android:theme="@style/Theme.App.Start"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
|
||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
tools:replace="screenOrientation" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -198,7 +195,10 @@
|
|||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
append(context.getString(R.string.donation_prompt_suffix))
|
append(context.getString(R.string.donation_prompt_suffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(uiState.shouldShowDonationSnackbar) {
|
LaunchedEffect(Unit) {
|
||||||
if (
|
if (
|
||||||
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
|
uiState.shouldShowDonationSnackbar && !uiState.settings.alreadyDonated
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
@@ -34,6 +34,7 @@ class RestartReceiver : BroadcastReceiver() {
|
|||||||
tunnelManager.handleReboot()
|
tunnelManager.handleReboot()
|
||||||
}
|
}
|
||||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||||
|
Timber.i("Restoring state on package upgrade")
|
||||||
tunnelManager.handleRestore()
|
tunnelManager.handleRestore()
|
||||||
logReader.deleteAndClearLogs()
|
logReader.deleteAndClearLogs()
|
||||||
appStateRepository.setShouldShowDonationSnackbar(true)
|
appStateRepository.setShouldShowDonationSnackbar(true)
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ constructor(
|
|||||||
description =
|
description =
|
||||||
StringValue.StringResource(
|
StringValue.StringResource(
|
||||||
R.string.tunnel_error_template,
|
R.string.tunnel_error_template,
|
||||||
error.toStringValue(),
|
error.stringRes,
|
||||||
),
|
),
|
||||||
groupKey = NotificationManager.VPN_GROUP_KEY,
|
groupKey = NotificationManager.VPN_GROUP_KEY,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-1
@@ -63,6 +63,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
private var autoTunnelJob: Job? = null
|
private var autoTunnelJob: Job? = null
|
||||||
private var permissionsJob: Job? = null
|
private var permissionsJob: Job? = null
|
||||||
|
private var autoTunnelFailoverJob: Job? = null
|
||||||
|
|
||||||
class LocalBinder(service: AutoTunnelService) : Binder() {
|
class LocalBinder(service: AutoTunnelService) : Binder() {
|
||||||
private val serviceRef = WeakReference(service)
|
private val serviceRef = WeakReference(service)
|
||||||
@@ -212,6 +213,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
|
|
||||||
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
|
handleAutoTunnelEvent(autoTunnelStateFlow.value.determineAutoTunnelEvent(change))
|
||||||
|
|
||||||
|
// re-evaluate network state after a short duration to prevent missed state changes
|
||||||
reevaluationJob = launch {
|
reevaluationJob = launch {
|
||||||
val snapshotNetwork = autoTunnelStateFlow.value.networkState
|
val snapshotNetwork = autoTunnelStateFlow.value.networkState
|
||||||
delay(REEVALUATE_CHECK_DELAY)
|
delay(REEVALUATE_CHECK_DELAY)
|
||||||
@@ -234,7 +236,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
return combine(
|
return combine(
|
||||||
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
|
settingsRepository.flow.map { it.appMode }.distinctUntilChanged(),
|
||||||
autoTunnelRepository.get().flow,
|
autoTunnelRepository.get().flow,
|
||||||
tunnelsRepository.flow.map { tunnels ->
|
tunnelsRepository.userTunnelsFlow.map { tunnels ->
|
||||||
// isActive is ignored for equality checks so user can manually toggle off
|
// isActive is ignored for equality checks so user can manually toggle off
|
||||||
// tunnel with auto-tunnel
|
// tunnel with auto-tunnel
|
||||||
tunnels.map { it.copy(isActive = false) }
|
tunnels.map { it.copy(isActive = false) }
|
||||||
@@ -358,6 +360,7 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restart network flow on debounce changes
|
||||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
private val debouncedConnectivityStateFlow: Flow<ConnectivityState> by lazy {
|
||||||
autoTunnelRepository
|
autoTunnelRepository
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.exists(id: Int): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
|
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isUp(id: Int): Boolean {
|
||||||
return this.value.any { it.key.id == id && it.value.status == TunnelStatus.Up }
|
return this.value.any { it.key.id == id && it.value.status is TunnelStatus.Up }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
|
fun MutableStateFlow<Map<TunnelConfig, TunnelState>>.isStarting(id: Int): Boolean {
|
||||||
|
|||||||
@@ -322,11 +322,11 @@ constructor(
|
|||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
val settings = settingsRepository.getGeneralSettings()
|
val settings = settingsRepository.getGeneralSettings()
|
||||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||||
val tunnels = tunnelsRepository.getAll()
|
val tunnels = tunnelsRepository.userTunnelsFlow.firstOrNull()
|
||||||
if (autoTunnelSettings.isAutoTunnelEnabled)
|
if (autoTunnelSettings.isAutoTunnelEnabled)
|
||||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||||
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
|
if (settings.appMode == AppMode.LOCK_DOWN) handleLockDownModeInit()
|
||||||
if (tunnels.any { it.isActive }) {
|
if (tunnels?.any { it.isActive } == true) {
|
||||||
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
|
if (settings.appMode == AppMode.VPN && !serviceManager.hasVpnPermission())
|
||||||
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
return@withContext localErrorEvents.emit(null to NotAuthorized())
|
||||||
when (settings.appMode) {
|
when (settings.appMode) {
|
||||||
@@ -350,7 +350,7 @@ constructor(
|
|||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
val settings = settingsRepository.getGeneralSettings()
|
val settings = settingsRepository.getGeneralSettings()
|
||||||
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
val autoTunnelSettings = autoTunnelSettingsRepository.getAutoTunnelSettings()
|
||||||
val defaultTunnel = tunnelsRepository.getStartTunnel()
|
val defaultTunnel = tunnelsRepository.getDefaultTunnel()
|
||||||
if (autoTunnelSettings.startOnBoot)
|
if (autoTunnelSettings.startOnBoot)
|
||||||
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
return@withContext restoreAutoTunnel(autoTunnelSettings)
|
||||||
if (settings.isRestoreOnBootEnabled) {
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the tunnel to be fully active
|
// Wait for the tunnel to be fully active
|
||||||
tunStateFlow.filter { state -> state?.status == TunnelStatus.Up }.first()
|
tunStateFlow.filter { state -> state?.status is TunnelStatus.Up }.first()
|
||||||
|
|
||||||
// small delay to make sure tunnel is fully up before we actively monitor
|
// small delay to make sure tunnel is fully up before we actively monitor
|
||||||
delay(3_000L)
|
delay(3_000L)
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ constructor(
|
|||||||
} catch (e: BackendException) {
|
} catch (e: BackendException) {
|
||||||
throw e.toBackendCoreException()
|
throw e.toBackendCoreException()
|
||||||
// TODO this should be mapped to BackendException in the lib
|
// TODO this should be mapped to BackendException in the lib
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
throw VpnUnauthorized()
|
throw VpnUnauthorized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,23 +50,27 @@ interface TunnelConfigDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM tunnel_config
|
SELECT * FROM tunnel_config
|
||||||
ORDER BY
|
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||||
|
ORDER BY
|
||||||
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
|
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
|
||||||
position ASC
|
position ASC
|
||||||
LIMIT 1"""
|
LIMIT 1
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
suspend fun getDefaultTunnel(): TunnelConfig?
|
suspend fun getDefaultTunnel(): TunnelConfig?
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM tunnel_config
|
SELECT * FROM tunnel_config
|
||||||
ORDER BY
|
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
|
||||||
|
ORDER BY
|
||||||
CASE WHEN is_Active = 1 THEN 0
|
CASE WHEN is_Active = 1 THEN 0
|
||||||
WHEN is_primary_tunnel = 1 THEN 1
|
WHEN is_primary_tunnel = 1 THEN 1
|
||||||
ELSE 2 END,
|
ELSE 2 END,
|
||||||
position ASC
|
position ASC
|
||||||
LIMIT 1"""
|
LIMIT 1
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
suspend fun getStartTunnel(): TunnelConfig?
|
suspend fun getStartTunnel(): TunnelConfig?
|
||||||
|
|
||||||
|
|||||||
+2
@@ -55,6 +55,8 @@ class DataStoreAppStateRepository(
|
|||||||
pref[DataStoreManager.locationDisclosureShown] ?: false,
|
pref[DataStoreManager.locationDisclosureShown] ?: false,
|
||||||
isBatteryOptimizationDisableShown =
|
isBatteryOptimizationDisableShown =
|
||||||
pref[DataStoreManager.batteryDisableShown] ?: false,
|
pref[DataStoreManager.batteryDisableShown] ?: false,
|
||||||
|
shouldShowDonationSnackbar =
|
||||||
|
pref[DataStoreManager.shouldShowDonationSnackbar] ?: false,
|
||||||
)
|
)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
|
|||||||
|
|
||||||
sealed class TunnelStatus {
|
sealed class TunnelStatus {
|
||||||
|
|
||||||
data object Up : TunnelStatus()
|
data class Up(val startTime: Long) : TunnelStatus()
|
||||||
|
|
||||||
data object Down : TunnelStatus()
|
data object Down : TunnelStatus()
|
||||||
|
|
||||||
@@ -15,11 +15,11 @@ sealed class TunnelStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isUp(): Boolean {
|
fun isUp(): Boolean {
|
||||||
return this == Up
|
return this is Up
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUpOrStarting(): Boolean {
|
fun isUpOrStarting(): Boolean {
|
||||||
return this == Up || this == Starting
|
return this is Up || this == Starting
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownOrStopping(): Boolean {
|
fun isDownOrStopping(): Boolean {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ data class TunnelState(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun health(): Health {
|
fun health(): Health {
|
||||||
|
if (status !is TunnelStatus.Up) return Health.UNKNOWN
|
||||||
|
val uptime = uptime()
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (pingStates == null && logHealthState == null && statistics == null)
|
if (pingStates == null && logHealthState == null && statistics == null)
|
||||||
@@ -37,13 +39,21 @@ data class TunnelState(
|
|||||||
// Stats health if no logs or pings
|
// Stats health if no logs or pings
|
||||||
statistics?.let { stats ->
|
statistics?.let { stats ->
|
||||||
if (stats.isTunnelStale()) return Health.STALE
|
if (stats.isTunnelStale()) return Health.STALE
|
||||||
if (stats.rx() == 0L) return Health.UNKNOWN
|
val rx = stats.rx()
|
||||||
|
if (uptime >= STATS_HEALTH_SUCCESS_TIMEOUT_MS && rx == 0L) return Health.UNHEALTHY
|
||||||
|
if (rx == 0L) return Health.UNKNOWN
|
||||||
return Health.HEALTHY
|
return Health.HEALTHY
|
||||||
}
|
}
|
||||||
|
|
||||||
return Health.UNKNOWN
|
return Health.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun uptime(): Long {
|
||||||
|
val up = status as? TunnelStatus.Up ?: return 0L
|
||||||
|
if (up.startTime == 0L) return 0L
|
||||||
|
return System.currentTimeMillis() - up.startTime
|
||||||
|
}
|
||||||
|
|
||||||
enum class Health {
|
enum class Health {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
UNHEALTHY,
|
UNHEALTHY,
|
||||||
@@ -53,5 +63,6 @@ data class TunnelState(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOG_HEALTH_SUCCESS_TIMEOUT_MS = 2 * 60 * 1000L // 2 minutes
|
const val LOG_HEALTH_SUCCESS_TIMEOUT_MS = 2 * 60 * 1000L // 2 minutes
|
||||||
|
const val STATS_HEALTH_SUCCESS_TIMEOUT_MS = 15 * 1000L // 15 sec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -1,8 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
package com.zaneschepke.wireguardautotunnel.ui.common.snackbar
|
||||||
|
|
||||||
import android.R.attr.padding
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Favorite
|
import androidx.compose.material.icons.outlined.Favorite
|
||||||
|
|||||||
+24
-14
@@ -12,9 +12,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
|
||||||
@@ -29,7 +26,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components.UrlImpo
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
|
import io.github.g00fy2.quickie.QRResult
|
||||||
|
import io.github.g00fy2.quickie.ScanQRCode
|
||||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TunnelsScreen() {
|
fun TunnelsScreen() {
|
||||||
@@ -65,14 +65,26 @@ fun TunnelsScreen() {
|
|||||||
onData = { data -> viewModel.importFromUri(data) },
|
onData = { data -> viewModel.importFromUri(data) },
|
||||||
)
|
)
|
||||||
|
|
||||||
val scanLauncher =
|
val scanQrCodeLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ScanQRCode()) { result ->
|
||||||
contract = ScanContract(),
|
when (result) {
|
||||||
onResult = { result ->
|
is QRResult.QRError -> {
|
||||||
if (result != null && result.contents.isNotEmpty())
|
Timber.e(result.exception, "QR Code")
|
||||||
viewModel.importFromQr(result.contents)
|
}
|
||||||
},
|
QRResult.QRMissingPermission -> {
|
||||||
)
|
viewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.camera_permission_required)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is QRResult.QRSuccess -> {
|
||||||
|
result.content.rawValue?.let { viewModel.importFromQr(it) }
|
||||||
|
?: viewModel.showSnackMessage(
|
||||||
|
StringValue.StringResource(R.string.config_error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
QRResult.QRUserCanceled -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val requestPermissionLauncher =
|
val requestPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted
|
||||||
@@ -83,9 +95,7 @@ fun TunnelsScreen() {
|
|||||||
)
|
)
|
||||||
return@rememberLauncherForActivityResult
|
return@rememberLauncherForActivityResult
|
||||||
}
|
}
|
||||||
scanLauncher.launch(
|
scanQrCodeLauncher.launch(null)
|
||||||
ScanOptions().setDesiredBarcodeFormats(ScanOptions.QR_CODE).setBeepEnabled(false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDeleteModal) {
|
if (showDeleteModal) {
|
||||||
|
|||||||
+2
-2
@@ -17,11 +17,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GettingStartedLabel(onClick: (url: String) -> Unit) {
|
fun GettingStartedLabel(onClick: (url: String) -> Unit, modifier: Modifier = Modifier) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.padding(top = 100.dp).fillMaxSize(),
|
modifier = modifier.padding(top = 100.dp).fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val url = stringResource(id = R.string.docs_url)
|
val url = stringResource(id = R.string.docs_url)
|
||||||
val gettingStarted = buildAnnotatedString {
|
val gettingStarted = buildAnnotatedString {
|
||||||
|
|||||||
+7
-1
@@ -59,7 +59,12 @@ fun TunnelList(
|
|||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
if (sharedState.tunnels.isEmpty()) {
|
if (sharedState.tunnels.isEmpty()) {
|
||||||
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
|
item {
|
||||||
|
GettingStartedLabel(
|
||||||
|
onClick = { context.openWebUrl(it) },
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items(sharedState.tunnels, key = { it.id }) { tunnel ->
|
items(sharedState.tunnels, key = { it.id }) { tunnel ->
|
||||||
val tunnelState =
|
val tunnelState =
|
||||||
@@ -81,6 +86,7 @@ fun TunnelList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SurfaceRow(
|
SurfaceRow(
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
leading = {
|
leading = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
Icons.Rounded.Circle,
|
||||||
|
|||||||
+24
-16
@@ -19,8 +19,10 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.label.lowercaseLabel
|
import com.zaneschepke.wireguardautotunnel.ui.common.label.lowercaseLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.abbreviateKey
|
import com.zaneschepke.wireguardautotunnel.util.extensions.abbreviateKey
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.localizedDuration
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.millisAgo
|
||||||
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -33,6 +35,7 @@ fun TunnelStatisticsRow(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val textStyle = MaterialTheme.typography.bodySmall
|
val textStyle = MaterialTheme.typography.bodySmall
|
||||||
val textColor = MaterialTheme.colorScheme.outline
|
val textColor = MaterialTheme.colorScheme.outline
|
||||||
|
val locale = remember { Locale.getDefault() }
|
||||||
|
|
||||||
// needs to be set as peer stats for duplicates return as a single set of stats
|
// needs to be set as peer stats for duplicates return as a single set of stats
|
||||||
val peers by
|
val peers by
|
||||||
@@ -65,6 +68,19 @@ fun TunnelStatisticsRow(
|
|||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"uptime: ${tunnelState.uptime().localizedDuration(locale)}",
|
||||||
|
style = textStyle,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
peers.forEach { peerBase64 ->
|
peers.forEach { peerBase64 ->
|
||||||
key(peerBase64) {
|
key(peerBase64) {
|
||||||
val peerStats = remember(stats, peerBase64) { stats.peerStats(peerBase64) }
|
val peerStats = remember(stats, peerBase64) { stats.peerStats(peerBase64) }
|
||||||
@@ -88,11 +104,7 @@ fun TunnelStatisticsRow(
|
|||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
stats.latestHandshakeEpochMillis.let { lastHandshake ->
|
stats.latestHandshakeEpochMillis.let { lastHandshake ->
|
||||||
if (lastHandshake == 0L) null
|
if (lastHandshake == 0L) null
|
||||||
else
|
else lastHandshake.millisAgo().localizedDuration(locale)
|
||||||
NumberUtils.getSecondsBetween(
|
|
||||||
lastHandshake,
|
|
||||||
currentTimeMillis,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +117,10 @@ fun TunnelStatisticsRow(
|
|||||||
val lastPingedSeconds by
|
val lastPingedSeconds by
|
||||||
remember(pingState, currentTimeMillis) {
|
remember(pingState, currentTimeMillis) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
pingState?.lastSuccessfulPingMillis?.let { lastPing ->
|
pingState
|
||||||
NumberUtils.getSecondsBetween(lastPing, currentTimeMillis)
|
?.lastSuccessfulPingMillis
|
||||||
}
|
?.millisAgo()
|
||||||
|
?.localizedDuration(locale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +169,7 @@ fun TunnelStatisticsRow(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"$handshakeText: ${handshake?.let { lowercaseLabel(stringResource(R.string.sec_ago_template, it.toString())) } ?: neverText}",
|
"$handshakeText: ${handshake?.let { lowercaseLabel(it) } ?: neverText}",
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
)
|
)
|
||||||
@@ -219,12 +232,7 @@ fun TunnelStatisticsRow(
|
|||||||
stringResource(
|
stringResource(
|
||||||
R.string.ping_success_template,
|
R.string.ping_success_template,
|
||||||
lastPingedSeconds?.let { sec ->
|
lastPingedSeconds?.let { sec ->
|
||||||
lowercaseLabel(
|
lowercaseLabel(sec)
|
||||||
stringResource(
|
|
||||||
R.string.sec_ago_template,
|
|
||||||
sec.toString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} ?: neverText,
|
} ?: neverText,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.util
|
|||||||
|
|
||||||
import com.vdurmont.semver4j.Semver
|
import com.vdurmont.semver4j.Semver
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -28,16 +26,6 @@ object NumberUtils {
|
|||||||
return (Math.random() * 100000).toInt()
|
return (Math.random() * 100000).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecondsBetween(start: Long, end: Long): Long? {
|
|
||||||
return if (start != 0L && end != 0L) {
|
|
||||||
val startInstant = Instant.ofEpochMilli(start)
|
|
||||||
val endInstant = Instant.ofEpochMilli(end)
|
|
||||||
return Duration.between(startInstant, endInstant).seconds
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compareVersions(newVersion: String, currentVersion: String): Int {
|
fun compareVersions(newVersion: String, currentVersion: String): Int {
|
||||||
try {
|
try {
|
||||||
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
|
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
|
||||||
|
|||||||
+16
-6
@@ -186,13 +186,23 @@ fun Context.launchAppSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.requestTunnelTileServiceStateUpdate() {
|
fun Context.requestTunnelTileServiceStateUpdate() =
|
||||||
TileService.requestListeningState(this, ComponentName(this, TunnelControlTile::class.java))
|
runCatching {
|
||||||
}
|
TileService.requestListeningState(
|
||||||
|
this,
|
||||||
|
ComponentName(this, TunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.w(it) }
|
||||||
|
|
||||||
fun Context.requestAutoTunnelTileServiceUpdate() {
|
fun Context.requestAutoTunnelTileServiceUpdate() =
|
||||||
TileService.requestListeningState(this, ComponentName(this, AutoTunnelControlTile::class.java))
|
runCatching {
|
||||||
}
|
TileService.requestListeningState(
|
||||||
|
this,
|
||||||
|
ComponentName(this, AutoTunnelControlTile::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.w(it) }
|
||||||
|
|
||||||
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
|
fun Context.getAllInternetCapablePackages(): List<PackageInfo> {
|
||||||
val permissions = arrayOf(Manifest.permission.INTERNET)
|
val permissions = arrayOf(Manifest.permission.INTERNET)
|
||||||
|
|||||||
+2
-2
@@ -77,7 +77,7 @@ fun BackendMode.asAmBackendMode(): Backend.BackendMode {
|
|||||||
fun Tunnel.State.asTunnelState(): TunnelStatus {
|
fun Tunnel.State.asTunnelState(): TunnelStatus {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
Tunnel.State.DOWN -> TunnelStatus.Down
|
Tunnel.State.DOWN -> TunnelStatus.Down
|
||||||
Tunnel.State.UP -> TunnelStatus.Up
|
Tunnel.State.UP -> TunnelStatus.Up(System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,6 @@ fun org.amnezia.awg.backend.BackendException.toBackendCoreException(): BackendCo
|
|||||||
fun com.wireguard.android.backend.Tunnel.State.asTunnelState(): TunnelStatus {
|
fun com.wireguard.android.backend.Tunnel.State.asTunnelState(): TunnelStatus {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
com.wireguard.android.backend.Tunnel.State.DOWN -> TunnelStatus.Down
|
com.wireguard.android.backend.Tunnel.State.DOWN -> TunnelStatus.Down
|
||||||
com.wireguard.android.backend.Tunnel.State.UP -> TunnelStatus.Up
|
com.wireguard.android.backend.Tunnel.State.UP -> TunnelStatus.Up(System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.icu.text.MeasureFormat
|
||||||
|
import android.icu.util.Measure
|
||||||
|
import android.icu.util.MeasureUnit
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
import androidx.compose.material.icons.outlined.Lock
|
||||||
import androidx.compose.material.icons.outlined.Terminal
|
import androidx.compose.material.icons.outlined.Terminal
|
||||||
@@ -18,6 +21,8 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.AlertRed
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.CoolGray
|
import com.zaneschepke.wireguardautotunnel.ui.theme.CoolGray
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
fun WifiDetectionMethod.asTitleString(context: Context): String {
|
fun WifiDetectionMethod.asTitleString(context: Context): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
@@ -82,3 +87,35 @@ fun TunnelState.Health.asColor(): Color {
|
|||||||
TunnelState.Health.STALE -> Straw
|
TunnelState.Health.STALE -> Straw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Long.localizedDuration(locale: Locale = Locale.getDefault()): String {
|
||||||
|
require(this >= 0L) { "Duration cannot be negative" }
|
||||||
|
|
||||||
|
val duration = this.milliseconds
|
||||||
|
|
||||||
|
if (duration < 1000.milliseconds) {
|
||||||
|
return MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT)
|
||||||
|
.format(Measure(0, MeasureUnit.SECOND))
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalSeconds = duration.inWholeSeconds
|
||||||
|
|
||||||
|
val days = totalSeconds / 86_400
|
||||||
|
val hours = (totalSeconds % 86_400) / 3_600
|
||||||
|
val minutes = (totalSeconds % 3_600) / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
val measures = buildList {
|
||||||
|
if (days > 0) add(Measure(days, MeasureUnit.DAY))
|
||||||
|
if (hours > 0) add(Measure(hours, MeasureUnit.HOUR))
|
||||||
|
if (minutes > 0) add(Measure(minutes, MeasureUnit.MINUTE))
|
||||||
|
if (seconds > 0) add(Measure(seconds, MeasureUnit.SECOND))
|
||||||
|
}
|
||||||
|
|
||||||
|
return MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT)
|
||||||
|
.formatMeasures(*measures.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Long.millisAgo(): Long {
|
||||||
|
return System.currentTimeMillis() - this
|
||||||
|
}
|
||||||
|
|||||||
@@ -302,7 +302,6 @@
|
|||||||
<string name="set_custom_ping_target">Vlastní cíl pingu (volitelné)</string>
|
<string name="set_custom_ping_target">Vlastní cíl pingu (volitelné)</string>
|
||||||
<string name="tunnel_ping_interval">Interval pingování tunelu</string>
|
<string name="tunnel_ping_interval">Interval pingování tunelu</string>
|
||||||
<string name="ping_timeout">Časový limit pingování tunelu</string>
|
<string name="ping_timeout">Časový limit pingování tunelu</string>
|
||||||
<string name="sec_ago_template">Před %1$s s</string>
|
|
||||||
<string name="latency_template">Latence: %1$s</string>
|
<string name="latency_template">Latence: %1$s</string>
|
||||||
<string name="ping_target_template">Cíl pingu: %1$s</string>
|
<string name="ping_target_template">Cíl pingu: %1$s</string>
|
||||||
<string name="backup_success">Úspěšně zazálohováno. %1$s</string>
|
<string name="backup_success">Úspěšně zazálohováno. %1$s</string>
|
||||||
|
|||||||
@@ -247,7 +247,6 @@
|
|||||||
<string name="display_detailed_ping_stats">Detaillierte Ping-Statistiken anzeigen</string>
|
<string name="display_detailed_ping_stats">Detaillierte Ping-Statistiken anzeigen</string>
|
||||||
<string name="reachable_template">Erreichbar: %1$s</string>
|
<string name="reachable_template">Erreichbar: %1$s</string>
|
||||||
<string name="ping_success_template">Letzter erfolgreicher Ping: %1$s</string>
|
<string name="ping_success_template">Letzter erfolgreicher Ping: %1$s</string>
|
||||||
<string name="sec_ago_template">vor %1$s Sek</string>
|
|
||||||
<string name="latency_template">Latenz: %1$s</string>
|
<string name="latency_template">Latenz: %1$s</string>
|
||||||
<string name="jitter_template">Jitter: %1$s</string>
|
<string name="jitter_template">Jitter: %1$s</string>
|
||||||
<string name="packets_sent_template">Versendete Pakete: %1$s</string>
|
<string name="packets_sent_template">Versendete Pakete: %1$s</string>
|
||||||
|
|||||||
@@ -247,7 +247,6 @@
|
|||||||
<string name="display_detailed_ping_stats">Mostrar estadísticas detalladas del ping</string>
|
<string name="display_detailed_ping_stats">Mostrar estadísticas detalladas del ping</string>
|
||||||
<string name="reachable_template">Alcanzable: %1$s</string>
|
<string name="reachable_template">Alcanzable: %1$s</string>
|
||||||
<string name="ping_success_template">Último ping recibido: %1$s</string>
|
<string name="ping_success_template">Último ping recibido: %1$s</string>
|
||||||
<string name="sec_ago_template">hace %1$s segundos</string>
|
|
||||||
<string name="latency_template">Latencia: %1$s</string>
|
<string name="latency_template">Latencia: %1$s</string>
|
||||||
<string name="jitter_template">Jitter: %1$s</string>
|
<string name="jitter_template">Jitter: %1$s</string>
|
||||||
<string name="packets_sent_template">Paquetes enviados: %1$s</string>
|
<string name="packets_sent_template">Paquetes enviados: %1$s</string>
|
||||||
|
|||||||
@@ -263,7 +263,6 @@
|
|||||||
<string name="pinger_bounce_successful">Tunneli taaskäivitamine pingija poolt õnnestus.</string>
|
<string name="pinger_bounce_successful">Tunneli taaskäivitamine pingija poolt õnnestus.</string>
|
||||||
<string name="reachable_template">Leitav: %1$s</string>
|
<string name="reachable_template">Leitav: %1$s</string>
|
||||||
<string name="ping_success_template">Viimane õnnestunud ping: %1$s</string>
|
<string name="ping_success_template">Viimane õnnestunud ping: %1$s</string>
|
||||||
<string name="sec_ago_template">%1$s sekundi eest</string>
|
|
||||||
<string name="dns_provider">Nimelahenduse pakkuja</string>
|
<string name="dns_provider">Nimelahenduse pakkuja</string>
|
||||||
<string name="dns_protocol">Nimelahenduse protokoll</string>
|
<string name="dns_protocol">Nimelahenduse protokoll</string>
|
||||||
<string name="system">Süsteemne</string>
|
<string name="system">Süsteemne</string>
|
||||||
|
|||||||
@@ -252,7 +252,6 @@
|
|||||||
<string name="packets_sent_template">Pacchetti inviati: %1$s</string>
|
<string name="packets_sent_template">Pacchetti inviati: %1$s</string>
|
||||||
<string name="jitter_template">Jitter: %1$s</string>
|
<string name="jitter_template">Jitter: %1$s</string>
|
||||||
<string name="latency_template">Latenza: %1$s</string>
|
<string name="latency_template">Latenza: %1$s</string>
|
||||||
<string name="sec_ago_template">%1$s sec fa</string>
|
|
||||||
<string name="ping_success_template">Ultimo ping riuscito: %1$s</string>
|
<string name="ping_success_template">Ultimo ping riuscito: %1$s</string>
|
||||||
<string name="reachable_template">Raggiungibile: %1$s</string>
|
<string name="reachable_template">Raggiungibile: %1$s</string>
|
||||||
<string name="current_template">Corrente: %1$s</string>
|
<string name="current_template">Corrente: %1$s</string>
|
||||||
|
|||||||
@@ -248,7 +248,6 @@
|
|||||||
<string name="display_detailed_ping_stats">Wyświetl szczegółowe statystyki pingowania</string>
|
<string name="display_detailed_ping_stats">Wyświetl szczegółowe statystyki pingowania</string>
|
||||||
<string name="reachable_template">Osiągalny: %1$s</string>
|
<string name="reachable_template">Osiągalny: %1$s</string>
|
||||||
<string name="ping_success_template">Ostatni udany ping: %1$s</string>
|
<string name="ping_success_template">Ostatni udany ping: %1$s</string>
|
||||||
<string name="sec_ago_template">%1$s sek. temu</string>
|
|
||||||
<string name="latency_template">Opóźnienie: %1$s</string>
|
<string name="latency_template">Opóźnienie: %1$s</string>
|
||||||
<string name="jitter_template">Jitter: %1$s</string>
|
<string name="jitter_template">Jitter: %1$s</string>
|
||||||
<string name="packets_sent_template">Pakiety wysłane: %1$s</string>
|
<string name="packets_sent_template">Pakiety wysłane: %1$s</string>
|
||||||
|
|||||||
@@ -240,7 +240,6 @@
|
|||||||
<string name="backup_application">Резервирование данных</string>
|
<string name="backup_application">Резервирование данных</string>
|
||||||
<string name="restore_application">Восстановление данных</string>
|
<string name="restore_application">Восстановление данных</string>
|
||||||
<string name="tunnel_monitoring">Отслеживание туннеля</string>
|
<string name="tunnel_monitoring">Отслеживание туннеля</string>
|
||||||
<string name="sec_ago_template">%1$s сек.</string>
|
|
||||||
<string name="latency_template">Задержка: %1$s</string>
|
<string name="latency_template">Задержка: %1$s</string>
|
||||||
<string name="packets_sent_template">Отправлено пакетов: %1$s</string>
|
<string name="packets_sent_template">Отправлено пакетов: %1$s</string>
|
||||||
<string name="packet_loss_template">Потеряно пакетов: %.2f%%</string>
|
<string name="packet_loss_template">Потеряно пакетов: %.2f%%</string>
|
||||||
|
|||||||
@@ -247,7 +247,6 @@
|
|||||||
<string name="display_detailed_ping_stats">پِنگ کے تفصیلی اعدادوشمار دکھائیں</string>
|
<string name="display_detailed_ping_stats">پِنگ کے تفصیلی اعدادوشمار دکھائیں</string>
|
||||||
<string name="reachable_template">قابل رسائی: %1$s</string>
|
<string name="reachable_template">قابل رسائی: %1$s</string>
|
||||||
<string name="ping_success_template">آخری کامیاب پِنگ: %1$s</string>
|
<string name="ping_success_template">آخری کامیاب پِنگ: %1$s</string>
|
||||||
<string name="sec_ago_template">%1$s سیکنڈ پہلے</string>
|
|
||||||
<string name="latency_template">تاخیر: %1$s</string>
|
<string name="latency_template">تاخیر: %1$s</string>
|
||||||
<string name="jitter_template">ہلچل: %1$s</string>
|
<string name="jitter_template">ہلچل: %1$s</string>
|
||||||
<string name="packets_sent_template">پیکٹ بھیجے گئے: %1$s</string>
|
<string name="packets_sent_template">پیکٹ بھیجے گئے: %1$s</string>
|
||||||
|
|||||||
@@ -247,7 +247,6 @@
|
|||||||
<string name="display_detailed_ping_stats">展示详细的 ping 数据</string>
|
<string name="display_detailed_ping_stats">展示详细的 ping 数据</string>
|
||||||
<string name="reachable_template">可抵达:%1$s</string>
|
<string name="reachable_template">可抵达:%1$s</string>
|
||||||
<string name="ping_success_template">上次成功的 ping 操作:%1$s</string>
|
<string name="ping_success_template">上次成功的 ping 操作:%1$s</string>
|
||||||
<string name="sec_ago_template">%1$s 秒前</string>
|
|
||||||
<string name="latency_template">延迟:%1$s</string>
|
<string name="latency_template">延迟:%1$s</string>
|
||||||
<string name="jitter_template">抖动:%1$s</string>
|
<string name="jitter_template">抖动:%1$s</string>
|
||||||
<string name="packets_sent_template">已发送数据包:%1$s</string>
|
<string name="packets_sent_template">已发送数据包:%1$s</string>
|
||||||
|
|||||||
@@ -205,7 +205,6 @@
|
|||||||
<string name="jitter_template">抖動: %1$s</string>
|
<string name="jitter_template">抖動: %1$s</string>
|
||||||
<string name="warning">警告</string>
|
<string name="warning">警告</string>
|
||||||
<string name="ip_or_hostname">IP 或主機名稱</string>
|
<string name="ip_or_hostname">IP 或主機名稱</string>
|
||||||
<string name="sec_ago_template">%1$s 秒前</string>
|
|
||||||
<string name="restarting_app">正在重啟應用程式以應用變更…</string>
|
<string name="restarting_app">正在重啟應用程式以應用變更…</string>
|
||||||
<string name="packets_sent_template">已發送封包: %1$s</string>
|
<string name="packets_sent_template">已發送封包: %1$s</string>
|
||||||
<string name="packet_loss_template">丟失封包: %.2f%%</string>
|
<string name="packet_loss_template">丟失封包: %.2f%%</string>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
<string name="root_accepted">Root shell accepted</string>
|
<string name="root_accepted">Root shell accepted</string>
|
||||||
<string name="show_amnezia_properties">Show Amnezia properties</string>
|
<string name="show_amnezia_properties">Show Amnezia properties</string>
|
||||||
<string name="never">Never</string>
|
<string name="never">Never</string>
|
||||||
<string name="handshake">Handshake</string>
|
<string name="handshake">Last handshake</string>
|
||||||
<string name="logs">Logs</string>
|
<string name="logs">Logs</string>
|
||||||
<string name="appearance">Appearance</string>
|
<string name="appearance">Appearance</string>
|
||||||
<string name="notifications">Notifications</string>
|
<string name="notifications">Notifications</string>
|
||||||
@@ -273,7 +273,6 @@
|
|||||||
<string name="display_detailed_ping_stats">Display detailed ping stats</string>
|
<string name="display_detailed_ping_stats">Display detailed ping stats</string>
|
||||||
<string name="reachable_template">Reachable: %1$s</string>
|
<string name="reachable_template">Reachable: %1$s</string>
|
||||||
<string name="ping_success_template">Last successful ping: %1$s</string>
|
<string name="ping_success_template">Last successful ping: %1$s</string>
|
||||||
<string name="sec_ago_template">%1$s seconds ago</string>
|
|
||||||
<string name="latency_template">Latency: %1$s</string>
|
<string name="latency_template">Latency: %1$s</string>
|
||||||
<string name="jitter_template">Jitter: %1$s</string>
|
<string name="jitter_template">Jitter: %1$s</string>
|
||||||
<string name="packets_sent_template">Packets sent: %1$s</string>
|
<string name="packets_sent_template">Packets sent: %1$s</string>
|
||||||
@@ -434,7 +433,7 @@
|
|||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="already_donated">Already donated</string>
|
<string name="already_donated">Already donated</string>
|
||||||
<string name="already_donated_description">Disables future donation prompts</string>
|
<string name="already_donated_description">Disables future donation prompts</string>
|
||||||
<string name="donation_prompt_prefix">Thanks for using WG Tunnel! If you can, please consider</string>
|
<string name="donation_prompt_prefix">Thanks for using WG Tunnel! If you are able, please consider</string>
|
||||||
<string name="donation_prompt_link">supporting the project</string>
|
<string name="donation_prompt_link">supporting the project</string>
|
||||||
<string name="donation_prompt_suffix">to keep it free and improving.</string>
|
<string name="donation_prompt_suffix">to keep it free and improving.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "4.1.5"
|
const val VERSION_NAME = "4.1.8"
|
||||||
const val VERSION_CODE = 40105
|
const val VERSION_CODE = 40108
|
||||||
const val TARGET_SDK = 36
|
const val TARGET_SDK = 36
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fun allowedLicenses(): List<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun allowedLicenseUrls(): List<String> {
|
fun allowedLicenseUrls(): List<String> {
|
||||||
return listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
|
return listOf("https://jsoup.org/license", "http://opensource.org/licenses/bsd-license.php", "https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
|
||||||
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE", "https://github.com/rafi0101/Android-Room-Database-Backup/blob/master/LICENSE",
|
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE", "https://github.com/rafi0101/Android-Room-Database-Backup/blob/master/LICENSE",
|
||||||
"https://opensource.org/license/mit")
|
"https://opensource.org/license/mit")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Ein WireGuard- und AmneziaWG-VPN-Client mit automatischem Tunneling, Sperrung und Proxying.
|
Ein WireGuard & AmneziaWG VPN-Client mit Auto-Tunnel, Sperre & Proxy.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
What's new:
|
||||||
|
- Auto-tunnel regression bugfix
|
||||||
|
- Resource usage bugfix for kill switch mode
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
What's new:
|
||||||
|
- Improved QR scanning and device support
|
||||||
|
- Display tunnel uptime
|
||||||
|
- Fixes quick tile crash bug when running app in multiple profiles
|
||||||
|
- Fixes global overrides regression causing unexpected tunnel start errors
|
||||||
|
- Fixes network detection race while VPN is active
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
What's new:
|
||||||
|
- Rapid network changes cause invalid network state bugfix
|
||||||
@@ -1 +1 @@
|
|||||||
آٹو ٹنلنگ، لاک ڈاؤن اور پراکسینگ کے ساتھ ایک وائرس گارڈ اور یمنیزیا ویپیاین کلائنٹ۔
|
وائرس گارڈ اور یمنیزیا وی پی این کلائنٹ۔ آٹو ٹنلنگ، لاک ڈاؤن، پراکسی۔
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
amneziawgAndroid = "2.2.2"
|
amneziawgAndroid = "2.2.3"
|
||||||
androidx-junit = "1.3.0"
|
androidx-junit = "1.3.0"
|
||||||
icmp4a = "1.0.0"
|
icmp4a = "1.0.0"
|
||||||
ipaddress = "5.5.1"
|
ipaddress = "5.5.1"
|
||||||
leakcanaryAndroid = "3.0-alpha-8"
|
leakcanaryAndroid = "3.0-alpha-8"
|
||||||
orbitCompose = "10.0.0"
|
orbitCompose = "11.0.0"
|
||||||
roomdatabasebackup = "1.1.0"
|
roomdatabasebackup = "1.1.0"
|
||||||
shizuku = "13.1.5"
|
shizuku = "13.1.5"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
@@ -20,7 +20,7 @@ hiltNavigationCompose = "1.3.0"
|
|||||||
navigation3 = "1.0.0-rc01"
|
navigation3 = "1.0.0-rc01"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
ktorClientCore = "3.3.1"
|
ktorClientCore = "3.3.2"
|
||||||
lifecycle-runtime-compose = "2.9.4"
|
lifecycle-runtime-compose = "2.9.4"
|
||||||
material3 = "1.5.0-alpha08"
|
material3 = "1.5.0-alpha08"
|
||||||
pinLockCompose = "1.0.5"
|
pinLockCompose = "1.0.5"
|
||||||
@@ -32,12 +32,12 @@ timber = "5.0.1"
|
|||||||
tunnel = "1.4.0"
|
tunnel = "1.4.0"
|
||||||
androidGradlePlugin = "8.12.3"
|
androidGradlePlugin = "8.12.3"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.2.21"
|
||||||
ksp = "2.3.0"
|
ksp = "2.3.1"
|
||||||
composeBom = "2025.11.00"
|
composeBom = "2025.11.00"
|
||||||
compose = "1.9.4"
|
compose = "1.9.4"
|
||||||
icons = "1.7.8"
|
icons = "1.7.8"
|
||||||
workRuntimeKtxVersion = "2.11.0"
|
workRuntimeKtxVersion = "2.11.0"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
quickieFoss = "1.15.7"
|
||||||
coreSplashscreen = "1.2.0"
|
coreSplashscreen = "1.2.0"
|
||||||
gradlePlugins-grgit = "5.3.3"
|
gradlePlugins-grgit = "5.3.3"
|
||||||
reorderable = "3.0.0"
|
reorderable = "3.0.0"
|
||||||
@@ -94,7 +94,7 @@ wireguard-tunnel = ["tunnel", "amneziawg-android"]
|
|||||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
|
|
||||||
# UI utilities
|
# UI utilities
|
||||||
ui-utilities = ["pin-lock-compose", "qrose", "reorderable", "zxing-android-embedded", "androidx-core-splashscreen"]
|
ui-utilities = ["pin-lock-compose", "qrose", "reorderable", "quickie-foss", "androidx-core-splashscreen"]
|
||||||
|
|
||||||
# Misc utilities
|
# Misc utilities
|
||||||
misc-utilities = ["semver4j", "icmp4a", "slf4j-android", "timber"]
|
misc-utilities = ["semver4j", "icmp4a", "slf4j-android", "timber"]
|
||||||
@@ -184,7 +184,7 @@ qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
|
|||||||
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
|
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
|
||||||
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
|
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
|
||||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
|
|||||||
+158
-110
@@ -17,10 +17,13 @@ import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
|||||||
import com.zaneschepke.networkmonitor.util.*
|
import com.zaneschepke.networkmonitor.util.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
|
||||||
class AndroidNetworkMonitor(
|
class AndroidNetworkMonitor(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
@@ -67,11 +70,17 @@ class AndroidNetworkMonitor(
|
|||||||
|
|
||||||
private var permissionReceiver: BroadcastReceiver? = null
|
private var permissionReceiver: BroadcastReceiver? = null
|
||||||
private var locationServicesReceiver: BroadcastReceiver? = null
|
private var locationServicesReceiver: BroadcastReceiver? = null
|
||||||
|
private var airplaneReceiver: BroadcastReceiver? = null
|
||||||
private var defaultNetworkCallback: ConnectivityManager.NetworkCallback? = null
|
private var defaultNetworkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
private var wifiCallback: ConnectivityManager.NetworkCallback? = null
|
private var wifiCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
private var cellularCallback: ConnectivityManager.NetworkCallback? = null
|
private var cellularCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
private var ethernetCallback: ConnectivityManager.NetworkCallback? = null
|
private var ethernetCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
|
|
||||||
|
private val airplaneModeState = MutableStateFlow(appContext.isAirplaneModeOn())
|
||||||
|
private val airplaneModeFlow: Flow<Boolean> = airplaneModeState.asStateFlow()
|
||||||
|
|
||||||
|
// recreate defaultNetwork flow on permission/detection method changes to get newly available
|
||||||
|
// network info
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private val defaultNetworkFlow: Flow<TransportEvent> =
|
private val defaultNetworkFlow: Flow<TransportEvent> =
|
||||||
combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _
|
combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _
|
||||||
@@ -87,6 +96,8 @@ class AndroidNetworkMonitor(
|
|||||||
object :
|
object :
|
||||||
ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
|
// ignore onAvailable has it doesn't contain detailed network
|
||||||
|
// information in capabilities
|
||||||
Timber.d("Default onAvailable: $network")
|
Timber.d("Default onAvailable: $network")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +109,6 @@ class AndroidNetworkMonitor(
|
|||||||
network: Network,
|
network: Network,
|
||||||
caps: NetworkCapabilities,
|
caps: NetworkCapabilities,
|
||||||
) {
|
) {
|
||||||
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
|
||||||
Timber.d("Ignoring VPN default network change: $network")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
trySend(TransportEvent.CapabilitiesChanged(network, caps))
|
trySend(TransportEvent.CapabilitiesChanged(network, caps))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,6 +148,8 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recreate Wi-Fi flow on permission/detection method changes to get newly available network
|
||||||
|
// info
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private val wifiFlow: Flow<TransportEvent> =
|
private val wifiFlow: Flow<TransportEvent> =
|
||||||
combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _
|
combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _
|
||||||
@@ -152,7 +161,11 @@ class AndroidNetworkMonitor(
|
|||||||
private fun createWifiNetworkCallbackFlow(
|
private fun createWifiNetworkCallbackFlow(
|
||||||
detectionMethod: WifiDetectionMethod
|
detectionMethod: WifiDetectionMethod
|
||||||
): Flow<TransportEvent> = callbackFlow {
|
): Flow<TransportEvent> = callbackFlow {
|
||||||
val onAvailable: (Network) -> Unit = { network -> Timber.d("WiFi onAvailable: $network") }
|
val onAvailable: (Network) -> Unit = { network ->
|
||||||
|
// ignore onAvailable has it doesn't contain detailed network information in
|
||||||
|
// capabilities
|
||||||
|
Timber.d("WiFi onAvailable: $network")
|
||||||
|
}
|
||||||
val onLost: (Network) -> Unit = { network ->
|
val onLost: (Network) -> Unit = { network ->
|
||||||
Timber.d("WiFi onLost: $network")
|
Timber.d("WiFi onLost: $network")
|
||||||
trySend(TransportEvent.Lost(network))
|
trySend(TransportEvent.Lost(network))
|
||||||
@@ -189,7 +202,10 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build()
|
NetworkRequest.Builder()
|
||||||
|
.apply { addTransportType(NetworkCapabilities.TRANSPORT_WIFI) }
|
||||||
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
|
connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
@@ -223,8 +239,9 @@ class AndroidNetworkMonitor(
|
|||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder()
|
NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
.apply { addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, cellularCallback!!)
|
connectivityManager?.registerNetworkCallback(request, cellularCallback!!)
|
||||||
|
|
||||||
trySend(TransportEvent.Unknown)
|
trySend(TransportEvent.Unknown)
|
||||||
@@ -260,8 +277,9 @@ class AndroidNetworkMonitor(
|
|||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder()
|
NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
.apply { addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, ethernetCallback!!)
|
connectivityManager?.registerNetworkCallback(request, ethernetCallback!!)
|
||||||
|
|
||||||
trySend(TransportEvent.Unknown)
|
trySend(TransportEvent.Unknown)
|
||||||
@@ -272,29 +290,6 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val airplaneModeFlow: Flow<Boolean> = callbackFlow {
|
|
||||||
val receiver =
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
|
||||||
Timber.d("Received airplane mode changed broadcast")
|
|
||||||
trySend(appContext.isAirplaneModeOn())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
|
|
||||||
appContext.registerReceiver(receiver, filter)
|
|
||||||
|
|
||||||
// initial state
|
|
||||||
trySend(appContext.isAirplaneModeOn())
|
|
||||||
|
|
||||||
awaitClose {
|
|
||||||
runCatching { appContext.unregisterReceiver(receiver) }
|
|
||||||
.onFailure { Timber.e(it, "Error unregistering airplane mode receiver") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val wifiStateFlow: Flow<NetworkCapabilities?> =
|
private val wifiStateFlow: Flow<NetworkCapabilities?> =
|
||||||
wifiFlow
|
wifiFlow
|
||||||
.map { event ->
|
.map { event ->
|
||||||
@@ -310,14 +305,7 @@ class AndroidNetworkMonitor(
|
|||||||
cellularFlow
|
cellularFlow
|
||||||
.map { event ->
|
.map { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is TransportEvent.CapabilitiesChanged ->
|
is TransportEvent.CapabilitiesChanged -> event.networkCapabilities
|
||||||
if (
|
|
||||||
event.networkCapabilities.hasCapability(
|
|
||||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
|
||||||
)
|
|
||||||
)
|
|
||||||
event.networkCapabilities
|
|
||||||
else null
|
|
||||||
is TransportEvent.Lost -> null
|
is TransportEvent.Lost -> null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -328,14 +316,7 @@ class AndroidNetworkMonitor(
|
|||||||
ethernetFlow
|
ethernetFlow
|
||||||
.map { event ->
|
.map { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is TransportEvent.CapabilitiesChanged ->
|
is TransportEvent.CapabilitiesChanged -> event.networkCapabilities
|
||||||
if (
|
|
||||||
event.networkCapabilities.hasCapability(
|
|
||||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
|
||||||
)
|
|
||||||
)
|
|
||||||
event.networkCapabilities
|
|
||||||
else null
|
|
||||||
is TransportEvent.Lost -> null
|
is TransportEvent.Lost -> null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -373,13 +354,16 @@ class AndroidNetworkMonitor(
|
|||||||
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default network events don't contain detailed capability information of underlying networks,
|
||||||
|
// so we need to track separately
|
||||||
private data class NetworkData(
|
private data class NetworkData(
|
||||||
val defaultEvent: TransportEvent,
|
val defaultNetworkEvent: TransportEvent,
|
||||||
val wifiCaps: NetworkCapabilities?,
|
val wifiCapabilities: NetworkCapabilities?,
|
||||||
val cellularCaps: NetworkCapabilities?,
|
val cellularCaps: NetworkCapabilities?,
|
||||||
val ethernetCaps: NetworkCapabilities?,
|
val ethernetCaps: NetworkCapabilities?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// combine our network flows to keep sync
|
||||||
private val networkFlows: Flow<NetworkData> =
|
private val networkFlows: Flow<NetworkData> =
|
||||||
combine(defaultNetworkFlow, wifiStateFlow, cellularStateFlow, ethernetStateFlow) {
|
combine(defaultNetworkFlow, wifiStateFlow, cellularStateFlow, ethernetStateFlow) {
|
||||||
defaultEvent,
|
defaultEvent,
|
||||||
@@ -389,13 +373,18 @@ class AndroidNetworkMonitor(
|
|||||||
NetworkData(defaultEvent, wifiCaps, cellularCaps, ethernetCaps)
|
NetworkData(defaultEvent, wifiCaps, cellularCaps, ethernetCaps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tracking to prevent races that occur when VPN is first activated
|
||||||
|
private val lastKnownActiveNetwork = MutableStateFlow<ActiveNetwork>(ActiveNetwork.Disconnected)
|
||||||
|
@OptIn(ExperimentalAtomicApi::class) private val vpnActiveState = AtomicReference(false)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class, FlowPreview::class)
|
||||||
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
||||||
combine(networkFlows, airplaneModeFlow, configurationListener.detectionMethod) {
|
combine(networkFlows, airplaneModeFlow, configurationListener.detectionMethod) {
|
||||||
networkData,
|
networkData,
|
||||||
isAirplaneOn,
|
isAirplaneOn,
|
||||||
detectionMethod ->
|
detectionMethod ->
|
||||||
val defaultEvent = networkData.defaultEvent
|
val defaultEvent = networkData.defaultNetworkEvent
|
||||||
val wifiCaps = networkData.wifiCaps
|
val wifiCaps = networkData.wifiCapabilities
|
||||||
val cellularCaps = networkData.cellularCaps
|
val cellularCaps = networkData.cellularCaps
|
||||||
val ethernetCaps = networkData.ethernetCaps
|
val ethernetCaps = networkData.ethernetCaps
|
||||||
|
|
||||||
@@ -409,82 +398,125 @@ class AndroidNetworkMonitor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine default network capabilities
|
||||||
val defaultCaps =
|
val defaultCaps =
|
||||||
when (defaultEvent) {
|
when (defaultEvent) {
|
||||||
is TransportEvent.CapabilitiesChanged -> defaultEvent.networkCapabilities
|
is TransportEvent.CapabilitiesChanged -> defaultEvent.networkCapabilities
|
||||||
else ->
|
else ->
|
||||||
connectivityManager?.getNetworkCapabilities(
|
connectivityManager?.activeNetwork?.let {
|
||||||
connectivityManager.activeNetwork
|
connectivityManager.getNetworkCapabilities(it)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
?: return@combine ConnectivityState(
|
?: return@combine ConnectivityState(
|
||||||
ActiveNetwork.Disconnected,
|
activeNetwork = ActiveNetwork.Disconnected,
|
||||||
permissions.locationServicesEnabled,
|
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||||
permissions.locationPermissionGranted,
|
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||||
|
vpnState = VpnState.Inactive,
|
||||||
)
|
)
|
||||||
|
|
||||||
val isValidated =
|
val vpnPreviouslyActive =
|
||||||
defaultCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
vpnActiveState.exchange(
|
||||||
val hasInternet =
|
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
defaultCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
|
|
||||||
if (!isValidated || !hasInternet) {
|
|
||||||
return@combine ConnectivityState(
|
|
||||||
ActiveNetwork.Disconnected,
|
|
||||||
permissions.locationServicesEnabled,
|
|
||||||
permissions.locationPermissionGranted,
|
|
||||||
)
|
)
|
||||||
} else {
|
val isVpnActive = vpnActiveState.load()
|
||||||
val activeNetwork: ActiveNetwork =
|
|
||||||
if (defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
// determine vpn state
|
||||||
// Ignore VPN, determine underlying
|
val vpnState: VpnState =
|
||||||
when {
|
if (!isVpnActive) {
|
||||||
wifiCaps != null -> {
|
VpnState.Inactive
|
||||||
val ssid = getSsidByDetectionMethod(detectionMethod, wifiCaps)
|
} else {
|
||||||
ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType())
|
VpnState.Active(
|
||||||
|
hasInternet =
|
||||||
|
defaultCaps.hasCapability(
|
||||||
|
NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeNetwork: ActiveNetwork =
|
||||||
|
run {
|
||||||
|
if (!isVpnActive) {
|
||||||
|
when {
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_ETHERNET
|
||||||
|
) -> ActiveNetwork.Ethernet
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_WIFI
|
||||||
|
) -> {
|
||||||
|
val ssid =
|
||||||
|
getSsidByDetectionMethod(detectionMethod, defaultCaps)
|
||||||
|
ActiveNetwork.Wifi(
|
||||||
|
ssid,
|
||||||
|
wifiManager?.getCurrentSecurityType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_CELLULAR
|
||||||
|
) && !isAirplaneOn -> ActiveNetwork.Cellular
|
||||||
|
else -> ActiveNetwork.Disconnected
|
||||||
}
|
}
|
||||||
ethernetCaps != null -> ActiveNetwork.Ethernet
|
} else {
|
||||||
cellularCaps != null && !isAirplaneOn -> ActiveNetwork.Cellular
|
val fromCaps =
|
||||||
else -> ActiveNetwork.Disconnected
|
when {
|
||||||
}
|
ethernetCaps != null -> ActiveNetwork.Ethernet
|
||||||
} else {
|
wifiCaps != null -> {
|
||||||
when {
|
val ssid =
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
|
getSsidByDetectionMethod(detectionMethod, wifiCaps)
|
||||||
val ssid =
|
ActiveNetwork.Wifi(
|
||||||
getSsidByDetectionMethod(detectionMethod, defaultCaps)
|
ssid,
|
||||||
ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType())
|
wifiManager?.getCurrentSecurityType(),
|
||||||
}
|
)
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
}
|
||||||
!isAirplaneOn -> ActiveNetwork.Cellular
|
cellularCaps != null && !isAirplaneOn ->
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
ActiveNetwork.Cellular
|
||||||
ActiveNetwork.Ethernet
|
else -> null
|
||||||
else -> ActiveNetwork.Disconnected
|
}
|
||||||
|
|
||||||
|
fromCaps
|
||||||
|
?: if (!vpnPreviouslyActive) {
|
||||||
|
lastKnownActiveNetwork.value
|
||||||
|
} else {
|
||||||
|
ActiveNetwork.Disconnected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectivityState(
|
.also { network -> lastKnownActiveNetwork.value = network }
|
||||||
activeNetwork,
|
|
||||||
permissions.locationServicesEnabled,
|
ConnectivityState(
|
||||||
permissions.locationPermissionGranted,
|
activeNetwork = activeNetwork,
|
||||||
)
|
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||||
}
|
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||||
|
vpnState = vpnState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.debounce { 300L }
|
||||||
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
|
// utility to send local broadcast to trigger a recheck of location permissions onResume,
|
||||||
|
// especially for getting SSID
|
||||||
|
// that we did not have permission to read before, will trigger a recreation of the Wi-Fi flows
|
||||||
|
// if permission was changed
|
||||||
override fun checkPermissionsAndUpdateState() {
|
override fun checkPermissionsAndUpdateState() {
|
||||||
val action = actionPermissionCheck
|
val action = actionPermissionCheck
|
||||||
val intent = Intent(action)
|
val intent = Intent(action).apply { setPackage(appContext.packageName) }
|
||||||
Timber.d("Sending broadcast: $action")
|
Timber.d("Sending broadcast: $action")
|
||||||
appContext.sendBroadcast(intent)
|
appContext.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val receiverFlags =
|
val exportedFlags =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
Context.RECEIVER_EXPORTED
|
Context.RECEIVER_EXPORTED
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
val localFlags =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Context.RECEIVER_NOT_EXPORTED
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
permissionReceiver =
|
permissionReceiver =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
@@ -505,10 +537,11 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
appContext.registerReceiver(
|
||||||
permissionReceiver?.let {
|
permissionReceiver,
|
||||||
appContext.registerReceiver(it, IntentFilter(actionPermissionCheck), receiverFlags)
|
IntentFilter(actionPermissionCheck),
|
||||||
}
|
localFlags,
|
||||||
|
)
|
||||||
|
|
||||||
locationServicesReceiver =
|
locationServicesReceiver =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
@@ -529,19 +562,34 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
locationServicesReceiver?.let {
|
appContext.registerReceiver(
|
||||||
appContext.registerReceiver(
|
locationServicesReceiver,
|
||||||
locationServicesReceiver,
|
IntentFilter(LOCATION_SERVICES_FILTER),
|
||||||
IntentFilter(LOCATION_SERVICES_FILTER),
|
exportedFlags,
|
||||||
receiverFlags,
|
)
|
||||||
)
|
|
||||||
}
|
airplaneReceiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
||||||
|
Timber.d("Received airplane mode changed broadcast")
|
||||||
|
airplaneModeState.update { appContext.isAirplaneModeOn() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appContext.registerReceiver(
|
||||||
|
airplaneReceiver,
|
||||||
|
IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
|
||||||
|
exportedFlags,
|
||||||
|
)
|
||||||
|
airplaneModeState.update { appContext.isAirplaneModeOn() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
runCatching {
|
runCatching {
|
||||||
permissionReceiver?.let { appContext.unregisterReceiver(it) }
|
permissionReceiver?.let { appContext.unregisterReceiver(it) }
|
||||||
locationServicesReceiver?.let { appContext.unregisterReceiver(it) }
|
locationServicesReceiver?.let { appContext.unregisterReceiver(it) }
|
||||||
|
airplaneReceiver?.let { appContext.unregisterReceiver(it) }
|
||||||
|
|
||||||
defaultNetworkCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
|
defaultNetworkCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
|
||||||
wifiCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
|
wifiCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ data class ConnectivityState(
|
|||||||
val activeNetwork: ActiveNetwork,
|
val activeNetwork: ActiveNetwork,
|
||||||
val locationPermissionsGranted: Boolean,
|
val locationPermissionsGranted: Boolean,
|
||||||
val locationServicesEnabled: Boolean,
|
val locationServicesEnabled: Boolean,
|
||||||
|
val vpnState: VpnState,
|
||||||
) {
|
) {
|
||||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||||
|
|
||||||
@@ -38,3 +39,9 @@ sealed class ActiveNetwork {
|
|||||||
|
|
||||||
data object Ethernet : ActiveNetwork()
|
data object Ethernet : ActiveNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface VpnState {
|
||||||
|
object Inactive : VpnState
|
||||||
|
|
||||||
|
data class Active(val hasInternet: Boolean) : VpnState
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user