Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 9553374460 chore(deps): bump roomVersion from 2.7.1 to 2.7.2
Bumps `roomVersion` from 2.7.1 to 2.7.2.

Updates `androidx.room:room-compiler` from 2.7.1 to 2.7.2

Updates `androidx.room:room-ktx` from 2.7.1 to 2.7.2

Updates `androidx.room:room-runtime` from 2.7.1 to 2.7.2

Updates `androidx.room:room-testing` from 2.7.1 to 2.7.2

---
updated-dependencies:
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.room:room-testing
  dependency-version: 2.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 16:56:52 +00:00
37 changed files with 374 additions and 1010 deletions
-2
View File
@@ -223,8 +223,6 @@ dependencies {
// shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.reorderable)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -1,302 +0,0 @@
{
"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,7 +1,9 @@
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
@@ -25,6 +27,7 @@ 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
@@ -51,7 +54,6 @@ 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
@@ -72,6 +74,7 @@ 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() {
@@ -319,7 +322,6 @@ class MainActivity : AppCompatActivity() {
)
}
}
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
}
}
}
@@ -328,4 +330,22 @@ 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
}
}
}
@@ -8,6 +8,7 @@ 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
@@ -216,8 +217,9 @@ class TunnelForegroundService : LifecycleService() {
}
private suspend fun startNetworkMonitorJob() {
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status ->
isNetworkConnected.value = status.hasConnectivity()
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
Timber.d("Network available: $status")
}
}
@@ -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,13 +158,20 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName =
when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
}
else -> null
}
copy(
isWifiConnected = connectivityState.wifiState.connected,
isMobileDataConnected = connectivityState.cellularConnected,
isEthernetConnected = connectivityState.ethernetConnected,
wifiName = connectivityState.wifiState.ssid,
isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName,
)
}
}
@@ -182,7 +189,7 @@ class AutoTunnelService : LifecycleService() {
old.isKernelEnabled == new.isKernelEnabled
} // Only emit when isKernelEnabled changes
.flatMapLatest {
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).map {
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
buildNetworkState(it)
}
}
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 18,
version = 17,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -32,7 +32,6 @@ 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,6 +46,5 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig ORDER BY position ASC")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}
@@ -24,10 +24,9 @@ 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")
val isEthernetTunnel: Boolean = false,
var isEthernetTunnel: Boolean = false,
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
var isIpv4Preferred: Boolean = true,
) {
companion object {
@@ -21,7 +21,6 @@ object TunnelConfigMapper {
pingIp,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
@@ -43,7 +42,6 @@ object TunnelConfigMapper {
pingIp,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
@@ -26,7 +26,6 @@ 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,6 +45,4 @@ sealed class Route {
@Serializable data class TunnelAutoTunnel(val id: Int) : Route()
@Serializable data object Logs : Route()
@Serializable data object Sort : Route()
}
@@ -3,41 +3,66 @@ 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,
trailing: @Composable () -> Unit,
isSelected: Boolean,
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()
},
)
} else Modifier
)
) {
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
@@ -10,6 +10,7 @@ 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
@@ -67,42 +68,53 @@ fun BottomNavbar(appUiState: AppUiState) {
onClick = { navController.goFromRoot(Route.Support) },
),
)
// Define ripple configuration based on platform
val rippleConfiguration =
if (isTv) {
RippleConfiguration()
} else {
null
}
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
items.forEach { item ->
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
val interactionSource = remember { MutableInteractionSource() }
// 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() }
NavigationBarItem(
icon = {
if (item.active) {
BadgedBox(
badge = {
Badge(
modifier = Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
containerColor = SilverTree,
)
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)
}
) {
} else {
Icon(imageVector = item.icon, contentDescription = item.name)
}
} 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,
)
},
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,
)
}
}
}
}
@@ -4,7 +4,6 @@ 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
@@ -12,7 +11,6 @@ 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
@@ -30,7 +28,6 @@ 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(
@@ -63,40 +60,35 @@ 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)
)
}
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)
)
} else {
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
}
}
if (selectedCount == 1) {
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
// 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 (showDelete) {
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
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))
}
}
}
}
@@ -220,25 +212,6 @@ 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,
@@ -40,8 +40,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
},
description = {
val cellularActive =
remember(uiState.connectivityState) {
uiState.connectivityState?.cellularConnected ?: false
remember(uiState.networkStatus) {
uiState.networkStatus?.cellularConnected ?: false
}
Text(
text =
@@ -77,8 +77,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
},
description = {
val ethernetActive =
remember(uiState.connectivityState) {
uiState.connectivityState?.ethernetConnected ?: false
remember(uiState.networkStatus) {
uiState.networkStatus?.ethernetConnected ?: false
}
Text(
text =
@@ -18,6 +18,7 @@ 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
@@ -67,12 +68,11 @@ fun WifiTunnelingItems(
},
description = {
val wifiInfo by
remember(uiState.connectivityState) {
remember(uiState.networkStatus) {
derivedStateOf {
uiState.connectivityState
?.wifiState
?.takeIf { it.connected }
.let { Pair(it?.ssid, it?.securityType) }
(uiState.networkStatus as? NetworkStatus.Connected)
?.takeIf { it.wifiConnected }
.let { Pair(it?.wifiSsid, it?.securityType) }
}
}
val (wifiName, securityType) = wifiInfo
@@ -1,9 +1,7 @@
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
@@ -11,7 +9,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -28,6 +25,8 @@ 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
@@ -41,10 +40,17 @@ fun TunnelList(
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val navController = LocalNavController.current
val lazyListState = rememberLazyListState()
val sortedTunnels = remember(appUiState.tunnels) { appUiState.tunnels.sortedBy { it.position } }
val collator = Collator.getInstance(Locale.getDefault())
val sortedTunnels =
remember(appUiState.tunnels) {
appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
}
LazyColumn(
horizontalAlignment = Alignment.Start,
@@ -53,7 +59,7 @@ fun TunnelList(
modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect()),
state = lazyListState,
state = rememberLazyListState(0, appUiState.tunnels.count()),
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
@@ -72,33 +78,19 @@ fun TunnelList(
isSelected = selected,
tunnel = tunnel,
tunnelState = tunnelState,
onTvClick = {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
onClick = {
if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
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,
)
}
}
@@ -34,11 +34,10 @@ fun TunnelRowItem(
isSelected: Boolean,
tunnel: TunnelConf,
tunnelState: TunnelState,
onTvClick: () -> Unit,
onClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit,
isTv: Boolean,
modifier: Modifier = Modifier,
) {
val leadingIconColor =
remember(state) {
@@ -77,6 +76,8 @@ fun TunnelRowItem(
}
},
text = tunnel.tunName,
onHold = { if (!isTv) onToggleSelectedTunnel(tunnel) },
onClick = { if (!isTv) onClick() },
expanded = {
if (tunnelState.status != TunnelStatus.Down) {
TunnelStatisticsRow(tunnelState.statistics, tunnel)
@@ -88,7 +89,7 @@ fun TunnelRowItem(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
if (isTv) {
IconButton(onClick = onTvClick) {
IconButton(onClick = onClick) {
Icon(
Icons.Rounded.Settings,
contentDescription = stringResource(R.string.settings),
@@ -99,6 +100,5 @@ fun TunnelRowItem(
}
},
isSelected = isSelected,
modifier = modifier,
)
}
@@ -1,168 +0,0 @@
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.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkStatus
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 connectivityState: ConnectivityState? = null,
val networkStatus: NetworkStatus? = null,
)
@@ -3,17 +3,18 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.*
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.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(
@@ -48,11 +49,9 @@ 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 =
@@ -106,22 +105,5 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
}
}
// 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)
}
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}
@@ -24,7 +24,3 @@ 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,7 +34,6 @@ 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
@@ -80,9 +79,6 @@ 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
@@ -94,14 +90,14 @@ constructor(
appDataRepository.appState.flow,
tunnelManager.activeTunnels,
serviceManager.autoTunnelService.map { it != null },
networkMonitor.connectivityStateFlow,
networkMonitor.networkStatusFlow,
) { 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 ConnectivityState
val network = array[5] as NetworkStatus
AppUiState(
appSettings = settings,
@@ -110,7 +106,7 @@ constructor(
appState = appState,
isAutoTunnelActive = autoTunnel,
isAppLoaded = true,
connectivityState = network,
networkStatus = network,
)
}
.stateIn(
@@ -130,9 +126,6 @@ constructor(
}
}
fun handleUiEvent(event: UiEvent) =
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
fun handleEvent(event: AppEvent) =
viewModelScope.launch(ioDispatcher) {
uiState.withFirstState { state ->
@@ -222,15 +215,10 @@ 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,6 +125,4 @@ sealed class AppEvent {
data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent()
data object ToggleSelectAllTunnels : AppEvent()
data class SaveAllConfigs(val tunnels: List<TunnelConf>) : AppEvent()
}
@@ -1,5 +0,0 @@
package com.zaneschepke.wireguardautotunnel.viewmodel.event
sealed class UiEvent {
data object SortTunnels : UiEvent()
}
-4
View File
@@ -281,8 +281,4 @@
</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>
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.9.4"
const val VERSION_NAME = "3.9.3"
const val JVM_TARGET = "17"
const val VERSION_CODE = 39400
const val VERSION_CODE = 39300
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -1,6 +0,0 @@
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 -3
View File
@@ -20,7 +20,7 @@ material3 = "1.3.2"
navigationCompose = "2.9.0"
pinLockCompose = "1.0.4"
qrose = "1.0.1"
roomVersion = "2.7.1"
roomVersion = "2.7.2"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
@@ -35,7 +35,6 @@ workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.2"
reorderable = "2.5.1"
#plugins
material = "1.12.0"
@@ -111,7 +110,6 @@ 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" }
@@ -1,32 +0,0 @@
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
}
}
@@ -1,11 +1,9 @@
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
@@ -13,17 +11,13 @@ 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(
@@ -51,31 +45,76 @@ class AndroidNetworkMonitor(
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
entries.find { it.value == value } ?: DEFAULT
WifiDetectionMethod.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
// Track active Wi-Fi networks, their capabilities, and last active network ID
private val activeWifiNetworks = ActiveWifiStateManager()
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)
@OptIn(ExperimentalCoroutinesApi::class)
private val wifiFlow: Flow<TransportEvent> =
private val wifiFlow: Flow<WifiState> =
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<TransportEvent> = callbackFlow {
): 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))
}
}
}
val locationPermissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -86,15 +125,7 @@ class AndroidNetworkMonitor(
Timber.d(
"Received update: Precise and all-the-time location permissions are enabled"
)
activeWifiNetworks.getLatestValue()?.let { details ->
trySend(
TransportEvent.LocationPermissionGranted(
details.first,
details.second,
detectionMethod,
)
)
}
applicationScope.launch { handleUnknownWifi() }
}
}
}
@@ -104,39 +135,23 @@ class AndroidNetworkMonitor(
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LOCATION_SERVICES_FILTER) {
val isGpsEnabled =
locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER)
?: false
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled =
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
?: false
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
Timber.d(
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
)
activeWifiNetworks.getLatestValue()?.let { details ->
trySend(
TransportEvent.LocationServicesChanged(
isLocationServicesEnabled,
details.first,
details.second,
detectionMethod,
)
)
}
if (isLocationServicesEnabled)
applicationScope.launch { handleUnknownWifi() }
}
}
}
val permissionReceiverFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Context.RECEIVER_NOT_EXPORTED // Internal broadcast
} else {
0
}
val servicesReceiverFlags =
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Context.RECEIVER_EXPORTED // System broadcast
Context.RECEIVER_EXPORTED
} else {
0
}
@@ -144,42 +159,64 @@ class AndroidNetworkMonitor(
appContext.registerReceiver(
locationPermissionReceiver,
IntentFilter("$packageName.$LOCATION_GRANTED"),
permissionReceiverFlags,
flags,
)
appContext.registerReceiver(
locationServicesReceiver,
IntentFilter(LOCATION_SERVICES_FILTER),
servicesReceiverFlags,
flags,
)
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 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 handleOnWifiAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, null))
trySend(TransportEvent.Available(network, detectionMethod))
}
fun handleOnWifiCapabilitiesChanged(
suspend fun handleOnWifiAvailable(
network: Network,
networkCapabilities: NetworkCapabilities,
networkCapabilities: NetworkCapabilities?,
) {
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, networkCapabilities))
trySend(
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod)
)
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)
)
}
}
val callback =
@@ -188,11 +225,11 @@ class AndroidNetworkMonitor(
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
handleOnWifiAvailable(network)
applicationScope.launch { handleOnWifiAvailable(network, null) }
}
override fun onLost(network: Network) {
handleOnWifiLost(network)
applicationScope.launch { handleOnWifiLost(network) }
}
}
else ->
@@ -200,7 +237,7 @@ class AndroidNetworkMonitor(
override fun onAvailable(network: Network) {
if (detectionMethod != WifiDetectionMethod.DEFAULT)
handleOnWifiAvailable(network)
applicationScope.launch { handleOnWifiAvailable(network, null) }
}
override fun onCapabilitiesChanged(
@@ -208,11 +245,13 @@ class AndroidNetworkMonitor(
networkCapabilities: NetworkCapabilities,
) {
if (detectionMethod == WifiDetectionMethod.DEFAULT)
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
applicationScope.launch {
handleOnWifiAvailable(network, networkCapabilities)
}
}
override fun onLost(network: Network) {
handleOnWifiLost(network)
applicationScope.launch { handleOnWifiLost(network) }
}
}
}
@@ -223,31 +262,34 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
awaitClose {
runCatching {
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
connectivityManager?.unregisterNetworkCallback(callback)
}
.onFailure { Timber.e(it, "Error unregistering network callback") }
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)
}
}
private val cellularFlow: Flow<TransportEvent> = callbackFlow {
private val cellularFlow: Flow<TransportState> = callbackFlow {
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Cellular onAvailable: network=$network")
trySend(TransportEvent.Available(network))
trySend(TransportState(connected = true))
}
override fun onLost(network: Network) {
Timber.d("Cellular onLost: network=$network")
trySend(TransportEvent.Lost(network))
trySend(TransportState(connected = false))
}
}
@@ -257,26 +299,23 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering cellular network callback") }
}
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
private val ethernetFlow: Flow<TransportEvent> = callbackFlow {
private val ethernetFlow: Flow<TransportState> = callbackFlow {
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Ethernet onAvailable: network=$network")
trySend(TransportEvent.Available(network))
trySend(TransportState(connected = true))
}
override fun onLost(network: Network) {
Timber.d("Ethernet onLost: network=$network")
trySend(TransportEvent.Lost(network))
trySend(TransportState(connected = false))
}
}
@@ -286,118 +325,35 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build()
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering ethernet network callback") }
}
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
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
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
}
.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") }
.also { Timber.d("NetworkStatus: $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)
}
}
@@ -1,19 +0,0 @@
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,
)
@@ -1,15 +1,11 @@
package com.zaneschepke.networkmonitor.util
package com.zaneschepke.networkmonitor
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"
@@ -23,25 +19,12 @@ fun RootShell.getCurrentWifiName(): String {
@Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.Companion.from(connectionInfo.currentSecurityType)
WifiSecurityType.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
@@ -52,14 +35,3 @@ 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
}
}
@@ -3,5 +3,7 @@ package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val connectivityStateFlow: Flow<ConnectivityState>
val networkStatusFlow: Flow<NetworkStatus>
fun sendLocationPermissionsGrantedBroadcast()
}
@@ -0,0 +1,21 @@
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
}
@@ -1,34 +0,0 @@
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()
}
@@ -1,4 +1,4 @@
package com.zaneschepke.networkmonitor.util
package com.zaneschepke.networkmonitor
import android.net.wifi.WifiInfo