mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21e56cda80 | |||
| b5196fbf01 | |||
| e46fe93ae0 | |||
| 872ff83a12 | |||
| 5563292a87 | |||
| 8ba760a5ff | |||
| d431c2d39f | |||
| 33437ab237 | |||
| 4a432d2bb7 | |||
| 3df972d031 |
@@ -223,6 +223,8 @@ dependencies {
|
||||
// shizuku
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
implementation(libs.reorderable)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "505728bad740c12bab998a066b569333",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAutoTunnelEnabled",
|
||||
"columnName": "is_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "trustedNetworkSSIDs",
|
||||
"columnName": "trusted_network_ssids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
||||
"columnName": "is_always_on_vpn_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShortcutsEnabled",
|
||||
"columnName": "is_shortcuts_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnWifiEnabled",
|
||||
"columnName": "is_tunnel_on_wifi_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelEnabled",
|
||||
"columnName": "is_kernel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isRestoreOnBootEnabled",
|
||||
"columnName": "is_restore_on_boot_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiTunnelEnabled",
|
||||
"columnName": "is_multi_tunnel_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAmneziaEnabled",
|
||||
"columnName": "is_amnezia_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isWildcardsEnabled",
|
||||
"columnName": "is_wildcards_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStopOnNoInternetEnabled",
|
||||
"columnName": "is_stop_on_no_internet_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isVpnKillSwitchEnabled",
|
||||
"columnName": "is_vpn_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isKernelKillSwitchEnabled",
|
||||
"columnName": "is_kernel_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLanOnKillSwitchEnabled",
|
||||
"columnName": "is_lan_on_kill_switch_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "debounceDelaySeconds",
|
||||
"columnName": "debounce_delay_seconds",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
|
||||
"columnName": "is_disable_kill_switch_on_trusted_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isTunnelOnUnsecureEnabled",
|
||||
"columnName": "is_tunnel_on_unsecure_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "splitTunnelApps",
|
||||
"columnName": "split_tunnel_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "wifiDetectionMethod",
|
||||
"columnName": "wifi_detection_method",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "TunnelConfig",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wgQuick",
|
||||
"columnName": "wg_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tunnelNetworks",
|
||||
"columnName": "tunnel_networks",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMobileDataTunnel",
|
||||
"columnName": "is_mobile_data_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPrimaryTunnel",
|
||||
"columnName": "is_primary_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amQuick",
|
||||
"columnName": "am_quick",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "is_Active",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPingEnabled",
|
||||
"columnName": "is_ping_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingInterval",
|
||||
"columnName": "ping_interval",
|
||||
"affinity": "INTEGER",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isEthernetTunnel",
|
||||
"columnName": "is_ethernet_tunnel",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isIpv4Preferred",
|
||||
"columnName": "is_ipv4_preferred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TunnelConfig_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '505728bad740c12bab998a066b569333')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -27,7 +25,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -54,6 +51,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.Loca
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.sort.SortScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
|
||||
@@ -74,7 +72,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.exitProcess
|
||||
import org.amnezia.awg.backend.GoBackend.VpnService
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -322,6 +319,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,22 +328,4 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkPermissionAndNotify()
|
||||
}
|
||||
|
||||
private fun checkPermissionAndNotify() {
|
||||
val hasLocation =
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (lastLocationPermissionState != hasLocation) {
|
||||
Timber.d("Location permission changed to: $hasLocation")
|
||||
if (hasLocation) {
|
||||
networkMonitor.sendLocationPermissionsGrantedBroadcast()
|
||||
}
|
||||
lastLocationPermissionState = hasLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -8,7 +8,6 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
@@ -217,9 +216,8 @@ class TunnelForegroundService : LifecycleService() {
|
||||
}
|
||||
|
||||
private suspend fun startNetworkMonitorJob() {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
|
||||
val isAvailable = status !is NetworkStatus.Disconnected
|
||||
isNetworkConnected.value = isAvailable
|
||||
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status ->
|
||||
isNetworkConnected.value = status.hasConnectivity()
|
||||
Timber.d("Network available: $status")
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -7,8 +7,8 @@ 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.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
@@ -158,20 +158,13 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||
private fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
val wifiName =
|
||||
when (networkStatus) {
|
||||
is NetworkStatus.Connected -> {
|
||||
networkStatus.wifiSsid
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
copy(
|
||||
isWifiConnected = networkStatus.wifiConnected,
|
||||
isMobileDataConnected = networkStatus.cellularConnected,
|
||||
isEthernetConnected = networkStatus.ethernetConnected,
|
||||
wifiName = wifiName,
|
||||
isWifiConnected = connectivityState.wifiState.connected,
|
||||
isMobileDataConnected = connectivityState.cellularConnected,
|
||||
isEthernetConnected = connectivityState.ethernetConnected,
|
||||
wifiName = connectivityState.wifiState.ssid,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +182,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
old.isKernelEnabled == new.isKernelEnabled
|
||||
} // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest {
|
||||
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
|
||||
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).map {
|
||||
buildNetworkState(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 17,
|
||||
version = 18,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -32,6 +32,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
||||
@@ -46,5 +46,6 @@ interface TunnelConfigDao {
|
||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
@Query("SELECT * FROM tunnelconfig ORDER BY position ASC")
|
||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ data class TunnelConfig(
|
||||
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
|
||||
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
|
||||
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
|
||||
var isEthernetTunnel: Boolean = false,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
|
||||
var isIpv4Preferred: Boolean = true,
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ object TunnelConfigMapper {
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +43,7 @@ object TunnelConfigMapper {
|
||||
pingIp,
|
||||
isEthernetTunnel,
|
||||
isIpv4Preferred,
|
||||
position,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ data class TunnelConf(
|
||||
val pingIp: String? = null,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
val isIpv4Preferred: Boolean = true,
|
||||
val position: Int = 0,
|
||||
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
|
||||
|
||||
@@ -45,4 +45,6 @@ sealed class Route {
|
||||
@Serializable data class TunnelAutoTunnel(val id: Int) : Route()
|
||||
|
||||
@Serializable data object Logs : Route()
|
||||
|
||||
@Serializable data object Sort : Route()
|
||||
}
|
||||
|
||||
+6
-33
@@ -3,68 +3,41 @@ package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ExpandingRowListItem(
|
||||
leading: @Composable () -> Unit,
|
||||
text: String,
|
||||
onHold: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onDoubleClick: () -> Unit,
|
||||
trailing: @Composable () -> Unit,
|
||||
isSelected: Boolean,
|
||||
expanded: (@Composable () -> Unit)?,
|
||||
expanded: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.animateContentSize()
|
||||
modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.then(
|
||||
if (!isTv) {
|
||||
Modifier.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onHold()
|
||||
},
|
||||
onDoubleClick = onDoubleClick,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -84,7 +57,7 @@ fun ExpandingRowListItem(
|
||||
}
|
||||
trailing()
|
||||
}
|
||||
expanded?.invoke()
|
||||
expanded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-43
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.rounded.QuestionMark
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -68,53 +67,42 @@ fun BottomNavbar(appUiState: AppUiState) {
|
||||
onClick = { navController.goFromRoot(Route.Support) },
|
||||
),
|
||||
)
|
||||
// Define ripple configuration based on platform
|
||||
val rippleConfiguration =
|
||||
if (isTv) {
|
||||
RippleConfiguration()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Apply ripple configuration only if needed
|
||||
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) {
|
||||
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
|
||||
items.forEach { item ->
|
||||
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
|
||||
items.forEach { item ->
|
||||
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
if (item.active) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(
|
||||
modifier =
|
||||
Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
|
||||
containerColor = SilverTree,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
if (item.active) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(
|
||||
modifier = Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
|
||||
containerColor = SilverTree,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
) {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
}
|
||||
},
|
||||
onClick = { navController.goFromRoot(item.route) },
|
||||
selected = isSelected,
|
||||
enabled = true,
|
||||
label = null,
|
||||
alwaysShowLabel = false,
|
||||
colors =
|
||||
NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
indicatorColor = Color.Transparent,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(imageVector = item.icon, contentDescription = item.name)
|
||||
}
|
||||
},
|
||||
onClick = { navController.goFromRoot(item.route) },
|
||||
selected = isSelected,
|
||||
enabled = true,
|
||||
label = null,
|
||||
alwaysShowLabel = false,
|
||||
colors =
|
||||
NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
|
||||
indicatorColor = Color.Transparent,
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-20
@@ -4,6 +4,7 @@ import android.os.Build
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Sort
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -11,6 +12,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -28,6 +30,7 @@ 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
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
|
||||
@Composable
|
||||
fun currentNavBackStackEntryAsNavBarState(
|
||||
@@ -60,35 +63,40 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
|
||||
Row {
|
||||
if (selectedCount == 0) {
|
||||
val showSort = remember(uiState.tunnels) { uiState.tunnels.size > 1 }
|
||||
if (showSort)
|
||||
ActionIconButton(Icons.AutoMirrored.Rounded.Sort, R.string.sort) {
|
||||
navController.navigate(Route.Sort)
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.IMPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
|
||||
}
|
||||
// due to permissions, and SAF issues on TV, not support less than Android 10 on
|
||||
// Android TV for file exports
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActionIconButton(Icons.Rounded.Download, R.string.download) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
return@Row
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
|
||||
}
|
||||
// due to permissions, and SAF issues on TV, not support less than Android 10 on
|
||||
// Android TV for file exports
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActionIconButton(Icons.Rounded.Download, R.string.download) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCount == 1) {
|
||||
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
|
||||
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
|
||||
}
|
||||
if (selectedCount == 1) {
|
||||
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
|
||||
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDelete) {
|
||||
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
|
||||
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
|
||||
}
|
||||
if (showDelete) {
|
||||
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
|
||||
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,6 +220,25 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
route = Route.Support,
|
||||
)
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.Sort::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
showBottom = true,
|
||||
topTitle = { Text(stringResource(R.string.sort)) },
|
||||
topTrailing = {
|
||||
Row {
|
||||
ActionIconButton(Icons.Rounded.SortByAlpha, R.string.sort) {
|
||||
viewModel.handleUiEvent(UiEvent.SortTunnels)
|
||||
}
|
||||
ActionIconButton(Icons.Rounded.Save, R.string.save) {
|
||||
viewModel.handleEvent(AppEvent.InvokeScreenAction)
|
||||
}
|
||||
}
|
||||
},
|
||||
route = Route.Sort,
|
||||
)
|
||||
}
|
||||
|
||||
backStackEntry.isCurrentRoute(Route.License::class) -> {
|
||||
NavBarState(
|
||||
showTop = true,
|
||||
|
||||
+4
-4
@@ -40,8 +40,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
},
|
||||
description = {
|
||||
val cellularActive =
|
||||
remember(uiState.networkStatus) {
|
||||
uiState.networkStatus?.cellularConnected ?: false
|
||||
remember(uiState.connectivityState) {
|
||||
uiState.connectivityState?.cellularConnected ?: false
|
||||
}
|
||||
Text(
|
||||
text =
|
||||
@@ -77,8 +77,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
|
||||
},
|
||||
description = {
|
||||
val ethernetActive =
|
||||
remember(uiState.networkStatus) {
|
||||
uiState.networkStatus?.ethernetConnected ?: false
|
||||
remember(uiState.connectivityState) {
|
||||
uiState.connectivityState?.ethernetConnected ?: false
|
||||
}
|
||||
Text(
|
||||
text =
|
||||
|
||||
+5
-5
@@ -18,7 +18,6 @@ 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.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
@@ -68,11 +67,12 @@ fun WifiTunnelingItems(
|
||||
},
|
||||
description = {
|
||||
val wifiInfo by
|
||||
remember(uiState.networkStatus) {
|
||||
remember(uiState.connectivityState) {
|
||||
derivedStateOf {
|
||||
(uiState.networkStatus as? NetworkStatus.Connected)
|
||||
?.takeIf { it.wifiConnected }
|
||||
.let { Pair(it?.wifiSsid, it?.securityType) }
|
||||
uiState.connectivityState
|
||||
?.wifiState
|
||||
?.takeIf { it.connected }
|
||||
.let { Pair(it?.ssid, it?.securityType) }
|
||||
}
|
||||
}
|
||||
val (wifiName, securityType) = wifiInfo
|
||||
|
||||
+29
-25
@@ -1,7 +1,9 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -9,6 +11,7 @@ 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
|
||||
@@ -25,8 +28,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -40,17 +41,10 @@ fun TunnelList(
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val collator = Collator.getInstance(Locale.getDefault())
|
||||
val sortedTunnels =
|
||||
remember(appUiState.tunnels) {
|
||||
appUiState.tunnels.sortedWith(
|
||||
compareBy(
|
||||
// primary tunnel first
|
||||
{ !it.isPrimaryTunnel },
|
||||
{ collator.compare(it.tunName, "") },
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val sortedTunnels = remember(appUiState.tunnels) { appUiState.tunnels.sortedBy { it.position } }
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -59,7 +53,7 @@ fun TunnelList(
|
||||
modifier
|
||||
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
|
||||
.overscroll(rememberOverscrollEffect()),
|
||||
state = rememberLazyListState(0, appUiState.tunnels.count()),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
@@ -75,26 +69,36 @@ fun TunnelList(
|
||||
val selected = remember(selectedTunnels) { selectedTunnels.any { it.id == tunnel.id } }
|
||||
TunnelRowItem(
|
||||
state = tunnelState,
|
||||
expanded = appUiState.appState.expandedTunnelIds.contains(tunnel.id),
|
||||
isSelected = selected,
|
||||
tunnel = tunnel,
|
||||
tunnelState = tunnelState,
|
||||
onClick = {
|
||||
if (selectedTunnels.isNotEmpty() && !isTv) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
} else {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
}
|
||||
},
|
||||
onDoubleClick = {
|
||||
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
|
||||
onTvClick = {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
},
|
||||
onToggleSelectedTunnel = {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
|
||||
},
|
||||
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
|
||||
isTv = isTv,
|
||||
modifier =
|
||||
if (!isTv)
|
||||
Modifier.combinedClickable(
|
||||
onClick = {
|
||||
if (selectedTunnels.isNotEmpty()) {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
} else {
|
||||
navController.navigate(Route.TunnelOptions(tunnel.id))
|
||||
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
)
|
||||
else Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-16
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.SettingsEthernet
|
||||
import androidx.compose.material.icons.rounded.Smartphone
|
||||
@@ -22,6 +21,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
@@ -32,14 +32,13 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||
fun TunnelRowItem(
|
||||
state: TunnelState,
|
||||
isSelected: Boolean,
|
||||
expanded: Boolean,
|
||||
tunnel: TunnelConf,
|
||||
tunnelState: TunnelState,
|
||||
onClick: () -> Unit,
|
||||
onDoubleClick: () -> Unit,
|
||||
onTvClick: () -> Unit,
|
||||
onToggleSelectedTunnel: (TunnelConf) -> Unit,
|
||||
onSwitchClick: (Boolean) -> Unit,
|
||||
isTv: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val leadingIconColor =
|
||||
remember(state) {
|
||||
@@ -78,13 +77,10 @@ fun TunnelRowItem(
|
||||
}
|
||||
},
|
||||
text = tunnel.tunName,
|
||||
onHold = { if (!isTv) onToggleSelectedTunnel(tunnel) },
|
||||
onClick = { if (!isTv) onClick() },
|
||||
onDoubleClick = { if (!isTv) onDoubleClick() },
|
||||
expanded = {
|
||||
if (expanded) {
|
||||
if (tunnelState.status != TunnelStatus.Down) {
|
||||
TunnelStatisticsRow(tunnelState.statistics, tunnel)
|
||||
} else null
|
||||
}
|
||||
},
|
||||
trailing = {
|
||||
Row(
|
||||
@@ -92,13 +88,7 @@ fun TunnelRowItem(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
) {
|
||||
if (isTv) {
|
||||
IconButton(onClick = onDoubleClick) {
|
||||
Icon(
|
||||
Icons.Rounded.KeyboardArrowDown,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClick) {
|
||||
IconButton(onClick = onTvClick) {
|
||||
Icon(
|
||||
Icons.Rounded.Settings,
|
||||
contentDescription = stringResource(R.string.settings),
|
||||
@@ -109,5 +99,6 @@ fun TunnelRowItem(
|
||||
}
|
||||
},
|
||||
isSelected = isSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,10 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -97,15 +94,17 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
|
||||
)
|
||||
}
|
||||
if (endpoint != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
||||
) {
|
||||
Text(
|
||||
"endpoint: $endpoint",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
AnimatedVisibility(visible = true) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.endpoint).lowercase() + ": $endpoint",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.sort
|
||||
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
import sh.calvin.reorderable.DragGestureDetector
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@Composable
|
||||
fun SortScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
var sortAscending by remember { mutableStateOf<Boolean?>(null) }
|
||||
var sortedTunnels by remember { mutableStateOf(appUiState.tunnels.sortedBy { it.position }) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEvent.collect { uiEvent ->
|
||||
when (uiEvent) {
|
||||
UiEvent.SortTunnels -> {
|
||||
sortAscending =
|
||||
when (sortAscending) {
|
||||
null -> !sortedTunnels.isSortedBy { it.name }
|
||||
true -> false
|
||||
false -> null
|
||||
}
|
||||
sortedTunnels =
|
||||
when (sortAscending) {
|
||||
true -> sortedTunnels.sortedBy { it.name }
|
||||
false -> sortedTunnels.sortedByDescending { it.name }
|
||||
null -> sortedTunnels.sortedBy { it.position }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SetScreenAction {
|
||||
viewModel.handleEvent(
|
||||
AppEvent.SaveAllConfigs(
|
||||
sortedTunnels.mapIndexed { index, conf -> conf.copy(position = index) }
|
||||
)
|
||||
)
|
||||
viewModel.handleEvent(AppEvent.PopBackStack(true))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val reorderableLazyListState =
|
||||
rememberReorderableLazyListState(
|
||||
lazyListState,
|
||||
scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(),
|
||||
) { from, to ->
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply { add(to.index, removeAt(from.index)) }
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
|
||||
.overscroll(rememberOverscrollEffect())
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp),
|
||||
state = lazyListState,
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
) {
|
||||
itemsIndexed(sortedTunnels, key = { _, tunnel -> tunnel.id }) { index, tunnel ->
|
||||
ReorderableItem(reorderableLazyListState, tunnel.id) { isDragging ->
|
||||
ExpandingRowListItem(
|
||||
leading = {},
|
||||
text = tunnel.name,
|
||||
trailing = {
|
||||
if (!isTv)
|
||||
Icon(
|
||||
Icons.Default.DragHandle,
|
||||
stringResource(
|
||||
com.zaneschepke.wireguardautotunnel.R.string.drag_handle
|
||||
),
|
||||
)
|
||||
else
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply {
|
||||
add(index - 1, removeAt(index))
|
||||
}
|
||||
},
|
||||
enabled = index != 0,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowUpward,
|
||||
stringResource(
|
||||
com.zaneschepke.wireguardautotunnel.R.string.move_up
|
||||
),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
sortedTunnels =
|
||||
sortedTunnels.toMutableList().apply {
|
||||
add(index + 1, removeAt(index))
|
||||
}
|
||||
},
|
||||
enabled = index != sortedTunnels.count() - 1,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ArrowDownward,
|
||||
stringResource(R.string.move_down),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected = isDragging,
|
||||
expanded = {},
|
||||
modifier =
|
||||
Modifier.draggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureThresholdActivate
|
||||
)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||
},
|
||||
dragGestureDetector = DragGestureDetector.LongPress,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
|
||||
@@ -16,5 +16,5 @@ data class AppUiState(
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val appConfigurationChange: Boolean = false,
|
||||
val isAppLoaded: Boolean = false,
|
||||
val networkStatus: NetworkStatus? = null,
|
||||
val connectivityState: ConnectivityState? = null,
|
||||
)
|
||||
|
||||
@@ -3,18 +3,17 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
|
||||
|
||||
private val DarkColorScheme =
|
||||
darkColorScheme(
|
||||
@@ -49,9 +48,11 @@ enum class Theme {
|
||||
DYNAMIC,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
var isDark = isSystemInDarkTheme()
|
||||
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
|
||||
val colorScheme =
|
||||
@@ -105,5 +106,22 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||||
// Make hover/ripple more obvious on TV
|
||||
val rippleConfig =
|
||||
if (isTv) {
|
||||
RippleConfiguration(
|
||||
color = colorScheme.outline.copy(alpha = 0.12f),
|
||||
rippleAlpha =
|
||||
RippleAlpha(
|
||||
pressedAlpha = 0.7f,
|
||||
focusedAlpha = 0.6f,
|
||||
draggedAlpha = 0.9f,
|
||||
hoveredAlpha = 0.3f,
|
||||
),
|
||||
)
|
||||
} else null
|
||||
|
||||
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfig) {
|
||||
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,3 +24,7 @@ typealias Packages = List<PackageInfo>
|
||||
fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T) -> Boolean) {
|
||||
addAll(elements.filterNot { new -> this.any { existing -> comparator(existing, new) } })
|
||||
}
|
||||
|
||||
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
|
||||
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.ConnectivityState
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
@@ -34,6 +34,7 @@ import com.zaneschepke.wireguardautotunnel.util.*
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.addAllUnique
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
@@ -79,6 +80,9 @@ constructor(
|
||||
private val _appViewState = MutableStateFlow(AppViewState())
|
||||
val appViewState = _appViewState.asStateFlow()
|
||||
|
||||
private val _uiEvent = MutableSharedFlow<UiEvent>()
|
||||
val uiEvent: SharedFlow<UiEvent> = _uiEvent.asSharedFlow()
|
||||
|
||||
private val _logs = MutableStateFlow<List<LogMessage>>(emptyList())
|
||||
val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow()
|
||||
private val maxLogSize = Constants.MAX_LOG_SIZE
|
||||
@@ -90,14 +94,14 @@ constructor(
|
||||
appDataRepository.appState.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
serviceManager.autoTunnelService.map { it != null },
|
||||
networkMonitor.networkStatusFlow,
|
||||
networkMonitor.connectivityStateFlow,
|
||||
) { array ->
|
||||
val settings = array[0] as AppSettings
|
||||
val tunnels = array[1] as List<TunnelConf>
|
||||
val appState = array[2] as AppState
|
||||
val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
|
||||
val autoTunnel = array[4] as Boolean
|
||||
val network = array[5] as NetworkStatus
|
||||
val network = array[5] as ConnectivityState
|
||||
|
||||
AppUiState(
|
||||
appSettings = settings,
|
||||
@@ -106,7 +110,7 @@ constructor(
|
||||
appState = appState,
|
||||
isAutoTunnelActive = autoTunnel,
|
||||
isAppLoaded = true,
|
||||
networkStatus = network,
|
||||
connectivityState = network,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
@@ -126,6 +130,9 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUiEvent(event: UiEvent) =
|
||||
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
|
||||
|
||||
fun handleEvent(event: AppEvent) =
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
uiState.withFirstState { state ->
|
||||
@@ -215,10 +222,15 @@ constructor(
|
||||
|
||||
is AppEvent.SetDetectionMethod ->
|
||||
handleSetDetectionMethod(event.detectionMethod, state.appSettings)
|
||||
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveAllTunnels(tunnels: List<TunnelConf>) {
|
||||
appDataRepository.tunnels.saveAll(tunnels)
|
||||
}
|
||||
|
||||
private suspend fun handleSetDetectionMethod(
|
||||
detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
appSettings: AppSettings,
|
||||
|
||||
@@ -125,4 +125,6 @@ sealed class AppEvent {
|
||||
data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent()
|
||||
|
||||
data object ToggleSelectAllTunnels : AppEvent()
|
||||
|
||||
data class SaveAllConfigs(val tunnels: List<TunnelConf>) : AppEvent()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel.event
|
||||
|
||||
sealed class UiEvent {
|
||||
data object SortTunnels : UiEvent()
|
||||
}
|
||||
@@ -163,76 +163,6 @@
|
||||
<string name="learn_more">Zjistit více</string>
|
||||
<string name="stop">zastavit</string>
|
||||
<string name="server_ipv4">Překlad názvu hostitele IPv4</string>
|
||||
<string name="select_all">Vybrat vše</string>
|
||||
<string name="share">Sdílet</string>
|
||||
<string name="trusted_ssid_value_description">Odeslat SSID</string>
|
||||
<string name="app_settings">nastavení aplikace</string>
|
||||
<string name="debounce_delay">Zpoždění odezvy</string>
|
||||
<string name="always_on_message">Autorizace připojení VPN byla zamítnuta. Zkontrolujte prosím</string>
|
||||
<string name="bio_not_created">Biometrické údaje nebyly vytvořeny</string>
|
||||
<string name="bio_not_supported">Biometrie není podporována</string>
|
||||
<string name="bio_subtitle">Přihlášení pomocí biometrických údajů</string>
|
||||
<string name="config_error">Chybná konfigurace</string>
|
||||
<string name="prominent_background_location_title">Zpřístupnění stávající polohy na pozadí</string>
|
||||
<string name="vpn_denied_dialog_title">Povolení zamítnuto</string>
|
||||
<string name="app_permission_title">Řídicí most pro WG tunely</string>
|
||||
<string name="app_permission_description">Ovládání funkcí tunelu a automatického tunelu.</string>
|
||||
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
|
||||
<string name="tunnel_starting">Spuštění tunelu</string>
|
||||
<string name="bio_auth_title">Biometrické ověření</string>
|
||||
<string name="nothing_here_yet">Zatím zde nic není!</string>
|
||||
<string name="export_success">Export byl úspěšně dokončen</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="check_for_update">Zkontrolovat aktualizaci</string>
|
||||
<string name="update_check_failed">Kontrola aktualizace se nezdařila.</string>
|
||||
<string name="version_template">Verze: %1$s</string>
|
||||
<string name="update_download_failed">Stažení aktualizace se nezdařilo.</string>
|
||||
<string name="update_available">Dostupná aktualizace!</string>
|
||||
<string name="download_and_install">Stáhnout a nainstalovat</string>
|
||||
<string name="allow">Povolit</string>
|
||||
<string name="permission_required">Je vyžadováno oprávnění</string>
|
||||
<string name="licenses">Licence</string>
|
||||
<string name="latest_installed">Již používáte nejnovější verzi.</string>
|
||||
<string name="install_updated_permission">Tato aplikace potřebuje oprávnění k instalaci aktualizací.</string>
|
||||
<string name="checking_for_update">Kontrola aktualizací</string>
|
||||
<string name="add_from_url">Přidat z adresy URL</string>
|
||||
<string name="inactive">Neaktivní</string>
|
||||
<string name="auth_error">Chyba: neautorizováno</string>
|
||||
<string name="kernel_name_error">Chyba názvu modulu jádra</string>
|
||||
<string name="export_failed">Export se nezdařil</string>
|
||||
<string name="delete">Smazat</string>
|
||||
<string name="export_tunnels_wireguard">Exportovat tunely jako WireGuard</string>
|
||||
<string name="export_tunnels_amnezia">Exportovat tunely jako Amnezia</string>
|
||||
<string name="remote_key_template">Klíč: %1$s</string>
|
||||
<string name="active">Aktivní</string>
|
||||
<string name="service_running_error">Chyba: Služba není spuštěna</string>
|
||||
<string name="wifi_name_template">Aktivní: %1$s</string>
|
||||
<string name="tunnel_error_template">Tunel selhal s: %1$s</string>
|
||||
<string name="camera_permission_required">Vyžadováno oprávnění k použití fotoaparátu</string>
|
||||
<string name="info">Informace</string>
|
||||
<string name="copy">Kopírovat</string>
|
||||
<string name="status">Stav</string>
|
||||
<string name="launch_app_settings">Spustit nastavení aplikace</string>
|
||||
<string name="tunnel_running">Tunel je v provozu</string>
|
||||
<string name="wildcards_active">Zástupné znaky(wildcards) aktivní</string>
|
||||
<string name="root_accepted">Root shell přijata</string>
|
||||
<string name="background_location_message">Autorizace povolit vždy polohu a/nebo přesná poloha je vyžadováno pro tuto funkci. Viz</string>
|
||||
<string name="update_check_unsupported">Kontrola aktualizací není u tohoto typu sestavení podporována.</string>
|
||||
<string name="background_location_message2">abyste se ujistili, že jsou tato oprávnění povolena</string>
|
||||
<string name="darker">Tmavší</string>
|
||||
<string name="amoled">AMOLED</string>
|
||||
<string name="default_ping_ip">(nepovinné, výchozí hodnota je peers)</string>
|
||||
<string name="monitoring_state_changes">Monitorování změn stavu</string>
|
||||
<string name="pre_up">Před aktivací</string>
|
||||
<string name="pre_down">Před deaktivací</string>
|
||||
<string name="post_up">Po aktivaci</string>
|
||||
<string name="optional_default">"nepovinné, výchozí: "</string>
|
||||
<string name="flavor_template">Varianta: %1$s</string>
|
||||
<string name="security_template">Zabezpečení: %1$s</string>
|
||||
<string name="done">Hotovo</string>
|
||||
<string name="wireguard">WireGuard</string>
|
||||
<string name="amnezia">Amnezia</string>
|
||||
<string name="show_qr">Zobrazit QR kód</string>
|
||||
<string name="always_on_message2">ujistěte se, že je pro všechny ostatní aplikace vypnutá funkce trvalé připojení VPN, a zkuste to znovu</string>
|
||||
<string name="use_wildcards">Použít zástupné znaky(wildcards) pro názvy</string>
|
||||
<string name="multiple">Několik</string>
|
||||
|
||||
@@ -190,20 +190,4 @@
|
||||
<string name="allow">Autoriser</string>
|
||||
<string name="app_permission_title">Pont de contrôle du tunnel WG</string>
|
||||
<string name="app_permission_description">Contrôler les tunnels et les fonctions automatiques des tunnels.</string>
|
||||
<string name="select">Sélectionner</string>
|
||||
<string name="join_telegram">Rejoindre la communauté Telegram</string>
|
||||
<string name="join_matrix">Rejoindre la communauté Matrix</string>
|
||||
<string name="auto_tunnel_channel_description">Un canal pour les notifications de l\'état du tunnel automatique</string>
|
||||
<string name="tunnel_control">Contrôle du tunnel</string>
|
||||
<string name="auto_tunnel">Tunnel automatique</string>
|
||||
<string name="add_tunnel">Ajouter un tunnel</string>
|
||||
<string name="error_download_failed">Le téléchargement de la configuration a échouée</string>
|
||||
<string name="multiple">Multiple</string>
|
||||
<string name="add_from_url">Ajouter depuis un URL</string>
|
||||
<string name="enter_config_url">Saisissez l\'URL de configuration</string>
|
||||
<string name="search">Rechercher</string>
|
||||
<string name="save">Sauvegarder</string>
|
||||
<string name="copy">Copier</string>
|
||||
<string name="info">Informations</string>
|
||||
<string name="prefer_ipv4">Préférer une connexion IPv4</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
</resources>
|
||||
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="app_permission_description">Alagutak és automatikus alagút funkciók vezérlése.</string>
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="tunnel_name">Nome do Túnel</string>
|
||||
<string name="exclude">Excluir</string>
|
||||
<string name="include">Incluir</string>
|
||||
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
|
||||
<string name="public_key">Chave pública</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="vpn_channel_name">Canal de notificações VPN</string>
|
||||
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
|
||||
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
|
||||
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
|
||||
<string name="tunnels">Túneis</string>
|
||||
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
|
||||
<string name="privacy_policy">Ver a Política de Privacidade</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
|
||||
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
|
||||
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
||||
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Envie o SSID</string>
|
||||
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
|
||||
<string name="open_file">Abrir Ficheiro</string>
|
||||
<string name="add_from_qr">Adicionar a partir de código QR</string>
|
||||
<string name="qr_scan">Escanear o código QR</string>
|
||||
<string name="addresses">Endereços</string>
|
||||
<string name="dns_servers">Servidores DNS</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Par</string>
|
||||
<string name="allowed_ips">IPs Permitidos</string>
|
||||
<string name="name">Nome</string>
|
||||
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
|
||||
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
|
||||
<string name="auto_tunneling">Auto-túnel</string>
|
||||
<string name="vpn_on">VPN ligada</string>
|
||||
<string name="vpn_off">VPN desligada</string>
|
||||
<string name="create_import">Criar do zero</string>
|
||||
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
|
||||
<string name="add_peer">Adicionar par</string>
|
||||
<string name="interface_">Interface</string>
|
||||
<string name="rotate_keys">Revezar chaves</string>
|
||||
<string name="private_key">Chave privada</string>
|
||||
<string name="copy_public_key">Copiar chave pública</string>
|
||||
<string name="base64_key">Chave base64</string>
|
||||
<string name="comma_separated_list">Lista separada por vírgulas</string>
|
||||
<string name="listen_port">Porta de escuta</string>
|
||||
<string name="random">(aleatório)</string>
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="preshared_key">Chave pré-partilhada</string>
|
||||
<string name="seconds">segundos</string>
|
||||
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="error_authentication_failed">Autenticação falhou</string>
|
||||
<string name="error_authorization_failed">Autorização falhou</string>
|
||||
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
|
||||
<string name="unknown_error">Ocorreu um erro desconhecido</string>
|
||||
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
|
||||
<string name="email_subject">Apoio para o WG Tunnel</string>
|
||||
<string name="email_chooser">Enviar um email…</string>
|
||||
<string name="docs_description">Ler a documentação</string>
|
||||
<string name="email_description">Me envie um email</string>
|
||||
<string name="use_kernel">Usar o módulo do kernel</string>
|
||||
<string name="error_ssid_exists">SSID já existe</string>
|
||||
<string name="error_root_denied">Shell Root negado</string>
|
||||
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
|
||||
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
|
||||
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
|
||||
<string name="delete_tunnel">Apagar túnel</string>
|
||||
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
|
||||
<string name="yes">Sim</string>
|
||||
<string name="all">todos</string>
|
||||
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
|
||||
<string name="no_browser_detected">Nenhum navegador detetado</string>
|
||||
<string name="open_issue">Abrir um problema</string>
|
||||
<string name="read_logs">Ler os registos</string>
|
||||
<string name="auto">(automático)</string>
|
||||
<string name="incorrect_pin">O Pin está errado</string>
|
||||
<string name="pin_created">Pin criado com sucesso</string>
|
||||
<string name="enter_pin">Digite o seu pin</string>
|
||||
<string name="create_pin">Criar um pin</string>
|
||||
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
|
||||
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
|
||||
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
|
||||
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
|
||||
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="version">Versão</string>
|
||||
<string name="settings">Configurações</string>
|
||||
<string name="support">Suporte</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
|
||||
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
|
||||
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
|
||||
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
|
||||
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
|
||||
<string name="unsure_how">se não tiver certeza em como continuar</string>
|
||||
<string name="see_the">Veja o</string>
|
||||
<string name="getting_started_guide">guia de início rápido</string>
|
||||
<string name="error_file_format">Formato de configuração inválido</string>
|
||||
<string name="restart_at_boot">Ativar na inicialização</string>
|
||||
<string name="vpn_denied_dialog_title">Permissão negada</string>
|
||||
<string name="vpn_settings">Configurações do sistema VPN</string>
|
||||
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
|
||||
<string name="always_on_message2">para ter certeza que VPN Sempre-ligada é desligada para todas as outras aplicações e tente novamente</string>
|
||||
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
|
||||
<string name="app_settings">configurações da app</string>
|
||||
<string name="root_accepted">Shell root aceito</string>
|
||||
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
|
||||
<string name="default_ping_ip">(opcional, padrão para pares)</string>
|
||||
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
|
||||
<string name="optional_default">"opcional, padrão: "</string>
|
||||
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
|
||||
<string name="never">nunca</string>
|
||||
<string name="sec">seg</string>
|
||||
<string name="handshake">handshake</string>
|
||||
<string name="appearance">Aparência</string>
|
||||
<string name="notifications">Notificações</string>
|
||||
<string name="automatic">Automático</string>
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Escuro</string>
|
||||
<string name="dynamic">Dinâmico</string>
|
||||
<string name="language">Idioma</string>
|
||||
<string name="display_theme">Tema</string>
|
||||
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
|
||||
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
|
||||
<string name="mobile_tunnel">Túnel com dados móveis</string>
|
||||
<string name="skip">Pular</string>
|
||||
<string name="use_wildcards">Usar nomes coringas</string>
|
||||
<string name="learn_more">Saber mais</string>
|
||||
<string name="wildcards_active">Wildcards ativos</string>
|
||||
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
|
||||
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
|
||||
<string name="kernel_not_supported">Kernel não suportado</string>
|
||||
<string name="start_auto">Iniciar túnel automático</string>
|
||||
<string name="stop_auto">Pausar túnel automático</string>
|
||||
<string name="tunnel_running">Túnel em execução</string>
|
||||
<string name="monitoring_state_changes">Monitorar estado de alterações</string>
|
||||
<string name="donate">Contribua com projeto</string>
|
||||
<string name="local_logging">Registo local</string>
|
||||
<string name="enable_local_logging">Ativar registo local</string>
|
||||
<string name="add_from_clipboard">Adicionar da área de transferência</string>
|
||||
<string name="stop_on_no_internet">Interromper quando não há internet</string>
|
||||
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
|
||||
<string name="ethernet_tunnel">Túnel ethernet</string>
|
||||
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
|
||||
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
|
||||
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
|
||||
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
|
||||
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
|
||||
<string name="stop">pausar</string>
|
||||
<string name="splt_tunneling">Tunelamento dividido</string>
|
||||
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
|
||||
<string name="show_scripts">Mostrar scripts</string>
|
||||
<string name="quick_actions">Ações rápidas</string>
|
||||
<string name="advanced_settings">Configurações avançadas</string>
|
||||
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
|
||||
<string name="hide_scripts">Ocultar scripts</string>
|
||||
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
|
||||
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
|
||||
<string name="exclude_lan">Excluir LAN</string>
|
||||
<string name="include_lan">Incluir LAN</string>
|
||||
</resources>
|
||||
@@ -281,4 +281,8 @@
|
||||
</string>
|
||||
<string name="release_notes">Release notes</string>
|
||||
<string name="shizuku_not_detected">Shizuku not detected</string>
|
||||
<string name="sort">Sort</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="move_up">Move Up</string>
|
||||
<string name="move_down">Move Down</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,5 +9,5 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation("org.semver4j:semver4j:5.7.0")
|
||||
implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
|
||||
implementation("org.ajoberstar.grgit:grgit-core:5.3.2")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.9.3"
|
||||
const val VERSION_NAME = "3.9.4"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 39300
|
||||
const val VERSION_CODE = 39400
|
||||
const val TARGET_SDK = 35
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
@@ -13,5 +13,6 @@ object Constants {
|
||||
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", "https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
|
||||
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
|
||||
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
WG Tunnel
|
||||
@@ -0,0 +1,6 @@
|
||||
What's new:
|
||||
- Tunnel sorting
|
||||
- Shizuku support for Wi-Fi SSIDs
|
||||
- Android TV hover visibility improvements
|
||||
- Auto-tunnel default detection method bug fix
|
||||
- Other UI changes and improvements
|
||||
@@ -1 +0,0 @@
|
||||
Um cliente de VPN alternativo para WireGuard com recursos adicionais
|
||||
@@ -1 +0,0 @@
|
||||
WG Tunnel
|
||||
@@ -1 +1 @@
|
||||
WG Tunel
|
||||
WG Tunnel
|
||||
@@ -0,0 +1 @@
|
||||
WG Tunnel
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanist = "0.37.3"
|
||||
activityCompose = "1.10.1"
|
||||
amneziawgAndroid = "1.4.0"
|
||||
amneziawgAndroid = "1.5.0"
|
||||
androidx-junit = "1.2.1"
|
||||
shizuku = "13.1.5"
|
||||
appcompat = "1.7.1"
|
||||
@@ -24,17 +24,18 @@ roomVersion = "2.7.1"
|
||||
semver4j = "3.1.0"
|
||||
slf4jAndroid = "1.7.36"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.3.0"
|
||||
tunnel = "1.4.0"
|
||||
androidGradlePlugin = "8.10.1"
|
||||
kotlin = "2.1.21"
|
||||
ksp = "2.1.21-2.0.2"
|
||||
kotlin = "2.2.0"
|
||||
ksp = "2.2.0-2.0.2"
|
||||
composeBom = "2025.06.00"
|
||||
compose = "1.8.2"
|
||||
icons = "1.7.8"
|
||||
workRuntimeKtxVersion = "2.10.1"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
coreSplashscreen = "1.0.1"
|
||||
gradlePlugins-grgit = "5.3.0"
|
||||
gradlePlugins-grgit = "5.3.2"
|
||||
reorderable = "2.5.1"
|
||||
|
||||
#plugins
|
||||
material = "1.12.0"
|
||||
@@ -110,6 +111,7 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
|
||||
# tunnel
|
||||
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
// keep track of the currently active network(s)
|
||||
class ActiveWifiStateManager {
|
||||
private val _stateFlow =
|
||||
MutableStateFlow(linkedMapOf<String, Pair<Network?, NetworkCapabilities?>>())
|
||||
|
||||
@Synchronized
|
||||
fun put(key: String, value: Pair<Network?, NetworkCapabilities?>) {
|
||||
_stateFlow.update { currentMap ->
|
||||
linkedMapOf(*currentMap.toList().toTypedArray()).apply { put(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun remove(key: String) {
|
||||
_stateFlow.update { currentMap ->
|
||||
linkedMapOf(*currentMap.toList().toTypedArray()).apply { remove(key) }
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean = _stateFlow.value.isEmpty()
|
||||
|
||||
fun getLatestValue(): Pair<Network?, NetworkCapabilities?>? {
|
||||
return _stateFlow.value.entries.lastOrNull()?.value
|
||||
}
|
||||
}
|
||||
+205
-161
@@ -1,9 +1,11 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -11,13 +13,17 @@ import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.WifiManager
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
class AndroidNetworkMonitor(
|
||||
@@ -45,76 +51,31 @@ class AndroidNetworkMonitor(
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): WifiDetectionMethod =
|
||||
WifiDetectionMethod.entries.find { it.value == value } ?: DEFAULT
|
||||
entries.find { it.value == value } ?: DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
private val packageName = appContext.packageName
|
||||
private val connectivityManager =
|
||||
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||
private val locationManager =
|
||||
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
|
||||
|
||||
private val wifiMutex = Mutex()
|
||||
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
private var currentSsid: String? = null
|
||||
private var securityType: WifiSecurityType? = null
|
||||
|
||||
private var wifiConnected = false
|
||||
|
||||
// Track active Wi-Fi networks and last active network ID
|
||||
private val activeWifiNetworks = mutableSetOf<String>()
|
||||
|
||||
data class WifiState(
|
||||
val connected: Boolean = false,
|
||||
val ssid: String? = null,
|
||||
val securityType: WifiSecurityType? = null,
|
||||
)
|
||||
|
||||
data class TransportState(val connected: Boolean = false)
|
||||
// Track active Wi-Fi networks, their capabilities, and last active network ID
|
||||
private val activeWifiNetworks = ActiveWifiStateManager()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val wifiFlow: Flow<WifiState> =
|
||||
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
|
||||
}
|
||||
|
||||
private fun createWifiNetworkCallbackFlow(
|
||||
detectionMethod: WifiDetectionMethod
|
||||
): Flow<WifiState> = callbackFlow {
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getWifiSsid(): String {
|
||||
return withContext(ioDispatcher) {
|
||||
if (wifiManager == null) return@withContext ANDROID_UNKNOWN_SSID
|
||||
try {
|
||||
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
||||
?: ANDROID_UNKNOWN_SSID
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleUnknownWifi() {
|
||||
wifiMutex.withLock {
|
||||
val newSsid = getWifiSsid()
|
||||
val securityType = wifiManager?.getCurrentSecurityType()
|
||||
// Only update if new SSID is valid; preserve existing valid SSID otherwise
|
||||
if (newSsid != WifiManager.UNKNOWN_SSID) {
|
||||
currentSsid = newSsid
|
||||
trySend(WifiState(wifiConnected, currentSsid, securityType))
|
||||
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
|
||||
currentSsid = newSsid
|
||||
trySend(WifiState(wifiConnected, currentSsid, securityType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
): Flow<TransportEvent> = callbackFlow {
|
||||
val locationPermissionReceiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
@@ -125,7 +86,15 @@ class AndroidNetworkMonitor(
|
||||
Timber.d(
|
||||
"Received update: Precise and all-the-time location permissions are enabled"
|
||||
)
|
||||
applicationScope.launch { handleUnknownWifi() }
|
||||
activeWifiNetworks.getLatestValue()?.let { details ->
|
||||
trySend(
|
||||
TransportEvent.LocationPermissionGranted(
|
||||
details.first,
|
||||
details.second,
|
||||
detectionMethod,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,23 +104,39 @@ class AndroidNetworkMonitor(
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == LOCATION_SERVICES_FILTER) {
|
||||
val isGpsEnabled =
|
||||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
?: false
|
||||
val isNetworkEnabled =
|
||||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
?: false
|
||||
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
|
||||
Timber.d(
|
||||
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
|
||||
)
|
||||
if (isLocationServicesEnabled)
|
||||
applicationScope.launch { handleUnknownWifi() }
|
||||
activeWifiNetworks.getLatestValue()?.let { details ->
|
||||
trySend(
|
||||
TransportEvent.LocationServicesChanged(
|
||||
isLocationServicesEnabled,
|
||||
details.first,
|
||||
details.second,
|
||||
detectionMethod,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
|
||||
val flags =
|
||||
val permissionReceiverFlags =
|
||||
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
|
||||
Context.RECEIVER_EXPORTED // System broadcast
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -159,64 +144,42 @@ class AndroidNetworkMonitor(
|
||||
appContext.registerReceiver(
|
||||
locationPermissionReceiver,
|
||||
IntentFilter("$packageName.$LOCATION_GRANTED"),
|
||||
flags,
|
||||
permissionReceiverFlags,
|
||||
)
|
||||
|
||||
appContext.registerReceiver(
|
||||
locationServicesReceiver,
|
||||
IntentFilter(LOCATION_SERVICES_FILTER),
|
||||
flags,
|
||||
servicesReceiverFlags,
|
||||
)
|
||||
|
||||
suspend fun handleOnWifiLost(network: Network) {
|
||||
wifiMutex.withLock {
|
||||
Timber.d("Wi-Fi onLost: network=$network")
|
||||
activeWifiNetworks.remove(network.toString())
|
||||
if (activeWifiNetworks.isEmpty()) {
|
||||
Timber.d(
|
||||
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
|
||||
)
|
||||
currentSsid = null
|
||||
wifiConnected = false
|
||||
trySend(WifiState(connected = false, ssid = null, securityType = null))
|
||||
} else {
|
||||
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
|
||||
}
|
||||
fun handleOnWifiLost(network: Network) {
|
||||
Timber.d("Wi-Fi onLost: network=$network")
|
||||
activeWifiNetworks.remove(network.toString())
|
||||
if (activeWifiNetworks.isEmpty()) {
|
||||
Timber.d("All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected")
|
||||
trySend(TransportEvent.Lost(network))
|
||||
} else {
|
||||
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
|
||||
// This can happen when switching between APs of the same SSID
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleOnWifiAvailable(
|
||||
fun handleOnWifiAvailable(network: Network) {
|
||||
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||
activeWifiNetworks.put(network.toString(), Pair(network, null))
|
||||
trySend(TransportEvent.Available(network, detectionMethod))
|
||||
}
|
||||
|
||||
fun handleOnWifiCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities?,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
wifiMutex.withLock {
|
||||
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||
activeWifiNetworks.add(network.toString())
|
||||
currentSsid =
|
||||
try {
|
||||
when (detectionMethod) {
|
||||
WifiDetectionMethod.DEFAULT ->
|
||||
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
|
||||
WifiDetectionMethod.LEGACY -> getWifiSsid()
|
||||
WifiDetectionMethod.ROOT ->
|
||||
configurationListener.rootShell.getCurrentWifiName()
|
||||
WifiDetectionMethod.SHIZUKU ->
|
||||
ShizukuShell(applicationScope)
|
||||
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
|
||||
}
|
||||
.trim()
|
||||
.replace(Regex("[\n\r]"), "")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
|
||||
securityType = wifiManager?.getCurrentSecurityType()
|
||||
wifiConnected = true
|
||||
trySend(
|
||||
WifiState(connected = true, ssid = currentSsid, securityType = securityType)
|
||||
)
|
||||
}
|
||||
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network")
|
||||
activeWifiNetworks.put(network.toString(), Pair(network, networkCapabilities))
|
||||
trySend(
|
||||
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod)
|
||||
)
|
||||
}
|
||||
|
||||
val callback =
|
||||
@@ -225,11 +188,11 @@ class AndroidNetworkMonitor(
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
applicationScope.launch { handleOnWifiLost(network) }
|
||||
handleOnWifiLost(network)
|
||||
}
|
||||
}
|
||||
else ->
|
||||
@@ -237,7 +200,7 @@ class AndroidNetworkMonitor(
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
if (detectionMethod != WifiDetectionMethod.DEFAULT)
|
||||
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||
handleOnWifiAvailable(network)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
@@ -245,13 +208,11 @@ class AndroidNetworkMonitor(
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
if (detectionMethod == WifiDetectionMethod.DEFAULT)
|
||||
applicationScope.launch {
|
||||
handleOnWifiAvailable(network, networkCapabilities)
|
||||
}
|
||||
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
applicationScope.launch { handleOnWifiLost(network) }
|
||||
handleOnWifiLost(network)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,34 +223,31 @@ class AndroidNetworkMonitor(
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(WifiState())
|
||||
connectivityManager?.registerNetworkCallback(request, callback)
|
||||
|
||||
trySend(TransportEvent.Unknown)
|
||||
|
||||
awaitClose {
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(
|
||||
e,
|
||||
"Flow failed to unregister NetworkCallback, was already unregistered or not registered correctly.",
|
||||
)
|
||||
}
|
||||
appContext.unregisterReceiver(locationPermissionReceiver)
|
||||
appContext.unregisterReceiver(locationServicesReceiver)
|
||||
runCatching {
|
||||
appContext.unregisterReceiver(locationPermissionReceiver)
|
||||
appContext.unregisterReceiver(locationServicesReceiver)
|
||||
connectivityManager?.unregisterNetworkCallback(callback)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Error unregistering network callback") }
|
||||
}
|
||||
}
|
||||
|
||||
private val cellularFlow: Flow<TransportState> = callbackFlow {
|
||||
private val cellularFlow: Flow<TransportEvent> = callbackFlow {
|
||||
val callback =
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.d("Cellular onAvailable: network=$network")
|
||||
trySend(TransportState(connected = true))
|
||||
trySend(TransportEvent.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Timber.d("Cellular onLost: network=$network")
|
||||
trySend(TransportState(connected = false))
|
||||
trySend(TransportEvent.Lost(network))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,23 +257,26 @@ class AndroidNetworkMonitor(
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(TransportState())
|
||||
connectivityManager?.registerNetworkCallback(request, callback)
|
||||
trySend(TransportEvent.Unknown)
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
awaitClose {
|
||||
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
|
||||
.onFailure { Timber.e(it, "Error unregistering cellular network callback") }
|
||||
}
|
||||
}
|
||||
|
||||
private val ethernetFlow: Flow<TransportState> = callbackFlow {
|
||||
private val ethernetFlow: Flow<TransportEvent> = callbackFlow {
|
||||
val callback =
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.d("Ethernet onAvailable: network=$network")
|
||||
trySend(TransportState(connected = true))
|
||||
trySend(TransportEvent.Available(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Timber.d("Ethernet onLost: network=$network")
|
||||
trySend(TransportState(connected = false))
|
||||
trySend(TransportEvent.Lost(network))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,35 +286,118 @@ class AndroidNetworkMonitor(
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(TransportState())
|
||||
connectivityManager?.registerNetworkCallback(request, callback)
|
||||
trySend(TransportEvent.Unknown)
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
awaitClose {
|
||||
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
|
||||
.onFailure { Timber.e(it, "Error unregistering ethernet network callback") }
|
||||
}
|
||||
}
|
||||
|
||||
override val networkStatusFlow =
|
||||
combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
|
||||
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
|
||||
if (hasAnyConnection) {
|
||||
NetworkStatus.Connected(
|
||||
wifiSsid = wifi.ssid,
|
||||
securityType = wifi.securityType,
|
||||
wifiConnected = wifi.connected,
|
||||
cellularConnected = cellular.connected,
|
||||
ethernetConnected = ethernet.connected,
|
||||
)
|
||||
} else {
|
||||
NetworkStatus.Disconnected
|
||||
suspend fun getSsidByDetectionMethod(
|
||||
detectionMethod: WifiDetectionMethod?,
|
||||
networkCapabilities: NetworkCapabilities?,
|
||||
): String {
|
||||
val method = detectionMethod ?: WifiDetectionMethod.DEFAULT
|
||||
return try {
|
||||
when (method) {
|
||||
WifiDetectionMethod.DEFAULT ->
|
||||
networkCapabilities?.getWifiSsid()
|
||||
?: wifiManager?.getWifiSsid()
|
||||
?: ANDROID_UNKNOWN_SSID
|
||||
WifiDetectionMethod.LEGACY ->
|
||||
wifiManager?.getWifiSsid() ?: ANDROID_UNKNOWN_SSID
|
||||
WifiDetectionMethod.ROOT ->
|
||||
withTimeoutOrNull(2000) { // 2-second timeout
|
||||
configurationListener.rootShell.getCurrentWifiName()
|
||||
} ?: ANDROID_UNKNOWN_SSID
|
||||
WifiDetectionMethod.SHIZUKU ->
|
||||
withTimeoutOrNull(2000) { // 2-second timeout
|
||||
ShizukuShell(applicationScope)
|
||||
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
|
||||
} ?: ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
.also { Timber.d("NetworkStatus: $it") }
|
||||
.trim()
|
||||
.replace(Regex("[\n\r]"), "")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get SSID with method: ${method.name}")
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
||||
}
|
||||
|
||||
override val connectivityStateFlow =
|
||||
combine(
|
||||
wifiFlow.scan(
|
||||
WifiState(
|
||||
locationPermissionsGranted =
|
||||
ContextCompat.checkSelfPermission(
|
||||
appContext,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
) == PackageManager.PERMISSION_GRANTED,
|
||||
locationServicesEnabled =
|
||||
locationManager?.isLocationServicesEnabled() ?: false,
|
||||
)
|
||||
) { previous, event ->
|
||||
when (event) {
|
||||
is TransportEvent.Available ->
|
||||
previous.copy(
|
||||
connected = true,
|
||||
ssid =
|
||||
getSsidByDetectionMethod(
|
||||
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
|
||||
null,
|
||||
),
|
||||
securityType = wifiManager?.getCurrentSecurityType(),
|
||||
)
|
||||
is TransportEvent.CapabilitiesChanged ->
|
||||
previous.copy(
|
||||
connected = true,
|
||||
ssid =
|
||||
getSsidByDetectionMethod(
|
||||
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
|
||||
null,
|
||||
),
|
||||
securityType = wifiManager?.getCurrentSecurityType(),
|
||||
)
|
||||
is TransportEvent.LocationPermissionGranted ->
|
||||
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(),
|
||||
)
|
||||
is TransportEvent.Lost ->
|
||||
previous.copy(connected = false, securityType = null, ssid = null)
|
||||
TransportEvent.Unknown -> previous
|
||||
}
|
||||
},
|
||||
cellularFlow,
|
||||
ethernetFlow,
|
||||
) { wifi, cellular, ethernet ->
|
||||
val cellularConnected = cellular is TransportEvent.Available
|
||||
val ethernetConnected = ethernet is TransportEvent.Available
|
||||
ConnectivityState(
|
||||
wifi,
|
||||
cellularConnected = cellularConnected,
|
||||
ethernetConnected = ethernetConnected,
|
||||
)
|
||||
.also { Timber.d("Connectivity Status: $it") }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||
|
||||
override fun sendLocationPermissionsGrantedBroadcast() {
|
||||
val action = "$packageName.$LOCATION_GRANTED"
|
||||
val intent = Intent(action)
|
||||
Timber.d("Sending broadcast: $action")
|
||||
appContext.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import com.zaneschepke.networkmonitor.util.WifiSecurityType
|
||||
|
||||
data class ConnectivityState(
|
||||
val wifiState: WifiState,
|
||||
val ethernetConnected: Boolean = false,
|
||||
val cellularConnected: Boolean = false,
|
||||
) {
|
||||
fun hasConnectivity(): Boolean = wifiState.connected || ethernetConnected || cellularConnected
|
||||
}
|
||||
|
||||
data class WifiState(
|
||||
val connected: Boolean = false,
|
||||
val ssid: String? = null,
|
||||
val securityType: WifiSecurityType? = null,
|
||||
val locationPermissionsGranted: Boolean,
|
||||
val locationServicesEnabled: Boolean,
|
||||
)
|
||||
@@ -3,7 +3,5 @@ package com.zaneschepke.networkmonitor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val networkStatusFlow: Flow<NetworkStatus>
|
||||
|
||||
fun sendLocationPermissionsGrantedBroadcast()
|
||||
val connectivityStateFlow: Flow<ConnectivityState>
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
sealed class NetworkStatus {
|
||||
data object Disconnected : NetworkStatus() {
|
||||
override val wifiConnected = false
|
||||
override val ethernetConnected = false
|
||||
override val cellularConnected = false
|
||||
}
|
||||
|
||||
data class Connected(
|
||||
val wifiSsid: String? = null,
|
||||
val securityType: WifiSecurityType? = null,
|
||||
override val wifiConnected: Boolean = false,
|
||||
override val ethernetConnected: Boolean = false,
|
||||
override val cellularConnected: Boolean = false,
|
||||
) : NetworkStatus()
|
||||
|
||||
abstract val wifiConnected: Boolean
|
||||
abstract val ethernetConnected: Boolean
|
||||
abstract val cellularConnected: Boolean
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
|
||||
sealed class TransportEvent {
|
||||
data class Available(
|
||||
val network: Network,
|
||||
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
|
||||
) : TransportEvent()
|
||||
|
||||
data class Lost(val network: Network) : TransportEvent()
|
||||
|
||||
data class CapabilitiesChanged(
|
||||
val network: Network,
|
||||
val networkCapabilities: NetworkCapabilities,
|
||||
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 object Unknown : TransportEvent()
|
||||
}
|
||||
+30
-2
@@ -1,11 +1,15 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
package com.zaneschepke.networkmonitor.util
|
||||
|
||||
import android.location.LocationManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
const val WIFI_SSID_SHELL_COMMAND =
|
||||
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2"
|
||||
@@ -19,12 +23,25 @@ fun RootShell.getCurrentWifiName(): String {
|
||||
@Suppress("DEPRECATION")
|
||||
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
WifiSecurityType.from(connectionInfo.currentSecurityType)
|
||||
WifiSecurityType.Companion.from(connectionInfo.currentSecurityType)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun WifiManager?.getWifiSsid(): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
this@getWifiSsid?.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
||||
?: ANDROID_UNKNOWN_SSID
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NetworkCapabilities.getWifiSsid(): String {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val info: WifiInfo
|
||||
@@ -35,3 +52,14 @@ fun NetworkCapabilities.getWifiSsid(): String {
|
||||
}
|
||||
return ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
|
||||
fun LocationManager.isLocationServicesEnabled(): Boolean {
|
||||
return try {
|
||||
val isGpsEnabled = isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
val isNetworkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||
isGpsEnabled || isNetworkEnabled
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error checking location services")
|
||||
false
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
package com.zaneschepke.networkmonitor.util
|
||||
|
||||
import android.net.wifi.WifiInfo
|
||||
|
||||
Reference in New Issue
Block a user