mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7e4f3c3e5 | |||
| dedef38541 | |||
| aa5adec902 | |||
| e269aa88d9 | |||
| 7c3eabb19b | |||
| acdefd80fa | |||
| 09fa9dabdd | |||
| 33a02262a8 | |||
| 14a7278747 | |||
| 8fd2d8f62f | |||
| 7be051a664 | |||
| 88fff0b31c | |||
| 99a3fba97f |
@@ -15,7 +15,7 @@ A clear and concise description of what the bug is.
|
|||||||
- Device: [e.g. Pixel 4a]
|
- Device: [e.g. Pixel 4a]
|
||||||
- Android Version: [e.g. Android 13]
|
- Android Version: [e.g. Android 13]
|
||||||
- App Version [e.g. 3.3.3]
|
- App Version [e.g. 3.3.3]
|
||||||
- Backend: [e.g. Kernel, Userspace]
|
- App mode: [e.g. Kernel, VPN, Proxy, Lockdown]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|||||||
@@ -126,14 +126,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var pinManagerInitialized by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(appState.isAppLoaded) {
|
LaunchedEffect(appState.isAppLoaded) {
|
||||||
if (appState.isAppLoaded) {
|
if (appState.isAppLoaded) {
|
||||||
if (appState.pinLockEnabled && !pinManagerInitialized) {
|
|
||||||
PinManager.initialize(this@MainActivity)
|
|
||||||
pinManagerInitialized = true
|
|
||||||
}
|
|
||||||
appState.locale.let { LocaleUtil.changeLocale(it) }
|
appState.locale.let { LocaleUtil.changeLocale(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +191,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
|
requestingAppMode = Pair(sideEffect.requestingMode, sideEffect.config)
|
||||||
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
vpnActivity.launch(VpnService.prepare(this@MainActivity))
|
||||||
}
|
}
|
||||||
is GlobalSideEffect.ShareFile -> context.launchShareFile(sideEffect.file)
|
|
||||||
is GlobalSideEffect.Snackbar ->
|
is GlobalSideEffect.Snackbar ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbar.showSnackbar(sideEffect.message.asString(context))
|
snackbar.showSnackbar(sideEffect.message.asString(context))
|
||||||
@@ -271,7 +264,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Route.Lock
|
Route.Lock
|
||||||
else Route.TunnelsGraph,
|
else Route.TunnelsGraph,
|
||||||
) {
|
) {
|
||||||
composable<Route.Lock> { PinLockScreen() }
|
composable<Route.Lock> {
|
||||||
|
PinManager.initialize(context = this@MainActivity)
|
||||||
|
PinLockScreen()
|
||||||
|
}
|
||||||
navigation<Route.TunnelsGraph>(
|
navigation<Route.TunnelsGraph>(
|
||||||
startDestination = Route.Tunnels
|
startDestination = Route.Tunnels
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
|
|||||||
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
val tunnelId = intent.getIntExtra(NotificationManager.EXTRA_ID, 0)
|
||||||
if (tunnelId == STOP_ALL_TUNNELS_ID)
|
if (tunnelId == STOP_ALL_TUNNELS_ID)
|
||||||
return@launch tunnelManager.stopActiveTunnels()
|
return@launch tunnelManager.stopActiveTunnels()
|
||||||
tunnelRepository.getById(tunnelId)?.let { tunnelManager.stopTunnel(it.id) }
|
tunnelManager.stopTunnel(tunnelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -51,7 +51,8 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
|||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
Intent(context, MainActivity::class.java),
|
Intent(context, MainActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||||
PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -94,12 +95,12 @@ class WireGuardNotification @Inject constructor(@ApplicationContext override val
|
|||||||
val pendingIntent =
|
val pendingIntent =
|
||||||
PendingIntent.getBroadcast(
|
PendingIntent.getBroadcast(
|
||||||
context,
|
context,
|
||||||
0,
|
extraId ?: 0,
|
||||||
Intent(context, NotificationActionReceiver::class.java).apply {
|
Intent(context, NotificationActionReceiver::class.java).apply {
|
||||||
action = notificationAction.name
|
action = notificationAction.name
|
||||||
if (extraId != null) putExtra(EXTRA_ID, extraId)
|
if (extraId != null) putExtra(EXTRA_ID, extraId)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
)
|
)
|
||||||
return NotificationCompat.Action.Builder(
|
return NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_notification,
|
R.drawable.ic_notification,
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ abstract class BaseTunnel(@ApplicationScope protected val applicationScope: Coro
|
|||||||
) {
|
) {
|
||||||
tunStatusMutex.withLock {
|
tunStatusMutex.withLock {
|
||||||
activeTuns.update { currentTuns ->
|
activeTuns.update { currentTuns ->
|
||||||
|
if (!currentTuns.containsKey(tunnelId) && status != TunnelStatus.Starting) {
|
||||||
|
Timber.d("Ignoring update for inactive tunnel $tunnelId")
|
||||||
|
return@update currentTuns
|
||||||
|
}
|
||||||
val existingState = currentTuns[tunnelId] ?: TunnelState()
|
val existingState = currentTuns[tunnelId] ?: TunnelState()
|
||||||
val newStatus = status ?: existingState.status
|
val newStatus = status ?: existingState.status
|
||||||
if (newStatus == TunnelStatus.Down) {
|
if (newStatus == TunnelStatus.Down) {
|
||||||
|
|||||||
+99
-84
@@ -21,8 +21,9 @@ import kotlin.concurrent.atomics.AtomicBoolean
|
|||||||
import kotlin.concurrent.atomics.AtomicReference
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.amnezia.awg.crypto.Key
|
import org.amnezia.awg.crypto.Key
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ constructor(
|
|||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
|
private val monitoringMutex = Mutex()
|
||||||
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
|
private val monitoringJobs = ConcurrentHashMap<Int, Job>()
|
||||||
|
|
||||||
private data class SideEffectState(
|
private data class SideEffectState(
|
||||||
@@ -55,9 +57,6 @@ constructor(
|
|||||||
val condition: (SideEffectState) -> Boolean,
|
val condition: (SideEffectState) -> Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val sideEffectChannelFlow =
|
|
||||||
MutableStateFlow<Channel<SideEffectState>>(Channel(Channel.CONFLATED))
|
|
||||||
|
|
||||||
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
|
private val tunnelProviderFlow: StateFlow<TunnelProvider> = run {
|
||||||
val currentBackend = AtomicReference(userspaceTunnel)
|
val currentBackend = AtomicReference(userspaceTunnel)
|
||||||
val currentSettings = AtomicReference(GeneralSettings())
|
val currentSettings = AtomicReference(GeneralSettings())
|
||||||
@@ -105,61 +104,9 @@ constructor(
|
|||||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
|
override val activeTunnels: StateFlow<Map<Int, TunnelState>> = run {
|
||||||
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
|
val activeTunsReference: AtomicReference<Map<Int, TunnelState>> =
|
||||||
AtomicReference(emptyMap())
|
AtomicReference(emptyMap())
|
||||||
|
|
||||||
tunnelProviderFlow
|
tunnelProviderFlow
|
||||||
.flatMapLatest { backend ->
|
.flatMapLatest { backend ->
|
||||||
// Create a new channel for each backend to reset side-effect processing
|
|
||||||
val newChannel = Channel<SideEffectState>(Channel.CONFLATED)
|
|
||||||
sideEffectChannelFlow.value = newChannel
|
|
||||||
|
|
||||||
val sideEffects =
|
|
||||||
listOf(
|
|
||||||
SideEffectWithCondition(
|
|
||||||
effect = { s ->
|
|
||||||
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
|
|
||||||
},
|
|
||||||
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
|
|
||||||
),
|
|
||||||
SideEffectWithCondition(
|
|
||||||
effect = { s ->
|
|
||||||
handleTunnelsActiveChange(s.previouslyActive, s.activeTuns, s.tuns)
|
|
||||||
},
|
|
||||||
condition = { s -> s.activeTuns.size != s.previouslyActive.size },
|
|
||||||
),
|
|
||||||
// TODO Not for kernel mode for now
|
|
||||||
SideEffectWithCondition(
|
|
||||||
effect = { s -> handleTunnelMonitoringChanges(s.activeTuns, s.tuns) },
|
|
||||||
condition = { s ->
|
|
||||||
s.tuns.any {
|
|
||||||
it.restartOnPingFailure && s.activeTuns.keys.contains(it.id)
|
|
||||||
} && s.settings.appMode != AppMode.KERNEL
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SideEffectWithCondition(
|
|
||||||
effect = { s ->
|
|
||||||
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
|
|
||||||
},
|
|
||||||
condition = { s -> s.activeTuns.keys != s.previouslyActive.keys },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
for (state in newChannel) {
|
|
||||||
supervisorScope {
|
|
||||||
sideEffects
|
|
||||||
.filter { it.condition(state) }
|
|
||||||
.forEach { sideEffect ->
|
|
||||||
launch {
|
|
||||||
try {
|
|
||||||
sideEffect.effect(state)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Side effect failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combine(
|
combine(
|
||||||
backend.activeTunnels,
|
backend.activeTunnels,
|
||||||
tunnelsRepository.flow,
|
tunnelsRepository.flow,
|
||||||
@@ -171,9 +118,67 @@ constructor(
|
|||||||
.onStart { handleStateRestore() }
|
.onStart { handleStateRestore() }
|
||||||
.onEach { (activeTuns, tuns, settings) ->
|
.onEach { (activeTuns, tuns, settings) ->
|
||||||
val previouslyActive = activeTunsReference.exchange(activeTuns)
|
val previouslyActive = activeTunsReference.exchange(activeTuns)
|
||||||
sideEffectChannelFlow.value.trySend(
|
val state = SideEffectState(activeTuns, tuns, settings, previouslyActive)
|
||||||
SideEffectState(activeTuns, tuns, settings, previouslyActive)
|
|
||||||
)
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
supervisorScope {
|
||||||
|
val sideEffects =
|
||||||
|
listOf(
|
||||||
|
SideEffectWithCondition(
|
||||||
|
effect = { s ->
|
||||||
|
handleTunnelServiceChange(s.settings.appMode, s.activeTuns)
|
||||||
|
},
|
||||||
|
condition = { s ->
|
||||||
|
s.activeTuns.size != s.previouslyActive.size
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SideEffectWithCondition(
|
||||||
|
effect = { s ->
|
||||||
|
handleTunnelsActiveChange(
|
||||||
|
s.previouslyActive,
|
||||||
|
s.activeTuns,
|
||||||
|
s.tuns,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
condition = { s ->
|
||||||
|
s.activeTuns.size != s.previouslyActive.size
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// TODO Not for kernel mode for now
|
||||||
|
SideEffectWithCondition(
|
||||||
|
effect = { s ->
|
||||||
|
handleTunnelMonitoringChanges(s.activeTuns, s.tuns)
|
||||||
|
},
|
||||||
|
condition = { s ->
|
||||||
|
s.tuns.any {
|
||||||
|
it.restartOnPingFailure &&
|
||||||
|
s.activeTuns.keys.contains(it.id)
|
||||||
|
} && s.settings.appMode != AppMode.KERNEL
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SideEffectWithCondition(
|
||||||
|
effect = { s ->
|
||||||
|
handleFullTunnelMonitoring(s.activeTuns, s.tuns, s.settings)
|
||||||
|
},
|
||||||
|
condition = { s ->
|
||||||
|
s.activeTuns.keys != s.previouslyActive.keys
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
sideEffects
|
||||||
|
.filter { it.condition(state) }
|
||||||
|
.forEach { sideEffect ->
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
sideEffect.effect(state)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Side effect failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.map { (activeTuns, _, _) -> activeTuns }
|
.map { (activeTuns, _, _) -> activeTuns }
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -375,29 +380,39 @@ constructor(
|
|||||||
activeTuns: Map<Int, TunnelState>,
|
activeTuns: Map<Int, TunnelState>,
|
||||||
configs: List<TunnelConf>,
|
configs: List<TunnelConf>,
|
||||||
settings: GeneralSettings,
|
settings: GeneralSettings,
|
||||||
) {
|
) =
|
||||||
val activeIds = activeTuns.keys
|
monitoringMutex.withLock {
|
||||||
val obsoleteIds = monitoringJobs.keys - activeIds
|
val activeIds = activeTuns.keys.toSet()
|
||||||
obsoleteIds.forEach { id ->
|
val currentJobs = monitoringJobs.keys.toSet()
|
||||||
monitoringJobs[id]?.cancel()
|
val obsoleteIds = currentJobs - activeIds
|
||||||
monitoringJobs.remove(id)
|
|
||||||
|
Timber.d(
|
||||||
|
"Monitoring: Active IDs: $activeIds, Obsolete IDs: $obsoleteIds, Total jobs before: ${monitoringJobs.size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
obsoleteIds.forEach { id ->
|
||||||
|
monitoringJobs[id]?.cancel()
|
||||||
|
monitoringJobs.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIds.forEach { id ->
|
||||||
|
if (monitoringJobs.containsKey(id)) return@forEach // Skip if already monitored
|
||||||
|
val config = configs.find { it.id == id } ?: return@forEach
|
||||||
|
val tunStateFlow =
|
||||||
|
activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
||||||
|
val newJob =
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
tunnelMonitor.startMonitoring(
|
||||||
|
id,
|
||||||
|
withLogs = settings.appMode != AppMode.KERNEL,
|
||||||
|
tunStateFlow = tunStateFlow,
|
||||||
|
getStatistics = { tunnelId -> getStatistics(tunnelId) },
|
||||||
|
updateTunnelStatus = { tid, status, stats, pings, logHealth ->
|
||||||
|
updateTunnelStatus(tid, null, stats, pings, logHealth)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
monitoringJobs[id] = newJob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activeIds.forEach { id ->
|
|
||||||
if (monitoringJobs.contains(id)) return@forEach
|
|
||||||
configs.find { it.id == id } ?: return@forEach
|
|
||||||
val tunStateFlow = activeTunnels.map { it[id] }.stateIn(applicationScope + ioDispatcher)
|
|
||||||
monitoringJobs[id] =
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
tunnelMonitor.startMonitoring(
|
|
||||||
id,
|
|
||||||
withLogs = settings.appMode != AppMode.KERNEL,
|
|
||||||
tunStateFlow = tunStateFlow,
|
|
||||||
getStatistics = { tunnelId -> getStatistics(tunnelId) },
|
|
||||||
updateTunnelStatus = { tid, status, stats, pings, logHealth ->
|
|
||||||
updateTunnelStatus(tid, null, stats, pings, logHealth)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-33
@@ -139,6 +139,7 @@ constructor(
|
|||||||
val updates = ConcurrentMap<Key, PingState>()
|
val updates = ConcurrentMap<Key, PingState>()
|
||||||
|
|
||||||
pingablePeers.forEach { peer ->
|
pingablePeers.forEach { peer ->
|
||||||
|
ensureActive()
|
||||||
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState()
|
val previousState = pingStatsFlow.value[peer.publicKey] ?: PingState()
|
||||||
|
|
||||||
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
|
val allowedIpStr = peer.allowedIps.firstOrNull()?.toString()
|
||||||
@@ -172,41 +173,45 @@ constructor(
|
|||||||
|
|
||||||
val attemptTime = System.currentTimeMillis()
|
val attemptTime = System.currentTimeMillis()
|
||||||
runCatching {
|
runCatching {
|
||||||
val pingStats =
|
withTimeout(
|
||||||
settings.tunnelPingTimeoutSeconds?.let {
|
settings.tunnelPingTimeoutSeconds?.toMillis() ?: 5000L
|
||||||
networkUtils.pingWithStats(
|
) {
|
||||||
host,
|
val pingStats =
|
||||||
settings.tunnelPingAttempts,
|
settings.tunnelPingTimeoutSeconds?.let {
|
||||||
it.toMillis(),
|
networkUtils.pingWithStats(
|
||||||
)
|
host,
|
||||||
}
|
settings.tunnelPingAttempts,
|
||||||
?: networkUtils.pingWithStats(
|
it.toMillis(),
|
||||||
host,
|
)
|
||||||
settings.tunnelPingAttempts,
|
}
|
||||||
)
|
?: networkUtils.pingWithStats(
|
||||||
|
host,
|
||||||
|
settings.tunnelPingAttempts,
|
||||||
|
)
|
||||||
|
|
||||||
updates[peer.publicKey] =
|
updates[peer.publicKey] =
|
||||||
previousState.copy(
|
previousState.copy(
|
||||||
transmitted = pingStats.transmitted,
|
transmitted = pingStats.transmitted,
|
||||||
received = pingStats.received,
|
received = pingStats.received,
|
||||||
packetLoss = pingStats.packetLoss,
|
packetLoss = pingStats.packetLoss,
|
||||||
rttMin = pingStats.rttMin,
|
rttMin = pingStats.rttMin,
|
||||||
rttMax = pingStats.rttMax,
|
rttMax = pingStats.rttMax,
|
||||||
rttAvg = pingStats.rttAvg,
|
rttAvg = pingStats.rttAvg,
|
||||||
rttStddev = pingStats.rttStddev,
|
rttStddev = pingStats.rttStddev,
|
||||||
isReachable = pingStats.isReachable,
|
isReachable = pingStats.isReachable,
|
||||||
failureReason =
|
failureReason =
|
||||||
if (pingStats.isReachable) null
|
if (pingStats.isReachable) null
|
||||||
else FailureReason.PingFailed,
|
else FailureReason.PingFailed,
|
||||||
lastSuccessfulPingMillis =
|
lastSuccessfulPingMillis =
|
||||||
pingStats.lastSuccessfulPingMillis
|
pingStats.lastSuccessfulPingMillis
|
||||||
?: previousState.lastSuccessfulPingMillis,
|
?: previousState.lastSuccessfulPingMillis,
|
||||||
pingTarget = host,
|
pingTarget = host,
|
||||||
lastPingAttemptMillis = attemptTime,
|
lastPingAttemptMillis = attemptTime,
|
||||||
|
)
|
||||||
|
Timber.d(
|
||||||
|
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
|
||||||
)
|
)
|
||||||
Timber.d(
|
}
|
||||||
"Ping completed for peer ${peer.publicKey.toBase64().substring(0, 5)}.. to host $host with stats: $pingStats"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Timber.e(
|
Timber.e(
|
||||||
@@ -224,6 +229,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updates.isNotEmpty()) {
|
if (updates.isNotEmpty()) {
|
||||||
|
ensureActive()
|
||||||
pingStatsFlow.update { updates }
|
pingStatsFlow.update { updates }
|
||||||
updateTunnelStatus(tunnelConf.id, null, null, updates, null)
|
updateTunnelStatus(tunnelConf.id, null, null, updates, null)
|
||||||
}
|
}
|
||||||
@@ -236,6 +242,7 @@ constructor(
|
|||||||
delay(3_000L)
|
delay(3_000L)
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
|
ensureActive()
|
||||||
if (isNetworkConnected.value) {
|
if (isNetworkConnected.value) {
|
||||||
performPing()
|
performPing()
|
||||||
} else {
|
} else {
|
||||||
@@ -248,6 +255,7 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ensureActive()
|
||||||
updateTunnelStatus(tunnelConf.id, null, null, pingStatsFlow.value, null)
|
updateTunnelStatus(tunnelConf.id, null, null, pingStatsFlow.value, null)
|
||||||
}
|
}
|
||||||
delay(settings.tunnelPingIntervalSeconds.toMillis())
|
delay(settings.tunnelPingIntervalSeconds.toMillis())
|
||||||
@@ -264,7 +272,9 @@ constructor(
|
|||||||
) -> Unit,
|
) -> Unit,
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
|
ensureActive()
|
||||||
val stats = getStatistics(tunnelId)
|
val stats = getStatistics(tunnelId)
|
||||||
|
ensureActive()
|
||||||
updateTunnelStatus(tunnelId, null, stats, null, null)
|
updateTunnelStatus(tunnelId, null, stats, null, null)
|
||||||
delay(STATS_DELAY)
|
delay(STATS_DELAY)
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -14,8 +14,6 @@ sealed class GlobalSideEffect {
|
|||||||
|
|
||||||
data object PopBackStack : GlobalSideEffect()
|
data object PopBackStack : GlobalSideEffect()
|
||||||
|
|
||||||
data class ShareFile(val file: File) : GlobalSideEffect()
|
|
||||||
|
|
||||||
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
data class LaunchUrl(val url: String) : GlobalSideEffect()
|
||||||
|
|
||||||
data object ConfigChanged : GlobalSideEffect()
|
data object ConfigChanged : GlobalSideEffect()
|
||||||
|
|||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Save
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SubmitConfigurationTextBox(
|
|
||||||
value: String?,
|
|
||||||
label: String,
|
|
||||||
hint: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isErrorValue: (value: String?) -> Boolean,
|
|
||||||
onSubmit: (value: String) -> Unit,
|
|
||||||
supportingText: @Composable (() -> Unit)? = null,
|
|
||||||
keyboardOptions: KeyboardOptions =
|
|
||||||
KeyboardOptions(capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done),
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
var stateValue by remember { mutableStateOf(value ?: "") }
|
|
||||||
|
|
||||||
CustomTextField(
|
|
||||||
isError = isErrorValue(stateValue),
|
|
||||||
textStyle =
|
|
||||||
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
|
|
||||||
value = stateValue,
|
|
||||||
onValueChange = { stateValue = it },
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
supportingText = supportingText,
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
hint,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = modifier.fillMaxWidth().height(48.dp),
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions =
|
|
||||||
KeyboardActions(
|
|
||||||
onDone = {
|
|
||||||
onSubmit(stateValue)
|
|
||||||
keyboardController?.hide()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
trailing = {
|
|
||||||
if (!isErrorValue(stateValue) && isFocused) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onSubmit(stateValue)
|
|
||||||
keyboardController?.hide()
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val icon = Icons.Outlined.Save
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = icon.name,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+9
-6
@@ -24,12 +24,18 @@ fun PinLockScreen() {
|
|||||||
val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) }
|
val pinAlreadyExists by rememberSaveable { mutableStateOf(PinManager.pinExists()) }
|
||||||
var pinCreated by rememberSaveable { mutableStateOf(false) }
|
var pinCreated by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun onPinCorrect() {
|
||||||
|
sharedViewModel.authenticated()
|
||||||
|
navController.popBackStack()
|
||||||
|
navController.navigate(Route.TunnelsGraph)
|
||||||
|
}
|
||||||
|
|
||||||
PinLock(
|
PinLock(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
text =
|
text =
|
||||||
if (pinAlreadyExists || pinCreated) {
|
if (pinAlreadyExists) {
|
||||||
stringResource(id = R.string.enter_pin)
|
stringResource(id = R.string.enter_pin)
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.create_pin)
|
stringResource(id = R.string.create_pin)
|
||||||
@@ -38,11 +44,7 @@ fun PinLockScreen() {
|
|||||||
},
|
},
|
||||||
backgroundColor = MaterialTheme.colorScheme.surface,
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
textColor = MaterialTheme.colorScheme.onSurface,
|
textColor = MaterialTheme.colorScheme.onSurface,
|
||||||
onPinCorrect = {
|
onPinCorrect = { onPinCorrect() },
|
||||||
sharedViewModel.authenticated()
|
|
||||||
navController.popBackStack()
|
|
||||||
navController.navigate(Route.TunnelsGraph)
|
|
||||||
},
|
|
||||||
onPinIncorrect = {
|
onPinIncorrect = {
|
||||||
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
sharedViewModel.showToast(StringValue.StringResource(R.string.incorrect_pin))
|
||||||
},
|
},
|
||||||
@@ -50,6 +52,7 @@ fun PinLockScreen() {
|
|||||||
pinCreated = true
|
pinCreated = true
|
||||||
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
sharedViewModel.showToast(StringValue.StringResource(R.string.pin_created))
|
||||||
sharedViewModel.setPinLockEnabled(true)
|
sharedViewModel.setPinLockEnabled(true)
|
||||||
|
onPinCorrect()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
BackHandler(enabled = (!pinAlreadyExists && !pinCreated)) {
|
BackHandler(enabled = (!pinAlreadyExists && !pinCreated)) {
|
||||||
|
|||||||
-2
@@ -32,7 +32,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asString
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.SettingsViewModel
|
||||||
import org.orbitmvi.orbit.compose.collectSideEffect
|
import org.orbitmvi.orbit.compose.collectSideEffect
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
||||||
@@ -158,7 +157,6 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
|
|||||||
add(
|
add(
|
||||||
pinLockItem(settingsState.isPinLockEnabled) { enabled ->
|
pinLockItem(settingsState.isPinLockEnabled) { enabled ->
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
PinManager.initialize(context)
|
|
||||||
navController.navigate(Route.Lock)
|
navController.navigate(Route.Lock)
|
||||||
} else {
|
} else {
|
||||||
sharedViewModel.setPinLockEnabled(false)
|
sharedViewModel.setPinLockEnabled(false)
|
||||||
|
|||||||
+10
-1
@@ -69,7 +69,16 @@ fun LogsScreen(viewModel: LoggerViewModel = hiltViewModel()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showLogsSheet) {
|
if (showLogsSheet) {
|
||||||
LogsBottomSheet({ viewModel.exportLogs() }, { viewModel.deleteLogs() }) {
|
LogsBottomSheet(
|
||||||
|
{ uri ->
|
||||||
|
viewModel.exportLogs(uri)
|
||||||
|
showLogsSheet = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewModel.deleteLogs()
|
||||||
|
showLogsSheet = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
showLogsSheet = false
|
showLogsSheet = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -1,24 +1,53 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.FolderZip
|
import androidx.compose.material.icons.outlined.FolderZip
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.hasSAFSupport
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsBottomSheet(onExport: () -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit) {
|
fun LogsBottomSheet(onExport: (file: Uri?) -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val selectedTunnelsExportLauncher =
|
||||||
|
rememberFileExportLauncherForResult(
|
||||||
|
mimeType = FileUtils.ZIP_FILE_MIME_TYPE,
|
||||||
|
onResult = { file ->
|
||||||
|
if (file != null) {
|
||||||
|
onExport(file)
|
||||||
|
} else onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fun handleFileExport() {
|
||||||
|
if (context.hasSAFSupport(FileUtils.ZIP_FILE_MIME_TYPE)) {
|
||||||
|
selectedTunnelsExportLauncher.launch(
|
||||||
|
"${Constants.BASE_LOG_FILE_NAME}_${BuildConfig.VERSION_NAME}_${BuildConfig.FLAVOR}.zip"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onExport(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomBottomSheet(
|
CustomBottomSheet(
|
||||||
listOf(
|
listOf(
|
||||||
SheetOption(
|
SheetOption(
|
||||||
Icons.Outlined.FolderZip,
|
Icons.Outlined.FolderZip,
|
||||||
stringResource(R.string.export_logs),
|
stringResource(R.string.export_logs),
|
||||||
onClick = onExport,
|
onClick = { handleFileExport() },
|
||||||
),
|
),
|
||||||
SheetOption(
|
SheetOption(
|
||||||
Icons.Outlined.Delete,
|
Icons.Outlined.Delete,
|
||||||
|
|||||||
+7
-2
@@ -11,9 +11,9 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileExportLauncherForResult
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.hasSAFSupport
|
import com.zaneschepke.wireguardautotunnel.util.extensions.hasSAFSupport
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExportTunnelsBottomSheet(
|
fun ExportTunnelsBottomSheet(
|
||||||
@@ -37,7 +37,12 @@ fun ExportTunnelsBottomSheet(
|
|||||||
|
|
||||||
fun handleFileExport() {
|
fun handleFileExport() {
|
||||||
if (context.hasSAFSupport(FileUtils.ZIP_FILE_MIME_TYPE)) {
|
if (context.hasSAFSupport(FileUtils.ZIP_FILE_MIME_TYPE)) {
|
||||||
selectedTunnelsExportLauncher.launch(Constants.DEFAULT_EXPORT_FILE_NAME)
|
val fileName =
|
||||||
|
when (exportConfigType) {
|
||||||
|
ConfigType.AM -> "am_export_${Instant.now().epochSecond}.zip"
|
||||||
|
ConfigType.WG -> "wg_export_${Instant.now().epochSecond}.zip"
|
||||||
|
}
|
||||||
|
selectedTunnelsExportLauncher.launch(fileName)
|
||||||
} else {
|
} else {
|
||||||
onExport(exportConfigType, null)
|
onExport(exportConfigType, null)
|
||||||
}
|
}
|
||||||
|
|||||||
+85
-10
@@ -1,16 +1,36 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
|
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.tunneloptions.components
|
||||||
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.SubmitConfigurationTextBox
|
import com.zaneschepke.wireguardautotunnel.ui.common.textbox.CustomTextField
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -18,18 +38,73 @@ fun pingConfigItem(tunnelConf: TunnelConf, onSubmit: (ip: String) -> Unit): Sele
|
|||||||
return SelectionItem(
|
return SelectionItem(
|
||||||
title = {},
|
title = {},
|
||||||
description = {
|
description = {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
var stateValue by remember { mutableStateOf(tunnelConf.pingTarget ?: "") }
|
||||||
|
val isError by
|
||||||
|
remember(stateValue) {
|
||||||
|
derivedStateOf {
|
||||||
|
stateValue.isNotBlank() &&
|
||||||
|
!stateValue.isValidIpv4orIpv6Address() &&
|
||||||
|
!Patterns.DOMAIN_NAME.matcher(stateValue).matches()
|
||||||
|
}
|
||||||
|
}
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
SubmitConfigurationTextBox(
|
CustomTextField(
|
||||||
value = tunnelConf.pingTarget,
|
isError = isError,
|
||||||
label = stringResource(R.string.set_custom_ping_target),
|
textStyle =
|
||||||
hint = stringResource(R.string.ip_or_hostname),
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
isErrorValue = {
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
it?.isNotBlank() == true &&
|
),
|
||||||
!it.isValidIpv4orIpv6Address() &&
|
value = stateValue,
|
||||||
!Patterns.DOMAIN_NAME.matcher(it).matches()
|
onValueChange = { stateValue = it },
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.set_custom_ping_target),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.ip_or_hostname),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
supportingText = { Text(stringResource(R.string.ping_target_description)) },
|
supportingText = { Text(stringResource(R.string.ping_target_description)) },
|
||||||
onSubmit = onSubmit,
|
modifier =
|
||||||
|
Modifier.padding(top = 5.dp, bottom = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 16.dp),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onSubmit(stateValue) }),
|
||||||
|
trailing = {
|
||||||
|
if (!isError) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onSubmit(stateValue)
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val icon = Icons.Outlined.Save
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = icon.name,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ object Constants {
|
|||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1 shl 10
|
||||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||||
|
|
||||||
const val DEFAULT_EXPORT_FILE_NAME = "wgtunnel-export.zip"
|
|
||||||
|
|
||||||
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
const val QR_CODE_NAME_PROPERTY = "# Name ="
|
||||||
|
|
||||||
const val FDROID_FLAVOR = "fdroid"
|
const val FDROID_FLAVOR = "fdroid"
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setTunnelOnEthernet(to: Boolean) = intent {
|
fun setTunnelOnEthernet(to: Boolean) = intent {
|
||||||
settingsRepository.save(state.generalSettings.copy(isTunnelOnMobileDataEnabled = to))
|
settingsRepository.save(state.generalSettings.copy(isTunnelOnEthernetEnabled = to))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDebounceDelay(to: Int) = intent {
|
fun setDebounceDelay(to: Int) = intent {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.zaneschepke.logcatter.LogReader
|
import com.zaneschepke.logcatter.LogReader
|
||||||
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.GlobalEffectRepository
|
||||||
@@ -11,7 +13,6 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
|
|||||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -71,18 +72,30 @@ constructor(
|
|||||||
globalEffectRepository.post(globalSideEffect)
|
globalEffectRepository.post(globalSideEffect)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportLogs() = intent {
|
fun exportLogs(uri: Uri?) = intent {
|
||||||
val result =
|
val result =
|
||||||
fileUtils.createNewShareFile(
|
fileUtils.createNewShareFile(
|
||||||
"${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip"
|
"${Constants.BASE_LOG_FILE_NAME}_${BuildConfig.VERSION_NAME}_${BuildConfig.FLAVOR}.zip"
|
||||||
)
|
)
|
||||||
result.onSuccess { file -> postSideEffect(GlobalSideEffect.ShareFile(file)) }
|
val onFailure = { action: Throwable ->
|
||||||
result.onFailure { error ->
|
Timber.e(action)
|
||||||
val message =
|
intent {
|
||||||
error.message?.let { StringValue.DynamicString(it) }
|
postSideEffect(
|
||||||
?: StringValue.StringResource(R.string.unknown_error)
|
GlobalSideEffect.Toast(
|
||||||
postSideEffect(GlobalSideEffect.Toast(message))
|
StringValue.StringResource(
|
||||||
|
R.string.export_failed,
|
||||||
|
": ${action.localizedMessage}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Unit
|
||||||
}
|
}
|
||||||
|
result.onSuccess { file ->
|
||||||
|
logReader.zipLogFiles(file.absolutePath)
|
||||||
|
fileUtils.exportFile(file, uri, FileUtils.ZIP_FILE_MIME_TYPE).onFailure(onFailure)
|
||||||
|
}
|
||||||
|
result.onFailure(onFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLogs() = intent {
|
fun deleteLogs() = intent {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<files-path name="files" path="." />
|
<files-path name="files" path="." />
|
||||||
<external-path name="external_files" path="." />
|
<external-path name="external_files" path="." />
|
||||||
<external-files-path name="apks" path="/" />
|
<external-files-path name="apks" path="/" />
|
||||||
|
<cache-path name="cache_files" path="external_files/" />
|
||||||
</paths>
|
</paths>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "4.0.0"
|
const val VERSION_NAME = "4.0.3"
|
||||||
const val VERSION_CODE = 40000
|
const val VERSION_CODE = 40003
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
What's new:
|
||||||
|
- Search domain tunnels fail to start bugfix
|
||||||
|
- DNS fallback to IPv4 on IPv4 only networks bugfix
|
||||||
|
- Ping target not editable bugfix
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
What's new:
|
||||||
|
- App lock crash bugfix
|
||||||
|
- Fdroid publishing bugfix
|
||||||
|
- Exporting logs bugfix
|
||||||
|
- Auto-tunnel ethernet toggle bugfix
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
What's new:
|
||||||
|
- Monitoring failing to shut down race bugfix
|
||||||
|
- Notifications stop action bugfix
|
||||||
|
- Notification relaunch activity when already active bugfix
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.37.3"
|
accompanist = "0.37.3"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
amneziawgAndroid = "2.1.8"
|
amneziawgAndroid = "2.1.9"
|
||||||
androidx-junit = "1.3.0"
|
androidx-junit = "1.3.0"
|
||||||
icmp4a = "1.0.0"
|
icmp4a = "1.0.0"
|
||||||
orbitCompose = "10.0.0"
|
orbitCompose = "10.0.0"
|
||||||
@@ -12,28 +12,28 @@ coreKtx = "1.17.0"
|
|||||||
datastorePreferences = "1.2.0-alpha02"
|
datastorePreferences = "1.2.0-alpha02"
|
||||||
desugar_jdk_libs = "2.1.5"
|
desugar_jdk_libs = "2.1.5"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
hiltAndroid = "2.57.1"
|
hiltAndroid = "2.57.2"
|
||||||
hiltCompiler = "1.3.0"
|
hiltCompiler = "1.3.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.9.0"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
ktorClientCore = "3.3.0"
|
ktorClientCore = "3.3.0"
|
||||||
lifecycle-runtime-compose = "2.9.4"
|
lifecycle-runtime-compose = "2.9.4"
|
||||||
material3 = "1.3.2"
|
material3 = "1.4.0"
|
||||||
navigationCompose = "2.9.4"
|
navigationCompose = "2.9.5"
|
||||||
pinLockCompose = "1.0.5"
|
pinLockCompose = "1.0.5"
|
||||||
qrose = "1.0.1"
|
qrose = "1.0.1"
|
||||||
roomVersion = "2.8.0"
|
roomVersion = "2.8.1"
|
||||||
semver4j = "3.1.0"
|
semver4j = "3.1.0"
|
||||||
slf4jAndroid = "1.7.36"
|
slf4jAndroid = "1.7.36"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.4.0"
|
tunnel = "1.4.0"
|
||||||
androidGradlePlugin = "8.12.0"
|
androidGradlePlugin = "8.11.1"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.20"
|
||||||
ksp = "2.2.20-2.0.3"
|
ksp = "2.2.20-2.0.3"
|
||||||
composeBom = "2025.09.00"
|
composeBom = "2025.09.01"
|
||||||
compose = "1.9.1"
|
compose = "1.9.2"
|
||||||
icons = "1.7.8"
|
icons = "1.7.8"
|
||||||
workRuntimeKtxVersion = "2.10.4"
|
workRuntimeKtxVersion = "2.10.5"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
coreSplashscreen = "1.0.1"
|
coreSplashscreen = "1.0.1"
|
||||||
gradlePlugins-grgit = "5.3.3"
|
gradlePlugins-grgit = "5.3.3"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface LogReader {
|
|||||||
|
|
||||||
fun stop()
|
fun stop()
|
||||||
|
|
||||||
fun zipLogFiles(path: String)
|
suspend fun zipLogFiles(path: String)
|
||||||
|
|
||||||
suspend fun deleteAndClearLogs()
|
suspend fun deleteAndClearLogs()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zaneschepke.logcatter
|
package com.zaneschepke.logcatter
|
||||||
|
|
||||||
|
import android.R.attr.path
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
import com.zaneschepke.logcatter.model.LogMessage
|
||||||
@@ -61,15 +62,13 @@ class LogcatManager(pid: Int, logDir: String, maxFileSize: Long, maxFolderSize:
|
|||||||
isStarted = false
|
isStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun zipLogFiles(path: String) {
|
override suspend fun zipLogFiles(path: String) {
|
||||||
logScope.launch {
|
val wasStarted = isStarted
|
||||||
val wasStarted = isStarted
|
stop()
|
||||||
stop()
|
fileManager.zipLogs(path)
|
||||||
fileManager.zipLogs(path)
|
if (wasStarted) {
|
||||||
if (wasStarted) {
|
logcatReader.clearLogs()
|
||||||
logcatReader.clearLogs()
|
start()
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user