mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 196d855579 | |||
| 230cd0adb8 | |||
| 33b51823ab | |||
| f333319576 | |||
| e6ad1531c9 | |||
| 030082df34 | |||
| a825a2f2a4 | |||
| aa1a344bb2 | |||
| 3aa03c1896 |
@@ -12,7 +12,6 @@ on:
|
||||
default: debug
|
||||
options:
|
||||
- debug
|
||||
- prerelease
|
||||
- nightly
|
||||
- release
|
||||
flavor:
|
||||
@@ -105,9 +104,6 @@ jobs:
|
||||
"release")
|
||||
./gradlew :app:assemble${flavor^}Release --info
|
||||
;;
|
||||
"prerelease")
|
||||
./gradlew :app:assemble${flavor^}Prerelease --info
|
||||
;;
|
||||
"nightly")
|
||||
./gradlew :app:assemble${flavor^}Nightly --info
|
||||
;;
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
|
||||
@@ -25,7 +25,6 @@ on:
|
||||
description: "GitHub release type"
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
default: release
|
||||
required: true
|
||||
@@ -60,7 +59,7 @@ jobs:
|
||||
flavor: fdroid
|
||||
|
||||
build-standalone:
|
||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'prerelease' || inputs.flavor == 'standalone' }}
|
||||
if: ${{ github.event_name == 'push' || inputs.release_type == 'release' || inputs.release_type == 'debug' || inputs.flavor == 'standalone' }}
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
@@ -109,7 +108,7 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/temp
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: android_artifacts_*
|
||||
path: ${{ github.workspace }}/temp
|
||||
@@ -124,11 +123,6 @@ jobs:
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: On prerelease release notes
|
||||
if: ${{ github.event_name != 'push' && inputs.release_type == 'prerelease' }}
|
||||
run: |
|
||||
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: |
|
||||
@@ -162,8 +156,8 @@ jobs:
|
||||
tag_name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
|
||||
name: ${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag_name }}
|
||||
draft: false
|
||||
prerelease: ${{ github.event_name != 'push' && inputs.release_type == 'prerelease' }}
|
||||
make_latest: ${{ github.event_name == 'push' || inputs.release_type == 'release' }}
|
||||
prerelease: false
|
||||
make_latest: true
|
||||
files: |
|
||||
${{ github.workspace }}/temp/**/*.apk
|
||||
env:
|
||||
|
||||
+11
-17
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -20,6 +22,8 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
defaultConfig {
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
@@ -27,15 +31,10 @@ android {
|
||||
versionCode = computeVersionCode()
|
||||
versionName = computeVersionName()
|
||||
|
||||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||||
|
||||
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
|
||||
|
||||
buildConfigField(
|
||||
"String[]",
|
||||
"LANGUAGES",
|
||||
"new String[]{ ${languageList().joinToString(separator = ", ") { "\"$it\"" }} }",
|
||||
)
|
||||
val languagesArray = buildLanguagesArray(languageList())
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ $languagesArray }")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
@@ -73,22 +72,15 @@ android {
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "WG Tunnel - Debug")
|
||||
resValue("string", "app_name", "WG Tunnel Debug")
|
||||
isDebuggable = true
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.debug\"")
|
||||
}
|
||||
|
||||
create(Constants.PRERELEASE) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".prerelease"
|
||||
resValue("string", "app_name", "WG Tunnel - Pre")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.pre\"")
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(buildTypes.getByName(Constants.RELEASE))
|
||||
applicationIdSuffix = ".nightly"
|
||||
resValue("string", "app_name", "WG Tunnel - Nightly")
|
||||
resValue("string", "app_name", "WG Tunnel Nightly")
|
||||
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
|
||||
}
|
||||
}
|
||||
@@ -114,7 +106,9 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#648DB3</color>
|
||||
</resources>
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -107,7 +107,6 @@ class MainActivity : AppCompatActivity() {
|
||||
val isTv = isRunningOnTv()
|
||||
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
|
||||
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
|
||||
|
||||
val navController = rememberNavController()
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
@@ -150,15 +149,6 @@ class MainActivity : AppCompatActivity() {
|
||||
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
|
||||
}
|
||||
|
||||
LaunchedEffect(tunnelError) {
|
||||
if (tunnelError == null) return@LaunchedEffect
|
||||
val message = tunnelError!!.second.toStringRes()
|
||||
val context = this@MainActivity
|
||||
snackbar.showSnackbar(
|
||||
context.getString(R.string.tunnel_error_template, context.getString(message))
|
||||
)
|
||||
}
|
||||
|
||||
with(appViewState) {
|
||||
LaunchedEffect(isConfigChanged) {
|
||||
if (isConfigChanged) {
|
||||
@@ -265,7 +255,7 @@ class MainActivity : AppCompatActivity() {
|
||||
SettingsAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen(appUiState, viewModel)
|
||||
LocationDisclosureScreen(viewModel)
|
||||
}
|
||||
composable<Route.AutoTunnel> {
|
||||
AutoTunnelScreen(appUiState, viewModel)
|
||||
@@ -328,4 +318,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
WireGuardAutoTunnel.setUiActive(true)
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
WireGuardAutoTunnel.setUiActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.Configuration
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
@@ -23,6 +20,10 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
@@ -50,7 +51,6 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
StrictMode.setThreadPolicy(
|
||||
@@ -90,30 +90,20 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
applicationScope.launch {
|
||||
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||
}
|
||||
applicationScope.cancel()
|
||||
tunnelManager.setBackendState(BackendState.INACTIVE, emptyList())
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
class AppLifecycleObserver : DefaultLifecycleObserver {
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered foreground")
|
||||
foreground = true
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Timber.d("Application entered background")
|
||||
foreground = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var foreground = false
|
||||
|
||||
fun isForeground(): Boolean {
|
||||
return foreground
|
||||
private val _uiActive = MutableStateFlow(false)
|
||||
|
||||
val uiActive: StateFlow<Boolean>
|
||||
get() = _uiActive
|
||||
|
||||
fun setUiActive(active: Boolean) {
|
||||
_uiActive.update { active }
|
||||
}
|
||||
|
||||
@Volatile private var lastActiveTunnels: List<Int> = emptyList()
|
||||
|
||||
+5
@@ -43,8 +43,13 @@ interface NotificationManager {
|
||||
fun show(notificationId: Int, notification: Notification)
|
||||
|
||||
companion object {
|
||||
const val AUTO_TUNNEL_LOCATION_PERMISSION_ID = 123
|
||||
const val AUTO_TUNNEL_LOCATION_SERVICES_ID = 124
|
||||
// For auto tunnel foreground notification
|
||||
const val AUTO_TUNNEL_NOTIFICATION_ID = 122
|
||||
// for tunnel foreground notification
|
||||
const val VPN_NOTIFICATION_ID = 100
|
||||
const val TUNNEL_STATUS_NOTIFICATION_ID = 101
|
||||
const val EXTRA_ID = "id"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ constructor(
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(autoTunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to unbind AutoTunnelService")
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
@@ -120,6 +122,8 @@ constructor(
|
||||
service.stop()
|
||||
try {
|
||||
context.unbindService(tunnelServiceConnection)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to stop TunnelForegroundService")
|
||||
} finally {
|
||||
_tunnelService.value = null
|
||||
}
|
||||
|
||||
+83
-48
@@ -3,11 +3,10 @@ package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
@@ -26,6 +25,7 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.zipWithPrevious
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -52,16 +52,12 @@ class AutoTunnelService : LifecycleService() {
|
||||
|
||||
private val autoTunnelStateFlow = MutableStateFlow(defaultState)
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
private var killSwitchJob: Job? = null
|
||||
|
||||
class LocalBinder(val service: AutoTunnelService) : Binder()
|
||||
|
||||
private val binder = LocalBinder(this)
|
||||
|
||||
private var isServiceRunning = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
launchWatcherNotification()
|
||||
@@ -80,22 +76,14 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (isServiceRunning) return
|
||||
isServiceRunning = true
|
||||
kotlin
|
||||
.runCatching {
|
||||
launchWatcherNotification()
|
||||
initWakeLock()
|
||||
startAutoTunnelJob()
|
||||
startAutoTunnelStateJob()
|
||||
killSwitchJob = startKillSwitchJob()
|
||||
}
|
||||
.onFailure { Timber.e(it) }
|
||||
launchWatcherNotification()
|
||||
startAutoTunnelJob()
|
||||
startAutoTunnelStateJob()
|
||||
killSwitchJob = startKillSwitchJob()
|
||||
startNotificationJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
isServiceRunning = false
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
@@ -143,32 +131,6 @@ class AutoTunnelService : LifecycleService() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(POWER_SERVICE) as PowerManager).run {
|
||||
val tag = this.javaClass.name
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
copy(
|
||||
isWifiConnected = connectivityState.wifiState.connected,
|
||||
isMobileDataConnected = connectivityState.cellularConnected,
|
||||
isEthernetConnected = connectivityState.ethernetConnected,
|
||||
wifiName = connectivityState.wifiState.ssid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startAutoTunnelStateJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
@@ -182,9 +144,9 @@ class AutoTunnelService : LifecycleService() {
|
||||
old.isKernelEnabled == new.isKernelEnabled
|
||||
} // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest {
|
||||
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).map {
|
||||
buildNetworkState(it)
|
||||
}
|
||||
networkMonitor.connectivityStateFlow
|
||||
.flowOn(ioDispatcher)
|
||||
.map(NetworkState::from)
|
||||
}
|
||||
.distinctUntilChanged(),
|
||||
) { double, networkState ->
|
||||
@@ -221,6 +183,79 @@ class AutoTunnelService : LifecycleService() {
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
// watch for changes to location permission and notify user it will impact auto-tunneling
|
||||
// TODO can add deeplinks later back to the app for fixing
|
||||
// TODO or a recheck button for location permission so we dont have to poll it
|
||||
private fun startNotificationJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
var locationServicesShown = false
|
||||
var locationPermissionsShown = false
|
||||
autoTunnelStateFlow.zipWithPrevious().collect { (previous, current) ->
|
||||
when (current.settings.wifiDetectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.DEFAULT,
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.LEGACY -> {
|
||||
with(current.networkState) {
|
||||
if (
|
||||
locationPermissionGranted == false &&
|
||||
(previous?.networkState?.locationPermissionGranted == true ||
|
||||
!locationServicesShown)
|
||||
) {
|
||||
locationServicesShown = true
|
||||
Timber.i("Detected location permission lost")
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_permissions_missing),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationServicesEnabled == false &&
|
||||
(previous?.networkState?.locationServicesEnabled == true ||
|
||||
!locationPermissionsShown)
|
||||
) {
|
||||
locationPermissionsShown = true
|
||||
Timber.i("Detected location services lost")
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.AUTO_TUNNEL,
|
||||
title = getString(R.string.warning),
|
||||
description =
|
||||
getString(R.string.location_services_not_detected),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationServicesEnabled == true &&
|
||||
previous?.networkState?.locationServicesEnabled == false
|
||||
) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_SERVICES_ID
|
||||
)
|
||||
}
|
||||
if (
|
||||
locationPermissionGranted == true &&
|
||||
previous?.networkState?.locationPermissionGranted == false
|
||||
) {
|
||||
notificationManager.remove(
|
||||
NotificationManager.AUTO_TUNNEL_LOCATION_PERMISSION_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startKillSwitchJob() =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collect {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
@@ -35,6 +36,8 @@ constructor(
|
||||
}
|
||||
|
||||
override suspend fun startBackend(tunnel: TunnelConf) {
|
||||
// name too long for kernel mode
|
||||
if (!tunnel.isNameKernelCompatible) throw BackendError.TunnelNameTooLong
|
||||
try {
|
||||
updateTunnelStatus(tunnel, TunnelStatus.Starting)
|
||||
backend.setState(tunnel, Tunnel.State.UP, tunnel.toWgConfig())
|
||||
|
||||
+50
-14
@@ -1,5 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
@@ -10,20 +14,13 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TunnelManager
|
||||
@Inject
|
||||
constructor(
|
||||
@@ -32,6 +29,7 @@ constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val notificationManager: NotificationManager,
|
||||
) : TunnelProvider {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -64,8 +62,46 @@ constructor(
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
|
||||
get() = tunnelProviderFlow.value.errorEvents
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>> =
|
||||
combine(
|
||||
tunnelProviderFlow.flatMapLatest { it.errorEvents },
|
||||
WireGuardAutoTunnel.uiActive,
|
||||
) { errorEvent, isEnabled ->
|
||||
if (isEnabled) errorEvent else null
|
||||
}
|
||||
.filterNotNull()
|
||||
.shareIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
replay = 0,
|
||||
)
|
||||
|
||||
// observe tunnel errors and launch notifications if ui is inactive
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
tunnelProviderFlow
|
||||
.flatMapLatest { it.errorEvents }
|
||||
.collect { (tunnelConf, error) ->
|
||||
if (!WireGuardAutoTunnel.uiActive.value) {
|
||||
val notification =
|
||||
notificationManager.createNotification(
|
||||
WireGuardNotification.NotificationChannels.VPN,
|
||||
title = StringValue.DynamicString(tunnelConf.name),
|
||||
description =
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
error.toStringRes(),
|
||||
),
|
||||
)
|
||||
notificationManager.show(
|
||||
NotificationManager.TUNNEL_STATUS_NOTIFICATION_ID,
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
|
||||
tunnelProviderFlow.value.bouncingTunnelIds
|
||||
@@ -106,7 +142,7 @@ constructor(
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||
}
|
||||
|
||||
fun restorePreviousState() =
|
||||
fun restorePreviousState(): Job =
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
|
||||
@@ -46,6 +46,6 @@ interface TunnelConfigDao {
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig ORDER BY position ASC")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
@Query("SELECT * FROM tunnelconfig ORDER BY position")
|
||||
fun getAllFlow(): Flow<List<TunnelConfig>>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
@@ -99,6 +100,7 @@ class TunnelModule {
|
||||
appDataRepository: AppDataRepository,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
notificationManager: NotificationManager,
|
||||
): TunnelManager {
|
||||
return TunnelManager(
|
||||
kernelTunnel,
|
||||
@@ -106,6 +108,7 @@ class TunnelModule {
|
||||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
notificationManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,23 @@ sealed class BackendError : Exception() {
|
||||
|
||||
data object KernelModuleName : BackendError()
|
||||
|
||||
data object InvalidConfig : BackendError()
|
||||
|
||||
data object NotAuthorized : BackendError()
|
||||
|
||||
data object ServiceNotRunning : BackendError()
|
||||
|
||||
data object Unknown : BackendError()
|
||||
|
||||
data object TunnelNameTooLong : BackendError()
|
||||
|
||||
fun toStringRes() =
|
||||
when (this) {
|
||||
Config -> R.string.config_error
|
||||
DNS -> R.string.dns_resolve_error
|
||||
InvalidConfig -> R.string.invalid_config_error
|
||||
KernelModuleName -> R.string.kernel_name_error
|
||||
NotAuthorized,
|
||||
Unauthorized -> R.string.auth_error
|
||||
ServiceNotRunning -> R.string.service_running_error
|
||||
Unknown -> R.string.unknown_error
|
||||
TunnelNameTooLong -> R.string.error_tunnel_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ data class TunnelConf(
|
||||
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
|
||||
val isNameKernelCompatible: Boolean = (name.length <= 15)
|
||||
|
||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||
stateChangeCallback = callback
|
||||
}
|
||||
@@ -95,6 +97,7 @@ data class TunnelConf(
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
.apply { stateChangeCallback = this@TunnelConf.stateChangeCallback }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.util.WifiSecurityType
|
||||
|
||||
data class NetworkState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val wifiName: String? = null,
|
||||
val isWifiSecure: Boolean? = null,
|
||||
val locationServicesEnabled: Boolean? = null,
|
||||
val locationPermissionGranted: Boolean? = null,
|
||||
) {
|
||||
fun hasNoCapabilities(): Boolean {
|
||||
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(connectivityState: ConnectivityState): NetworkState {
|
||||
return NetworkState(
|
||||
isWifiSecure =
|
||||
when (connectivityState.wifiState.securityType) {
|
||||
WifiSecurityType.OPEN,
|
||||
WifiSecurityType.UNKNOWN -> false
|
||||
null -> null
|
||||
else -> true
|
||||
},
|
||||
isWifiConnected = connectivityState.wifiState.connected,
|
||||
isMobileDataConnected = connectivityState.cellularConnected,
|
||||
isEthernetConnected = connectivityState.ethernetConnected,
|
||||
wifiName = connectivityState.wifiState.ssid,
|
||||
locationPermissionGranted = connectivityState.wifiState.locationPermissionsGranted,
|
||||
locationServicesEnabled = connectivityState.wifiState.locationServicesEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.banner
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
|
||||
@Composable
|
||||
fun WarningBanner(
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
AnimatedVisibility(visible = visible, enter = expandVertically(), exit = shrinkVertically()) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp)),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(start = 2.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.Start),
|
||||
modifier = Modifier.weight(4f, false).fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Warning,
|
||||
stringResource(R.string.warning),
|
||||
Modifier.size(18.dp),
|
||||
tint = Straw,
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
trailing?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
) {
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-23
@@ -2,34 +2,23 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
|
||||
@androidx.compose.runtime.Composable
|
||||
fun IconSurfaceButton(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
leadingIcon: ImageVector? = null,
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
description: String? = null,
|
||||
) {
|
||||
val border: BorderStroke? =
|
||||
@@ -64,15 +53,7 @@ fun IconSurfaceButton(
|
||||
modifier =
|
||||
Modifier.padding(vertical = if (description == null) 10.dp else 0.dp),
|
||||
) {
|
||||
leadingIcon?.let {
|
||||
Icon(
|
||||
leadingIcon,
|
||||
leadingIcon.name,
|
||||
Modifier.size(iconSize),
|
||||
if (selected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
leading?.invoke()
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||
description?.let {
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class SelectionItem(
|
||||
val leadingIcon: ImageVector? = null,
|
||||
val leading: (@Composable () -> Unit)? = null,
|
||||
val trailing: (@Composable () -> Unit)? = null,
|
||||
val title: (@Composable () -> Unit),
|
||||
val description: (@Composable () -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
val height: Int = 64,
|
||||
val modifier: Modifier = Modifier.height(64.dp),
|
||||
)
|
||||
|
||||
+8
-15
@@ -5,19 +5,18 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
|
||||
@Composable
|
||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>, modifier: Modifier = Modifier) {
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
@@ -25,9 +24,10 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier),
|
||||
.then(item.onClick?.let { modifier.clickable { it() } } ?: modifier),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -37,21 +37,14 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(4f, false).fillMaxWidth(),
|
||||
) {
|
||||
item.leadingIcon?.let { icon ->
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
modifier = Modifier.size(iconSize),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
item.leading?.invoke()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(start = if (item.leadingIcon != null) 16.dp else 0.dp)
|
||||
.padding(start = if (item.leading != null) 16.dp else 0.dp)
|
||||
.weight(1f)
|
||||
.padding(
|
||||
vertical = if (item.description == null) 16.dp else 6.dp
|
||||
|
||||
+4
-5
@@ -20,7 +20,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.BottomNavItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.isCurrentRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
@@ -31,7 +30,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
@Composable
|
||||
fun BottomNavbar(appUiState: AppUiState) {
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
val items =
|
||||
@@ -48,8 +46,9 @@ fun BottomNavbar(appUiState: AppUiState) {
|
||||
icon = Icons.Rounded.Bolt,
|
||||
onClick = {
|
||||
val route =
|
||||
if (appUiState.appState.isLocationDisclosureShown) Route.AutoTunnel
|
||||
else Route.LocationDisclosure
|
||||
if (appUiState.appState.isLocationDisclosureShown) {
|
||||
Route.AutoTunnel
|
||||
} else Route.LocationDisclosure
|
||||
navController.goFromRoot(route)
|
||||
},
|
||||
active = appUiState.isAutoTunnelActive,
|
||||
@@ -90,7 +89,7 @@ fun BottomNavbar(appUiState: AppUiState) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
}
|
||||
},
|
||||
onClick = { navController.goFromRoot(item.route) },
|
||||
onClick = item.onClick,
|
||||
selected = isSelected,
|
||||
enabled = true,
|
||||
label = null,
|
||||
|
||||
-39
@@ -25,8 +25,6 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.isCurrentRoute
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.NavBarState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Brick
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@@ -112,8 +110,6 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
when {
|
||||
backStackEntry.isCurrentRoute(Route.Main::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.tunnels)) },
|
||||
topTrailing = { TunnelActionBar() },
|
||||
route = Route.Main,
|
||||
@@ -121,36 +117,15 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.AutoTunnel::class) -> {
|
||||
val (icon, label, tint) =
|
||||
if (uiState.appSettings.isAutoTunnelEnabled) {
|
||||
Triple(Icons.Rounded.Stop, R.string.stop_auto, Brick)
|
||||
} else {
|
||||
Triple(Icons.Rounded.PlayArrow, R.string.start_auto, SilverTree)
|
||||
}
|
||||
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.auto_tunnel)) },
|
||||
topTrailing = {
|
||||
IconButton(
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(label),
|
||||
tint = tint,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
route = Route.AutoTunnel,
|
||||
)
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Logs::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = false,
|
||||
topTitle = { Text(stringResource(R.string.logs)) },
|
||||
topTrailing = {
|
||||
@@ -166,56 +141,42 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Settings::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.settings)) },
|
||||
route = Route.Settings,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Appearance::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.appearance)) },
|
||||
route = Route.Appearance,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Language::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.language)) },
|
||||
route = Route.Language,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Display::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.display_theme)) },
|
||||
route = Route.Display,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.WifiDetectionMethod::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.wifi_detection_method)) },
|
||||
route = Route.WifiDetectionMethod,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.KillSwitch::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.kill_switch)) },
|
||||
route = Route.KillSwitch,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Support::class) ->
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.support)) },
|
||||
route = Route.Support,
|
||||
)
|
||||
|
||||
+109
-59
@@ -1,93 +1,93 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.WarningBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.NetworkTunnelingItems
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WifiTunnelingItems
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchLocationServicesSettings
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
val context = LocalContext.current
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var showLocationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
isBackgroundLocationGranted = fineLocationState.status.isGranted
|
||||
}
|
||||
|
||||
fun isWifiNameReadable(): Boolean {
|
||||
return when {
|
||||
!isBackgroundLocationGranted || !fineLocationState.status.isGranted -> {
|
||||
showLocationDialog = true
|
||||
false
|
||||
val showLocationServicesWarning by
|
||||
remember(
|
||||
uiState.connectivityState?.wifiState,
|
||||
uiState.appSettings.trustedNetworkSSIDs,
|
||||
uiState.appSettings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
uiState.connectivityState?.wifiState?.locationServicesEnabled == false &&
|
||||
uiState.appSettings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
uiState.appSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
!context.isLocationServicesEnabled() -> {
|
||||
showLocationServicesAlertDialog = true
|
||||
false
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (isTv && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
val backgroundLocationState =
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
val showLocationPermissionsWarning by
|
||||
remember(
|
||||
uiState.connectivityState?.wifiState,
|
||||
uiState.appSettings.trustedNetworkSSIDs,
|
||||
uiState.appSettings.wifiDetectionMethod,
|
||||
) {
|
||||
derivedStateOf {
|
||||
uiState.connectivityState?.wifiState?.locationPermissionsGranted == false &&
|
||||
uiState.appSettings.wifiDetectionMethod.needsLocationPermissions() &&
|
||||
uiState.appSettings.trustedNetworkSSIDs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.appSettings.trustedNetworkSSIDs) { currentText = "" }
|
||||
|
||||
LocationServicesDialog(
|
||||
showLocationServicesAlertDialog,
|
||||
onDismiss = { showLocationServicesAlertDialog = false },
|
||||
onAttest = { showLocationServicesAlertDialog = false },
|
||||
)
|
||||
|
||||
BackgroundLocationDialog(
|
||||
showLocationDialog,
|
||||
onDismiss = { showLocationDialog = false },
|
||||
onAttest = { showLocationDialog = false },
|
||||
)
|
||||
if (showLocationDialog) {
|
||||
InfoDialog(
|
||||
onAttest = {
|
||||
context.launchAppSettings()
|
||||
showLocationDialog = false
|
||||
},
|
||||
onDismiss = { showLocationDialog = false },
|
||||
title = { Text(stringResource(R.string.location_permissions)) },
|
||||
body = { Text(stringResource(R.string.location_justification)) },
|
||||
confirmText = { Text(stringResource(R.string.open_settings)) },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -98,16 +98,66 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
.padding(vertical = 24.dp)
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
WarningBanner(
|
||||
stringResource(R.string.location_services_not_detected),
|
||||
showLocationServicesWarning,
|
||||
trailing = {
|
||||
TextButton({ context.launchLocationServicesSettings() }) {
|
||||
Text(
|
||||
stringResource(R.string.fix),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
WarningBanner(
|
||||
stringResource(R.string.location_permissions_missing),
|
||||
showLocationPermissionsWarning,
|
||||
trailing = {
|
||||
TextButton({ showLocationDialog = true }) {
|
||||
Text(
|
||||
stringResource(R.string.fix),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
val (title, buttonText, icon) =
|
||||
remember(uiState.isAutoTunnelActive) {
|
||||
when (uiState.isAutoTunnelActive) {
|
||||
true ->
|
||||
Triple(
|
||||
context.getString(R.string.auto_tunnel_running),
|
||||
context.getString(R.string.stop),
|
||||
Icons.Outlined.CheckCircle,
|
||||
)
|
||||
false ->
|
||||
Triple(
|
||||
context.getString(R.string.auto_tunnel_not_running),
|
||||
context.getString(R.string.start),
|
||||
Icons.Outlined.Info,
|
||||
)
|
||||
}
|
||||
}
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
WifiTunnelingItems(
|
||||
uiState,
|
||||
viewModel,
|
||||
currentText,
|
||||
{ currentText = it },
|
||||
{ isWifiNameReadable() },
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leading = { Icon(icon, null) },
|
||||
title = { Text(title) },
|
||||
trailing = {
|
||||
Button({ viewModel.handleEvent(AppEvent.ToggleAutoTunnel) }) {
|
||||
Text(buttonText, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
items = WifiTunnelingItems(uiState, viewModel, currentText) { currentText = it }
|
||||
)
|
||||
SectionDivider()
|
||||
SurfaceSelectionGroupButton(items = NetworkTunnelingItems(uiState, viewModel))
|
||||
SectionDivider()
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PauseCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,7 +24,7 @@ fun DebounceDelaySelector(currentDelay: Int, onEvent: (AppEvent) -> Unit) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PauseCircle,
|
||||
leading = { Icon(Icons.Outlined.PauseCircle, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.debounce_delay),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,7 +14,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
@Composable
|
||||
fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Settings,
|
||||
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.advanced_settings),
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PublicOff
|
||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||
import androidx.compose.material.icons.outlined.SignalCellular4Bar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,7 +22,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<SelectionItem> {
|
||||
return listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SignalCellular4Bar,
|
||||
leading = { Icon(Icons.Outlined.SignalCellular4Bar, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
@@ -58,7 +59,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnCellular) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
||||
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_ethernet),
|
||||
@@ -95,7 +96,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PublicOff,
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.stop_on_no_internet),
|
||||
|
||||
+5
-15
@@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
@@ -39,7 +38,6 @@ fun WifiTunnelingItems(
|
||||
viewModel: AppViewModel,
|
||||
currentText: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
isWifiNameReadable: () -> Boolean,
|
||||
): List<SelectionItem> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
@@ -48,7 +46,7 @@ fun WifiTunnelingItems(
|
||||
val baseItems =
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Wifi,
|
||||
leading = { Icon(Icons.Outlined.Wifi, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_wifi),
|
||||
@@ -111,7 +109,7 @@ fun WifiTunnelingItems(
|
||||
baseItems +
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.WifiFind,
|
||||
leading = { Icon(Icons.Outlined.WifiFind, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.wifi_detection_method),
|
||||
@@ -139,7 +137,7 @@ fun WifiTunnelingItems(
|
||||
onClick = { navController.navigate(Route.WifiDetectionMethod) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Filter1,
|
||||
leading = { Icon(Icons.Outlined.Filter1, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.use_wildcards),
|
||||
@@ -201,15 +199,7 @@ fun WifiTunnelingItems(
|
||||
onDelete = { viewModel.handleEvent(AppEvent.DeleteTrustedSSID(it)) },
|
||||
currentText = currentText,
|
||||
onSave = { ssid ->
|
||||
if (
|
||||
uiState.appSettings.wifiDetectionMethod ==
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
|
||||
uiState.appSettings.wifiDetectionMethod ==
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU ||
|
||||
isWifiNameReadable()
|
||||
) {
|
||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||
}
|
||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||
},
|
||||
onValueChange = onTextChange,
|
||||
supporting = {
|
||||
@@ -219,7 +209,7 @@ fun WifiTunnelingItems(
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKeyOff,
|
||||
leading = { Icon(Icons.Outlined.VpnKeyOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.kill_switch_off),
|
||||
+7
-14
@@ -9,24 +9,17 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.AppSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.LocationDisclosureHeader
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.SkipItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.appSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.components.skipItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
val navController = LocalNavController.current
|
||||
fun LocationDisclosureScreen(viewModel: AppViewModel) {
|
||||
|
||||
LaunchedEffect(Unit, appUiState) {
|
||||
if (appUiState.appState.isLocationDisclosureShown)
|
||||
navController.goFromRoot(Route.AutoTunnel)
|
||||
}
|
||||
LaunchedEffect(Unit) { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -34,7 +27,7 @@ fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
modifier = Modifier.fillMaxSize().padding(top = 18.dp).padding(horizontal = 24.dp),
|
||||
) {
|
||||
LocationDisclosureHeader()
|
||||
SurfaceSelectionGroupButton(items = listOf(AppSettingsItem(viewModel)))
|
||||
SurfaceSelectionGroupButton(items = listOf(SkipItem(viewModel)))
|
||||
SurfaceSelectionGroupButton(items = listOf(appSettingsItem()))
|
||||
SurfaceSelectionGroupButton(items = listOf(skipItem()))
|
||||
}
|
||||
}
|
||||
|
||||
+6
-16
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.com
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -11,31 +12,20 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun AppSettingsItem(viewModel: AppViewModel): SelectionItem {
|
||||
fun appSettingsItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.LocationOn,
|
||||
leading = { Icon(Icons.Outlined.LocationOn, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.launch_app_settings),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.launchAppSettings().also {
|
||||
viewModel.handleEvent(AppEvent.SetLocationDisclosureShown)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
context.launchAppSettings().also {
|
||||
viewModel.handleEvent(AppEvent.SetLocationDisclosureShown)
|
||||
}
|
||||
},
|
||||
trailing = { ForwardButton { context.launchAppSettings() } },
|
||||
onClick = { context.launchAppSettings() },
|
||||
)
|
||||
}
|
||||
|
||||
+7
-5
@@ -5,13 +5,15 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.goFromRoot
|
||||
|
||||
@Composable
|
||||
fun SkipItem(viewModel: AppViewModel): SelectionItem {
|
||||
fun skipItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
title = {
|
||||
Text(
|
||||
@@ -19,7 +21,7 @@ fun SkipItem(viewModel: AppViewModel): SelectionItem {
|
||||
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = { ForwardButton { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) } },
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetLocationDisclosureShown) },
|
||||
trailing = { ForwardButton { navController.goFromRoot(Route.AutoTunnel) } },
|
||||
onClick = { navController.goFromRoot(Route.AutoTunnel) },
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.EthernetTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.MobileDataTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.WifiTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.components.ethernetTunnelItem
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@Composable
|
||||
@@ -46,7 +46,7 @@ fun TunnelAutoTunnelScreen(
|
||||
items =
|
||||
buildList {
|
||||
add(MobileDataTunnelItem(tunnelConf, viewModel))
|
||||
add(EthernetTunnelItem(tunnelConf, viewModel))
|
||||
add(ethernetTunnelItem(tunnelConf, viewModel))
|
||||
add(
|
||||
WifiTunnelItem(tunnelConf, appSettings, viewModel, currentText) {
|
||||
currentText = it
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PhoneAndroid
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun MobileDataTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.PhoneAndroid,
|
||||
leading = { Icon(Icons.Outlined.PhoneAndroid, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.mobile_tunnel),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SettingsEthernet
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun EthernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
fun ethernetTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.SettingsEthernet,
|
||||
leading = { Icon(Icons.Outlined.SettingsEthernet, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.ethernet_tunnel),
|
||||
+4
-14
@@ -5,19 +5,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FolderZip
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.outlined.FolderZip
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -121,7 +111,7 @@ fun ExportTunnelsBottomSheet(viewModel: AppViewModel) {
|
||||
private fun ExportOptionRow(label: String, onClick: () -> Unit) {
|
||||
Row(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(10.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.FolderZip,
|
||||
imageVector = Icons.Outlined.FolderZip,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
|
||||
+1
-4
@@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -44,8 +43,6 @@ fun TunnelList(
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val sortedTunnels = remember(appUiState.tunnels) { appUiState.tunnels.sortedBy { it.position } }
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||
@@ -61,7 +58,7 @@ fun TunnelList(
|
||||
if (appUiState.tunnels.isEmpty()) {
|
||||
item { GettingStartedLabel(onClick = { context.openWebUrl(it) }) }
|
||||
}
|
||||
items(sortedTunnels, key = { it.id }) { tunnel ->
|
||||
items(appUiState.tunnels, key = { it.id }) { tunnel ->
|
||||
val tunnelState =
|
||||
remember(appUiState.activeTunnels) {
|
||||
appUiState.activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
||||
|
||||
+41
-8
@@ -18,7 +18,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
@@ -40,22 +42,54 @@ fun TunnelRowItem(
|
||||
isTv: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val leadingIconColor =
|
||||
remember(state) {
|
||||
if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray
|
||||
}
|
||||
|
||||
val (leadingIcon, size) =
|
||||
val (leadingIcon, size, typeDescription) =
|
||||
remember(tunnel) {
|
||||
when {
|
||||
tunnel.isPrimaryTunnel -> Pair(Icons.Rounded.Star, 16.dp)
|
||||
tunnel.isMobileDataTunnel -> Pair(Icons.Rounded.Smartphone, 16.dp)
|
||||
tunnel.isEthernetTunnel -> Pair(Icons.Rounded.SettingsEthernet, 16.dp)
|
||||
else -> Pair(Icons.Rounded.Circle, 14.dp)
|
||||
tunnel.isPrimaryTunnel ->
|
||||
Triple(Icons.Rounded.Star, 16.dp, context.getString(R.string.primary_tunnel))
|
||||
tunnel.isMobileDataTunnel ->
|
||||
Triple(
|
||||
Icons.Rounded.Smartphone,
|
||||
16.dp,
|
||||
context.getString(R.string.mobile_data_tunnel),
|
||||
)
|
||||
tunnel.isEthernetTunnel ->
|
||||
Triple(
|
||||
Icons.Rounded.SettingsEthernet,
|
||||
16.dp,
|
||||
context.getString(R.string.ethernet_tunnel),
|
||||
)
|
||||
else -> Triple(Icons.Rounded.Circle, 14.dp, context.getString(R.string.tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
// Status description based on tunnel state
|
||||
val statusDescription =
|
||||
remember(state) {
|
||||
if (state.status.isUpOrStarting()) {
|
||||
context.getString(R.string.active)
|
||||
} else {
|
||||
context.getString(R.string.inactive)
|
||||
}
|
||||
}
|
||||
|
||||
// Combined content description for accessibility
|
||||
val combinedContentDescription =
|
||||
stringResource(
|
||||
R.string.tunnel_item_description,
|
||||
tunnel.tunName,
|
||||
typeDescription,
|
||||
statusDescription,
|
||||
)
|
||||
|
||||
ExpandingRowListItem(
|
||||
modifier = modifier.semantics(mergeDescendants = true) { combinedContentDescription },
|
||||
leading = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -63,14 +97,14 @@ fun TunnelRowItem(
|
||||
) {
|
||||
if (isTv) {
|
||||
Checkbox(
|
||||
isSelected,
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelectedTunnel(tunnel) },
|
||||
modifier = Modifier.minimumInteractiveComponentSize().size(12.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
leadingIcon,
|
||||
stringResource(R.string.status),
|
||||
contentDescription = null,
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.size(size),
|
||||
)
|
||||
@@ -99,6 +133,5 @@ fun TunnelRowItem(
|
||||
}
|
||||
},
|
||||
isSelected = isSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ fun TunnelOptionsScreen(
|
||||
listOf(
|
||||
PrimaryTunnelItem(tunnelConf, viewModel),
|
||||
AutoTunnelingItem(tunnelConf),
|
||||
ServerIpv4Item(tunnelConf, viewModel),
|
||||
serverIpv4Item(tunnelConf, viewModel),
|
||||
SplitTunnelingItem(tunnelConf),
|
||||
)
|
||||
)
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -17,7 +18,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun AutoTunnelingItem(tunnelConf: TunnelConf): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Bolt,
|
||||
leading = { Icon(Icons.Outlined.Bolt, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.auto_tunneling),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NetworkPing
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun PingRestartItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.NetworkPing,
|
||||
leading = { Icon(Icons.Outlined.NetworkPing, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.restart_on_ping),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Star
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun PrimaryTunnelItem(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Star,
|
||||
leading = { Icon(Icons.Outlined.Star, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.primary_tunnel),
|
||||
|
||||
+6
-5
@@ -157,11 +157,12 @@ private fun ConfigTypeSelector(selectedOption: ConfigType, onOptionSelected: (Co
|
||||
}
|
||||
},
|
||||
colors =
|
||||
SegmentedButtonDefaults.colors()
|
||||
.copy(
|
||||
activeContainerColor = Color.White,
|
||||
inactiveContainerColor = Color.White,
|
||||
),
|
||||
SegmentedButtonDefaults.colors(
|
||||
activeContainerColor = Color.White,
|
||||
inactiveContainerColor = Color.White,
|
||||
activeContentColor = Color.Black,
|
||||
inactiveContentColor = Color.Black,
|
||||
),
|
||||
onCheckedChange = { onOptionSelected(entry) },
|
||||
checked = isActive,
|
||||
) {
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.CallSplit
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -17,7 +18,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun SplitTunnelingItem(tunnelConf: TunnelConf): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.AutoMirrored.Outlined.CallSplit,
|
||||
leading = { Icon(Icons.AutoMirrored.Outlined.CallSplit, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.splt_tunneling),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.compon
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun ServerIpv4Item(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
fun serverIpv4Item(tunnelConf: TunnelConf, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Dns,
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.server_ipv4),
|
||||
+6
-6
@@ -20,15 +20,15 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AlwaysOnVpnItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppShortcutsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppearanceItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.KernelModeItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.KillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocalLoggingItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.PinLockItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ReadLogsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.RestartAtBootItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.alwaysOnVpnItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.appearanceItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.killSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@@ -64,8 +64,8 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
items =
|
||||
buildList {
|
||||
add(AppShortcutsItem(uiState, viewModel))
|
||||
if (!isTv) add(AlwaysOnVpnItem(uiState, viewModel))
|
||||
add(KillSwitchItem())
|
||||
if (!isTv) add(alwaysOnVpnItem(uiState, viewModel))
|
||||
add(killSwitchItem())
|
||||
add(RestartAtBootItem(uiState, viewModel))
|
||||
}
|
||||
)
|
||||
@@ -73,7 +73,7 @@ fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
SurfaceSelectionGroupButton(
|
||||
items =
|
||||
buildList {
|
||||
add(AppearanceItem())
|
||||
add(appearanceItem())
|
||||
add(LocalLoggingItem(uiState, viewModel))
|
||||
if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem())
|
||||
add(PinLockItem(uiState, viewModel))
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,7 +24,7 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt
|
||||
val clipboardManager = rememberClipboardHelper()
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.SmartToy,
|
||||
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appState.isRemoteControlEnabled,
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Contrast
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun DisplayThemeItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Contrast,
|
||||
leading = { Icon(Icons.Outlined.Contrast, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.display_theme),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Translate
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun LanguageItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Translate,
|
||||
leading = { Icon(Icons.Outlined.Translate, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.language),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Notifications
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSet
|
||||
fun NotificationsItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Notifications,
|
||||
leading = { Icon(Icons.Outlined.Notifications, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.notifications),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AppShortcut
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun AppShortcutsItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.AppShortcut,
|
||||
leading = { Icon(Icons.Filled.AppShortcut, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appSettings.isShortcutsEnabled,
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
|
||||
@Composable
|
||||
fun BackgroundLocationDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
val alwaysOnDescription = buildAnnotatedString {
|
||||
append(stringResource(R.string.background_location_message))
|
||||
append(" ")
|
||||
pushStringAnnotation(tag = "appSettings", annotation = "")
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.app_settings))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.background_location_message2))
|
||||
append(".")
|
||||
}
|
||||
InfoDialog(
|
||||
onDismiss = { onDismiss() },
|
||||
onAttest = { onDismiss() },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
ClickableText(
|
||||
text = alwaysOnDescription,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
) {
|
||||
alwaysOnDescription
|
||||
.getStringAnnotations(tag = "appSettings", it, it)
|
||||
.firstOrNull()
|
||||
?.let { context.launchAppSettings() }
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun KernelModeItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Code,
|
||||
leading = { Icon(Icons.Outlined.Code, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel),
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnKeyOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun KillSwitchItem(): SelectionItem {
|
||||
fun killSwitchItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKeyOff,
|
||||
leading = { Icon(Icons.Outlined.VpnKeyOff, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kill_switch_options),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ViewHeadline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun LocalLoggingItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.ViewHeadline,
|
||||
leading = { Icon(Icons.Outlined.ViewHeadline, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(stringResource(R.string.local_logging), SelectionLabelType.TITLE)
|
||||
},
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Pin
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -32,7 +33,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
}
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Pin,
|
||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.enable_app_lock),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ViewTimeline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
fun ReadLogsItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.ViewTimeline,
|
||||
leading = { Icon(Icons.Filled.ViewTimeline, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(stringResource(R.string.read_logs), SelectionLabelType.TITLE)
|
||||
},
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun RestartAtBootItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Restore,
|
||||
leading = { Icon(Icons.Outlined.Restore, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appSettings.isRestoreOnBootEnabled,
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnLock
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,9 +15,9 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun AlwaysOnVpnItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
fun alwaysOnVpnItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnLock,
|
||||
leading = { Icon(Icons.Outlined.VpnLock, contentDescription = null) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled =
|
||||
+3
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ViewQuilt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun AppearanceItem(): SelectionItem {
|
||||
fun appearanceItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.AutoMirrored.Outlined.ViewQuilt,
|
||||
leading = { Icon(Icons.AutoMirrored.Outlined.ViewQuilt, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.appearance),
|
||||
+3
-1
@@ -11,7 +11,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.LanTrafficItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.VpnKillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components.nativeKillSwitchItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@@ -26,7 +28,7 @@ fun KillSwitchScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
modifier = Modifier.fillMaxSize().padding(vertical = 24.dp).padding(horizontal = 12.dp),
|
||||
) {
|
||||
if (!isTv) {
|
||||
SurfaceSelectionGroupButton(items = listOf(NativeKillSwitchItem()))
|
||||
SurfaceSelectionGroupButton(items = listOf(nativeKillSwitchItem()))
|
||||
SectionDivider()
|
||||
}
|
||||
SurfaceSelectionGroupButton(
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lan
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +17,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
@Composable
|
||||
fun LanTrafficItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Lan,
|
||||
leading = { Icon(Icons.Outlined.Lan, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.allow_lan_traffic),
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.compo
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.VpnKey
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -14,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
@Composable
|
||||
fun VpnKillSwitchItem(uiState: AppUiState, toggleVpnSwitch: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.VpnKey,
|
||||
leading = { Icon(Icons.Outlined.VpnKey, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.vpn_kill_switch),
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -13,10 +14,10 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||
|
||||
@Composable
|
||||
fun NativeKillSwitchItem(): SelectionItem {
|
||||
fun nativeKillSwitchItem(): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.AdminPanelSettings,
|
||||
leading = { Icon(Icons.Outlined.AdminPanelSettings, contentDescription = null) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.native_kill_switch),
|
||||
+29
-7
@@ -1,9 +1,12 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.Mail
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -14,6 +17,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
@@ -26,7 +30,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
addAll(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.matrix),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.matrix),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.join_matrix),
|
||||
@@ -41,7 +51,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
onClick = { context.openWebUrl(context.getString(R.string.matrix_url)) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.telegram),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.telegram),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.join_telegram),
|
||||
@@ -58,7 +74,13 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = ImageVector.vectorResource(R.drawable.github),
|
||||
leading = {
|
||||
Icon(
|
||||
ImageVector.vectorResource(R.drawable.github),
|
||||
contentDescription = null,
|
||||
Modifier.size(iconSize),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.open_issue),
|
||||
@@ -73,7 +95,7 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
onClick = { context.openWebUrl(context.getString(R.string.github_url)) },
|
||||
),
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Mail,
|
||||
leading = { Icon(Icons.Outlined.Mail, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.email_description),
|
||||
@@ -88,7 +110,7 @@ fun ContactSupportOptions(context: android.content.Context) {
|
||||
if (BuildConfig.FLAVOR != Constants.GOOGLE_PLAY_FLAVOR) {
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Favorite,
|
||||
leading = { Icon(Icons.Outlined.Favorite, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.donate),
|
||||
|
||||
+7
-3
@@ -4,6 +4,10 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Balance
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Policy
|
||||
import androidx.compose.material.icons.outlined.Balance
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Policy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -24,7 +28,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
buildList {
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Book,
|
||||
leading = { Icon(Icons.Outlined.Book, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.docs_description),
|
||||
@@ -41,7 +45,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
)
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Policy,
|
||||
leading = { Icon(Icons.Outlined.Policy, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.privacy_policy),
|
||||
@@ -60,7 +64,7 @@ fun GeneralSupportOptions(context: android.content.Context) {
|
||||
)
|
||||
add(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.Balance,
|
||||
leading = { Icon(Icons.Outlined.Balance, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.licenses),
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.outlined.CloudDownload
|
||||
import androidx.compose.material.icons.rounded.CloudDownload
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
@@ -18,7 +20,7 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
leadingIcon = Icons.Filled.CloudDownload,
|
||||
leading = { Icon(Icons.Outlined.CloudDownload, contentDescription = null) },
|
||||
title = {
|
||||
SelectionItemLabel(
|
||||
stringResource(R.string.check_for_update),
|
||||
|
||||
@@ -6,7 +6,7 @@ val OffWhite = Color(0xFFF2F2F4)
|
||||
val CoolGray = Color(0xFF8D9D9F)
|
||||
val LightGrey = Color(0xFFECEDEF)
|
||||
val Aqua = Color(0xFF76BEBD)
|
||||
val Plantation = Color(0xFF264A49)
|
||||
val Plantation = Color(0xFF2E3538)
|
||||
val Shark = Color(0xFF21272A)
|
||||
val BalticSea = Color(0xFF1C1B1F)
|
||||
|
||||
|
||||
@@ -14,7 +14,18 @@ sealed class StringValue {
|
||||
return when (this) {
|
||||
is Empty -> ""
|
||||
is DynamicString -> value
|
||||
is StringResource -> context?.getString(resId, *args).orEmpty()
|
||||
is StringResource -> {
|
||||
val stringArgs =
|
||||
args
|
||||
.map { arg ->
|
||||
when (arg) {
|
||||
is Int -> context?.getString(arg) ?: arg.toString()
|
||||
else -> arg
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
context?.getString(resId, *stringArgs).orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -176,14 +176,19 @@ fun Context.launchSettings(): Result<Unit> {
|
||||
}
|
||||
|
||||
fun Context.launchAppSettings() {
|
||||
kotlin.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
kotlin
|
||||
.runCatching {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.onFailure {
|
||||
val fallback = Intent(Settings.ACTION_SETTINGS)
|
||||
startActivity(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.requestTunnelTileServiceStateUpdate() {
|
||||
|
||||
+7
-53
@@ -1,64 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.channels.ticker
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Chunks based on a time or size threshold.
|
||||
*
|
||||
* Borrowed from this
|
||||
* [Stack Overflow question](https://stackoverflow.com/questions/51022533/kotlin-chunk-sequence-based-on-size-and-time).
|
||||
*/
|
||||
@OptIn(ObsoleteCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||
fun <T> ReceiveChannel<T>.chunked(scope: CoroutineScope, size: Int, time: Duration) =
|
||||
scope.produce<List<T>> {
|
||||
while (true) { // this loop goes over each chunk
|
||||
val chunk = ConcurrentLinkedQueue<T>() // current chunk
|
||||
val ticker = ticker(time.toMillis()) // time-limit for this chunk
|
||||
try {
|
||||
whileSelect {
|
||||
ticker.onReceive {
|
||||
false // done with chunk when timer ticks, takes priority over received
|
||||
// elements
|
||||
}
|
||||
this@chunked.onReceive {
|
||||
chunk += it
|
||||
chunk.size < size // continue whileSelect if chunk is not full
|
||||
}
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
Timber.e(e)
|
||||
return@produce
|
||||
} finally {
|
||||
ticker.cancel()
|
||||
if (chunk.isNotEmpty()) {
|
||||
send(chunk.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <K, V> Flow<Map<K, V>>.distinctByKeys(): Flow<Map<K, V>> {
|
||||
return distinctUntilChanged { old, new -> old.keys == new.keys }
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
|
||||
flow.collect { value -> channel.send(value) }
|
||||
fun <T> Flow<T>.zipWithPrevious(): Flow<Pair<T?, T>> = flow {
|
||||
var previous: T? = null
|
||||
collect { current ->
|
||||
emit(previous to current)
|
||||
previous = current
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <R> StateFlow<AppUiState>.withFirstState(block: suspend (AppUiState) -> R): R {
|
||||
|
||||
@@ -65,7 +65,7 @@ constructor(
|
||||
private val logReader: LogReader,
|
||||
private val fileUtils: FileUtils,
|
||||
private val shortcutManager: ShortcutManager,
|
||||
networkMonitor: NetworkMonitor,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
) : ViewModel() {
|
||||
|
||||
private var logsJob: Job? = null
|
||||
@@ -78,7 +78,7 @@ constructor(
|
||||
private val _screenCallback = MutableStateFlow<(() -> Unit)?>(null)
|
||||
|
||||
private val _appViewState = MutableStateFlow(AppViewState())
|
||||
val appViewState = _appViewState.asStateFlow()
|
||||
val appViewState: StateFlow<AppViewState> = _appViewState.asStateFlow()
|
||||
|
||||
private val _uiEvent = MutableSharedFlow<UiEvent>()
|
||||
val uiEvent: SharedFlow<UiEvent> = _uiEvent.asSharedFlow()
|
||||
@@ -87,7 +87,7 @@ constructor(
|
||||
val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow()
|
||||
private val maxLogSize = Constants.MAX_LOG_SIZE
|
||||
|
||||
val uiState =
|
||||
val uiState: StateFlow<AppUiState> =
|
||||
combine(
|
||||
appDataRepository.settings.flow,
|
||||
appDataRepository.tunnels.flow,
|
||||
@@ -126,14 +126,15 @@ constructor(
|
||||
handleKillSwitchChange(state.appSettings)
|
||||
initServicesFromSavedState(state)
|
||||
if (state.appState.isLocalLogsEnabled) logsJob = startCollectingLogs()
|
||||
handleTunnelErrors()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUiEvent(event: UiEvent) =
|
||||
fun handleUiEvent(event: UiEvent): Job =
|
||||
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
|
||||
|
||||
fun handleEvent(event: AppEvent) =
|
||||
fun handleEvent(event: AppEvent): Job =
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
uiState.withFirstState { state ->
|
||||
when (event) {
|
||||
@@ -314,9 +315,17 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
private fun handleTunnelErrors() =
|
||||
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
|
||||
viewModelScope.launch {
|
||||
tunnelManager.errorEvents.collect { errorEvent ->
|
||||
handleShowMessage(
|
||||
StringValue.StringResource(
|
||||
R.string.tunnel_error_template,
|
||||
errorEvent.second.toStringRes(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) {
|
||||
if (tunnels.size == appDataRepository.tunnels.count()) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="always_on_vpn_support">Allow Always-On VPN </string>
|
||||
<string name="location_services_not_detected">Location Services Not Detected</string>
|
||||
<string name="location_services_not_detected">Location services not detected</string>
|
||||
<string name="db_name" translatable="false">wg-tunnel-db</string>
|
||||
<string name="auto_tunneling">Auto-tunneling</string>
|
||||
<string name="vpn_on">VPN on</string>
|
||||
@@ -175,7 +175,7 @@
|
||||
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||
<string name="auto_tunnel_channel_name">Auto-tunnel Notification Channel</string>
|
||||
<string name="auto_tunnel_channel_description">A channel for auto-tunnel state notifications</string>
|
||||
<string name="stop">stop</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="splt_tunneling">Split tunneling</string>
|
||||
<string name="tunnel_specific_settings">Tunnel specific settings</string>
|
||||
<string name="show_scripts">Show scripts</string>
|
||||
@@ -224,12 +224,12 @@
|
||||
<string name="security_template">Security: %1$s</string>
|
||||
<string name="current_template">Current: %1$s</string>
|
||||
<string name="flavor_template">Flavor: %1$s</string>
|
||||
<string name="config_error">config error</string>
|
||||
<string name="dns_resolve_error">dns resolution error</string>
|
||||
<string name="config_error">Invalid config</string>
|
||||
<string name="dns_resolve_error">DNS resolution failed</string>
|
||||
<string name="invalid_config_error">invalid_config_error</string>
|
||||
<string name="kernel_name_error">kernel module name error</string>
|
||||
<string name="auth_error">not authorized error</string>
|
||||
<string name="service_running_error">service not running error</string>
|
||||
<string name="kernel_name_error">Kernel module name error</string>
|
||||
<string name="auth_error">Unauthorized</string>
|
||||
<string name="service_running_error">Service not running</string>
|
||||
<string name="inactive">Inactive</string>
|
||||
<string name="active">Active</string>
|
||||
<string name="status">Status</string>
|
||||
@@ -285,4 +285,19 @@
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="move_up">Move Up</string>
|
||||
<string name="move_down">Move Down</string>
|
||||
<string name="error_tunnel_name">Tunnel name must be 15 characters or fewer in kernel mode</string>
|
||||
<string name="tunnel">tunnel</string>
|
||||
<string name="tunnel_item_description">1$s, %2$s, %3$s</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="location_permissions">Location Permissions</string>
|
||||
<string name="location_justification">In order to read Wi-Fi names in the background with your current detection
|
||||
method, Android requires apps to be given \'Allow all the time\' and \'Precise\' (Background Location on older
|
||||
devices) permission. Please enable these permissions in your Android app settings.
|
||||
</string>
|
||||
<string name="open_settings">Open Settings</string>
|
||||
<string name="location_permissions_missing">Location permissions missing</string>
|
||||
<string name="fix">Fix</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="auto_tunnel_running">Auto-tunnel is running</string>
|
||||
<string name="auto_tunnel_not_running">Auto-tunnel is not running</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#52357B</color>
|
||||
</resources>
|
||||
@@ -10,7 +10,6 @@ object Constants {
|
||||
// build types
|
||||
const val RELEASE = "release"
|
||||
const val NIGHTLY = "nightly"
|
||||
const val PRERELEASE = "prerelease"
|
||||
|
||||
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
|
||||
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
|
||||
|
||||
@@ -15,6 +15,11 @@ fun Project.languageList(): List<String> {
|
||||
.toList() + "en"
|
||||
}
|
||||
|
||||
|
||||
fun buildLanguagesArray(languages: List<String>): String {
|
||||
return languages.joinToString(separator = ", ") { "\"$it\"" }
|
||||
}
|
||||
|
||||
// Get the Git commit hash
|
||||
fun Project.getGitCommitHash(): String {
|
||||
var grgit: Grgit? = null
|
||||
@@ -47,21 +52,19 @@ fun Project.getCommitCountSinceLastCommit(): Int {
|
||||
}
|
||||
}
|
||||
|
||||
// Get versionCode increment for nightly/pre-release
|
||||
// Get versionCode increment for nightly
|
||||
fun Project.getVersionCodeIncrement(): Int {
|
||||
val isNightlyBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("nightly") }
|
||||
val isPreReleaseBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("prerelease") }
|
||||
if (!isNightlyBuild && !isPreReleaseBuild) return 0
|
||||
if (!isNightlyBuild) return 0
|
||||
|
||||
return System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull()
|
||||
?: System.getenv("CI_BUILD_NUMBER")?.toIntOrNull()
|
||||
?: getCommitCountSinceLastCommit()
|
||||
}
|
||||
|
||||
// Compute versionName dynamic bumping for nightly/pre-release
|
||||
// Compute versionName dynamic bumping for nightly
|
||||
fun Project.computeVersionName(): String {
|
||||
val isNightlyBuild = isNightlyBuild()
|
||||
val isPreReleaseBuild = isPrereleaseBuild()
|
||||
|
||||
// Static version from Constants.kt
|
||||
val baseVersion = Semver.parse(Constants.VERSION_NAME) ?: Semver.of(0, 0, 0)
|
||||
@@ -76,15 +79,6 @@ fun Project.computeVersionName(): String {
|
||||
)
|
||||
"${nightlyVersion}-nightly+git.${getGitCommitHash()}"
|
||||
}
|
||||
isPreReleaseBuild -> {
|
||||
// Bump minor for pre-release
|
||||
val preReleaseVersion = Semver.of(
|
||||
baseVersion.major,
|
||||
baseVersion.minor,
|
||||
0 + 1,
|
||||
)
|
||||
"${preReleaseVersion}-beta+git.${getGitCommitHash()}"
|
||||
}
|
||||
else -> Constants.VERSION_NAME
|
||||
}
|
||||
}
|
||||
@@ -93,19 +87,11 @@ fun Project.isNightlyBuild(): Boolean {
|
||||
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.NIGHTLY) }
|
||||
}
|
||||
|
||||
fun Project.isPrereleaseBuild(): Boolean {
|
||||
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.PRERELEASE) }
|
||||
}
|
||||
|
||||
// Compute versionCode (static baseline, dynamic bumping for nightly/pre-release)
|
||||
// Compute versionCode (static baseline, dynamic bumping for nightly)
|
||||
fun Project.computeVersionCode(): Int {
|
||||
val isNightlyBuild = isNightlyBuild()
|
||||
val isPreReleaseBuild = isPrereleaseBuild()
|
||||
var versionCode = Constants.VERSION_CODE
|
||||
|
||||
if (isPreReleaseBuild) {
|
||||
versionCode += 100 // Minor bump
|
||||
}
|
||||
if (isNightlyBuild) {
|
||||
versionCode += 1 // Patch bump
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Features
|
||||
|
||||
- Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||
- Split tunneling by application with search
|
||||
- WireGuard support for kernel and userspace modes
|
||||
- Amnezia support for userspace mode for DPI/censorship protection
|
||||
- Always-On VPN support
|
||||
- Export Amnezia and WireGuard tunnels to zip
|
||||
- Quick tile support for VPN toggling
|
||||
- Static shortcuts support for primary tunnel for automation integration
|
||||
- Intent automation support for all tunnels
|
||||
- Automatic service restart after reboot
|
||||
- Battery preservation measures
|
||||
@@ -0,0 +1,14 @@
|
||||
Features
|
||||
|
||||
- Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||
- Split tunneling by application with search
|
||||
- WireGuard support for kernel and userspace modes
|
||||
- Amnezia support for userspace mode for DPI/censorship protection
|
||||
- Always-On VPN support
|
||||
- Export Amnezia and WireGuard tunnels to zip
|
||||
- Quick tile support for VPN toggling
|
||||
- Static shortcuts support for primary tunnel for automation integration
|
||||
- Intent automation support for all tunnels
|
||||
- Automatic service restart after reboot
|
||||
- Battery preservation measures
|
||||
@@ -0,0 +1,14 @@
|
||||
Features
|
||||
|
||||
- Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
- Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||
- Split tunneling by application with search
|
||||
- WireGuard support for kernel and userspace modes
|
||||
- Amnezia support for userspace mode for DPI/censorship protection
|
||||
- Always-On VPN support
|
||||
- Export Amnezia and WireGuard tunnels to zip
|
||||
- Quick tile support for VPN toggling
|
||||
- Static shortcuts support for primary tunnel for automation integration
|
||||
- Intent automation support for all tunnels
|
||||
- Automatic service restart after reboot
|
||||
- Battery preservation measures
|
||||
@@ -0,0 +1 @@
|
||||
An alternative VPN client app for WireGuard with additional features
|
||||
@@ -15,7 +15,7 @@ hiltCompiler = "1.2.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
ktorClientCore = "3.1.3"
|
||||
lifecycle-runtime-compose = "2.9.1"
|
||||
lifecycle-runtime-compose = "2.9.2"
|
||||
material3 = "1.3.2"
|
||||
navigationCompose = "2.9.0"
|
||||
pinLockCompose = "1.0.4"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -23,15 +25,14 @@ android {
|
||||
)
|
||||
}
|
||||
|
||||
create(Constants.PRERELEASE) { initWith(getByName(Constants.RELEASE)) }
|
||||
|
||||
create(Constants.NIGHTLY) { initWith(getByName(Constants.RELEASE)) }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -22,7 +24,6 @@ android {
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
create(Constants.PRERELEASE) { initWith(getByName(Constants.RELEASE)) }
|
||||
|
||||
create(Constants.NIGHTLY) { initWith(getByName(Constants.RELEASE)) }
|
||||
}
|
||||
@@ -30,7 +31,8 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions { jvmTarget = Constants.JVM_TARGET }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -29,4 +29,11 @@ class ActiveWifiStateManager {
|
||||
fun getLatestValue(): Pair<Network?, NetworkCapabilities?>? {
|
||||
return _stateFlow.value.entries.lastOrNull()?.value
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
_stateFlow.update { currentMap ->
|
||||
linkedMapOf(*currentMap.toList().toTypedArray()).apply { clear() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+133
-107
@@ -16,11 +16,7 @@ import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
||||
import com.zaneschepke.networkmonitor.util.WIFI_SSID_SHELL_COMMAND
|
||||
import com.zaneschepke.networkmonitor.util.getCurrentSecurityType
|
||||
import com.zaneschepke.networkmonitor.util.getCurrentWifiName
|
||||
import com.zaneschepke.networkmonitor.util.getWifiSsid
|
||||
import com.zaneschepke.networkmonitor.util.isLocationServicesEnabled
|
||||
import com.zaneschepke.networkmonitor.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -32,15 +28,17 @@ class AndroidNetworkMonitor(
|
||||
private val applicationScope: CoroutineScope,
|
||||
) : NetworkMonitor {
|
||||
|
||||
private val actionPermissionCheck = "${appContext.packageName}.PERMISSION_CHECK"
|
||||
|
||||
interface ConfigurationListener {
|
||||
val detectionMethod: Flow<WifiDetectionMethod>
|
||||
val rootShell: RootShell
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
|
||||
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
|
||||
const val ANDROID_UNKNOWN_SSID = "<unknown ssid>"
|
||||
const val LOCATION_SERVICES_FILTER: String = "android.location.PROVIDERS_CHANGED"
|
||||
|
||||
const val ANDROID_UNKNOWN_SSID: String = "<unknown ssid>"
|
||||
}
|
||||
|
||||
enum class WifiDetectionMethod(val value: Int) {
|
||||
@@ -53,9 +51,12 @@ class AndroidNetworkMonitor(
|
||||
fun fromValue(value: Int): WifiDetectionMethod =
|
||||
entries.find { it.value == value } ?: DEFAULT
|
||||
}
|
||||
|
||||
fun needsLocationPermissions(): Boolean {
|
||||
return this == DEFAULT || this == LEGACY
|
||||
}
|
||||
}
|
||||
|
||||
private val packageName = appContext.packageName
|
||||
private val connectivityManager =
|
||||
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||
@@ -65,35 +66,72 @@ class AndroidNetworkMonitor(
|
||||
// Track active Wi-Fi networks, their capabilities, and last active network ID
|
||||
private val activeWifiNetworks = ActiveWifiStateManager()
|
||||
|
||||
private val permissionsChangedFlow = MutableStateFlow(false)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val wifiFlow: Flow<TransportEvent> =
|
||||
configurationListener.detectionMethod.flatMapLatest { detectionMethod
|
||||
-> // cancels previous flow
|
||||
Timber.d("Updated detectionMethod=$detectionMethod, recreating wifiFlow")
|
||||
createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method
|
||||
}
|
||||
combine(configurationListener.detectionMethod, permissionsChangedFlow) {
|
||||
detectionMethod,
|
||||
changed ->
|
||||
Pair(detectionMethod, changed)
|
||||
}
|
||||
.flatMapLatest { (detectionMethod, _) -> // cancels previous flow
|
||||
Timber.d("Permission or detection method changed, recreating wifiFlow")
|
||||
createWifiNetworkCallbackFlow(detectionMethod)
|
||||
}
|
||||
|
||||
private fun isAndroidTv(): Boolean =
|
||||
appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
private fun hasRequiredLocationPermissions(): Boolean {
|
||||
val fineLocationGranted =
|
||||
ContextCompat.checkSelfPermission(
|
||||
appContext,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val backgroundLocationGranted =
|
||||
if (
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) &&
|
||||
// exclude Android TV on Q as background location is not required on this
|
||||
// version
|
||||
!(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && isAndroidTv())
|
||||
) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
appContext,
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true // No need for ACCESS_BACKGROUND_LOCATION on Android P or Android TV on Q
|
||||
}
|
||||
return fineLocationGranted && backgroundLocationGranted
|
||||
}
|
||||
|
||||
private fun createWifiNetworkCallbackFlow(
|
||||
detectionMethod: WifiDetectionMethod
|
||||
): Flow<TransportEvent> = callbackFlow {
|
||||
val locationPermissionReceiver =
|
||||
|
||||
// The primary purpose of this receiver is to handle the case that the user enables location
|
||||
// permissions and then returns to the app
|
||||
// When this happens, we should check if permissions changed. If so, we need to requery
|
||||
// Wi-Fi name for the currently connected network
|
||||
val permissionReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.d(
|
||||
"locationPermissionReceiver received intent with action: ${intent.action}"
|
||||
)
|
||||
if (intent.action == "$packageName.$LOCATION_GRANTED") {
|
||||
Timber.d(
|
||||
"Received update: Precise and all-the-time location permissions are enabled"
|
||||
)
|
||||
activeWifiNetworks.getLatestValue()?.let { details ->
|
||||
trySend(
|
||||
TransportEvent.LocationPermissionGranted(
|
||||
details.first,
|
||||
details.second,
|
||||
detectionMethod,
|
||||
)
|
||||
if (intent.action == actionPermissionCheck) {
|
||||
val isGranted = hasRequiredLocationPermissions()
|
||||
Timber.d("Received permission check broadcast, isGranted: $isGranted")
|
||||
// get Wi-Fi info on permission change and update permission state
|
||||
if (
|
||||
connectivityStateFlow.replayCache
|
||||
.firstOrNull()
|
||||
?.wifiState
|
||||
?.locationPermissionsGranted != isGranted
|
||||
) {
|
||||
Timber.d(
|
||||
"Location permissions have changed, canceling and restarting callback flow"
|
||||
)
|
||||
activeWifiNetworks.clear()
|
||||
permissionsChangedFlow.update { !permissionsChangedFlow.value }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,54 +141,42 @@ class AndroidNetworkMonitor(
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == LOCATION_SERVICES_FILTER) {
|
||||
val isGpsEnabled =
|
||||
locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
?: false
|
||||
val isNetworkEnabled =
|
||||
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
?: false
|
||||
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
|
||||
Timber.d(
|
||||
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
|
||||
)
|
||||
activeWifiNetworks.getLatestValue()?.let { details ->
|
||||
trySend(
|
||||
TransportEvent.LocationServicesChanged(
|
||||
isLocationServicesEnabled,
|
||||
details.first,
|
||||
details.second,
|
||||
detectionMethod,
|
||||
)
|
||||
Timber.d("Received location services broadcast")
|
||||
val isLocationServicesEnabled = locationManager?.isLocationServicesEnabled()
|
||||
if (
|
||||
connectivityStateFlow.replayCache
|
||||
.firstOrNull()
|
||||
?.wifiState
|
||||
?.locationServicesEnabled != isLocationServicesEnabled
|
||||
) {
|
||||
Timber.d(
|
||||
"Location services have changed, canceling and restarting callback flow"
|
||||
)
|
||||
// trigger cancel and recreate of callbackFlow
|
||||
activeWifiNetworks.clear()
|
||||
permissionsChangedFlow.update { !permissionsChangedFlow.value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val permissionReceiverFlags =
|
||||
val receiverFlags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Context.RECEIVER_NOT_EXPORTED // Internal broadcast
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val servicesReceiverFlags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
Context.RECEIVER_EXPORTED // System broadcast
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
appContext.registerReceiver(
|
||||
locationPermissionReceiver,
|
||||
IntentFilter("$packageName.$LOCATION_GRANTED"),
|
||||
permissionReceiverFlags,
|
||||
permissionReceiver,
|
||||
IntentFilter(actionPermissionCheck),
|
||||
receiverFlags,
|
||||
)
|
||||
|
||||
appContext.registerReceiver(
|
||||
locationServicesReceiver,
|
||||
IntentFilter(LOCATION_SERVICES_FILTER),
|
||||
servicesReceiverFlags,
|
||||
receiverFlags,
|
||||
)
|
||||
|
||||
fun handleOnWifiLost(network: Network) {
|
||||
@@ -187,34 +213,36 @@ class AndroidNetworkMonitor(
|
||||
detectionMethod == WifiDetectionMethod.LEGACY ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
override fun onAvailable(network: Network) {
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
handleOnWifiLost(network)
|
||||
override fun onLost(network: Network) {
|
||||
handleOnWifiLost(network)
|
||||
}
|
||||
}
|
||||
}
|
||||
.also { Timber.d("Creating Wi-Fi callback without location info flags") }
|
||||
else ->
|
||||
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
if (detectionMethod != WifiDetectionMethod.DEFAULT)
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
override fun onAvailable(network: Network) {
|
||||
if (detectionMethod != WifiDetectionMethod.DEFAULT)
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
if (detectionMethod == WifiDetectionMethod.DEFAULT)
|
||||
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
|
||||
}
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
if (detectionMethod == WifiDetectionMethod.DEFAULT)
|
||||
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
handleOnWifiLost(network)
|
||||
override fun onLost(network: Network) {
|
||||
handleOnWifiLost(network)
|
||||
}
|
||||
}
|
||||
}
|
||||
.also { Timber.d("Creating Wi-Fi callback with location info flags") }
|
||||
}
|
||||
|
||||
val request =
|
||||
@@ -225,11 +253,19 @@ class AndroidNetworkMonitor(
|
||||
|
||||
connectivityManager?.registerNetworkCallback(request, callback)
|
||||
|
||||
trySend(TransportEvent.Unknown)
|
||||
trySend(
|
||||
TransportEvent.Permissions(
|
||||
permissions =
|
||||
Permissions(
|
||||
locationManager?.isLocationServicesEnabled() ?: false,
|
||||
hasRequiredLocationPermissions(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
awaitClose {
|
||||
runCatching {
|
||||
appContext.unregisterReceiver(locationPermissionReceiver)
|
||||
appContext.unregisterReceiver(permissionReceiver)
|
||||
appContext.unregisterReceiver(locationServicesReceiver)
|
||||
connectivityManager?.unregisterNetworkCallback(callback)
|
||||
}
|
||||
@@ -295,7 +331,7 @@ class AndroidNetworkMonitor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSsidByDetectionMethod(
|
||||
private suspend fun getSsidByDetectionMethod(
|
||||
detectionMethod: WifiDetectionMethod?,
|
||||
networkCapabilities: NetworkCapabilities?,
|
||||
): String {
|
||||
@@ -327,15 +363,11 @@ class AndroidNetworkMonitor(
|
||||
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
||||
}
|
||||
|
||||
override val connectivityStateFlow =
|
||||
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
||||
combine(
|
||||
wifiFlow.scan(
|
||||
WifiState(
|
||||
locationPermissionsGranted =
|
||||
ContextCompat.checkSelfPermission(
|
||||
appContext,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
) == PackageManager.PERMISSION_GRANTED,
|
||||
locationPermissionsGranted = hasRequiredLocationPermissions(),
|
||||
locationServicesEnabled =
|
||||
locationManager?.isLocationServicesEnabled() ?: false,
|
||||
)
|
||||
@@ -361,29 +393,16 @@ class AndroidNetworkMonitor(
|
||||
),
|
||||
securityType = wifiManager?.getCurrentSecurityType(),
|
||||
)
|
||||
is TransportEvent.LocationPermissionGranted ->
|
||||
is TransportEvent.Permissions -> {
|
||||
previous.copy(
|
||||
locationPermissionsGranted = true,
|
||||
ssid =
|
||||
getSsidByDetectionMethod(
|
||||
event.wifiDetectionMethod,
|
||||
event.networkCapabilities,
|
||||
),
|
||||
securityType = wifiManager?.getCurrentSecurityType(),
|
||||
)
|
||||
is TransportEvent.LocationServicesChanged ->
|
||||
previous.copy(
|
||||
locationServicesEnabled = event.enabled,
|
||||
ssid =
|
||||
getSsidByDetectionMethod(
|
||||
event.wifiDetectionMethod,
|
||||
event.networkCapabilities,
|
||||
),
|
||||
securityType = wifiManager?.getCurrentSecurityType(),
|
||||
locationPermissionsGranted =
|
||||
event.permissions.locationPermissionGranted,
|
||||
locationServicesEnabled = event.permissions.locationServicesEnabled,
|
||||
)
|
||||
}
|
||||
is TransportEvent.Lost ->
|
||||
previous.copy(connected = false, securityType = null, ssid = null)
|
||||
TransportEvent.Unknown -> previous
|
||||
is TransportEvent.Unknown -> previous
|
||||
}
|
||||
},
|
||||
cellularFlow,
|
||||
@@ -400,4 +419,11 @@ class AndroidNetworkMonitor(
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||
|
||||
override fun checkPermissionsAndUpdateState() {
|
||||
val action = actionPermissionCheck
|
||||
val intent = Intent(action)
|
||||
Timber.d("Sending broadcast: $action")
|
||||
appContext.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,8 @@ data class WifiState(
|
||||
val locationPermissionsGranted: Boolean,
|
||||
val locationServicesEnabled: Boolean,
|
||||
)
|
||||
|
||||
data class Permissions(
|
||||
val locationServicesEnabled: Boolean = false,
|
||||
val locationPermissionGranted: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -4,4 +4,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val connectivityStateFlow: Flow<ConnectivityState>
|
||||
|
||||
fun checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
@@ -17,18 +17,8 @@ sealed class TransportEvent {
|
||||
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
|
||||
) : TransportEvent()
|
||||
|
||||
data class LocationPermissionGranted(
|
||||
val network: Network?,
|
||||
val networkCapabilities: NetworkCapabilities?,
|
||||
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
|
||||
) : TransportEvent()
|
||||
|
||||
data class LocationServicesChanged(
|
||||
val enabled: Boolean,
|
||||
val network: Network?,
|
||||
val networkCapabilities: NetworkCapabilities?,
|
||||
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
|
||||
) : TransportEvent()
|
||||
data class Permissions(val permissions: com.zaneschepke.networkmonitor.Permissions) :
|
||||
TransportEvent()
|
||||
|
||||
data object Unknown : TransportEvent()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user