parent
0784c96011
commit
d3ea75869a
|
@ -0,0 +1,232 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "4c9418386f72dfac5d28ab96c1e5ea0b",
|
||||
"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_wifi_by_shell_enabled` INTEGER NOT NULL DEFAULT false)",
|
||||
"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": "isWifiNameByShellEnabled",
|
||||
"columnName": "is_wifi_by_shell_enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingCooldown",
|
||||
"columnName": "ping_cooldown",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pingIp",
|
||||
"columnName": "ping_ip",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false,
|
||||
"defaultValue": "null"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '4c9418386f72dfac5d28ab96c1e5ea0b')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -186,10 +186,6 @@
|
|||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".receiver.BackgroundActionReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name=".receiver.AppUpdateReceiver"
|
||||
android:exported="false">
|
||||
|
|
|
@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 10,
|
||||
version = 11,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
|
@ -36,6 +36,11 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
AutoMigration(9, 10),
|
||||
AutoMigration(
|
||||
from = 10,
|
||||
to = 11,
|
||||
spec = RemoveTunnelPauseMigration::class,
|
||||
),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
@ -55,3 +60,9 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
columnName = "is_battery_saver_enabled",
|
||||
)
|
||||
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(
|
||||
tableName = "Settings",
|
||||
columnName = "is_auto_tunnel_paused",
|
||||
)
|
||||
class RemoveTunnelPauseMigration : AutoMigrationSpec
|
||||
|
|
|
@ -26,7 +26,6 @@ class DataStoreManager(
|
|||
val currentSSID = stringPreferencesKey("CURRENT_SSID")
|
||||
val pinLockEnabled = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||
val tunnelStatsExpanded = booleanPreferencesKey("TUNNEL_STATS_EXPANDED")
|
||||
val wildcardsEnabled = booleanPreferencesKey("WILDCARDS_ENABLED")
|
||||
val theme = stringPreferencesKey("THEME")
|
||||
}
|
||||
|
||||
|
|
|
@ -7,14 +7,12 @@ data class GeneralState(
|
|||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
|
||||
val isWildcardsEnabled: Boolean = IS_WILDCARDS_ENABLED,
|
||||
val theme: Theme = Theme.AUTOMATIC
|
||||
val theme: Theme = Theme.AUTOMATIC,
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
const val IS_TUNNEL_STATS_EXPANDED = false
|
||||
const val IS_WILDCARDS_ENABLED = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,6 @@ data class Settings(
|
|||
defaultValue = "false",
|
||||
)
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAutoTunnelPaused: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_ping_enabled",
|
||||
defaultValue = "false",
|
||||
|
@ -55,4 +50,14 @@ data class Settings(
|
|||
defaultValue = "false",
|
||||
)
|
||||
val isAmneziaEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_wildcards_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isWildcardsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_wifi_by_shell_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isWifiNameByShellEnabled: Boolean = false,
|
||||
)
|
||||
|
|
|
@ -13,10 +13,6 @@ interface AppStateRepository {
|
|||
|
||||
suspend fun setPinLockEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isWildcardsEnabled(): Boolean
|
||||
|
||||
suspend fun setWildcardsEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
||||
|
||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||
|
@ -31,7 +27,7 @@ interface AppStateRepository {
|
|||
|
||||
suspend fun setTheme(theme: Theme)
|
||||
|
||||
suspend fun getTheme() : Theme
|
||||
suspend fun getTheme(): Theme
|
||||
|
||||
val generalStateFlow: Flow<GeneralState>
|
||||
}
|
||||
|
|
|
@ -29,14 +29,6 @@ class DataStoreAppStateRepository(
|
|||
dataStoreManager.saveToDataStore(DataStoreManager.pinLockEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isWildcardsEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.wildcardsEnabled) ?: GeneralState.IS_WILDCARDS_ENABLED
|
||||
}
|
||||
|
||||
override suspend fun setWildcardsEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.wildcardsEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.batteryDisableShown)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
|
@ -71,7 +63,7 @@ class DataStoreAppStateRepository(
|
|||
return dataStoreManager.getFromStore(DataStoreManager.theme)?.let {
|
||||
try {
|
||||
Theme.valueOf(it)
|
||||
} catch (_ : IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
Theme.AUTOMATIC
|
||||
}
|
||||
} ?: Theme.AUTOMATIC
|
||||
|
@ -92,8 +84,7 @@ class DataStoreAppStateRepository(
|
|||
pref[DataStoreManager.pinLockEnabled]
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
||||
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
|
||||
isWildcardsEnabled = pref[DataStoreManager.wildcardsEnabled] ?: GeneralState.IS_WILDCARDS_ENABLED,
|
||||
theme = getTheme()
|
||||
theme = getTheme(),
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -26,8 +24,6 @@ class RoomTunnelConfigRepository(
|
|||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
}.also {
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,8 +56,6 @@ class RoomTunnelConfigRepository(
|
|||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
}.also {
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.wireguard.android.util.RootShell
|
|||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
|
@ -65,6 +66,7 @@ class TunnelModule {
|
|||
tunnelConfigRepository: TunnelConfigRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
serviceManager: ServiceManager,
|
||||
): TunnelService {
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
|
@ -73,6 +75,13 @@ class TunnelModule {
|
|||
appDataRepository,
|
||||
applicationScope,
|
||||
ioDispatcher,
|
||||
serviceManager,
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideServiceManager(@ApplicationContext context: Context): ServiceManager {
|
||||
return ServiceManager(context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
|||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppUpdateReceiver : BroadcastReceiver() {
|
||||
|
@ -25,7 +25,10 @@ class AppUpdateReceiver : BroadcastReceiver() {
|
|||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: TunnelService
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||
|
@ -33,11 +36,11 @@ class AppUpdateReceiver : BroadcastReceiver() {
|
|||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Restarting services after upgrade")
|
||||
ServiceManager.startWatcherServiceForeground(context)
|
||||
serviceManager.startAutoTunnel(true)
|
||||
}
|
||||
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
|
||||
if (!settings.isAutoTunnelEnabled) {
|
||||
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
|
||||
if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id)
|
||||
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
package com.zaneschepke.wireguardautotunnel.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BackgroundActionReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
|
||||
if (id == 0) return
|
||||
when (intent.action) {
|
||||
ACTION_CONNECT -> {
|
||||
Timber.d("Connect actions")
|
||||
applicationScope.launch {
|
||||
val tunnel = tunnelConfigRepository.getById(id)
|
||||
tunnel?.let {
|
||||
ServiceManager.startTunnelBackgroundService(context)
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTION_DISCONNECT -> {
|
||||
applicationScope.launch {
|
||||
val tunnel = tunnelConfigRepository.getById(id)
|
||||
tunnel?.let {
|
||||
ServiceManager.stopTunnelBackgroundService(context)
|
||||
tunnelService.get().stopTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_CONNECT = "ACTION_CONNECT"
|
||||
const val ACTION_DISCONNECT = "ACTION_DISCONNECT"
|
||||
const val TUNNEL_ID_EXTRA_KEY = "tunnelId"
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
|||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -28,6 +27,9 @@ class BootReceiver : BroadcastReceiver() {
|
|||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
||||
applicationScope.launch {
|
||||
|
@ -37,11 +39,11 @@ class BootReceiver : BroadcastReceiver() {
|
|||
val tunState = tunnelService.get().vpnState.value.status
|
||||
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
|
||||
Timber.i("Starting previously active tunnel")
|
||||
context.startTunnelBackground(activeTunnels.first().id)
|
||||
tunnelService.get().startTunnel(activeTunnels.first(), true)
|
||||
}
|
||||
if (isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
ServiceManager.startWatcherServiceForeground(context)
|
||||
serviceManager.startAutoTunnel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,11 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isDown
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -74,6 +75,9 @@ class AutoTunnelService : LifecycleService() {
|
|||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
@ -88,14 +92,11 @@ class AutoTunnelService : LifecycleService() {
|
|||
private var pingJob: Job? = null
|
||||
private var networkEventJob: Job? = null
|
||||
|
||||
@get:Synchronized @set:Synchronized
|
||||
private var running: Boolean = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
kotlin.runCatching {
|
||||
launchNotification()
|
||||
launchWatcherNotification()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
|
@ -110,32 +111,14 @@ class AutoTunnelService : LifecycleService() {
|
|||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Timber.d("onStartCommand executed with startId: $startId")
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
when (action) {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name,
|
||||
-> startService()
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
}
|
||||
}
|
||||
serviceManager.autoTunnelService.complete(this)
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private suspend fun launchNotification() {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else {
|
||||
launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
if (running) return
|
||||
running = true
|
||||
fun start() {
|
||||
kotlin.runCatching {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
launchNotification()
|
||||
launchWatcherNotification()
|
||||
initWakeLock()
|
||||
}
|
||||
startSettingsJob()
|
||||
|
@ -145,7 +128,7 @@ class AutoTunnelService : LifecycleService() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
fun stop() {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
|
@ -157,6 +140,7 @@ class AutoTunnelService : LifecycleService() {
|
|||
override fun onDestroy() {
|
||||
cancelAndResetNetworkJobs()
|
||||
cancelAndResetPingJob()
|
||||
serviceManager.autoTunnelService = CompletableDeferred()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -176,10 +160,6 @@ class AutoTunnelService : LifecycleService() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun launchWatcherPausedNotification() {
|
||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||
}
|
||||
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
|
@ -265,8 +245,7 @@ class AutoTunnelService : LifecycleService() {
|
|||
runCatching {
|
||||
do {
|
||||
val vpnState = tunnelService.get().vpnState.value
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (vpnState.status == TunnelState.UP && !settings.isAutoTunnelPaused) {
|
||||
if (vpnState.status == TunnelState.UP) {
|
||||
if (vpnState.tunnelConfig != null) {
|
||||
val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick)
|
||||
val results = if (vpnState.tunnelConfig.pingIp != null) {
|
||||
|
@ -296,17 +275,6 @@ class AutoTunnelService : LifecycleService() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun onAutoTunnelPause(paused: Boolean) {
|
||||
if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused
|
||||
!= paused
|
||||
) {
|
||||
when (paused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
false -> launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun watchForSettingsChanges() {
|
||||
Timber.i("Starting settings watcher")
|
||||
withContext(ioDispatcher) {
|
||||
|
@ -321,7 +289,7 @@ class AutoTunnelService : LifecycleService() {
|
|||
tunnels = tunnels,
|
||||
)
|
||||
}.collect {
|
||||
onAutoTunnelPause(it.settings.isAutoTunnelPaused)
|
||||
Timber.d("got new settings: ${it.settings}")
|
||||
manageJobsBySettings(it.settings)
|
||||
autoTunnelStateFlow.emit(it)
|
||||
}
|
||||
|
@ -331,7 +299,12 @@ class AutoTunnelService : LifecycleService() {
|
|||
private suspend fun watchForVpnStateChanges() {
|
||||
Timber.i("Starting vpn state watcher")
|
||||
withContext(ioDispatcher) {
|
||||
tunnelService.get().vpnState.collect { state ->
|
||||
tunnelService.get().vpnState.distinctUntilChanged { old, new ->
|
||||
old.tunnelConfig == new.tunnelConfig && old.status == new.status
|
||||
}.collect { state ->
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(vpnState = state)
|
||||
}
|
||||
state.tunnelConfig?.let {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (it.isPingEnabled && !settings.isPingEnabled) {
|
||||
|
@ -475,9 +448,8 @@ class AutoTunnelService : LifecycleService() {
|
|||
|
||||
private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
rootShell.get().getCurrentWifiName()
|
||||
} catch (_: Exception) {
|
||||
with(autoTunnelStateFlow.value.settings) {
|
||||
if (isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName()
|
||||
wifiService.getNetworkName(networkCapabilities)
|
||||
}
|
||||
}
|
||||
|
@ -487,100 +459,95 @@ class AutoTunnelService : LifecycleService() {
|
|||
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
||||
}
|
||||
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun handleNetworkEventChanges() {
|
||||
withContext(ioDispatcher) {
|
||||
Timber.i("Starting network event watcher")
|
||||
autoTunnelStateFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
// delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val activeTunnel = tunnelService.get().vpnState.value.tunnelConfig
|
||||
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (isTunnelDown()) {
|
||||
defaultTunnel?.let {
|
||||
Timber.d("New watcher state!")
|
||||
// delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
val activeTunnel = watcherState.vpnState.tunnelConfig
|
||||
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
if (watcherState.vpnState.isDown()) {
|
||||
defaultTunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: defaultTunnel
|
||||
if (watcherState.vpnState.isDown() || activeTunnel?.isMobileDataTunnel == false) {
|
||||
tunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!watcherState.vpnState.isDown()) {
|
||||
activeTunnel?.let {
|
||||
tunnelService.get().stopTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("Untrusted wifi condition met")
|
||||
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
|
||||
watcherState.vpnState.isDown()
|
||||
) {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
|
||||
)
|
||||
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (watcherState.vpnState.isDown() || activeTunnel?.id != it.id) {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: defaultTunnel
|
||||
if (isTunnelDown() || activeTunnel?.isMobileDataTunnel == false) {
|
||||
tunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
if (!isTunnelDown()) {
|
||||
activeTunnel?.let {
|
||||
tunnelService.get().stopTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("Untrusted wifi condition met")
|
||||
if (activeTunnel?.tunnelNetworks?.isMatchingToWildcardList(watcherState.currentNetworkSSID) == false ||
|
||||
activeTunnel == null || isTunnelDown()
|
||||
) {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
|
||||
)
|
||||
watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (isTunnelDown() || activeTunnel?.id != it.id) {
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != tunnelService.get().name || watcherState.vpnState.isDown()) {
|
||||
default?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != tunnelService.get().name || isTunnelDown()) {
|
||||
default?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
|
||||
)
|
||||
if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
else -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||
|
||||
data class AutoTunnelState(
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
||||
|
@ -41,7 +44,7 @@ data class AutoTunnelState(
|
|||
return (
|
||||
!isEthernetConnected &&
|
||||
isWifiConnected &&
|
||||
!settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) &&
|
||||
!isCurrentSSIDTrusted() &&
|
||||
settings.isTunnelOnWifiEnabled
|
||||
)
|
||||
}
|
||||
|
@ -51,7 +54,7 @@ data class AutoTunnelState(
|
|||
!isEthernetConnected &&
|
||||
(
|
||||
isWifiConnected &&
|
||||
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
|
||||
isCurrentSSIDTrusted()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -73,4 +76,32 @@ data class AutoTunnelState(
|
|||
!isMobileDataConnected
|
||||
)
|
||||
}
|
||||
|
||||
fun isCurrentSSIDTrusted(): Boolean {
|
||||
return if (settings.isWildcardsEnabled) {
|
||||
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
|
||||
} else {
|
||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
|
||||
}
|
||||
}
|
||||
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
|
||||
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
|
||||
return (
|
||||
if (settings.isWildcardsEnabled) {
|
||||
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
|
||||
} else {
|
||||
currentTunnelNetworks?.contains(currentNetworkSSID)
|
||||
}
|
||||
) == true
|
||||
}
|
||||
|
||||
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
|
||||
return tunnels.firstOrNull {
|
||||
if (settings.isWildcardsEnabled) {
|
||||
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
|
||||
} else {
|
||||
it.tunnelNetworks.contains(currentNetworkSSID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,69 +3,81 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
|
||||
object ServiceManager {
|
||||
private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) {
|
||||
if (VpnService.prepare(context) != null) return
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach { (k, v) -> it.putExtra(k, v) }
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND, Action.STOP_FOREGROUND ->
|
||||
context.startForegroundService(
|
||||
intent,
|
||||
)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ServiceManager
|
||||
@Inject constructor(private val context: Context) {
|
||||
|
||||
Action.START, Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
private val _autoTunnelActive = MutableStateFlow(false)
|
||||
|
||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||
|
||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||
var backgroundService = CompletableDeferred<TunnelBackgroundService>()
|
||||
|
||||
companion object : SingletonHolder<ServiceManager, Context>(::ServiceManager)
|
||||
|
||||
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
||||
val intent = Intent(context, cls)
|
||||
if (background) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(context: Context) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
suspend fun startAutoTunnel(background: Boolean) {
|
||||
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
||||
kotlin.runCatching {
|
||||
startService(AutoTunnelService::class.java, background)
|
||||
autoTunnelService.await()
|
||||
autoTunnelService.getCompleted().start()
|
||||
_autoTunnelActive.update { true }
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
suspend fun startBackgroundService() {
|
||||
if (backgroundService.isCompleted) return
|
||||
kotlin.runCatching {
|
||||
startService(TunnelBackgroundService::class.java, true)
|
||||
backgroundService.await()
|
||||
backgroundService.getCompleted().start()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopWatcherService(context: Context) {
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
fun stopBackgroundService() {
|
||||
if (!backgroundService.isCompleted) return
|
||||
runCatching {
|
||||
backgroundService.getCompleted().stop()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun startTunnelBackgroundService(context: Context) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
TunnelBackgroundService::class.java,
|
||||
)
|
||||
fun stopAutoTunnel() {
|
||||
if (!autoTunnelService.isCompleted) return
|
||||
runCatching {
|
||||
autoTunnelService.getCompleted().stop()
|
||||
_autoTunnelActive.update { false }
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTunnelBackgroundService(context: Context) {
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
TunnelBackgroundService::class.java,
|
||||
)
|
||||
fun requestTunnelTileUpdate() {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.LifecycleService
|
|||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -15,6 +16,9 @@ class TunnelBackgroundService : LifecycleService() {
|
|||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val foregroundId = 123
|
||||
|
||||
override fun onCreate() {
|
||||
|
@ -29,27 +33,24 @@ class TunnelBackgroundService : LifecycleService() {
|
|||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent != null) {
|
||||
val action = intent.action
|
||||
when (action) {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name,
|
||||
-> startService()
|
||||
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
|
||||
}
|
||||
}
|
||||
serviceManager.backgroundService.complete(this)
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
fun start() {
|
||||
startForeground(foregroundId, createNotification())
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
fun stop() {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceManager.backgroundService = CompletableDeferred()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
return notificationService.createNotification(
|
||||
getString(R.string.vpn_channel_id),
|
||||
|
|
|
@ -6,9 +6,8 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
|||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -24,6 +23,9 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
@ -44,26 +46,16 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
|
||||
tunnelConfig?.let {
|
||||
when (intent.action) {
|
||||
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
|
||||
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
|
||||
Action.START.name -> tunnelService.get().startTunnel(it, true)
|
||||
Action.STOP.name -> tunnelService.get().stopTunnel(it)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||
when (intent.action) {
|
||||
Action.START.name ->
|
||||
appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = false,
|
||||
),
|
||||
)
|
||||
Action.STOP.name ->
|
||||
appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = true,
|
||||
),
|
||||
)
|
||||
Action.START.name -> serviceManager.startAutoTunnel(true)
|
||||
Action.STOP.name -> serviceManager.stopAutoTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
|
@ -9,9 +8,9 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -23,51 +22,18 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (e: Throwable) {
|
||||
Timber.e("Failed to bind to AutoTunnelTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
|
||||
applicationScope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
kotlin.runCatching {
|
||||
when (it.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (it.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
|
@ -82,26 +48,28 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
lifecycleScope.launch {
|
||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||
updateTileState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTileState() {
|
||||
serviceManager.autoTunnelActive.value.let {
|
||||
if (it) setActive() else setInactive()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
kotlin.runCatching {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
return@launch appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = true,
|
||||
),
|
||||
)
|
||||
if (serviceManager.autoTunnelActive.value) {
|
||||
serviceManager.stopAutoTunnel()
|
||||
setInactive()
|
||||
} else {
|
||||
serviceManager.startAutoTunnel(true)
|
||||
setActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,16 +96,15 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
kotlin.runCatching {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
qsTile.updateTile()
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -11,8 +13,6 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -36,7 +36,6 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.d("onCreate for tile service")
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
|
@ -52,6 +51,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
Timber.d("Updating tile!")
|
||||
lifecycleScope.launch {
|
||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||
updateTileState()
|
||||
|
@ -60,6 +60,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
|
||||
private suspend fun updateTileState() {
|
||||
val lastActive = appDataRepository.getStartTunnelConfig()
|
||||
Timber.d("Got config $lastActive")
|
||||
lastActive?.let {
|
||||
updateTile(it)
|
||||
}
|
||||
|
@ -68,13 +69,15 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
Timber.d("Click")
|
||||
lifecycleScope.launch {
|
||||
val context = this@TunnelControlTile
|
||||
val lastActive = appDataRepository.getStartTunnelConfig()
|
||||
lastActive?.let { tunnel ->
|
||||
if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id)
|
||||
context.startTunnelBackground(tunnel.id)
|
||||
if (tunnel.isActive) {
|
||||
tunnelService.get().stopTunnel(tunnel)
|
||||
} else {
|
||||
tunnelService.get().startTunnel(tunnel, true)
|
||||
}
|
||||
updateTileState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +127,17 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
}
|
||||
}
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (_: Throwable) {
|
||||
Timber.e("Failed to bind to TunnelControlTile")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState>
|
||||
|
||||
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
|
||||
|
||||
|
@ -18,5 +18,6 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
|||
suspend fun getState(): TunnelState
|
||||
|
||||
fun cancelStatsJob()
|
||||
|
||||
fun startStatsJob()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositor
|
|||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
|
@ -36,6 +37,7 @@ constructor(
|
|||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val serviceManager: ServiceManager,
|
||||
) : TunnelService {
|
||||
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
|
@ -87,9 +89,9 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> {
|
||||
return withContext(ioDispatcher) {
|
||||
onBeforeStart(tunnelConfig)
|
||||
onBeforeStart(tunnelConfig, background)
|
||||
setState(tunnelConfig, TunnelState.UP).onSuccess {
|
||||
emitTunnelState(it)
|
||||
}.onFailure {
|
||||
|
@ -143,18 +145,28 @@ constructor(
|
|||
resetBackendStatistics()
|
||||
}
|
||||
|
||||
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
|
||||
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
|
||||
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig, background: Boolean) {
|
||||
if (_vpnState.value.status == TunnelState.UP &&
|
||||
tunnelConfig != _vpnState.value.tunnelConfig
|
||||
) {
|
||||
vpnState.value.tunnelConfig?.let { stopTunnel(it) }
|
||||
}
|
||||
if (background) serviceManager.startBackgroundService()
|
||||
resetBackendStatistics()
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
|
||||
emitVpnStateConfig(tunnelConfig)
|
||||
startStatsJob()
|
||||
Timber.d("Updating start")
|
||||
serviceManager.requestTunnelTileUpdate()
|
||||
}
|
||||
|
||||
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
|
||||
cancelStatsJob()
|
||||
resetBackendStatistics()
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
|
||||
serviceManager.stopBackgroundService()
|
||||
Timber.d("UPdating stop")
|
||||
serviceManager.requestTunnelTileUpdate()
|
||||
}
|
||||
|
||||
private fun emitTunnelState(state: TunnelState) {
|
||||
|
|
|
@ -10,6 +10,5 @@ data class AppUiState(
|
|||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val vpnState: VpnState = VpnState(),
|
||||
val generalState: GeneralState = GeneralState(),
|
||||
val isKernelAvailable: Boolean = false,
|
||||
val isRooted: Boolean = false,
|
||||
val autoTunnelActive: Boolean = false,
|
||||
)
|
||||
|
|
|
@ -2,8 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
|
@ -11,7 +9,6 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
|||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -20,13 +17,10 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
@ -38,6 +32,7 @@ constructor(
|
|||
private val appDataRepository: AppDataRepository,
|
||||
private val tunnelService: Provider<TunnelService>,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val serviceManager: ServiceManager,
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
|
@ -46,12 +41,14 @@ constructor(
|
|||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||
tunnelService.get().vpnState,
|
||||
appDataRepository.appState.generalStateFlow,
|
||||
) { settings, tunnels, tunnelState, generalState ->
|
||||
serviceManager.autoTunnelActive,
|
||||
) { settings, tunnels, tunnelState, generalState, autoTunnel ->
|
||||
AppUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
generalState,
|
||||
autoTunnel,
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope + ioDispatcher,
|
||||
|
@ -95,7 +92,7 @@ constructor(
|
|||
|
||||
private suspend fun initAutoTunnel() {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) ServiceManager.startWatcherService(WireGuardAutoTunnel.instance)
|
||||
if (settings.isAutoTunnelEnabled) serviceManager.startAutoTunnel(false)
|
||||
}
|
||||
|
||||
fun onPinLockDisabled() = viewModelScope.launch(ioDispatcher) {
|
||||
|
|
|
@ -39,9 +39,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.LocaleStorage
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusRequester
|
||||
|
@ -67,6 +65,7 @@ import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
|
|||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -100,17 +99,17 @@ class MainActivity : AppCompatActivity() {
|
|||
val navController = rememberNavController()
|
||||
val rootItemFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(appUiState.vpnState.status) {
|
||||
val context = this@MainActivity
|
||||
when (appUiState.vpnState.status) {
|
||||
TunnelState.DOWN -> ServiceManager.stopTunnelBackgroundService(context)
|
||||
else -> Unit
|
||||
}
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
LaunchedEffect(appUiState.tunnels) {
|
||||
Timber.d("Updating launched")
|
||||
requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.autoTunnelActive) {
|
||||
requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
with(appUiState.settings) {
|
||||
LaunchedEffect(isAutoTunnelPaused, isAutoTunnelEnabled) {
|
||||
LaunchedEffect(isAutoTunnelEnabled) {
|
||||
this@MainActivity.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -248,4 +247,3 @@ class MainActivity : AppCompatActivity() {
|
|||
tunnelService.cancelStatsJob()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ fun ClickableIconButton(onClick: () -> Unit, onIconClick: () -> Unit, text: Stri
|
|||
if (enabled) {
|
||||
onIconClick()
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.ui.common.button
|
|||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -31,21 +30,27 @@ import kotlin.let
|
|||
@androidx.compose.runtime.Composable
|
||||
fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, leadingIcon: ImageVector? = null, description: String? = null) {
|
||||
val border: BorderStroke? =
|
||||
if (selected) BorderStroke(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.primary
|
||||
) else null
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
border = border,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Box(modifier = Modifier.clickable { onClick() }
|
||||
.fillMaxWidth()) {
|
||||
if (selected) {
|
||||
BorderStroke(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
border = border,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.clickable { onClick() }
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
|
@ -61,7 +66,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
|
|||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
16.dp.scaledWidth()
|
||||
16.dp.scaledWidth(),
|
||||
),
|
||||
verticalAlignment = Alignment.Companion.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = if (description == null) 10.dp.scaledHeight() else 0.dp),
|
||||
|
@ -77,7 +82,7 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
|
|||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
description?.let {
|
||||
Text(
|
||||
|
@ -91,5 +96,5 @@ fun IconSurfaceButton(title: String, onClick: () -> Unit, selected: Boolean, lea
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.common.button.surface
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
data class SelectionItem(
|
||||
|
|
|
@ -24,65 +24,63 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||
|
||||
@Composable
|
||||
fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
items.mapIndexed { index, item ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.then(item.onClick?.let { Modifier.clickable { it() }} ?: Modifier)
|
||||
.fillMaxWidth()
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
items.mapIndexed { index, item ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.then(item.onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp.scaledHeight()),
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp.scaledWidth())
|
||||
.weight(4f, false)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp.scaledWidth())
|
||||
.weight(4f, false)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
item.leadingIcon?.let { icon ->
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
|
||||
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
|
||||
) {
|
||||
item.title()
|
||||
item.description?.let {
|
||||
it()
|
||||
}
|
||||
}
|
||||
item.leadingIcon?.let { icon ->
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
item.trailing?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
|
||||
.weight(1f),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = if (item.leadingIcon != null) 16.dp.scaledWidth() else 0.dp)
|
||||
.padding(vertical = if (item.description == null) 16.dp.scaledHeight() else 6.dp.scaledHeight()),
|
||||
) {
|
||||
item.title()
|
||||
item.description?.let {
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
item.trailing?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp.scaledWidth(), start = 16.dp.scaledWidth())
|
||||
.weight(1f),
|
||||
) {
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
if (index + 1 != items.size) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
|
@ -19,4 +19,3 @@ fun GroupLabel(title: String) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ fun VersionLabel() {
|
|||
color = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(BuildConfig.VERSION_NAME))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,21 +8,11 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
|
@ -34,7 +24,6 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
|
|||
}
|
||||
|
||||
if (showBottomBar) {
|
||||
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.common.navigation
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
|
@ -10,4 +9,3 @@ val LocalNavController = compositionLocalOf<NavHostController> {
|
|||
}
|
||||
|
||||
val LocalFocusRequester = compositionLocalOf<FocusRequester> { error("FocusRequester is not provided") }
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo
|
|||
Text(title)
|
||||
},
|
||||
navigationIcon = {
|
||||
if(showBack) IconButton(onClick = { navController.popBackStack() }) {
|
||||
val icon = Icons.AutoMirrored.Outlined.ArrowBack
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
if (showBack) {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
val icon = Icons.AutoMirrored.Outlined.ArrowBack
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
|
|
|
@ -234,7 +234,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
|
@ -347,7 +347,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
hint = stringResource(R.string.junk_packet_count).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMinSize,
|
||||
|
@ -360,7 +360,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.junkPacketMaxSize,
|
||||
|
@ -373,7 +373,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketJunkSize,
|
||||
|
@ -383,7 +383,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
hint = stringResource(R.string.init_packet_junk_size).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketJunkSize,
|
||||
|
@ -396,7 +396,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.initPacketMagicHeader,
|
||||
|
@ -409,7 +409,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.responsePacketMagicHeader,
|
||||
|
@ -422,7 +422,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.underloadPacketMagicHeader,
|
||||
|
@ -435,7 +435,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.transportPacketMagicHeader,
|
||||
|
@ -448,7 +448,7 @@ fun ConfigScreen(tunnelId: Int) {
|
|||
).lowercase(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
|
@ -53,9 +57,9 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImpo
|
|||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
@ -75,13 +79,19 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||
NestedScrollListener({ isFabVisible = false }, { isFabVisible = true })
|
||||
}
|
||||
|
||||
val vpnActivityResultState =
|
||||
val vpnActivity =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
if (it.resultCode != RESULT_OK) showVpnPermissionDialog = true
|
||||
},
|
||||
)
|
||||
val batteryActivity =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { result: ActivityResult ->
|
||||
viewModel.setBatteryOptimizeDisableShown()
|
||||
}
|
||||
|
||||
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
|
||||
snackbar.showMessage(
|
||||
|
@ -104,7 +114,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||
InfoDialog(
|
||||
onDismiss = { showDeleteTunnelAlertDialog = false },
|
||||
onAttest = {
|
||||
selectedTunnel?.let { viewModel.onDelete(it, context) }
|
||||
selectedTunnel?.let { viewModel::onDelete }
|
||||
showDeleteTunnelAlertDialog = false
|
||||
selectedTunnel = null
|
||||
},
|
||||
|
@ -114,15 +124,35 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||
)
|
||||
}
|
||||
|
||||
fun requestBatteryOptimizationsDisabled() {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
batteryActivity.launch(intent)
|
||||
}
|
||||
|
||||
fun onAutoTunnelToggle() {
|
||||
if (!uiState.generalState.isBatteryOptimizationDisableShown &&
|
||||
!context.isBatteryOptimizationsDisabled() && !isRunningOnTv
|
||||
) {
|
||||
return requestBatteryOptimizationsDisabled()
|
||||
}
|
||||
val intent = if (!uiState.settings.isKernelEnabled) {
|
||||
VpnService.prepare(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (intent != null) return vpnActivity.launch(intent)
|
||||
viewModel.onToggleAutoTunnel()
|
||||
}
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
|
||||
if (intent != null) return vpnActivityResultState.launch(intent)
|
||||
if (intent != null) return vpnActivity.launch(intent)
|
||||
if (!checked) viewModel.onTunnelStop(tunnel).also { return }
|
||||
if (uiState.settings.isKernelEnabled) {
|
||||
context.startTunnelBackground(tunnel.id)
|
||||
} else {
|
||||
viewModel.onTunnelStart(tunnel)
|
||||
}
|
||||
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
@ -137,34 +167,38 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
if(!isRunningOnTv) ScrollDismissFab({
|
||||
val icon = Icons.Filled.Add
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}, isVisible = isFabVisible, onClick = {
|
||||
showBottomSheet = true
|
||||
})
|
||||
if (!isRunningOnTv) {
|
||||
ScrollDismissFab({
|
||||
val icon = Icons.Filled.Add
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}, isVisible = isFabVisible, onClick = {
|
||||
showBottomSheet = true
|
||||
})
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
if(isRunningOnTv) TopNavBar(
|
||||
showBack = false,
|
||||
title = stringResource(R.string.app_name),
|
||||
trailing = {
|
||||
IconButton(onClick = {
|
||||
showBottomSheet = true
|
||||
}) {
|
||||
val icon = Icons.Outlined.Add
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isRunningOnTv) {
|
||||
TopNavBar(
|
||||
showBack = false,
|
||||
title = stringResource(R.string.app_name),
|
||||
trailing = {
|
||||
IconButton(onClick = {
|
||||
showBottomSheet = true
|
||||
}) {
|
||||
val icon = Icons.Outlined.Add
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
TunnelImportSheet(
|
||||
showBottomSheet,
|
||||
|
@ -196,7 +230,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||
}
|
||||
} else {
|
||||
item {
|
||||
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnel(context) })
|
||||
AutoTunnelRowItem(uiState, {
|
||||
onAutoTunnelToggle()
|
||||
})
|
||||
}
|
||||
}
|
||||
items(
|
||||
|
|
|
@ -30,26 +30,24 @@ import timber.log.Timber
|
|||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
val tunnelService: TunnelService,
|
||||
private val tunnelService: Provider<TunnelService>,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
private val serviceManager: ServiceManager,
|
||||
) : ViewModel() {
|
||||
|
||||
private fun stopWatcherService(context: Context) {
|
||||
ServiceManager.stopWatcherService(context)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig, context: Context) {
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
val isPrimary = tunnel.isPrimaryTunnel
|
||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||
stopWatcherService(context)
|
||||
serviceManager.stopAutoTunnel()
|
||||
resetTunnelSetting(settings)
|
||||
}
|
||||
appDataRepository.tunnels.delete(tunnel)
|
||||
|
@ -69,14 +67,14 @@ constructor(
|
|||
appDataRepository.appState.setTunnelStatsExpanded(expanded)
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch {
|
||||
Timber.i("Starting tunnel ${tunnelConfig.name}")
|
||||
tunnelService.startTunnel(tunnelConfig)
|
||||
tunnelService.get().startTunnel(tunnelConfig, background)
|
||||
}
|
||||
|
||||
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
|
||||
Timber.i("Stopping active tunnel")
|
||||
tunnelService.stopTunnel(tunnel)
|
||||
tunnelService.get().stopTunnel(tunnel)
|
||||
}
|
||||
|
||||
private fun generateQrCodeDefaultName(config: String): String {
|
||||
|
@ -160,16 +158,17 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onToggleAutoTunnel(context: Context) = viewModelScope.launch {
|
||||
fun onToggleAutoTunnel() = viewModelScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.stopWatcherService(context)
|
||||
val toggled = !settings.isAutoTunnelEnabled
|
||||
if (toggled) {
|
||||
serviceManager.startAutoTunnel(false)
|
||||
} else {
|
||||
ServiceManager.startWatcherService(context)
|
||||
serviceManager.stopAutoTunnel()
|
||||
}
|
||||
appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelEnabled = !settings.isAutoTunnelEnabled,
|
||||
isAutoTunnelEnabled = toggled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -195,6 +194,10 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
|
||||
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
|
||||
private suspend fun saveTunnelFromConfUri(name: String, uri: Uri, context: Context) {
|
||||
val stream = getInputStreamFromUri(uri, context) ?: throw FileReadException
|
||||
saveTunnelConfigFromStream(stream, name)
|
||||
|
|
|
@ -4,30 +4,25 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
|
||||
@Composable
|
||||
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
|
||||
fun AutoTunnelRowItem(appUiState: AppUiState, onToggle: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val itemFocusRequester = remember { FocusRequester() }
|
||||
ExpandingRowListItem(
|
||||
|
@ -40,7 +35,7 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
|
|||
Modifier
|
||||
.size(16.dp.scaledHeight()).scale(1.5f),
|
||||
tint =
|
||||
if (!settings.isAutoTunnelEnabled) {
|
||||
if (!appUiState.autoTunnelActive) {
|
||||
Color.Gray
|
||||
} else {
|
||||
SilverTree
|
||||
|
@ -50,10 +45,10 @@ fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit) {
|
|||
text = stringResource(R.string.auto_tunneling),
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
settings.isAutoTunnelEnabled,
|
||||
appUiState.settings.isAutoTunnelEnabled,
|
||||
onClick = {
|
||||
onToggle()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
|
|
@ -9,8 +9,6 @@ import androidx.compose.material3.FloatingActionButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -182,14 +182,14 @@ fun TunnelRowItem(
|
|||
ScaledSwitch(
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
checked = isActive,
|
||||
onClick = onSwitchClick
|
||||
onClick = onSwitchClick,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ScaledSwitch(
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
checked = isActive,
|
||||
onClick = onSwitchClick
|
||||
onClick = onSwitchClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,14 +46,18 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConfig: TunnelConfi
|
|||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
|
||||
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
stringResource(R.string.peer).lowercase() + ": $peerId",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
|
||||
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall)
|
||||
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
|
||||
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,10 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.NetworkPing
|
||||
|
@ -27,18 +22,11 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
@ -56,7 +44,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.compon
|
|||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
@ -84,7 +71,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
|
@ -101,7 +88,12 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Star,
|
||||
title = { Text(stringResource(R.string.primary_tunnel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.primary_tunnel),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
description = {
|
||||
Text(
|
||||
stringResource(R.string.set_primary_tunnel),
|
||||
|
@ -114,7 +106,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
|
||||
)
|
||||
},
|
||||
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) }
|
||||
onClick = { optionsViewModel.onTogglePrimaryTunnel(config) },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.PhoneAndroid,
|
||||
|
@ -131,7 +123,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
|
||||
)
|
||||
},
|
||||
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) }
|
||||
onClick = { optionsViewModel.onToggleIsMobileDataTunnel(config) },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.NetworkPing,
|
||||
|
@ -147,7 +139,7 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
|
||||
)
|
||||
},
|
||||
onClick = { optionsViewModel.onToggleRestartOnPing(config) }
|
||||
onClick = { optionsViewModel.onToggleRestartOnPing(config) },
|
||||
),
|
||||
SelectionItem(
|
||||
title = {
|
||||
|
@ -180,24 +172,25 @@ fun OptionsScreen(optionsViewModel: OptionsViewModel = hiltViewModel(), appUiSta
|
|||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
description = {
|
||||
TrustedNetworkTextBox(
|
||||
config.tunnelNetworks, onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
|
||||
config.tunnelNetworks,
|
||||
onDelete = { optionsViewModel.onDeleteRunSSID(it, config) },
|
||||
currentText = currentText,
|
||||
onSave = { optionsViewModel.onSaveRunSSID(it, config) },
|
||||
onValueChange = { currentText = it },
|
||||
supporting = { if(appUiState.generalState.isWildcardsEnabled) {
|
||||
WildcardsLabel()
|
||||
}}
|
||||
supporting = {
|
||||
if (appUiState.settings.isWildcardsEnabled) {
|
||||
WildcardsLabel()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ constructor(
|
|||
}
|
||||
|
||||
fun onSaveRunSSID(ssid: String, tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
if(ssid.isBlank()) return@launch
|
||||
if (ssid.isBlank()) return@launch
|
||||
val trimmed = ssid.trim()
|
||||
val tunnelsWithName = appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
|
||||
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -49,7 +36,6 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
|
@ -63,13 +49,9 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalFocusReques
|
|||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.topPadding
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchNotificationSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
|
@ -91,114 +73,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val settingsUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var showLocationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { result: ActivityResult ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data
|
||||
// Handle the Intent
|
||||
}
|
||||
viewModel.setBatteryOptimizeDisableShown()
|
||||
}
|
||||
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (!accepted) {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun isBatteryOptimizationsDisabled(): Boolean {
|
||||
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
|
||||
fun requestBatteryOptimizationsDisabled() {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
startForResult.launch(intent)
|
||||
}
|
||||
|
||||
// fun handleAutoTunnelToggle() {
|
||||
// if (!uiState.generalState.isBatteryOptimizationDisableShown &&
|
||||
// !isBatteryOptimizationsDisabled() && !isRunningOnTv
|
||||
// ) {
|
||||
// return requestBatteryOptimizationsDisabled()
|
||||
// }
|
||||
// val intent = if (!uiState.settings.isKernelEnabled) {
|
||||
// VpnService.prepare(context)
|
||||
// } else {
|
||||
// null
|
||||
// }
|
||||
// if (intent != null) return vpnActivityResultState.launch(intent)
|
||||
// viewModel.onToggleAutoTunnel(context)
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// fun checkFineLocationGranted() {
|
||||
// isBackgroundLocationGranted =
|
||||
// if (!fineLocationState.status.isGranted) {
|
||||
// false
|
||||
// } else {
|
||||
// viewModel.setLocationDisclosureShown()
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// if (
|
||||
// isRunningOnTv &&
|
||||
// Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
// ) {
|
||||
// checkFineLocationGranted()
|
||||
// } else {
|
||||
// val backgroundLocationState =
|
||||
// rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
// isBackgroundLocationGranted =
|
||||
// if (!backgroundLocationState.status.isGranted) {
|
||||
// false
|
||||
// } else {
|
||||
// SideEffect { viewModel.setLocationDisclosureShown() }
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
// checkFineLocationGranted()
|
||||
// }
|
||||
|
||||
BackgroundLocationDialog(
|
||||
showLocationDialog,
|
||||
onDismiss = { showLocationDialog = false },
|
||||
onAttest = { showLocationDialog = false },
|
||||
)
|
||||
|
||||
// LocationServicesDialog(
|
||||
// showLocationServicesAlertDialog,
|
||||
// onDismiss = { showVpnPermissionDialog = false },
|
||||
// onAttest = { handleAutoTunnelToggle() },
|
||||
// )
|
||||
|
||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||
|
||||
if (showAuthPrompt) {
|
||||
AuthorizationPrompt(
|
||||
|
@ -231,12 +106,18 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
.padding(top = topPadding)
|
||||
.padding(bottom = 40.dp.scaledHeight())
|
||||
.padding(horizontal = 24.dp.scaledWidth())
|
||||
.then(if(!isRunningOnTv) Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
} else Modifier)
|
||||
.then(
|
||||
if (!isRunningOnTv) {
|
||||
Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
|
@ -250,73 +131,78 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
)
|
||||
},
|
||||
onClick = {
|
||||
if(!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
|
||||
if (!uiState.generalState.isLocationDisclosureShown) return@SelectionItem navController.navigate(Route.LocationDisclosure)
|
||||
navController.navigate(Route.AutoTunnel)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton(Modifier.focusable().focusRequester(rootFocusRequester)) { navController.navigate(Route.AutoTunnel) }
|
||||
},
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
buildList {
|
||||
if (!isRunningOnTv) addAll(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Filled.AppShortcut,
|
||||
{
|
||||
ScaledSwitch(
|
||||
uiState.settings.isShortcutsEnabled,
|
||||
onClick = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.enabled_app_shortcuts),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
|
||||
},
|
||||
onClick = { viewModel.onToggleShortcutsEnabled() }
|
||||
if (!isRunningOnTv) {
|
||||
addAll(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Filled.AppShortcut,
|
||||
{
|
||||
ScaledSwitch(
|
||||
uiState.settings.isShortcutsEnabled,
|
||||
onClick = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.enabled_app_shortcuts),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onToggleShortcutsEnabled() },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.VpnLock,
|
||||
{
|
||||
ScaledSwitch(
|
||||
enabled = !(
|
||||
(
|
||||
uiState.settings.isTunnelOnWifiEnabled ||
|
||||
uiState.settings.isTunnelOnEthernetEnabled ||
|
||||
uiState.settings.isTunnelOnMobileDataEnabled
|
||||
) &&
|
||||
uiState.settings.isAutoTunnelEnabled
|
||||
),
|
||||
onClick = { viewModel.onToggleAlwaysOnVPN() },
|
||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onToggleAlwaysOnVPN() },
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.AdminPanelSettings,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.kill_switch),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
context.launchVpnSettings()
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { context.launchVpnSettings() }
|
||||
},
|
||||
),
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.VpnLock,
|
||||
{
|
||||
ScaledSwitch(
|
||||
enabled = !(
|
||||
(
|
||||
uiState.settings.isTunnelOnWifiEnabled ||
|
||||
uiState.settings.isTunnelOnEthernetEnabled ||
|
||||
uiState.settings.isTunnelOnMobileDataEnabled
|
||||
) &&
|
||||
uiState.settings.isAutoTunnelEnabled
|
||||
),
|
||||
onClick = { viewModel.onToggleAlwaysOnVPN() },
|
||||
checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
|
||||
},
|
||||
onClick = { viewModel.onToggleAlwaysOnVPN() }
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.AdminPanelSettings,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.kill_switch),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
|
||||
},
|
||||
onClick = {
|
||||
context.launchVpnSettings()
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { context.launchVpnSettings() }
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Restore,
|
||||
|
@ -329,25 +215,27 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
title = {
|
||||
Text(
|
||||
stringResource(R.string.restart_at_boot),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface))
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onToggleRestartAtBoot() }
|
||||
)
|
||||
onClick = { viewModel.onToggleRestartAtBoot() },
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(SelectionItem(
|
||||
Icons.AutoMirrored.Outlined.ViewQuilt,
|
||||
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = {
|
||||
navController.navigate(Route.Appearance)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.Appearance) }
|
||||
},
|
||||
),
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.AutoMirrored.Outlined.ViewQuilt,
|
||||
title = { Text(stringResource(R.string.appearance), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = {
|
||||
navController.navigate(Route.Appearance)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.Appearance) }
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.Notifications,
|
||||
title = { Text(stringResource(R.string.notifications), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
|
@ -360,7 +248,12 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.Pin,
|
||||
title = { Text(stringResource(R.string.enable_app_lock), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.enable_app_lock),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
uiState.generalState.isPinLockEnabled,
|
||||
|
@ -374,370 +267,67 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
|||
},
|
||||
)
|
||||
},
|
||||
onClick = { if (uiState.generalState.isPinLockEnabled) {
|
||||
appViewModel.onPinLockDisabled()
|
||||
} else {
|
||||
PinManager.initialize(context)
|
||||
navController.navigate(Route.Lock)
|
||||
} }
|
||||
)
|
||||
))
|
||||
|
||||
if(!isRunningOnTv) SurfaceSelectionGroupButton(listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Code,
|
||||
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
description = {
|
||||
Text(
|
||||
stringResource(R.string.use_kernel),
|
||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
uiState.settings.isKernelEnabled,
|
||||
onClick = { viewModel.onToggleKernelMode() },
|
||||
enabled = !(
|
||||
uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == TunnelState.UP) ||
|
||||
!settingsUiState.isKernelAvailable
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleKernelMode()
|
||||
}
|
||||
),
|
||||
))
|
||||
|
||||
if(!isRunningOnTv) SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.FolderZip,
|
||||
title = { Text(stringResource(R.string.export_configs), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = {
|
||||
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
|
||||
showAuthPrompt = true
|
||||
if (uiState.generalState.isPinLockEnabled) {
|
||||
appViewModel.onPinLockDisabled()
|
||||
} else {
|
||||
PinManager.initialize(context)
|
||||
navController.navigate(Route.Lock)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if (!isRunningOnTv) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Code,
|
||||
title = { Text(stringResource(R.string.kernel), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
description = {
|
||||
Text(
|
||||
stringResource(R.string.use_kernel),
|
||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
uiState.settings.isKernelEnabled,
|
||||
onClick = { viewModel.onToggleKernelMode() },
|
||||
enabled = !(
|
||||
uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == TunnelState.UP)
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleKernelMode()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Surface(
|
||||
// tonalElevation = 2.dp,
|
||||
// shadowElevation = 2.dp,
|
||||
// shape = RoundedCornerShape(12.dp),
|
||||
// color = MaterialTheme.colorScheme.surface,
|
||||
// modifier =
|
||||
// (
|
||||
// if (isRunningOnTv) {
|
||||
// Modifier
|
||||
// .height(IntrinsicSize.Min)
|
||||
// .fillMaxWidth(fillMaxWidth)
|
||||
// .padding(top = 10.dp)
|
||||
// } else {
|
||||
// Modifier
|
||||
// .fillMaxWidth(fillMaxWidth)
|
||||
// .padding(top = 20.dp)
|
||||
// }
|
||||
// )
|
||||
// .padding(bottom = 10.dp),
|
||||
// ) {
|
||||
// Column(
|
||||
// horizontalAlignment = Alignment.Start,
|
||||
// verticalArrangement = Arrangement.Top,
|
||||
// modifier = Modifier.padding(15.dp),
|
||||
// ) {
|
||||
// SectionTitle(
|
||||
// title = stringResource(id = R.string.auto_tunneling),
|
||||
// padding = screenPadding,
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(id = R.string.tunnel_on_wifi),
|
||||
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
// checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
// onCheckChanged = { checked ->
|
||||
// if (!checked || settingsUiState.isRooted) viewModel.onToggleTunnelOnWifi().also { return@ConfigurationToggle }
|
||||
// onAutoTunnelWifiChecked()
|
||||
// },
|
||||
// modifier =
|
||||
// if (uiState.settings.isAutoTunnelEnabled) {
|
||||
// Modifier
|
||||
// } else {
|
||||
// Modifier
|
||||
// .focusRequester(focusRequester)
|
||||
// },
|
||||
// )
|
||||
// if (uiState.settings.isTunnelOnWifiEnabled) {
|
||||
// Column {
|
||||
// FlowRow(
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .padding(screenPadding)
|
||||
// .fillMaxWidth(),
|
||||
// horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
// ) {
|
||||
// uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
// ClickableIconButton(
|
||||
// onClick = {
|
||||
// if (isRunningOnTv) {
|
||||
// focusRequester.requestFocus()
|
||||
// viewModel.onDeleteTrustedSSID(ssid)
|
||||
// }
|
||||
// },
|
||||
// onIconClick = {
|
||||
// if (isRunningOnTv) focusRequester.requestFocus()
|
||||
// viewModel.onDeleteTrustedSSID(ssid)
|
||||
// },
|
||||
// text = ssid,
|
||||
// icon = Icons.Filled.Close,
|
||||
// )
|
||||
// }
|
||||
// if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||
// Text(
|
||||
// stringResource(R.string.none),
|
||||
// fontStyle = FontStyle.Italic,
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// color = MaterialTheme.colorScheme.onSurface,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// OutlinedTextField(
|
||||
// value = currentText,
|
||||
// onValueChange = { currentText = it },
|
||||
// label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .padding(
|
||||
// start = screenPadding,
|
||||
// top = 5.dp,
|
||||
// bottom = 10.dp,
|
||||
// ),
|
||||
// supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
|
||||
// maxLines = 1,
|
||||
// keyboardOptions =
|
||||
// KeyboardOptions(
|
||||
// capitalization = KeyboardCapitalization.None,
|
||||
// imeAction = ImeAction.Done,
|
||||
// ),
|
||||
// keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
// trailingIcon = {
|
||||
// if (currentText != "") {
|
||||
// IconButton(onClick = { saveTrustedSSID() }) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.Add,
|
||||
// contentDescription =
|
||||
// if (currentText == "") {
|
||||
// stringResource(
|
||||
// id =
|
||||
// R.string
|
||||
// .trusted_ssid_empty_description,
|
||||
// )
|
||||
// } else {
|
||||
// stringResource(
|
||||
// id =
|
||||
// R.string
|
||||
// .trusted_ssid_value_description,
|
||||
// )
|
||||
// },
|
||||
// tint = MaterialTheme.colorScheme.primary,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.tunnel_mobile_data),
|
||||
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
// checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
// onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(id = R.string.tunnel_on_ethernet),
|
||||
// enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
// checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
// onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.restart_on_ping),
|
||||
// checked = uiState.settings.isPingEnabled,
|
||||
// onCheckChanged = { viewModel.onToggleRestartOnPing() },
|
||||
// )
|
||||
// Row(
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// modifier =
|
||||
// (
|
||||
// if (!uiState.settings.isAutoTunnelEnabled) {
|
||||
// Modifier
|
||||
// } else {
|
||||
// Modifier.focusRequester(
|
||||
// focusRequester,
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
// .fillMaxSize()
|
||||
// .padding(top = 5.dp),
|
||||
// horizontalArrangement = Arrangement.Center,
|
||||
// ) {
|
||||
// TextButton(
|
||||
// onClick = {
|
||||
// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
|
||||
// handleAutoTunnelToggle()
|
||||
// },
|
||||
// ) {
|
||||
// val autoTunnelButtonText =
|
||||
// if (uiState.settings.isAutoTunnelEnabled) {
|
||||
// stringResource(R.string.disable_auto_tunnel)
|
||||
// } else {
|
||||
// stringResource(id = R.string.enable_auto_tunnel)
|
||||
// }
|
||||
// Text(autoTunnelButtonText)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Surface(
|
||||
// tonalElevation = 2.dp,
|
||||
// shadowElevation = 2.dp,
|
||||
// shape = RoundedCornerShape(12.dp),
|
||||
// color = MaterialTheme.colorScheme.surface,
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .fillMaxWidth(fillMaxWidth)
|
||||
// .padding(vertical = 10.dp),
|
||||
// ) {
|
||||
// Column(
|
||||
// horizontalAlignment = Alignment.Start,
|
||||
// verticalArrangement = Arrangement.Top,
|
||||
// modifier = Modifier.padding(15.dp),
|
||||
// ) {
|
||||
// SectionTitle(
|
||||
// title = stringResource(id = R.string.backend),
|
||||
// padding = screenPadding,
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.use_kernel),
|
||||
// enabled =
|
||||
// !(
|
||||
// uiState.settings.isAutoTunnelEnabled ||
|
||||
// uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
// (uiState.vpnState.status == TunnelState.UP) ||
|
||||
// !settingsUiState.isKernelAvailable
|
||||
// ),
|
||||
// checked = uiState.settings.isKernelEnabled,
|
||||
// onCheckChanged = {
|
||||
// viewModel.onToggleKernelMode()
|
||||
// },
|
||||
// )
|
||||
// Row(
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(top = 5.dp),
|
||||
// horizontalArrangement = Arrangement.Center,
|
||||
// ) {
|
||||
// TextButton(
|
||||
// onClick = {
|
||||
// viewModel.onRequestRoot()
|
||||
// },
|
||||
// ) {
|
||||
// Text(stringResource(R.string.request_root))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Surface(
|
||||
// tonalElevation = 2.dp,
|
||||
// shadowElevation = 2.dp,
|
||||
// shape = RoundedCornerShape(12.dp),
|
||||
// color = MaterialTheme.colorScheme.surface,
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .fillMaxWidth(fillMaxWidth)
|
||||
// .padding(vertical = 10.dp)
|
||||
// .padding(bottom = 10.dp),
|
||||
// ) {
|
||||
// Column(
|
||||
// horizontalAlignment = Alignment.Start,
|
||||
// verticalArrangement = Arrangement.Top,
|
||||
// modifier = Modifier.padding(15.dp),
|
||||
// ) {
|
||||
// SectionTitle(
|
||||
// title = stringResource(id = R.string.other),
|
||||
// padding = screenPadding,
|
||||
// )
|
||||
// if (!isRunningOnTv) {
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.always_on_vpn_support),
|
||||
// enabled = !(
|
||||
// (
|
||||
// uiState.settings.isTunnelOnWifiEnabled ||
|
||||
// uiState.settings.isTunnelOnEthernetEnabled ||
|
||||
// uiState.settings.isTunnelOnMobileDataEnabled
|
||||
// ) &&
|
||||
// uiState.settings.isAutoTunnelEnabled
|
||||
// ),
|
||||
// checked = uiState.settings.isAlwaysOnVpnEnabled,
|
||||
// onCheckChanged = { viewModel.onToggleAlwaysOnVPN() },
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.enabled_app_shortcuts),
|
||||
// enabled = true,
|
||||
// checked = uiState.settings.isShortcutsEnabled,
|
||||
// onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||
// )
|
||||
// }
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.restart_at_boot),
|
||||
// enabled = true,
|
||||
// checked = uiState.settings.isRestoreOnBootEnabled,
|
||||
// onCheckChanged = {
|
||||
// viewModel.onToggleRestartAtBoot()
|
||||
// },
|
||||
// )
|
||||
// ConfigurationToggle(
|
||||
// stringResource(R.string.enable_app_lock),
|
||||
// enabled = true,
|
||||
// checked = uiState.generalState.isPinLockEnabled,
|
||||
// onCheckChanged = {
|
||||
// if (uiState.generalState.isPinLockEnabled) {
|
||||
// appViewModel.onPinLockDisabled()
|
||||
// } else {
|
||||
// // TODO may want to show a dialog before proceeding in the future
|
||||
// PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
// navController.navigate(Route.Lock)
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// if (!isRunningOnTv) {
|
||||
// Row(
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// modifier =
|
||||
// Modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(top = 5.dp),
|
||||
// horizontalArrangement = Arrangement.Center,
|
||||
// ) {
|
||||
// TextButton(
|
||||
// enabled = !didExportFiles,
|
||||
// onClick = {
|
||||
// if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
|
||||
// showAuthPrompt = true
|
||||
// },
|
||||
// ) {
|
||||
// Text(stringResource(R.string.export_configs))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (!isRunningOnTv) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.FolderZip,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.export_configs),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (uiState.tunnels.isEmpty()) return@SelectionItem context.showToast(R.string.tunnel_required)
|
||||
showAuthPrompt = true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
|
||||
|
||||
data class SettingsUiState(
|
||||
val isRooted: Boolean = false,
|
||||
val isKernelAvailable: Boolean = false,
|
||||
)
|
|
@ -10,19 +10,14 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Instant
|
||||
|
@ -39,16 +34,6 @@ constructor(
|
|||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
val uiState = _uiState.onStart {
|
||||
_uiState.update {
|
||||
it.copy(isKernelAvailable = isKernelSupported(), isRooted = isRooted())
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
SettingsUiState(),
|
||||
)
|
||||
private val settings = appDataRepository.settings.getSettingsFlow()
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, Settings())
|
||||
|
||||
|
@ -56,10 +41,6 @@ constructor(
|
|||
appDataRepository.appState.setLocationDisclosureShown(true)
|
||||
}
|
||||
|
||||
fun setBatteryOptimizeDisableShown() = viewModelScope.launch {
|
||||
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
|
@ -90,23 +71,11 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onToggleAmnezia() = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
if (isKernelEnabled) {
|
||||
saveKernelMode(false)
|
||||
}
|
||||
appDataRepository.settings.save(
|
||||
copy(
|
||||
isAmneziaEnabled = !isAmneziaEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleKernelMode() = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
if (!isKernelEnabled) {
|
||||
requestRoot().onSuccess {
|
||||
if (!isKernelSupported()) return@onSuccess SnackbarController.showMessage(StringValue.StringResource(R.string.kernel_not_supported))
|
||||
appDataRepository.settings.save(
|
||||
copy(
|
||||
isKernelEnabled = true,
|
||||
|
@ -136,17 +105,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun isRooted(): Boolean {
|
||||
return try {
|
||||
withContext(ioDispatcher) {
|
||||
rootShell.get().start()
|
||||
}
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestRoot(): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
kotlin.runCatching {
|
||||
|
@ -158,10 +116,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onRequestRoot() = viewModelScope.launch {
|
||||
requestRoot()
|
||||
}
|
||||
|
||||
fun exportAllConfigs(context: Context) = viewModelScope.launch {
|
||||
kotlin.runCatching {
|
||||
val shareFile = fileUtils.createNewShareFile("wg-export_${Instant.now().epochSecond}.zip")
|
||||
|
|
|
@ -32,8 +32,8 @@ fun AppearanceScreen() {
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopNavBar(stringResource(R.string.appearance))
|
||||
}
|
||||
){
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||
|
@ -51,7 +51,7 @@ fun AppearanceScreen() {
|
|||
onClick = { navController.navigate(Route.Language) },
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.Language) }
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -63,7 +63,7 @@ fun AppearanceScreen() {
|
|||
onClick = { navController.navigate(Route.Display) },
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.Display) }
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -2,11 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.displ
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -24,11 +21,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||
|
||||
@Composable
|
||||
fun DisplayScreen(appUiState: AppUiState, viewModel: DisplayViewModel = hiltViewModel()) {
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopNavBar(stringResource(R.string.display_theme))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
|
|
|
@ -12,7 +12,7 @@ import javax.inject.Inject
|
|||
class DisplayViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val appStateRepository: AppStateRepository
|
||||
private val appStateRepository: AppStateRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
fun onThemeChange(theme: Theme) = viewModelScope.launch {
|
||||
|
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
@ -69,7 +68,7 @@ fun LanguageScreen(localeStorage: LocaleStorage) {
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopNavBar(stringResource(R.string.language))
|
||||
}
|
||||
},
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
|
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.Filter1
|
||||
import androidx.compose.material.icons.outlined.NetworkPing
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
|
@ -31,7 +33,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
@ -45,9 +46,12 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelec
|
|||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.BackgroundLocationDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isLocationServicesEnabled
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
||||
|
@ -63,11 +67,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var showLocationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
|
||||
currentText = ""
|
||||
fun checkFineLocationGranted() {
|
||||
isBackgroundLocationGranted = fineLocationState.status.isGranted
|
||||
}
|
||||
|
||||
fun onAutoTunnelWifiChecked() {
|
||||
if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return }
|
||||
when (false) {
|
||||
isBackgroundLocationGranted -> showLocationDialog = true
|
||||
fineLocationState.status.isGranted -> showLocationDialog = true
|
||||
|
@ -79,11 +84,39 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) checkFineLocationGranted()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (context.isRunningOnTv() && Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
val backgroundLocationState = rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
isBackgroundLocationGranted = backgroundLocationState.status.isGranted
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.settings.trustedNetworkSSIDs) {
|
||||
currentText = ""
|
||||
}
|
||||
|
||||
LocationServicesDialog(
|
||||
showLocationServicesAlertDialog,
|
||||
onDismiss = { showLocationServicesAlertDialog = false },
|
||||
onAttest = {
|
||||
viewModel.onToggleTunnelOnWifi()
|
||||
},
|
||||
)
|
||||
|
||||
BackgroundLocationDialog(
|
||||
showLocationDialog,
|
||||
onDismiss = { showLocationDialog = false },
|
||||
onAttest = { showLocationDialog = false },
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
TopNavBar(stringResource(R.string.auto_tunneling))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
|
@ -97,34 +130,60 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
) {
|
||||
SurfaceSelectionGroupButton(
|
||||
buildList {
|
||||
add(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Wifi,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_wifi),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
|
||||
)
|
||||
},
|
||||
description = {
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
onClick = {
|
||||
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
|
||||
.also { return@ScaledSwitch }
|
||||
onAutoTunnelWifiChecked()
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (!uiState.settings.isTunnelOnWifiEnabled || uiState.isRooted) viewModel.onToggleTunnelOnWifi()
|
||||
.also { return@SelectionItem }
|
||||
onAutoTunnelWifiChecked()
|
||||
}
|
||||
)
|
||||
addAll(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.Wifi,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_wifi),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
description = {
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
onClick = {
|
||||
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch }
|
||||
onAutoTunnelWifiChecked()
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem }
|
||||
onAutoTunnelWifiChecked()
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.Code,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.wifi_name_via_shell),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
description = {
|
||||
Text(
|
||||
stringResource(R.string.use_root_shell_for_wifi),
|
||||
style = MaterialTheme.typography.bodySmall.copy(MaterialTheme.colorScheme.outline),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.settings.isWifiNameByShellEnabled,
|
||||
onClick = {
|
||||
viewModel.onRootShellWifiToggle()
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onRootShellWifiToggle()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
if (uiState.settings.isTunnelOnWifiEnabled) {
|
||||
addAll(
|
||||
|
@ -134,15 +193,15 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
title = {
|
||||
Text(
|
||||
stringResource(R.string.use_wildcards),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
description = {
|
||||
LearnMoreLinkLabel({context.openWebUrl(it)}, stringResource(id = R.string.docs_wildcards))
|
||||
LearnMoreLinkLabel({ context.openWebUrl(it) }, stringResource(id = R.string.docs_wildcards))
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.generalState.isWildcardsEnabled,
|
||||
checked = uiState.settings.isWildcardsEnabled,
|
||||
onClick = {
|
||||
viewModel.onToggleWildcards()
|
||||
},
|
||||
|
@ -150,7 +209,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
},
|
||||
onClick = {
|
||||
viewModel.onToggleWildcards()
|
||||
}
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
title = {
|
||||
|
@ -183,87 +242,89 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
|
|||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
description = {
|
||||
TrustedNetworkTextBox(
|
||||
uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID,
|
||||
uiState.settings.trustedNetworkSSIDs,
|
||||
onDelete = viewModel::onDeleteTrustedSSID,
|
||||
currentText = currentText,
|
||||
onSave = viewModel::onSaveTrustedSSID,
|
||||
onValueChange = { currentText = it },
|
||||
supporting = { if(uiState.generalState.isWildcardsEnabled) {
|
||||
WildcardsLabel()
|
||||
}}
|
||||
supporting = {
|
||||
if (uiState.settings.isWildcardsEnabled) {
|
||||
WildcardsLabel()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
))
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.SignalCellular4Bar,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
onClick = { viewModel.onToggleTunnelOnMobileData() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleTunnelOnMobileData()
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.SettingsEthernet,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_ethernet),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
onClick = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleTunnelOnEthernet()
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.NetworkPing,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.restart_on_ping),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.settings.isPingEnabled,
|
||||
onClick = { viewModel.onToggleRestartOnPing() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleRestartOnPing()
|
||||
}
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.SignalCellular4Bar,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
onClick = { viewModel.onToggleTunnelOnMobileData() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleTunnelOnMobileData()
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.SettingsEthernet,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.tunnel_on_ethernet),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
onClick = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleTunnelOnEthernet()
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Outlined.NetworkPing,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.restart_on_ping),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.settings.isPingEnabled,
|
||||
onClick = { viewModel.onToggleRestartOnPing() },
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onToggleRestartOnPing()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,29 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@HiltViewModel
|
||||
class AutoTunnelViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val rootShell: Provider<RootShell>,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
|
||||
private val settings = appDataRepository.settings.getSettingsFlow()
|
||||
|
@ -37,29 +44,53 @@ constructor(
|
|||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
copy(
|
||||
isTunnelOnMobileDataEnabled = !this.isTunnelOnMobileDataEnabled,
|
||||
isTunnelOnMobileDataEnabled = !isTunnelOnMobileDataEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleWildcards() = viewModelScope.launch {
|
||||
val wildcards = appDataRepository.appState.isWildcardsEnabled()
|
||||
appDataRepository.appState.setWildcardsEnabled(
|
||||
!wildcards
|
||||
)
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
copy(
|
||||
isWildcardsEnabled = !isWildcardsEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteTrustedSSID(ssid: String) = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
copy(
|
||||
trustedNetworkSSIDs = (this.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||
trustedNetworkSSIDs = (trustedNetworkSSIDs - ssid).toMutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRootShellWifiToggle() = viewModelScope.launch {
|
||||
requestRoot().onSuccess {
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
copy(isWifiNameByShellEnabled = !isWifiNameByShellEnabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestRoot(): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
kotlin.runCatching {
|
||||
rootShell.get().start()
|
||||
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
|
||||
}.onFailure {
|
||||
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onToggleTunnelOnEthernet() = viewModelScope.launch {
|
||||
with(settings.value) {
|
||||
appDataRepository.settings.save(
|
||||
|
|
|
@ -32,9 +32,16 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
|
|||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String) -> Unit, currentText: String, onSave : (ssid: String) -> Unit, onValueChange: (network: String) -> Unit, supporting: @Composable () -> Unit) {
|
||||
fun TrustedNetworkTextBox(
|
||||
trustedNetworks: List<String>,
|
||||
onDelete: (ssid: String) -> Unit,
|
||||
currentText: String,
|
||||
onSave: (ssid: String) -> Unit,
|
||||
onValueChange: (network: String) -> Unit,
|
||||
supporting: @Composable () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())){
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp.scaledHeight())) {
|
||||
FlowRow(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(),
|
||||
|
@ -93,6 +100,5 @@ fun TrustedNetworkTextBox(trustedNetworks: List<String>, onDelete: (ssid: String
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.font.FontStyle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
|
||||
@Composable
|
||||
fun WildcardsLabel() {
|
||||
Text(
|
||||
|
|
|
@ -14,7 +14,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|||
fun ForwardButton(modifier: Modifier = Modifier.focusable(), onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
) {
|
||||
val icon = Icons.AutoMirrored.Outlined.ArrowForward
|
||||
Icon(icon, icon.name, Modifier.size(iconSize))
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.compose.ui.text.withStyle
|
|||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url : String) {
|
||||
fun LearnMoreLinkLabel(onClick: (url: String) -> Unit, url: String) {
|
||||
// TODO update link when docs are fully updated
|
||||
val gettingStarted =
|
||||
buildAnnotatedString {
|
||||
|
|
|
@ -39,7 +39,7 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState)
|
|||
val navController = LocalNavController.current
|
||||
|
||||
LaunchedEffect(Unit, appUiState) {
|
||||
if(appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
|
||||
if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
@ -56,40 +56,49 @@ fun LocationDisclosureScreen(appViewModel: AppViewModel, appUiState: AppUiState)
|
|||
.padding(30.dp.scaledHeight())
|
||||
.size(128.dp.scaledHeight()),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.LocationOn,
|
||||
title = { Text(stringResource(R.string.launch_app_settings), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = { context.launchAppSettings().also {
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Outlined.LocationOn,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.launch_app_settings),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
context.launchAppSettings().also {
|
||||
appViewModel.setLocationDisclosureShown()
|
||||
} },
|
||||
trailing = {
|
||||
ForwardButton { context.launchAppSettings().also {
|
||||
}
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.launchAppSettings().also {
|
||||
appViewModel.setLocationDisclosureShown()
|
||||
} }
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = { appViewModel.setLocationDisclosureShown() },
|
||||
trailing = {
|
||||
ForwardButton { appViewModel.setLocationDisclosureShown() }
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
title = { Text(stringResource(R.string.skip), style = MaterialTheme.typography.bodyLarge.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
onClick = { appViewModel.setLocationDisclosureShown() },
|
||||
trailing = {
|
||||
ForwardButton { appViewModel.setLocationDisclosureShown() }
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,94 +38,118 @@ fun SupportScreen() {
|
|||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topPadding)
|
||||
.padding(horizontal = 24.dp.scaledWidth()),
|
||||
) {
|
||||
GroupLabel(stringResource(R.string.thank_you))
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Filled.Book,
|
||||
title = { Text(stringResource(R.string.docs_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.docs_url))
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.LineStyle,
|
||||
title = { Text(stringResource(R.string.read_logs),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
navController.navigate(Route.Logs)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topPadding)
|
||||
.padding(horizontal = 24.dp.scaledWidth()),
|
||||
) {
|
||||
GroupLabel(stringResource(R.string.thank_you))
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
Icons.Filled.Book,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.docs_description),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { context.openWebUrl(context.getString(R.string.docs_url)) }
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.docs_url))
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.LineStyle,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.read_logs),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
navController.navigate(Route.Logs)
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.Policy,
|
||||
title = { Text(stringResource(R.string.privacy_policy), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.privacy_policy_url))
|
||||
}
|
||||
),
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(Route.Logs)
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.Policy,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.privacy_policy),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { context.openWebUrl(context.getString(R.string.privacy_policy_url)) }
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.privacy_policy_url))
|
||||
},
|
||||
),
|
||||
|
||||
)
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
ImageVector.vectorResource(R.drawable.telegram),
|
||||
title = { Text(stringResource(R.string.chat_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.openWebUrl(context.getString(R.string.telegram_url))
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
),
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
SelectionItem(
|
||||
ImageVector.vectorResource(R.drawable.telegram),
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.chat_description),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.openWebUrl(context.getString(R.string.telegram_url))
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
ImageVector.vectorResource(R.drawable.github),
|
||||
title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.openWebUrl(context.getString(R.string.github_url))
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.telegram_url))
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
ImageVector.vectorResource(R.drawable.github),
|
||||
title = { Text(stringResource(R.string.open_issue), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.openWebUrl(context.getString(R.string.github_url))
|
||||
}
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.Mail,
|
||||
title = { Text(stringResource(R.string.email_description), style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface)) },
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.launchSupportEmail()
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
},
|
||||
onClick = {
|
||||
context.openWebUrl(context.getString(R.string.github_url))
|
||||
},
|
||||
),
|
||||
SelectionItem(
|
||||
Icons.Filled.Mail,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.email_description),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton {
|
||||
context.launchSupportEmail()
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
VersionLabel()
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
context.launchSupportEmail()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
VersionLabel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,8 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
|
@ -12,8 +12,6 @@ val BalticSea = Color(0xFF1C1B1F)
|
|||
val Brick = Color(0xFFCE4257)
|
||||
val Straw = Color(0xFFD4C483)
|
||||
|
||||
|
||||
|
||||
sealed class ThemeColors(
|
||||
val background: Color,
|
||||
val surface: Color,
|
||||
|
|
|
@ -40,18 +40,15 @@ enum class Theme {
|
|||
AUTOMATIC,
|
||||
LIGHT,
|
||||
DARK,
|
||||
DYNAMIC
|
||||
DYNAMIC,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WireguardAutoTunnelTheme(
|
||||
theme: Theme = Theme.AUTOMATIC,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
var isDark = isSystemInDarkTheme()
|
||||
val autoTheme = if(isDark) DarkColorScheme else LightColorScheme
|
||||
val colorScheme = when(theme) {
|
||||
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
|
||||
val colorScheme = when (theme) {
|
||||
Theme.AUTOMATIC -> autoTheme
|
||||
Theme.DARK -> {
|
||||
isDark = true
|
||||
|
@ -68,7 +65,9 @@ fun WireguardAutoTunnelTheme(
|
|||
} else {
|
||||
dynamicLightColorScheme(context)
|
||||
}
|
||||
} else autoTheme
|
||||
} else {
|
||||
autoTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
val view = LocalView.current
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
|
||||
private var creator: ((A) -> T)? = creator
|
||||
|
||||
@Volatile private var instance: T? = null
|
||||
|
||||
fun getInstance(arg: A): T {
|
||||
val i = instance
|
||||
if (i != null) {
|
||||
return i
|
||||
}
|
||||
|
||||
return synchronized(this) {
|
||||
val i2 = instance
|
||||
if (i2 != null) {
|
||||
i2
|
||||
} else {
|
||||
val created = creator!!(arg)
|
||||
instance = created
|
||||
creator = null
|
||||
created
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,12 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
|||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.POWER_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
|
@ -13,7 +15,6 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.BackgroundActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
|
@ -34,6 +35,11 @@ fun Context.openWebUrl(url: String): Result<Unit> {
|
|||
}
|
||||
}
|
||||
|
||||
fun Context.isBatteryOptimizationsDisabled(): Boolean {
|
||||
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||
return pm.isIgnoringBatteryOptimizations(packageName)
|
||||
}
|
||||
|
||||
val Context.actionBarSize
|
||||
get() = theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize))
|
||||
.let { attrs -> attrs.getDimension(0, 0F).toInt().also { attrs.recycle() } }
|
||||
|
@ -66,7 +72,7 @@ fun Context.resizeWidth(dp: Dp): Dp {
|
|||
}
|
||||
|
||||
fun Context.launchNotificationSettings() {
|
||||
if(isRunningOnTv()) return launchAppSettings()
|
||||
if (isRunningOnTv()) return launchAppSettings()
|
||||
val settingsIntent: Intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
|
@ -159,23 +165,23 @@ fun Context.launchAppSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
fun Context.startTunnelBackground(tunnelId: Int) {
|
||||
sendBroadcast(
|
||||
Intent(this, BackgroundActionReceiver::class.java).apply {
|
||||
action = BackgroundActionReceiver.ACTION_CONNECT
|
||||
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.stopTunnelBackground(tunnelId: Int) {
|
||||
sendBroadcast(
|
||||
Intent(this, BackgroundActionReceiver::class.java).apply {
|
||||
action = BackgroundActionReceiver.ACTION_DISCONNECT
|
||||
putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
|
||||
},
|
||||
)
|
||||
}
|
||||
// fun Context.startTunnelBackground(tunnelId: Int) {
|
||||
// sendBroadcast(
|
||||
// Intent(this, BackgroundActionReceiver::class.java).apply {
|
||||
// action = BackgroundActionReceiver.ACTION_CONNECT
|
||||
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun Context.stopTunnelBackground(tunnelId: Int) {
|
||||
// sendBroadcast(
|
||||
// Intent(this, BackgroundActionReceiver::class.java).apply {
|
||||
// action = BackgroundActionReceiver.ACTION_DISCONNECT
|
||||
// putExtra(BackgroundActionReceiver.TUNNEL_ID_EXTRA_KEY, tunnelId)
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
|
||||
fun Context.requestTunnelTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
|
|
|
@ -28,9 +28,9 @@ fun String.extractNameAndNumber(): Pair<String, Int>? {
|
|||
}
|
||||
|
||||
fun List<String>.isMatchingToWildcardList(value: String): Boolean {
|
||||
val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").toRegexWithWildcards() }
|
||||
val excludeValues = this.filter { it.startsWith("!") }.map { it.removePrefix("!").transformWildcardsToRegex() }
|
||||
Timber.d("Excluded values: $excludeValues")
|
||||
val includedValues = this.filter { !it.startsWith("!") }.map { it.toRegexWithWildcards() }
|
||||
val includedValues = this.filter { !it.startsWith("!") }.map { it.transformWildcardsToRegex() }
|
||||
Timber.d("Included values: $includedValues")
|
||||
val matches = includedValues.filter { it.matches(value) }
|
||||
val excludedMatches = excludeValues.filter { it.matches(value) }
|
||||
|
@ -39,6 +39,32 @@ fun List<String>.isMatchingToWildcardList(value: String): Boolean {
|
|||
return matches.isNotEmpty() && excludedMatches.isEmpty()
|
||||
}
|
||||
|
||||
fun String.toRegexWithWildcards(): Regex {
|
||||
return this.replace("*", ".*").replace("?", ".").toRegex()
|
||||
fun String.transformWildcardsToRegex(): Regex {
|
||||
return this.replaceUnescapedChar("*", ".*").replaceUnescapedChar("?", ".").toRegex()
|
||||
}
|
||||
|
||||
fun String.replaceUnescapedChar(charToReplace: String, replacement: String): String {
|
||||
val escapedChar = Regex.escape(charToReplace)
|
||||
val regex = "(?<!\\\\)(?<!(?<!\\\\)\\\\)($escapedChar)".toRegex()
|
||||
return regex.replace(this) { matchResult ->
|
||||
if (matchResult.range.first == 0 ||
|
||||
this[matchResult.range.first - 1] != '\\' ||
|
||||
(matchResult.range.first > 1 && this[matchResult.range.first - 2] == '\\')
|
||||
) {
|
||||
replacement.toString()
|
||||
} else {
|
||||
matchResult.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isCharacterEscaped(index: Int): Boolean {
|
||||
if (index <= 0) return false
|
||||
var backslashCount = 0
|
||||
var currentIndex = index - 1
|
||||
while (currentIndex >= 0 && this[currentIndex] == '\\') {
|
||||
backslashCount++
|
||||
currentIndex--
|
||||
}
|
||||
return backslashCount % 2 != 0
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import androidx.compose.ui.graphics.Color
|
|||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.config.Peer
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import org.amnezia.awg.config.Config
|
||||
|
@ -21,6 +23,10 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
|||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun VpnState.isDown(): Boolean {
|
||||
return this.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
|
|
|
@ -220,4 +220,9 @@
|
|||
<string name="use_wildcards">Use name wildcards</string>
|
||||
<string name="learn_more">Learn more</string>
|
||||
<string name="wildcards_active">Wildcards active</string>
|
||||
<string name="wifi_name_via_shell">Wifi name via shell</string>
|
||||
<string name="use_root_shell_for_wifi">Use root shell to get wifi name</string>
|
||||
<string name="kernel_not_supported">Kernel not supported</string>
|
||||
<string name="start_auto">Start auto-tunnel</string>
|
||||
<string name="stop_auto">Stop auto-tunnel</string>
|
||||
</resources>
|
||||
|
|
|
@ -37,8 +37,8 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/auto_play"
|
||||
android:shortcutId="autoOn1"
|
||||
android:shortcutLongLabel="@string/auto_on"
|
||||
android:shortcutShortLabel="@string/auto_tun_on">
|
||||
android:shortcutLongLabel="@string/start_auto"
|
||||
android:shortcutShortLabel="@string/start_auto">
|
||||
<intent
|
||||
android:action="START"
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||
|
@ -53,8 +53,8 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/auto_pause"
|
||||
android:shortcutId="autoOff1"
|
||||
android:shortcutLongLabel="@string/auto_off"
|
||||
android:shortcutShortLabel="@string/auto_tun_off">
|
||||
android:shortcutLongLabel="@string/stop_auto"
|
||||
android:shortcutShortLabel="@string/stop_auto">
|
||||
<intent
|
||||
android:action="STOP"
|
||||
android:targetClass="com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsActivity"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
object Constants {
|
||||
const val VERSION_NAME = "3.5.3"
|
||||
const val VERSION_NAME = "3.6.0"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 35300
|
||||
const val VERSION_CODE = 36000
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
|
|
|
@ -6,7 +6,7 @@ androidx-junit = "1.2.1"
|
|||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreGoogleShortcuts = "1.1.0"
|
||||
coreKtx = "1.13.1"
|
||||
coreKtx = "1.15.0"
|
||||
datastorePreferences = "1.1.1"
|
||||
desugar_jdk_libs = "2.1.2"
|
||||
espressoCore = "3.6.1"
|
||||
|
@ -14,18 +14,18 @@ hiltAndroid = "2.52"
|
|||
hiltNavigationCompose = "1.2.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.7.3"
|
||||
lifecycle-runtime-compose = "2.8.6"
|
||||
material3 = "1.3.0"
|
||||
lifecycle-runtime-compose = "2.8.7"
|
||||
material3 = "1.3.1"
|
||||
navigationCompose = "2.8.3"
|
||||
pinLockCompose = "1.0.4"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.2.1"
|
||||
androidGradlePlugin = "8.7.1"
|
||||
androidGradlePlugin = "8.7.2"
|
||||
kotlin = "2.0.21"
|
||||
ksp = "2.0.21-1.0.25"
|
||||
composeBom = "2024.10.00"
|
||||
compose = "1.7.4"
|
||||
composeBom = "2024.10.01"
|
||||
compose = "1.7.5"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
coreSplashscreen = "1.0.1"
|
||||
gradlePlugins-grgit = "5.3.0"
|
||||
|
|
Loading…
Reference in New Issue