fix: improve tunnel reliability (#298)
- Attempts to fix tunnel and auto-tunnel reliability by removing the tunnel foreground service and circumventing the limitation of starting the vpn service from by background by using a broadcast receiver. - Removes tunnel foreground notification. - Improves the reliability auto-tunnel start on reboot by adding an additional notification launch calls. - Fixes bug where pin feature could be turned on without the pin being set. - Improves quick tile reliability and sync. - Improves reliability of app shortcuts. - Improves kernel mode - Improves permissions flow - Adds support for dynamic app colors Android 12+ - Add support for light/dark system modes
This commit is contained in:
parent
19961ca343
commit
bfb8d59827
|
@ -71,3 +71,4 @@ app/release/output.json
|
|||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
app/signing.properties
|
||||
/.kotlin/
|
||||
|
|
|
@ -134,6 +134,7 @@ dependencies {
|
|||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
// implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||
implementation(libs.tunnel)
|
||||
implementation(libs.amneziawg.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "e2c91dbf1885a9da592d3f54f1e08302",
|
||||
"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_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_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": "isAutoTunnelPaused",
|
||||
"columnName": "is_auto_tunnel_paused",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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, 'e2c91dbf1885a9da592d3f54f1e08302')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -31,6 +31,12 @@
|
|||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<!--android tv support-->
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
@ -137,23 +143,23 @@
|
|||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.tunnel.AlwaysOnVpnService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:name=".service.foreground.AutoTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
|
@ -174,8 +180,17 @@
|
|||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".receiver.NotificationActionReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".receiver.BackgroundActionReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
<receiver
|
||||
android:name=".receiver.KernelReceiver"
|
||||
android:exported="false"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
<intent-filter>
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
|
@ -35,23 +38,5 @@ class WireGuardAutoTunnel : Application() {
|
|||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
|
||||
fun isRunningOnAndroidTv(): Boolean {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestAutoTunnelTileServiceUpdate() {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 8,
|
||||
version = 9,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
|
@ -34,6 +34,7 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
|||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
@ -23,6 +23,9 @@ interface TunnelConfigDao {
|
|||
@Query("SELECT * FROM TunnelConfig WHERE name=:name")
|
||||
suspend fun getByName(name: String): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE is_Active=1")
|
||||
suspend fun getActive(): TunnelConfigs
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
|
||||
|
|
|
@ -24,9 +24,7 @@ class DataStoreManager(
|
|||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||
val TUNNEL_RUNNING_FROM_MANUAL_START =
|
||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
||||
val LAST_ACTIVE_TUNNEL = intPreferencesKey("LAST_ACTIVE_TUNNEL")
|
||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
||||
val IS_PIN_LOCK_ENABLED = booleanPreferencesKey("PIN_LOCK_ENABLED")
|
||||
}
|
||||
|
|
|
@ -3,14 +3,12 @@ package com.zaneschepke.wireguardautotunnel.data.domain
|
|||
data class GeneralState(
|
||||
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
||||
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
val isTunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val activeTunnelId: Int? = null,
|
||||
val lastActiveTunnelId: Int? = null,
|
||||
) {
|
||||
companion object {
|
||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,17 @@ data class TunnelConfig(
|
|||
defaultValue = "",
|
||||
)
|
||||
val amQuick: String = AM_QUICK_DEFAULT,
|
||||
@ColumnInfo(
|
||||
name = "is_Active",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isActive: Boolean = false,
|
||||
) {
|
||||
companion object {
|
||||
fun findDefault(tunnels: List<TunnelConfig>): TunnelConfig? {
|
||||
return tunnels.find { it.isPrimaryTunnel } ?: tunnels.firstOrNull()
|
||||
}
|
||||
|
||||
fun configFromWgQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
||||
|
|
|
@ -7,8 +7,6 @@ interface AppDataRepository {
|
|||
|
||||
suspend fun getStartTunnelConfig(): TunnelConfig?
|
||||
|
||||
suspend fun toggleWatcherServicePause()
|
||||
|
||||
val settings: SettingsRepository
|
||||
val tunnels: TunnelConfigRepository
|
||||
val appState: AppStateRepository
|
||||
|
|
|
@ -15,24 +15,8 @@ constructor(
|
|||
}
|
||||
|
||||
override suspend fun getStartTunnelConfig(): TunnelConfig? {
|
||||
return if (appState.isTunnelRunningFromManualStart()) {
|
||||
appState.getActiveTunnelId()?.let {
|
||||
tunnels.getById(it)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleWatcherServicePause() {
|
||||
val settings = settings.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
this.settings.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
return appState.getLastActiveTunnelId()?.let {
|
||||
tunnels.getById(it)
|
||||
} ?: getPrimaryOrFirstTunnel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,9 @@ interface AppStateRepository {
|
|||
|
||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
||||
|
||||
suspend fun isTunnelRunningFromManualStart(): Boolean
|
||||
suspend fun getLastActiveTunnelId(): Int?
|
||||
|
||||
suspend fun setTunnelRunningFromManualStart(id: Int)
|
||||
|
||||
suspend fun setManualStop()
|
||||
|
||||
suspend fun getActiveTunnelId(): Int?
|
||||
suspend fun setLastActiveTunnelId(id: Int)
|
||||
|
||||
suspend fun getCurrentSsid(): String?
|
||||
|
||||
|
|
|
@ -2,71 +2,65 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
|||
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
||||
class DataStoreAppStateRepository(
|
||||
private val dataStoreManager: DataStoreManager,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) :
|
||||
AppStateRepository {
|
||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||
return withContext(ioDispatcher) {
|
||||
dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
||||
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) }
|
||||
}
|
||||
|
||||
override suspend fun isPinLockEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||
return withContext(ioDispatcher) {
|
||||
dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setPinLockEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
|
||||
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) }
|
||||
}
|
||||
|
||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
return withContext(ioDispatcher) {
|
||||
dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
||||
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) }
|
||||
}
|
||||
|
||||
override suspend fun isTunnelRunningFromManualStart(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
|
||||
override suspend fun getLastActiveTunnelId(): Int? {
|
||||
return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.LAST_ACTIVE_TUNNEL) }
|
||||
}
|
||||
|
||||
override suspend fun setTunnelRunningFromManualStart(id: Int) {
|
||||
setTunnelRunningFromManualStart(true)
|
||||
setActiveTunnelId(id)
|
||||
}
|
||||
|
||||
override suspend fun setManualStop() {
|
||||
setTunnelRunningFromManualStart(false)
|
||||
}
|
||||
|
||||
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
|
||||
}
|
||||
|
||||
override suspend fun getActiveTunnelId(): Int? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
|
||||
}
|
||||
|
||||
private suspend fun setActiveTunnelId(id: Int) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
|
||||
override suspend fun setLastActiveTunnelId(id: Int) {
|
||||
return withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LAST_ACTIVE_TUNNEL, id) }
|
||||
}
|
||||
|
||||
override suspend fun getCurrentSsid(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
||||
return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) }
|
||||
}
|
||||
|
||||
override suspend fun setCurrentSsid(ssid: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
||||
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) }
|
||||
}
|
||||
|
||||
override val generalStateFlow: Flow<GeneralState> =
|
||||
|
@ -80,12 +74,10 @@ class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager
|
|||
isBatteryOptimizationDisableShown =
|
||||
pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
||||
isTunnelRunningFromManualStart =
|
||||
pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
isPinLockEnabled =
|
||||
pref[DataStoreManager.IS_PIN_LOCK_ENABLED]
|
||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
||||
lastActiveTunnelId = pref[DataStoreManager.LAST_ACTIVE_TUNNEL],
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e)
|
||||
|
|
|
@ -2,11 +2,16 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
|||
|
||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao, @IoDispatcher private val ioDispatcher: CoroutineDispatcher) : SettingsRepository {
|
||||
override suspend fun save(settings: Settings) {
|
||||
settingsDoa.save(settings)
|
||||
withContext(ioDispatcher) {
|
||||
settingsDoa.save(settings)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSettingsFlow(): Flow<Settings> {
|
||||
|
@ -14,10 +19,12 @@ class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRep
|
|||
}
|
||||
|
||||
override suspend fun getSettings(): Settings {
|
||||
return settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||
return withContext(ioDispatcher) {
|
||||
settingsDoa.getAll().firstOrNull() ?: Settings()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings> {
|
||||
return settingsDoa.getAll()
|
||||
return withContext(ioDispatcher) { settingsDoa.getAll() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,97 @@
|
|||
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.util.TunnelConfigs
|
||||
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
|
||||
|
||||
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
||||
class RoomTunnelConfigRepository(
|
||||
private val tunnelConfigDao: TunnelConfigDao,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) :
|
||||
TunnelConfigRepository {
|
||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||
return tunnelConfigDao.getAllFlow()
|
||||
}
|
||||
|
||||
override suspend fun getAll(): TunnelConfigs {
|
||||
return tunnelConfigDao.getAll()
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getAll() }
|
||||
}
|
||||
|
||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.save(tunnelConfig)
|
||||
}.also {
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isPrimaryTunnel = true,
|
||||
),
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetPrimaryTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isPrimaryTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isMobileDataTunnel = true,
|
||||
),
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.resetMobileDataTunnel()
|
||||
tunnelConfig?.let {
|
||||
save(
|
||||
it.copy(
|
||||
isMobileDataTunnel = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.delete(tunnelConfig)
|
||||
}.also {
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): TunnelConfig? {
|
||||
return tunnelConfigDao.getById(id.toLong())
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getById(id.toLong()) }
|
||||
}
|
||||
|
||||
override suspend fun getActive(): TunnelConfigs {
|
||||
return withContext(ioDispatcher) {
|
||||
tunnelConfigDao.getActive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count(): Int {
|
||||
return tunnelConfigDao.count().toInt()
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.count().toInt() }
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelName(name: String): TunnelConfig? {
|
||||
return tunnelConfigDao.getByName(name)
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.getByName(name) }
|
||||
}
|
||||
|
||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.findByTunnelNetworkName(name) }
|
||||
}
|
||||
|
||||
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByMobileDataTunnel()
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.findByMobileDataTunnel() }
|
||||
}
|
||||
|
||||
override suspend fun findPrimary(): TunnelConfigs {
|
||||
return tunnelConfigDao.findByPrimary()
|
||||
return withContext(ioDispatcher) { tunnelConfigDao.findByPrimary() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TunnelConfigRepository {
|
||||
|
@ -19,6 +19,8 @@ interface TunnelConfigRepository {
|
|||
|
||||
suspend fun getById(id: Int): TunnelConfig?
|
||||
|
||||
suspend fun getActive(): TunnelConfigs
|
||||
|
||||
suspend fun count(): Int
|
||||
|
||||
suspend fun findByTunnelName(name: String): TunnelConfig?
|
||||
|
|
|
@ -54,14 +54,14 @@ class RepositoryModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): TunnelConfigRepository {
|
||||
return RoomTunnelConfigRepository(tunnelConfigDao, ioDispatcher)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||
return RoomSettingsRepository(settingsDao)
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao, @IoDispatcher ioDispatcher: CoroutineDispatcher): SettingsRepository {
|
||||
return RoomSettingsRepository(settingsDao, ioDispatcher)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
|
@ -72,8 +72,8 @@ class RepositoryModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
||||
return DataStoreAppStateRepository(dataStoreManager)
|
||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppStateRepository {
|
||||
return DataStoreAppStateRepository(dataStoreManager, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
@ -3,12 +3,13 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||
import android.content.Context
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.RootTunnelActionHandler
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -29,24 +30,30 @@ class TunnelModule {
|
|||
return RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRootShellAm(@ApplicationContext context: Context): org.amnezia.awg.util.RootShell {
|
||||
return org.amnezia.awg.util.RootShell(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Userspace
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context): Backend {
|
||||
return GoBackend(context)
|
||||
fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return GoBackend(context, RootTunnelActionHandler(rootShell))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Kernel
|
||||
fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend {
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell))
|
||||
return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context)
|
||||
fun provideAmneziaBackend(@ApplicationContext context: Context, rootShell: org.amnezia.awg.util.RootShell): org.amnezia.awg.backend.Backend {
|
||||
return org.amnezia.awg.backend.GoBackend(context, org.amnezia.awg.backend.RootTunnelActionHandler(rootShell))
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -58,7 +65,7 @@ class TunnelModule {
|
|||
appDataRepository: AppDataRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): VpnService {
|
||||
): TunnelService {
|
||||
return WireGuardTunnel(
|
||||
amneziaBackend,
|
||||
userspaceBackend,
|
||||
|
@ -71,7 +78,7 @@ class TunnelModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository, @IoDispatcher ioDispatcher: CoroutineDispatcher): ServiceManager {
|
||||
return ServiceManager(appDataRepository, ioDispatcher)
|
||||
fun provideServiceManager(): ServiceManager {
|
||||
return ServiceManager()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
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.tunnel.TunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
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 -> {
|
||||
applicationScope.launch {
|
||||
val tunnel = tunnelConfigRepository.getById(id)
|
||||
tunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTION_DISCONNECT -> {
|
||||
applicationScope.launch {
|
||||
val tunnel = tunnelConfigRepository.getById(id)
|
||||
tunnel?.let {
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -6,17 +6,22 @@ import android.content.Intent
|
|||
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 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 BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
|
@ -24,32 +29,19 @@ class BootReceiver : BroadcastReceiver() {
|
|||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
|
||||
context?.run {
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
||||
Timber.i("Starting tunnel that was active before reboot")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
appDataRepository.tunnels.getById(it)?.id,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
Timber.i("Starting vpn service from boot AOVPN")
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
}
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
||||
applicationScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
appDataRepository.getStartTunnelConfig()?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
Timber.i("Starting watcher service from boot")
|
||||
serviceManager.startWatcherServiceForeground(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
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.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class KernelReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
applicationScope.launch {
|
||||
if (action == REFRESH_TUNNELS_ACTION) {
|
||||
tunnelService.get().runningTunnelNames().forEach { name ->
|
||||
// TODO can optimize later
|
||||
val tunnel = tunnelConfigRepository.findByTunnelName(name)
|
||||
tunnel?.let {
|
||||
tunnelConfigRepository.save(it.copy(isActive = true))
|
||||
}
|
||||
}
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REFRESH_TUNNELS_ACTION = "com.wireguard.android.action.REFRESH_TUNNEL_STATES"
|
||||
}
|
||||
}
|
|
@ -1,44 +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.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
// TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,13 +16,11 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
|||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
@ -32,9 +30,10 @@ import kotlinx.coroutines.withContext
|
|||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
class AutoTunnelService : ForegroundService() {
|
||||
private val foregroundId = 122
|
||||
|
||||
@Inject
|
||||
|
@ -53,10 +52,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
|
@ -66,37 +62,43 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||
|
||||
private var watcherJob: Job? = null
|
||||
private val networkEventsFlow = MutableStateFlow(AutoTunnelState())
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val tag = this.javaClass.name
|
||||
|
||||
private var running: Boolean = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else {
|
||||
launchWatcherNotification()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start watcher service, not enough permissions")
|
||||
kotlin.runCatching {
|
||||
launchNotification()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun launchNotification() {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else {
|
||||
launchWatcherNotification()
|
||||
}
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
try {
|
||||
// we need this lock so our service gets not affected by Doze Mode
|
||||
lifecycleScope.launch { initWakeLock() }
|
||||
cancelWatcherJob()
|
||||
if (running) return
|
||||
kotlin.runCatching {
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
launchNotification()
|
||||
initWakeLock()
|
||||
}
|
||||
startWatcherJob()
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to launch watcher service, no permissions")
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,8 +109,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
it.release()
|
||||
}
|
||||
}
|
||||
cancelWatcherJob()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) {
|
||||
|
@ -145,49 +145,39 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
try {
|
||||
watcherJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Watcher job cancelled")
|
||||
private fun startWatcherJob() = lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if (setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if (setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
running = true
|
||||
}
|
||||
|
||||
private suspend fun watchForMobileDataConnectivityChanges() {
|
||||
|
@ -226,12 +216,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
}
|
||||
|
||||
private suspend fun watchForPingFailure() {
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
do {
|
||||
if (vpnService.vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
if (tunnelService.get().vpnState.value.status == TunnelState.UP) {
|
||||
val tunnelConfig = tunnelService.get().vpnState.value.tunnelConfig
|
||||
tunnelConfig?.let {
|
||||
val config = TunnelConfig.configFromWgQuick(it.wgQuick)
|
||||
val results =
|
||||
|
@ -253,9 +242,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
}
|
||||
if (results.contains(false)) {
|
||||
Timber.i("Restarting VPN for ping failure")
|
||||
serviceManager.stopVpnServiceForeground(context)
|
||||
tunnelService.get().stopTunnel(it)
|
||||
delay(Constants.VPN_RESTART_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
tunnelService.get().startTunnel(it)
|
||||
delay(Constants.PING_COOLDOWN)
|
||||
}
|
||||
}
|
||||
|
@ -379,63 +368,67 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
}
|
||||
|
||||
private fun isTunnelDown(): Boolean {
|
||||
return vpnService.vpnState.value.status == TunnelState.DOWN
|
||||
return tunnelService.get().vpnState.value.status == TunnelState.DOWN
|
||||
}
|
||||
|
||||
private suspend fun manageVpn() {
|
||||
val context = this
|
||||
withContext(ioDispatcher) {
|
||||
networkEventsFlow.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 tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
||||
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()) serviceManager.startVpnServiceForeground(context)
|
||||
if (isTunnelDown()) {
|
||||
defaultTunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on mobile data condition met")
|
||||
val mobileDataTunnel = getMobileDataTunnel()
|
||||
val tunnel =
|
||||
mobileDataTunnel ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (isTunnelDown() || tunnelConfig?.isMobileDataTunnel == false) {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
tunnel?.id,
|
||||
)
|
||||
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()) serviceManager.stopVpnServiceForeground(context)
|
||||
if (!isTunnelDown()) {
|
||||
activeTunnel?.let {
|
||||
tunnelService.get().stopTunnel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
if (tunnelConfig?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
tunnelConfig == null
|
||||
if (activeTunnel?.tunnelNetworks?.contains(watcherState.currentNetworkSSID) == false ||
|
||||
activeTunnel == null
|
||||
) {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
|
||||
)
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
|
||||
if (isTunnelDown() || tunnelConfig?.id != it.id) {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
context,
|
||||
it.id,
|
||||
)
|
||||
if (isTunnelDown() || activeTunnel?.id != it.id) {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
val default = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (default?.name != vpnService.name) {
|
||||
if (default?.name != tunnelService.get().name || isTunnelDown()) {
|
||||
default?.let {
|
||||
serviceManager.startVpnServiceForeground(context, it.id)
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
}.invoke()
|
||||
|
@ -446,21 +439,21 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
Timber.i(
|
||||
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
||||
Timber.i(
|
||||
"$autoTunnel - tunnel off on no connectivity met, turning vpn off",
|
||||
)
|
||||
if (!isTunnelDown()) serviceManager.stopVpnServiceForeground(context)
|
||||
if (!isTunnelDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
|
||||
}
|
||||
|
||||
else -> {
|
|
@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
|
||||
data class WatcherState(
|
||||
data class AutoTunnelState(
|
||||
val isWifiConnected: Boolean = false,
|
||||
val isEthernetConnected: Boolean = false,
|
||||
val isMobileDataConnected: Boolean = false,
|
|
@ -3,17 +3,9 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class ServiceManager(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
class ServiceManager {
|
||||
private fun <T : Service> actionOnService(action: Action, context: Context, cls: Class<T>, extras: Map<String, Int>? = null) {
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
|
@ -35,67 +27,11 @@ class ServiceManager(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnService(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
appDataRepository.appState.setManualStop()
|
||||
}
|
||||
|
||||
private suspend fun onManualStart(tunnelId: Int?) {
|
||||
tunnelId?.let {
|
||||
appDataRepository.appState.setTunnelRunningFromManualStart(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnServiceForeground(context: Context, tunnelId: Int? = null, isManualStart: Boolean = false) {
|
||||
withContext(ioDispatcher) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun startWatcherServiceForeground(context: Context) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,7 +39,7 @@ class ServiceManager(
|
|||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -111,7 +47,7 @@ class ServiceManager(
|
|||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
WireGuardConnectivityWatcherService::class.java,
|
||||
AutoTunnelService::class.java,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
@MainImmediateDispatcher
|
||||
lateinit var mainImmediateDispatcher: CoroutineDispatcher
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(mainImmediateDispatcher) {
|
||||
// TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
launchVpnNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
job =
|
||||
lifecycleScope.launch {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
tunnelId?.let {
|
||||
appDataRepository.tunnels.getById(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
launch {
|
||||
handshakeNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
withContext(ioDispatcher) {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
when {
|
||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||
if (!didShowConnected) {
|
||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
didShowConnected = true
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAlwaysOnDisabledNotification() {
|
||||
launchVpnNotification(
|
||||
title = this.getString(R.string.vpn_connection_failed),
|
||||
description = this.getString(R.string.always_on_disabled),
|
||||
)
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
lifecycleScope.launch {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
}
|
||||
cancelJob()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun launchVpnNotification(title: String = getString(R.string.vpn_starting), description: String = getString(R.string.attempt_connection)) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = title,
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = description,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchVpnConnectionFailedNotification(message: String) {
|
||||
val notification =
|
||||
notificationService.createNotification(
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action =
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message,
|
||||
)
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
foregroundId,
|
||||
notification,
|
||||
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelJob() {
|
||||
try {
|
||||
job?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
Timber.i("Tunnel job cancelled")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,13 +5,14 @@ import androidx.activity.ComponentActivity
|
|||
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.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
|
||||
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 ShortcutsActivity : ComponentActivity() {
|
||||
|
@ -19,7 +20,7 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
|
@ -31,31 +32,23 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
WireGuardTunnelService::class.java.simpleName -> {
|
||||
LEGACY_TUNNEL_SERVICE_NAME, TunnelService::class.java.simpleName -> {
|
||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||
val tunnelConfig =
|
||||
tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
}
|
||||
Timber.d("Tunnel name extra: $tunnelName")
|
||||
val tunnelConfig = tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll()
|
||||
.firstOrNull { it.name == tunnelName }
|
||||
} ?: appDataRepository.getStartTunnelConfig()
|
||||
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
|
||||
tunnelConfig?.let {
|
||||
when (intent.action) {
|
||||
Action.START.name -> tunnelService.get().startTunnel(it)
|
||||
Action.STOP.name -> tunnelService.get().stopTunnel(it)
|
||||
else -> Unit
|
||||
}
|
||||
when (intent.action) {
|
||||
Action.START.name ->
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig?.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
|
||||
Action.STOP.name ->
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
isManualStop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WireGuardConnectivityWatcherService::class.java.simpleName -> {
|
||||
AutoTunnelService::class.java.simpleName, LEGACY_AUTO_TUNNEL_SERVICE_NAME -> {
|
||||
when (intent.action) {
|
||||
Action.START.name ->
|
||||
appDataRepository.settings.save(
|
||||
|
@ -63,7 +56,6 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
isAutoTunnelPaused = false,
|
||||
),
|
||||
)
|
||||
|
||||
Action.STOP.name ->
|
||||
appDataRepository.settings.save(
|
||||
settings.copy(
|
||||
|
@ -79,6 +71,8 @@ class ShortcutsActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
|
||||
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"
|
||||
const val TUNNEL_NAME_EXTRA_KEY = "tunnelName"
|
||||
const val CLASS_NAME_EXTRA_KEY = "className"
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
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
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
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.foreground.ServiceManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -25,80 +27,123 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
|||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
when (settings.isAutoTunnelEnabled) {
|
||||
true -> {
|
||||
if (settings.isAutoTunnelPaused) {
|
||||
setInactive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
||||
} else {
|
||||
setActive()
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
||||
/* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
||||
setUnavailable()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
override fun onStopListening() {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
appDataRepository.toggleWatcherServicePause()
|
||||
onStartListening()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
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()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
|
|
@ -5,18 +5,20 @@ import android.service.quicksettings.Tile
|
|||
import android.service.quicksettings.TileService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
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.cancel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||
|
@ -24,98 +26,103 @@ class TunnelControlTile : TileService(), LifecycleOwner {
|
|||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
private val dispatcher = ServiceLifecycleDispatcher(this)
|
||||
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.d("onCreate for tile service")
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
lifecycleScope.launch {
|
||||
when (vpnService.getState()) {
|
||||
TunnelState.UP -> {
|
||||
setActive()
|
||||
setTileDescription(vpnService.name)
|
||||
}
|
||||
|
||||
TunnelState.DOWN -> {
|
||||
setInactive()
|
||||
val config =
|
||||
appDataRepository.getStartTunnelConfig()?.also { config ->
|
||||
manualStartConfig = config
|
||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
config?.let {
|
||||
setTileDescription(it.name)
|
||||
} ?: setUnavailable()
|
||||
}
|
||||
|
||||
else -> setInactive()
|
||||
}
|
||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||
updateTileState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
private suspend fun updateTileState() {
|
||||
val lastActive = appDataRepository.getStartTunnelConfig()
|
||||
lastActive?.let {
|
||||
updateTile(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
if (vpnService.getState() == TunnelState.UP) {
|
||||
serviceManager.stopVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
)
|
||||
} else {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
manualStartConfig?.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
} finally {
|
||||
cancel()
|
||||
val context = this@TunnelControlTile
|
||||
val lastActive = appDataRepository.getStartTunnelConfig()
|
||||
lastActive?.let { tunnel ->
|
||||
if (tunnel.isActive) return@launch context.stopTunnelBackground(tunnel.id)
|
||||
context.startTunnelBackground(tunnel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActive() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInactive() {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
kotlin.runCatching {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
setTileDescription("")
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTileDescription(description: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
qsTile.subtitle = description
|
||||
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()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
qsTile.stateDescription = description
|
||||
}
|
||||
|
||||
private fun updateTile(tunnelConfig: TunnelConfig?) {
|
||||
kotlin.runCatching {
|
||||
tunnelConfig?.let {
|
||||
setTileDescription(it.name)
|
||||
if (it.isActive) return setActive()
|
||||
setInactive()
|
||||
}
|
||||
}
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle
|
||||
get() = dispatcher.lifecycle
|
||||
get() = lifecycleRegistry
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AlwaysOnVpnService : LifecycleService() {
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
// We don't provide binding, so return null
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null || intent.component == null || intent.component!!.packageName != packageName) {
|
||||
Timber.i("Always-on VPN requested started")
|
||||
lifecycleScope.launch {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel = appDataRepository.getPrimaryOrFirstTunnel()
|
||||
tunnel?.let {
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
} else {
|
||||
Timber.w("Always-on VPN is not enabled in app settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
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 stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
|
||||
suspend fun getState(): TunnelState
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState
|
||||
|
||||
suspend fun stopTunnel()
|
||||
|
||||
val vpnState: StateFlow<VpnState>
|
||||
|
||||
fun getState(): TunnelState
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
|
@ -14,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStat
|
|||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -32,103 +32,74 @@ import javax.inject.Provider
|
|||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
private val userspaceAmneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
|
||||
@Userspace private val userspaceBackend: Provider<Backend>,
|
||||
@Kernel private val kernelBackend: Provider<Backend>,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : VpnService {
|
||||
) : TunnelService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return when (val backend = backend()) {
|
||||
is Backend -> backend.runningTunnelNames
|
||||
is org.amnezia.awg.backend.Backend -> backend.runningTunnelNames
|
||||
else -> emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private var statsJob: Job? = null
|
||||
|
||||
private var backendIsWgUserspace = true
|
||||
|
||||
private var backendIsAmneziaUserspace = false
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) {
|
||||
Timber.i("Setting kernel backend")
|
||||
backendIsWgUserspace = false
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) {
|
||||
Timber.i("Setting WireGuard userspace backend")
|
||||
backendIsWgUserspace = true
|
||||
backendIsAmneziaUserspace = false
|
||||
} else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) {
|
||||
Timber.i("Setting Amnezia userspace backend")
|
||||
backendIsAmneziaUserspace = true
|
||||
backendIsWgUserspace = false
|
||||
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
|
||||
return runCatching {
|
||||
when (val backend = backend()) {
|
||||
is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
|
||||
is org.amnezia.awg.backend.Backend -> backend.setState(this, tunnelState.toAmState(), TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)).let {
|
||||
TunnelState.from(it)
|
||||
}
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
Timber.i("Using Amnezia backend")
|
||||
val config =
|
||||
tunnelConfig?.let {
|
||||
if (it.amQuick != "") {
|
||||
TunnelConfig.configFromAmQuick(it.amQuick)
|
||||
} else {
|
||||
Timber.w(
|
||||
"Using backwards compatible wg config, amnezia specific config not found.",
|
||||
)
|
||||
TunnelConfig.configFromAmQuick(it.wgQuick)
|
||||
}
|
||||
}
|
||||
val state =
|
||||
userspaceAmneziaBackend.get().setState(this, tunnelState.toAmState(), config)
|
||||
TunnelState.from(state)
|
||||
} else {
|
||||
Timber.i("Using Wg backend")
|
||||
val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) }
|
||||
val state =
|
||||
backend().setState(
|
||||
this,
|
||||
tunnelState.toWgState(),
|
||||
wgConfig,
|
||||
)
|
||||
TunnelState.from(state)
|
||||
}
|
||||
private suspend fun backend(): Any {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isKernelEnabled) return kernelBackend.get()
|
||||
if (settings.isAmneziaEnabled) return amneziaBackend.get()
|
||||
return userspaceBackend.get()
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
|
||||
return withContext(ioDispatcher) {
|
||||
try {
|
||||
// TODO we need better error handling here
|
||||
// need to bubble up these errors to the UI
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
setState(config, TunnelState.UP)
|
||||
} else {
|
||||
throw Exception("No tunnels")
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
TunnelState.from(State.DOWN)
|
||||
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
|
||||
appDataRepository.appState.setLastActiveTunnelId(tunnelConfig.id)
|
||||
emitTunnelConfig(tunnelConfig)
|
||||
setState(tunnelConfig, TunnelState.UP).onSuccess {
|
||||
emitTunnelState(it)
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}.onFailure {
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backend(): Backend {
|
||||
return when {
|
||||
backendIsWgUserspace -> {
|
||||
userspaceBackend.get()
|
||||
}
|
||||
|
||||
!backendIsWgUserspace && !backendIsAmneziaUserspace -> {
|
||||
kernelBackend.get()
|
||||
}
|
||||
|
||||
else -> {
|
||||
userspaceBackend.get()
|
||||
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
|
||||
return withContext(ioDispatcher) {
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
|
||||
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
|
||||
emitTunnelState(it)
|
||||
resetBackendStatistics()
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,41 +120,27 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
_vpnState.emit(
|
||||
private fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
tunnelConfig = tunnelConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun resetVpnState() {
|
||||
_vpnState.tryEmit(VpnState())
|
||||
private fun resetBackendStatistics() {
|
||||
_vpnState.tryEmit(
|
||||
_vpnState.value.copy(
|
||||
statistics = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel() {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
if (getState() == TunnelState.UP) {
|
||||
val state = setState(null, TunnelState.DOWN)
|
||||
resetVpnState()
|
||||
emitTunnelState(state)
|
||||
}
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to stop wireguard tunnel with error: ${e.message}")
|
||||
} catch (e: org.amnezia.awg.backend.BackendException) {
|
||||
Timber.e("Failed to stop amnezia tunnel with error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): TunnelState {
|
||||
return if (backendIsAmneziaUserspace) {
|
||||
TunnelState.from(
|
||||
userspaceAmneziaBackend.get().getState(this),
|
||||
)
|
||||
} else {
|
||||
TunnelState.from(backend().getState(this))
|
||||
override suspend fun getState(): TunnelState {
|
||||
return when (val backend = backend()) {
|
||||
is Backend -> backend.getState(this).let { TunnelState.from(it) }
|
||||
is org.amnezia.awg.backend.Backend -> backend.getState(this).let { TunnelState.from(it) }
|
||||
else -> TunnelState.DOWN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,7 +154,7 @@ constructor(
|
|||
|
||||
private fun handleStateChange(state: TunnelState) {
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
|
||||
if (state == TunnelState.UP) {
|
||||
statsJob = startTunnelStatisticsJob()
|
||||
}
|
||||
|
@ -211,17 +168,19 @@ constructor(
|
|||
}
|
||||
|
||||
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
|
||||
val backend = backend()
|
||||
while (true) {
|
||||
if (backendIsAmneziaUserspace) {
|
||||
emitBackendStatistics(
|
||||
AmneziaStatistics(
|
||||
userspaceAmneziaBackend.get().getStatistics(this@WireGuardTunnel),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
emitBackendStatistics(
|
||||
WireGuardStatistics(backend().getStatistics(this@WireGuardTunnel)),
|
||||
when (backend) {
|
||||
is Backend -> emitBackendStatistics(
|
||||
WireGuardStatistics(backend.getStatistics(this@WireGuardTunnel)),
|
||||
)
|
||||
is org.amnezia.awg.backend.Backend -> {
|
||||
emitBackendStatistics(
|
||||
AmneziaStatistics(
|
||||
backend.getStatistics(this@WireGuardTunnel),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui
|
|||
data class AppUiState(
|
||||
val snackbarMessage: String = "",
|
||||
val snackbarMessageConsumed: Boolean = true,
|
||||
val vpnPermissionAccepted: Boolean = false,
|
||||
val notificationPermissionAccepted: Boolean = false,
|
||||
val requestPermissions: Boolean = false,
|
||||
)
|
||||
|
|
|
@ -1,100 +1,29 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppViewModel
|
||||
@Inject
|
||||
constructor() : ViewModel() {
|
||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
||||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _appUiState =
|
||||
MutableStateFlow(
|
||||
AppUiState(
|
||||
vpnPermissionAccepted = vpnIntent == null,
|
||||
),
|
||||
AppUiState(),
|
||||
)
|
||||
val appUiState = _appUiState.asStateFlow()
|
||||
|
||||
fun isRequiredPermissionGranted(): Boolean {
|
||||
val allAccepted =
|
||||
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
||||
if (!allAccepted) requestPermissions()
|
||||
return allAccepted
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun permissionsRequested() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
requestPermissions = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openWebPage(url: String, context: Context) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(context.getString(R.string.no_browser_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun onVpnPermissionAccepted() {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
vpnPermissionAccepted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail(context: Context) {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Timber.e(e)
|
||||
showSnackbarMessage(context.getString(R.string.no_email_detected))
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbarMessage(message: String) {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
|
@ -113,11 +42,12 @@ constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
||||
_appUiState.update {
|
||||
it.copy(
|
||||
notificationPermissionAccepted = accepted,
|
||||
)
|
||||
}
|
||||
fun onPinLockDisabled() = viewModelScope.launch {
|
||||
PinManager.clearPin()
|
||||
appDataRepository.appState.setPinLockEnabled(false)
|
||||
}
|
||||
|
||||
fun onPinLockEnabled() = viewModelScope.launch {
|
||||
appDataRepository.appState.setPinLockEnabled(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -21,19 +15,15 @@ import androidx.compose.material3.SnackbarDuration
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
@ -44,16 +34,9 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
|
@ -82,9 +65,6 @@ class MainActivity : AppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -93,7 +73,6 @@ class MainActivity : AppCompatActivity() {
|
|||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
lifecycleScope.launch {
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
|
@ -105,30 +84,9 @@ class MainActivity : AppCompatActivity() {
|
|||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
appViewModel.onVpnPermissionAccepted()
|
||||
} else {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun showSnackBarMessage(message: StringValue) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result =
|
||||
|
@ -146,37 +104,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.requestPermissions) {
|
||||
if (appUiState.requestPermissions) {
|
||||
appViewModel.permissionsRequested()
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||
) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
return@LaunchedEffect if (notificationPermissionState.status.shouldShowRationale || !notificationPermissionState.status.isGranted) {
|
||||
showSnackBarMessage(
|
||||
StringValue.StringResource(R.string.notification_permission_required),
|
||||
)
|
||||
} else {
|
||||
Unit
|
||||
}
|
||||
}
|
||||
if (!appUiState.vpnPermissionAccepted) {
|
||||
return@LaunchedEffect appViewModel.vpnIntent?.let {
|
||||
vpnActivityResultState.launch(
|
||||
it,
|
||||
)
|
||||
} ?: Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
if (!appUiState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||
|
@ -186,21 +114,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
if (showVpnPermissionDialog) {
|
||||
InfoDialog(
|
||||
onDismiss = { showVpnPermissionDialog = false },
|
||||
onAttest = { showVpnPermissionDialog = false },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
|
||||
Text(text = stringResource(R.string.vpn_denied_dialog_message))
|
||||
Text(text = stringResource(R.string.vpn_denied_dialog_message2))
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||
|
@ -266,7 +179,6 @@ class MainActivity : AppCompatActivity() {
|
|||
) {
|
||||
SupportScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,14 +11,22 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.zaneschepke.logcatter.LocalLogCollector
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel.Companion.isRunningOnAndroidTv
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
@AndroidEntryPoint
|
||||
|
@ -26,6 +34,12 @@ class SplashActivity : ComponentActivity() {
|
|||
@Inject
|
||||
lateinit var appStateRepository: AppStateRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelService: Provider<TunnelService>
|
||||
|
||||
@Inject
|
||||
lateinit var localLogCollector: LocalLogCollector
|
||||
|
||||
|
@ -41,7 +55,7 @@ class SplashActivity : ComponentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
if (!isRunningOnAndroidTv()) localLogCollector.start()
|
||||
if (!this@SplashActivity.isRunningOnTv()) localLogCollector.start()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
@ -50,6 +64,21 @@ class SplashActivity : ComponentActivity() {
|
|||
if (pinLockEnabled) {
|
||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
}
|
||||
// TODO eventually make this support multi-tunnel
|
||||
Timber.d("Check for active tunnels")
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isKernelEnabled) {
|
||||
// delay in case state change is underway while app is opened
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
val activeTunnels = appDataRepository.tunnels.getActive()
|
||||
Timber.d("Kernel mode enabled, seeing if we need to start a tunnel")
|
||||
activeTunnels.firstOrNull()?.let {
|
||||
Timber.d("Trying to start active kernel tunnel: ${it.name}")
|
||||
tunnelService.get().startTunnel(it)
|
||||
}
|
||||
}
|
||||
requestTunnelTileServiceStateUpdate()
|
||||
requestAutoTunnelTileServiceUpdate()
|
||||
|
||||
val intent =
|
||||
Intent(this@SplashActivity, MainActivity::class.java).apply {
|
||||
|
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.common.functions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
|
||||
@Composable
|
||||
fun rememberFileImportLauncherForResult(onNoFileExplorer: () -> Unit, onData: (data: Uri) -> Unit): ManagedActivityResultLauncher<String, Uri?> {
|
||||
return rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||
val activitiesToResolveIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.ResolveInfoFlags.of(
|
||||
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY,
|
||||
)
|
||||
}
|
||||
if (
|
||||
activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
onNoFileExplorer()
|
||||
}
|
||||
return intent
|
||||
}
|
||||
},
|
||||
) { data ->
|
||||
if (data == null) return@rememberLauncherForActivityResult
|
||||
onData(data)
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
|
||||
|
@ -39,7 +40,19 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
|
|||
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = { navController.navigate(item.route) },
|
||||
onClick = {
|
||||
navController.navigate(item.route) {
|
||||
// Pop up to the start destination of the graph to
|
||||
// avoid building up a large stack of destinations
|
||||
// on the back stack as users select items
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
// Avoid multiple copies of the same destination when
|
||||
// reselecting the same item
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = item.name,
|
||||
|
|
|
@ -19,19 +19,21 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
|
||||
@Composable
|
||||
fun CustomSnackBar(message: String, isRtl: Boolean = true, containerColor: Color = MaterialTheme.colorScheme.surface) {
|
||||
val context = LocalContext.current
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||
if (context.isRunningOnTv()) 1 / 3f else 2 / 3f,
|
||||
)
|
||||
.padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
|
@ -51,10 +53,10 @@ fun CustomSnackBar(message: String, isRtl: Boolean = true, containerColor: Color
|
|||
Icon(
|
||||
icon,
|
||||
contentDescription = icon.name,
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
Text(message, color = Color.White, modifier = Modifier.padding(end = 5.dp))
|
||||
Text(message, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(end = 5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -71,7 +70,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||
|
@ -81,7 +79,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
|
@ -109,7 +108,7 @@ fun ConfigScreen(
|
|||
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
|
||||
|
||||
LaunchedEffect(uiState.loading) {
|
||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (!uiState.loading && context.isRunningOnTv()) {
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
@ -331,7 +330,7 @@ fun ConfigScreen(
|
|||
FloatingActionButton(
|
||||
modifier =
|
||||
Modifier.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
|
@ -351,7 +350,7 @@ fun ConfigScreen(
|
|||
Icon(
|
||||
imageVector = Icons.Rounded.Save,
|
||||
contentDescription = stringResource(id = R.string.save_changes),
|
||||
tint = Color.DarkGray,
|
||||
tint = MaterialTheme.colorScheme.background,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -373,7 +372,7 @@ fun ConfigScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -428,7 +427,7 @@ fun ConfigScreen(
|
|||
Icon(
|
||||
Icons.Rounded.Refresh,
|
||||
stringResource(R.string.rotate_keys),
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -458,7 +457,7 @@ fun ConfigScreen(
|
|||
Icon(
|
||||
Icons.Rounded.ContentCopy,
|
||||
stringResource(R.string.copy_public_key),
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -687,7 +686,7 @@ fun ConfigScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -810,7 +809,7 @@ fun ConfigScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Spacer(modifier = Modifier.weight(.17f))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.wireguard.config.Config
|
|||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Packages
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Packages
|
||||
|
||||
data class ConfigUiState(
|
||||
val proxyPeers: List<PeerProxy> = arrayListOf(PeerProxy()),
|
||||
|
|
|
@ -23,8 +23,8 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
|
|||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.update
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.removeAt
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.update
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -142,7 +142,6 @@ constructor(
|
|||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig).join()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,7 +293,7 @@ constructor(
|
|||
|
||||
fun onSaveAllChanges(configType: ConfigType): Result<Unit> {
|
||||
return try {
|
||||
val wgQuick = buildConfig().toWgQuickString()
|
||||
val wgQuick = buildConfig().toWgQuickString(true)
|
||||
val amQuick =
|
||||
if (configType == ConfigType.AMNEZIA) {
|
||||
buildAmConfig().toAwgQuickString()
|
||||
|
|
|
@ -1,40 +1,26 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.overscroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.CopyAll
|
||||
|
@ -43,22 +29,15 @@ import androidx.compose.material.icons.rounded.Info
|
|||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Smartphone
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
@ -79,23 +58,15 @@ import androidx.compose.ui.input.pointer.pointerInput
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
||||
|
@ -103,18 +74,27 @@ import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.corn
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = hiltViewModel(),
|
||||
|
@ -124,14 +104,16 @@ fun MainScreen(
|
|||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var configType by remember { mutableStateOf(ConfigType.WIREGUARD) }
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
val isVisible = rememberSaveable { mutableStateOf(true) }
|
||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
// Nested scroll for control FAB
|
||||
val nestedScrollConnection =
|
||||
remember {
|
||||
object : NestedScrollConnection {
|
||||
|
@ -144,67 +126,43 @@ fun MainScreen(
|
|||
if (available.y > 1) {
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
Timber.d("VPN permission granted")
|
||||
} else {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
val tunnelFileImportResultLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||
val activitiesToResolveIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.ResolveInfoFlags.of(
|
||||
PackageManager.MATCH_DEFAULT_ONLY.toLong(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
context.packageManager.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY,
|
||||
)
|
||||
}
|
||||
if (
|
||||
activitiesToResolveIntent.all {
|
||||
val name = it.activityInfo.packageName
|
||||
name.startsWith(Constants.GOOGLE_TV_EXPLORER_STUB) ||
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
context.getString(R.string.error_no_file_explorer),
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
},
|
||||
) { data ->
|
||||
if (data == null) return@rememberLauncherForActivityResult
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
val tunnelFileImportResultLauncher = rememberFileImportLauncherForResult(onNoFileExplorer = {
|
||||
appViewModel.showSnackbarMessage(
|
||||
context.getString(R.string.error_no_file_explorer),
|
||||
)
|
||||
}, onData = { data ->
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data, configType, context).onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val scanLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ScanContract(),
|
||||
|
@ -219,6 +177,8 @@ fun MainScreen(
|
|||
},
|
||||
)
|
||||
|
||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||
|
||||
if (showDeleteTunnelAlertDialog) {
|
||||
InfoDialog(
|
||||
onDismiss = { showDeleteTunnelAlertDialog = false },
|
||||
|
@ -234,14 +194,16 @@ fun MainScreen(
|
|||
}
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
if (checked) {
|
||||
viewModel.onTunnelStart(tunnel, context)
|
||||
if (checked) {
|
||||
if (uiState.settings.isKernelEnabled) {
|
||||
context.startTunnelBackground(tunnel.id)
|
||||
} else {
|
||||
viewModel.onTunnelStop(
|
||||
context,
|
||||
)
|
||||
viewModel.onTunnelStart(tunnel)
|
||||
}
|
||||
} else {
|
||||
viewModel.onTunnelStop(
|
||||
tunnel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,161 +235,23 @@ fun MainScreen(
|
|||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
MultiFloatingActionButton(
|
||||
fabIcon =
|
||||
FabIcon(
|
||||
iconRes = R.drawable.add,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption =
|
||||
FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab =
|
||||
listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.add,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
showBottomSheet = true
|
||||
configType = ConfigType.valueOf(it.value)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
ScrollDismissMultiFab(R.drawable.add, focusRequester, isVisible = isVisible.value, onFabItemClicked = {
|
||||
showBottomSheet = true
|
||||
configType = ConfigType.valueOf(it.value)
|
||||
})
|
||||
},
|
||||
) {
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
// Sheet content
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.FileOpen,
|
||||
contentDescription = stringResource(id = R.string.open_file),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_tunnels_text),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
launchQrScanner()
|
||||
}
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.QrCode,
|
||||
contentDescription = stringResource(id = R.string.qr_scan),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_from_qr),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Create,
|
||||
contentDescription = stringResource(id = R.string.create_import),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.create_import),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TunnelImportSheet(
|
||||
showBottomSheet,
|
||||
onDismiss = { showBottomSheet = false },
|
||||
onFileClick = { tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES) },
|
||||
onQrClick = { launchQrScanner() },
|
||||
onManualImportClick = {
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}?configType=$configType",
|
||||
)
|
||||
},
|
||||
)
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
|
@ -447,53 +271,7 @@ fun MainScreen(
|
|||
exit = fadeOut(),
|
||||
enter = fadeIn(),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 100.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
val gettingStarted =
|
||||
buildAnnotatedString {
|
||||
append(stringResource(id = R.string.see_the))
|
||||
append(" ")
|
||||
pushStringAnnotation(
|
||||
tag = "gettingStarted",
|
||||
annotation = stringResource(id = R.string.getting_started_url),
|
||||
)
|
||||
withStyle(
|
||||
style = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
||||
) {
|
||||
append(stringResource(id = R.string.getting_started_guide))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.unsure_how))
|
||||
append(".")
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.no_tunnels),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
ClickableText(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
text = gettingStarted,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
) {
|
||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
appViewModel.openWebPage(annotation.item, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
GettingStartedLabel(onClick = { context.openWebUrl(it) })
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
@ -550,7 +328,7 @@ fun MainScreen(
|
|||
}
|
||||
},
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
itemFocusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
|
@ -565,11 +343,11 @@ fun MainScreen(
|
|||
uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id },
|
||||
) { tunnel ->
|
||||
val isActive = uiState.tunnels.any { it.id == tunnel.id && it.isActive }
|
||||
val leadingIconColor =
|
||||
(
|
||||
if (
|
||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||
uiState.vpnState.status == TunnelState.UP
|
||||
isActive
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
|
@ -631,10 +409,9 @@ fun MainScreen(
|
|||
selectedTunnel = tunnel
|
||||
},
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (!context.isRunningOnTv()) {
|
||||
if (
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
isActive
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
@ -649,7 +426,7 @@ fun MainScreen(
|
|||
rowButton = {
|
||||
if (
|
||||
tunnel.id == selectedTunnel?.id &&
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
!context.isRunningOnTv()
|
||||
) {
|
||||
Row {
|
||||
IconButton(
|
||||
|
@ -690,26 +467,19 @@ fun MainScreen(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
val checked by remember {
|
||||
derivedStateOf {
|
||||
(
|
||||
uiState.vpnState.status == TunnelState.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!checked) expanded.value = false
|
||||
|
||||
if (!isActive) expanded.value = false
|
||||
@Composable
|
||||
fun TunnelSwitch() = Switch(
|
||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||
checked = checked,
|
||||
checked = isActive,
|
||||
onCheckedChange = { checked ->
|
||||
if (!checked) expanded.value = false
|
||||
val intent = if (uiState.settings.isKernelEnabled) null else GoBackend.VpnService.prepare(context)
|
||||
if (intent != null) return@Switch vpnActivityResultState.launch(intent)
|
||||
onTunnelToggle(checked, tunnel)
|
||||
},
|
||||
)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
|||
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
|
||||
|
||||
data class MainUiState(
|
||||
val settings: Settings = Settings(),
|
||||
|
|
|
@ -7,17 +7,16 @@ import android.provider.OpenableColumns
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import com.zaneschepke.wireguardautotunnel.util.toWgQuickString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
@ -36,14 +35,14 @@ class MainViewModel
|
|||
constructor(
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
val vpnService: VpnService,
|
||||
val tunnelService: TunnelService,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
val uiState =
|
||||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
tunnelService.vpnState,
|
||||
) { settings, tunnels, vpnState ->
|
||||
MainUiState(settings, tunnels, vpnState, false)
|
||||
}
|
||||
|
@ -66,7 +65,6 @@ constructor(
|
|||
resetTunnelSetting(settings)
|
||||
}
|
||||
appDataRepository.tunnels.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,18 +77,14 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig, context: Context) = viewModelScope.launch {
|
||||
Timber.d("On start called!")
|
||||
serviceManager.startVpnService(
|
||||
context,
|
||||
tunnelConfig.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
Timber.i("Starting tunnel ${tunnelConfig.name}")
|
||||
tunnelService.startTunnel(tunnelConfig)
|
||||
}
|
||||
|
||||
fun onTunnelStop(context: Context) = viewModelScope.launch {
|
||||
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
|
||||
Timber.i("Stopping active tunnel")
|
||||
serviceManager.stopVpnService(context, isManualStop = true)
|
||||
tunnelService.stopTunnel(tunnel)
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String, configType: ConfigType) {
|
||||
|
@ -171,7 +165,7 @@ constructor(
|
|||
var tunnelName = name
|
||||
var num = 1
|
||||
while (tunnels.any { it.name == tunnelName }) {
|
||||
tunnelName = name + "($num)"
|
||||
tunnelName = "$name($num)"
|
||||
num++
|
||||
}
|
||||
tunnelName
|
||||
|
@ -190,7 +184,7 @@ constructor(
|
|||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(it).toWgQuickString()
|
||||
Config.parse(it).toWgQuickString(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +257,7 @@ constructor(
|
|||
}
|
||||
|
||||
ConfigType.WIREGUARD -> {
|
||||
Config.parse(zip).toWgQuickString()
|
||||
Config.parse(zip).toWgQuickString(true)
|
||||
}
|
||||
}
|
||||
addTunnel(
|
||||
|
@ -301,23 +295,19 @@ constructor(
|
|||
}
|
||||
|
||||
private fun addTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||
saveTunnel(tunnelConfig)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
||||
appDataRepository.settings.save(
|
||||
uiState.value.settings.copy(isAutoTunnelPaused = true),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
||||
appDataRepository.settings.save(
|
||||
uiState.value.settings.copy(isAutoTunnelPaused = false),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
private fun saveTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun GettingStartedLabel(onClick: (url: String) -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 100.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
val gettingStarted =
|
||||
buildAnnotatedString {
|
||||
append(stringResource(id = R.string.see_the))
|
||||
append(" ")
|
||||
pushStringAnnotation(
|
||||
tag = "gettingStarted",
|
||||
annotation = stringResource(id = R.string.getting_started_url),
|
||||
)
|
||||
withStyle(
|
||||
style = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
||||
) {
|
||||
append(stringResource(id = R.string.getting_started_guide))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.unsure_how))
|
||||
append(".")
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.no_tunnels),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
ClickableText(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 10.dp, horizontal = 24.dp),
|
||||
text = gettingStarted,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
) {
|
||||
gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
onClick(annotation.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
|
||||
@Composable
|
||||
fun ScrollDismissMultiFab(
|
||||
@DrawableRes res: Int,
|
||||
focusRequester: FocusRequester,
|
||||
isVisible: Boolean,
|
||||
onFabItemClicked: (fabItem: MultiFabItem) -> Unit,
|
||||
) {
|
||||
// Nested scroll for control FAB
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
val fobColor = MaterialTheme.colorScheme.secondary
|
||||
val fobIconColor = MaterialTheme.colorScheme.background
|
||||
MultiFloatingActionButton(
|
||||
fabIcon =
|
||||
FabIcon(
|
||||
iconRes = res,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption =
|
||||
FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab =
|
||||
listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp),
|
||||
icon = res,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = res,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
onFabItemClicked(it)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TunnelImportSheet(show: Boolean, onDismiss: () -> Unit, onFileClick: () -> Unit, onQrClick: () -> Unit, onManualImportClick: () -> Unit) {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
onDismiss()
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onDismiss()
|
||||
onFileClick()
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.FileOpen,
|
||||
contentDescription = stringResource(id = R.string.open_file),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_tunnels_text),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
if (!context.isRunningOnTv()) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onDismiss()
|
||||
onQrClick()
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.QrCode,
|
||||
contentDescription = stringResource(id = R.string.qr_scan),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.add_from_qr),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onDismiss()
|
||||
onManualImportClick()
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Create,
|
||||
contentDescription = stringResource(id = R.string.create_import),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.create_import),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchVpnSettings
|
||||
|
||||
@Composable
|
||||
fun VpnDeniedDialog(show: Boolean, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
val alwaysOnDescription = buildAnnotatedString {
|
||||
append(stringResource(R.string.always_on_message))
|
||||
append(" ")
|
||||
pushStringAnnotation(tag = "vpnSettings", annotation = "")
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.vpn_settings))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.always_on_message2))
|
||||
append(".")
|
||||
}
|
||||
InfoDialog(
|
||||
onDismiss = { onDismiss() },
|
||||
onAttest = { onDismiss() },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
ClickableText(
|
||||
text = alwaysOnDescription,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
|
||||
) {
|
||||
alwaysOnDescription.getStringAnnotations(tag = "vpnSettings", it, it).firstOrNull()?.let {
|
||||
context.launchVpnSettings()
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -16,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
@ -32,7 +27,6 @@ import androidx.compose.material3.OutlinedTextField
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -44,32 +38,27 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
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.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.iamageo.multifablibrary.FabIcon
|
||||
import com.iamageo.multifablibrary.FabOption
|
||||
import com.iamageo.multifablibrary.MultiFabItem
|
||||
import com.iamageo.multifablibrary.MultiFloatingActionButton
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissMultiFab
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -97,7 +86,7 @@ fun OptionsScreen(
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
optionsViewModel.init(tunnelId)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
@ -117,82 +106,12 @@ fun OptionsScreen(
|
|||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
val tvFobColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
val fobColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) tvFobColor else secondaryColor
|
||||
val fobIconColor =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) Color.White else MaterialTheme.colorScheme.background
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(initialOffsetY = { it * 2 }),
|
||||
exit = slideOutVertically(targetOffsetY = { it * 2 }),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.focusGroup(),
|
||||
) {
|
||||
MultiFloatingActionButton(
|
||||
fabIcon =
|
||||
FabIcon(
|
||||
iconRes = R.drawable.edit,
|
||||
iconResAfterRotate = R.drawable.close,
|
||||
iconRotate = 180f,
|
||||
),
|
||||
fabOption =
|
||||
FabOption(
|
||||
iconTint = fobIconColor,
|
||||
backgroundTint = fobColor,
|
||||
),
|
||||
itemsMultiFab =
|
||||
listOf(
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.amnezia),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.size(40.dp),
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.AMNEZIA.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
MultiFabItem(
|
||||
label = {
|
||||
Text(
|
||||
stringResource(id = R.string.wireguard),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
},
|
||||
icon = R.drawable.edit,
|
||||
value = ConfigType.WIREGUARD.name,
|
||||
miniFabOption =
|
||||
FabOption(
|
||||
backgroundTint = fobColor,
|
||||
fobIconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onFabItemClicked = {
|
||||
val configType = ConfigType.valueOf(it.value)
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/$tunnelId?configType=${configType.name}",
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
ScrollDismissMultiFab(R.drawable.edit, focusRequester, isVisible = true, onFabItemClicked = {
|
||||
val configType = ConfigType.valueOf(it.value)
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/$tunnelId?configType=${configType.name}",
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
|
@ -216,7 +135,7 @@ fun OptionsScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -257,7 +176,7 @@ fun OptionsScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -297,13 +216,13 @@ fun OptionsScreen(
|
|||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
if (context.isRunningOnTv()) focusRequester.requestFocus()
|
||||
optionsViewModel.onDeleteRunSSID(ssid)
|
||||
},
|
||||
text = ssid,
|
||||
|
@ -315,7 +234,7 @@ fun OptionsScreen(
|
|||
Text(
|
||||
stringResource(R.string.no_wifi_names_configured),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
|||
import androidx.compose.ui.util.fastFirstOrNull
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
|
@ -104,7 +103,6 @@ constructor(
|
|||
false -> uiState.value.tunnel
|
||||
},
|
||||
)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import xyz.teamgravity.pin_lock_compose.PinLock
|
||||
|
||||
@Composable
|
||||
|
@ -32,7 +32,7 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
onPinCorrect = {
|
||||
// pin is correct, navigate or hide pin lock
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
navController.navigate(Screen.Main.route)
|
||||
} else {
|
||||
val isPopped = navController.popBackStack()
|
||||
|
@ -52,6 +52,7 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
|||
appViewModel.showSnackbarMessage(
|
||||
StringValue.StringResource(R.string.pin_created).asString(context),
|
||||
)
|
||||
appViewModel.onPinLockEnabled()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_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.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
@ -34,8 +33,6 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.rounded.LocationOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -55,16 +52,13 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
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.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
|
@ -81,9 +75,17 @@ import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
|||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.getMessage
|
||||
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.BackgroundLocationDisclosure
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LocationServicesDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getMessage
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
|
@ -109,9 +111,11 @@ fun SettingsScreen(
|
|||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showVpnPermissionDialog by remember { mutableStateOf(false) }
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var showLocationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
@ -120,6 +124,13 @@ fun SettingsScreen(
|
|||
viewModel.checkKernelSupport()
|
||||
}
|
||||
|
||||
val notificationPermissionState =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
|
@ -131,6 +142,19 @@ fun SettingsScreen(
|
|||
viewModel.setBatteryOptimizeDisableShown()
|
||||
}
|
||||
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
viewModel.onToggleAutoTunnel(context)
|
||||
} else {
|
||||
showVpnPermissionDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun exportAllConfigs() {
|
||||
try {
|
||||
val wgFiles =
|
||||
|
@ -183,13 +207,20 @@ fun SettingsScreen(
|
|||
}
|
||||
|
||||
fun handleAutoTunnelToggle() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
viewModel.onToggleAutoTunnel(context)
|
||||
}
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
if (!uiState.isBatteryOptimizeDisableShown || !isBatteryOptimizationsDisabled()) return requestBatteryOptimizationsDisabled()
|
||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
context.getString(R.string.notification_permission_required),
|
||||
)
|
||||
return notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
val intent = if (!uiState.settings.isKernelEnabled) {
|
||||
com.wireguard.android.backend.GoBackend.VpnService.prepare(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (intent != null) return vpnActivityResultState.launch(intent)
|
||||
viewModel.onToggleAutoTunnel(context)
|
||||
}
|
||||
|
||||
fun saveTrustedSSID() {
|
||||
|
@ -202,12 +233,6 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun openSettings() {
|
||||
val intentSettings = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data = Uri.fromParts("package", context.packageName, null)
|
||||
context.startActivity(intentSettings)
|
||||
}
|
||||
|
||||
fun checkFineLocationGranted() {
|
||||
isBackgroundLocationGranted =
|
||||
if (!fineLocationState.status.isGranted) {
|
||||
|
@ -218,9 +243,13 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun onRootDenied() = appViewModel.showSnackbarMessage(context.getString(R.string.error_root_denied))
|
||||
|
||||
fun onRootAccepted() = appViewModel.showSnackbarMessage(context.getString(R.string.root_accepted))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
context.isRunningOnTv() &&
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
) {
|
||||
checkFineLocationGranted()
|
||||
|
@ -241,87 +270,30 @@ fun SettingsScreen(
|
|||
checkFineLocationGranted()
|
||||
}
|
||||
|
||||
AnimatedVisibility(showLocationServicesAlertDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLocationServicesAlertDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showLocationServicesAlertDialog = false
|
||||
handleAutoTunnelToggle()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLocationServicesAlertDialog = false }) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||
)
|
||||
}
|
||||
BackgroundLocationDisclosure(
|
||||
!uiState.isLocationDisclosureShown,
|
||||
onDismiss = { viewModel.setLocationDisclosureShown() },
|
||||
onAttest = {
|
||||
context.launchAppSettings()
|
||||
viewModel.setLocationDisclosureShown()
|
||||
},
|
||||
scrollState,
|
||||
focusRequester,
|
||||
)
|
||||
|
||||
if (!uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
TextButton(onClick = { viewModel.setLocationDisclosureShown() }) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
openSettings()
|
||||
viewModel.setLocationDisclosureShown()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BackgroundLocationDialog(
|
||||
showLocationDialog,
|
||||
onDismiss = { showLocationDialog = false },
|
||||
onAttest = { showLocationDialog = false },
|
||||
)
|
||||
|
||||
LocationServicesDialog(
|
||||
showLocationServicesAlertDialog,
|
||||
onDismiss = { showVpnPermissionDialog = false },
|
||||
onAttest = { handleAutoTunnelToggle() },
|
||||
)
|
||||
|
||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||
|
||||
if (showAuthPrompt) {
|
||||
AuthorizationPrompt(
|
||||
|
@ -344,21 +316,7 @@ fun SettingsScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (uiState.tunnels.isEmpty() && uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (uiState.isLocationDisclosureShown && uiState.tunnels.isNotEmpty()) {
|
||||
if (uiState.isLocationDisclosureShown) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
|
@ -380,7 +338,7 @@ fun SettingsScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -432,13 +390,13 @@ fun SettingsScreen(
|
|||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
if (context.isRunningOnTv()) focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
},
|
||||
text = ssid,
|
||||
|
@ -454,7 +412,8 @@ fun SettingsScreen(
|
|||
Text(
|
||||
stringResource(R.string.none),
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -560,25 +519,14 @@ fun SettingsScreen(
|
|||
TextButton(
|
||||
enabled = !uiState.settings.isAlwaysOnVpnEnabled,
|
||||
onClick = {
|
||||
if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
|
||||
if (
|
||||
uiState.settings.isTunnelOnWifiEnabled &&
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
) {
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
context.getString(
|
||||
R.string.background_location_required,
|
||||
),
|
||||
)
|
||||
|
||||
fineLocationState.status.isGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
context.getString(
|
||||
R.string.precise_location_required,
|
||||
),
|
||||
)
|
||||
|
||||
isBackgroundLocationGranted -> showLocationDialog = true
|
||||
fineLocationState.status.isGranted -> showLocationDialog = true
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
showLocationServicesAlertDialog = true
|
||||
|
||||
|
@ -648,12 +596,26 @@ fun SettingsScreen(
|
|||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
viewModel.onToggleKernelMode().onFailure {
|
||||
appViewModel.showSnackbarMessage(it.getMessage(context))
|
||||
}
|
||||
viewModel.onToggleKernelMode({ onRootAccepted() }, { onRootDenied() })
|
||||
}
|
||||
},
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.requestRoot({ onRootAccepted() }, { onRootDenied() })
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.request_root))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -677,7 +639,7 @@ fun SettingsScreen(
|
|||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (!context.isRunningOnTv()) {
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||
|
@ -709,14 +671,15 @@ fun SettingsScreen(
|
|||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if (uiState.isPinLockEnabled) {
|
||||
viewModel.onPinLockDisabled()
|
||||
appViewModel.onPinLockDisabled()
|
||||
} else {
|
||||
viewModel.onPinLockEnabled()
|
||||
// TODO may want to show a dialog before proceeding in the future
|
||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
navController.navigate(Screen.Lock.route)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (!context.isRunningOnTv()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
|
@ -727,7 +690,10 @@ fun SettingsScreen(
|
|||
) {
|
||||
TextButton(
|
||||
enabled = !didExportFiles,
|
||||
onClick = { showAuthPrompt = true },
|
||||
onClick = {
|
||||
if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
|
||||
showAuthPrompt = true
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.export_configs))
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ 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.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.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
|
@ -27,7 +26,6 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
@ -41,7 +39,7 @@ constructor(
|
|||
private val rootShell: Provider<RootShell>,
|
||||
private val fileUtils: FileUtils,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
vpnService: VpnService,
|
||||
tunnelService: TunnelService,
|
||||
) : ViewModel() {
|
||||
private val _kernelSupport = MutableStateFlow(false)
|
||||
val kernelSupport = _kernelSupport.asStateFlow()
|
||||
|
@ -50,7 +48,7 @@ constructor(
|
|||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
tunnelService.vpnState,
|
||||
appDataRepository.appState.generalStateFlow,
|
||||
) { settings, tunnels, tunnelState, generalState ->
|
||||
SettingsUiState(
|
||||
|
@ -124,7 +122,6 @@ constructor(
|
|||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
|
||||
|
@ -161,10 +158,10 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun saveKernelMode(on: Boolean) {
|
||||
private fun saveKernelMode(enabled: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = on,
|
||||
isKernelEnabled = enabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -192,27 +189,25 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun onToggleKernelMode(): Result<Unit> {
|
||||
return withContext(ioDispatcher) {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.get().start()
|
||||
Timber.i("Root shell accepted!")
|
||||
fun onToggleKernelMode(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch {
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
requestRoot(
|
||||
{
|
||||
onSuccess()
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isKernelEnabled = true,
|
||||
isAmneziaEnabled = false,
|
||||
),
|
||||
)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return@withContext Result.failure(WgTunnelExceptions.RootDenied())
|
||||
}
|
||||
} else {
|
||||
saveKernelMode(on = false)
|
||||
}
|
||||
Result.success(Unit)
|
||||
},
|
||||
{
|
||||
onFailure()
|
||||
saveKernelMode(enabled = false)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
saveKernelMode(enabled = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,16 +229,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onPinLockDisabled() = viewModelScope.launch {
|
||||
PinManager.clearPin()
|
||||
appDataRepository.appState.setPinLockEnabled(false)
|
||||
}
|
||||
|
||||
fun onPinLockEnabled() = viewModelScope.launch {
|
||||
PinManager.initialize(WireGuardAutoTunnel.instance)
|
||||
appDataRepository.appState.setPinLockEnabled(true)
|
||||
}
|
||||
|
||||
fun onToggleRestartAtBoot() = viewModelScope.launch {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
|
@ -251,4 +236,16 @@ constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestRoot(onSuccess: () -> Unit, onFailure: () -> Unit) = viewModelScope.launch(ioDispatcher) {
|
||||
kotlin.runCatching {
|
||||
rootShell.get().start()
|
||||
Timber.i("Root shell accepted!")
|
||||
onSuccess()
|
||||
}.onFailure {
|
||||
onFailure()
|
||||
}.onSuccess {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
|
||||
|
||||
@Composable
|
||||
fun BackgroundLocationDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
val alwaysOnDescription = buildAnnotatedString {
|
||||
append(stringResource(R.string.background_location_message))
|
||||
append(" ")
|
||||
pushStringAnnotation(tag = "appSettings", annotation = "")
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
|
||||
append(stringResource(id = R.string.app_settings))
|
||||
}
|
||||
pop()
|
||||
append(" ")
|
||||
append(stringResource(R.string.background_location_message2))
|
||||
append(".")
|
||||
}
|
||||
InfoDialog(
|
||||
onDismiss = { onDismiss() },
|
||||
onAttest = { onDismiss() },
|
||||
title = { Text(text = stringResource(R.string.vpn_denied_dialog_title)) },
|
||||
body = {
|
||||
ClickableText(
|
||||
text = alwaysOnDescription,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
|
||||
) {
|
||||
alwaysOnDescription.getStringAnnotations(tag = "appSettings", it, it).firstOrNull()?.let {
|
||||
context.launchAppSettings()
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmText = { Text(text = stringResource(R.string.okay)) },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.LocationOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
|
||||
@Composable
|
||||
fun BackgroundLocationDisclosure(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onAttest: () -> Unit,
|
||||
scrollState: ScrollState,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
if (show) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_message),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(30.dp),
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(stringResource(id = R.string.no_thanks))
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
onAttest()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(id = R.string.turn_on))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LocationServicesDialog(show: Boolean, onDismiss: () -> Unit, onAttest: () -> Unit) {
|
||||
if (show) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismiss() },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
onAttest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(text = stringResource(R.string.location_services_not_detected)) },
|
||||
text = { Text(text = stringResource(R.string.location_services_missing_message)) },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -48,17 +48,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.launchSupportEmail
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(
|
||||
viewModel: SupportViewModel = hiltViewModel(),
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), navController: NavController, focusRequester: FocusRequester) {
|
||||
val context = LocalContext.current
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
|
@ -80,7 +76,7 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (context.isRunningOnTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
|
@ -110,9 +106,8 @@ fun SupportScreen(
|
|||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.openWebUrl(
|
||||
context.resources.getString(R.string.docs_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
|
@ -153,9 +148,8 @@ fun SupportScreen(
|
|||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.openWebUrl(
|
||||
context.resources.getString(R.string.telegram_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
|
@ -173,7 +167,7 @@ fun SupportScreen(
|
|||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.discord_description),
|
||||
stringResource(id = R.string.chat_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
|
@ -190,9 +184,8 @@ fun SupportScreen(
|
|||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
appViewModel.openWebPage(
|
||||
context.openWebUrl(
|
||||
context.resources.getString(R.string.github_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
|
@ -226,7 +219,7 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.launchEmail(context) },
|
||||
onClick = { context.launchSupportEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
|
@ -249,7 +242,7 @@ fun SupportScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (!context.isRunningOnTv()) {
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
|
@ -288,9 +281,8 @@ fun SupportScreen(
|
|||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
appViewModel.openWebPage(
|
||||
context.openWebUrl(
|
||||
context.resources.getString(R.string.privacy_policy_url),
|
||||
context,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
|
|||
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.chunked
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
|
|||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
|
@ -44,23 +45,21 @@ private val LightColorScheme =
|
|||
@Composable
|
||||
fun WireguardAutoTunnelTheme(
|
||||
// force dark theme
|
||||
darkTheme: Boolean = true,
|
||||
// darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
// turning off dynamic color for now
|
||||
dynamicColor: Boolean = false,
|
||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme =
|
||||
when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
val context = LocalContext.current
|
||||
val colorScheme = when {
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
|
||||
if (useDarkTheme) {
|
||||
dynamicDarkColorScheme(context)
|
||||
} else {
|
||||
dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
useDarkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
|
@ -69,7 +68,7 @@ fun WireguardAutoTunnelTheme(
|
|||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||
!darkTheme
|
||||
!useDarkTheme
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ object Constants {
|
|||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||
const val WATCHER_COLLECTION_DELAY = 1_000L
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
|
@ -19,6 +17,7 @@ object Constants {
|
|||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
||||
const val VPN_SETTINGS_PACKAGE = "android.net.vpn.SETTINGS"
|
||||
const val EMAIL_MIME_TYPE = "plain/text"
|
||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
|
@ -31,8 +30,6 @@ object Constants {
|
|||
const val PING_INTERVAL = 60_000L
|
||||
const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour
|
||||
|
||||
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
||||
|
||||
const val UNREADABLE_SSID = "<unknown ssid>"
|
||||
|
||||
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.service.quicksettings.TileService
|
||||
import android.widget.Toast
|
||||
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
|
||||
|
||||
fun Context.openWebUrl(url: String): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}.onFailure {
|
||||
showToast(R.string.no_browser_detected)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.showToast(resId: Int) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
this.getString(resId),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
fun Context.launchSupportEmail(): Result<Unit> {
|
||||
return runCatching {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, getString(R.string.email_subject))
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(
|
||||
Intent.createChooser(intent, getString(R.string.email_chooser)).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
)
|
||||
}.onFailure {
|
||||
showToast(R.string.no_email_detected)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isRunningOnTv(): Boolean {
|
||||
return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun Context.launchVpnSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent = Intent(Constants.VPN_SETTINGS_PACKAGE).apply {
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchLocationServicesSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchSettings(): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val intent = Intent(Settings.ACTION_SETTINGS).apply {
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.launchAppSettings() {
|
||||
kotlin.runCatching {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
this,
|
||||
ComponentName(this, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.requestAutoTunnelTileServiceUpdate() {
|
||||
TileService.requestListeningState(
|
||||
this,
|
||||
ComponentName(this, AutoTunnelControlTile::class.java),
|
||||
)
|
||||
}
|
|
@ -1,11 +1,5 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -18,71 +12,11 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.selects.whileSelect
|
||||
import org.amnezia.awg.config.Config
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
}
|
||||
|
||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||
|
||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||
|
||||
typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
|
||||
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
|
||||
}
|
||||
|
||||
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
when {
|
||||
it == null -> HandshakeStatus.NOT_STARTED
|
||||
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
||||
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
||||
else -> {
|
||||
HandshakeStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Config.toWgQuickString(): String {
|
||||
val amQuick = toAwgQuickString()
|
||||
val lines = amQuick.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
Constants.amneziaProperties.forEach {
|
||||
if (next.startsWith(it, ignoreCase = true)) {
|
||||
linesIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.joinToString(System.lineSeparator())
|
||||
}
|
||||
|
||||
fun Throwable.getMessage(context: Context): String {
|
||||
return when (this) {
|
||||
is WgTunnelExceptions -> this.getMessage(context)
|
||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunks based on a time or size threshold.
|
||||
*
|
|
@ -0,0 +1,30 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.WgTunnelExceptions
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
}
|
||||
|
||||
fun <T> List<T>.update(index: Int, item: T): List<T> = toMutableList().apply { this[index] = item }
|
||||
|
||||
fun <T> List<T>.removeAt(index: Int): List<T> = toMutableList().apply { this.removeAt(index) }
|
||||
|
||||
typealias TunnelConfigs = List<TunnelConfig>
|
||||
|
||||
typealias Packages = List<PackageInfo>
|
||||
|
||||
fun Throwable.getMessage(context: Context): String {
|
||||
return when (this) {
|
||||
is WgTunnelExceptions -> this.getMessage(context)
|
||||
else -> this.message ?: StringValue.StringResource(R.string.unknown_error).asString(context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import org.amnezia.awg.config.Config
|
||||
|
||||
fun TunnelStatistics.mapPeerStats(): Map<org.amnezia.awg.crypto.Key, TunnelStatistics.PeerStats?> {
|
||||
return this.getPeers().associateWith { key -> (this.peerStats(key)) }
|
||||
}
|
||||
|
||||
fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? {
|
||||
return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis)
|
||||
}
|
||||
|
||||
fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
// TODO add never connected status after duration
|
||||
return this.latestHandshakeSeconds().let {
|
||||
when {
|
||||
it == null -> HandshakeStatus.NOT_STARTED
|
||||
it <= HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.HEALTHY
|
||||
it > HandshakeStatus.STALE_TIME_LIMIT_SEC -> HandshakeStatus.STALE
|
||||
else -> {
|
||||
HandshakeStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Config.toWgQuickString(): String {
|
||||
val amQuick = toAwgQuickString()
|
||||
val lines = amQuick.lines().toMutableList()
|
||||
val linesIterator = lines.iterator()
|
||||
while (linesIterator.hasNext()) {
|
||||
val next = linesIterator.next()
|
||||
Constants.amneziaProperties.forEach {
|
||||
if (next.startsWith(it, ignoreCase = true)) {
|
||||
linesIterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.joinToString(System.lineSeparator())
|
||||
}
|
|
@ -6,8 +6,7 @@
|
|||
<string name="add_trusted_ssid">Přidat název důvěryhodné Wi-Fi</string>
|
||||
<string name="enable_auto_tunnel">Spustit automatické tunelování</string>
|
||||
<string name="tunnel_mobile_data">Tunelování na mobilních datech</string>
|
||||
<string name="one_tunnel_required">Alespoň jeden tunel je vyžadován pro použití této funkce</string>
|
||||
<string name="privacy_policy">Otevřít zásady soukromí</string>
|
||||
<string name="privacy_policy">Otevřít zásady soukromí</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="thank_you">Děkujeme za používání WG Tunnel!</string>
|
||||
<string name="add_tunnels_text">Přidat ze souboru nebo zipu</string>
|
||||
|
@ -116,7 +115,6 @@
|
|||
<string name="copy_public_key">Kopírovat veřejný klíč</string>
|
||||
<string name="base64_key">base64 klíč</string>
|
||||
<string name="docs_description">Přečíst si dokumentaci</string>
|
||||
<string name="discord_description">Přidat se ke komunitě</string>
|
||||
<string name="email_description">Poslat mi email</string>
|
||||
<string name="support_help_text">Pokud máte potíže, nápady pro zlepšení, nebo se chcete jen zapojit, následující prostředky jsou k dispozici:</string>
|
||||
<string name="location_services_missing_message">Aplikace nenašla žádné služby polohy zapnuté na Vašem zařízení. Dle Vašeho zařízení, tohle může způsobit, že funkce nedůvěryhodné Wi-Fi nedokáže přečíst jméno připojené Wi-Fi. Chcete i přesto pokračovat?</string>
|
||||
|
@ -151,4 +149,4 @@
|
|||
<string name="auto">(automaticky)</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="backend">Backend</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
<string name="tunnels">Tunnel</string>
|
||||
<string name="enable_auto_tunnel">Auto-Tunneln starten</string>
|
||||
<string name="tunnel_mobile_data">Tunnel für mobile Daten</string>
|
||||
<string name="one_tunnel_required">Mindestens ein Tunnel wird für diese Funktion benötigt</string>
|
||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="privacy_policy">Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="disable_auto_tunnel">Auto-Tunneln stoppen</string>
|
||||
<string name="okay">Ok</string>
|
||||
<string name="tunnel_on_ethernet">Tunnel für Ethernet</string>
|
||||
|
@ -75,7 +74,6 @@
|
|||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="background_location_required">Hintergrund Standortdienste erforderlich</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="discord_description">Tritt der Community bei</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
|
@ -168,4 +166,4 @@
|
|||
<string name="export_configs_failed">Konfigurationsexport fehlgeschlagen</string>
|
||||
<string name="error_file_format">Ungültige Konfiguration Tunnel-Format</string>
|
||||
<string name="response_packet_magic_header">Antwortpaket magic header</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="email_chooser">Enviar un email…</string>
|
||||
<string name="go">ir</string>
|
||||
<string name="discord_description">Únete a la comunidad</string>
|
||||
<string name="kernel">Kernel</string>
|
||||
<string name="restart">Reiniciar túnel</string>
|
||||
<string name="vpn_connection_failed">Error de conexión</string>
|
||||
|
@ -60,8 +59,7 @@
|
|||
<string name="enable_auto_tunnel">Iniciar túnel-automático</string>
|
||||
<string name="disable_auto_tunnel">Parar túnel-automático</string>
|
||||
<string name="tunnel_mobile_data">Activar túnel en datos móviles</string>
|
||||
<string name="one_tunnel_required">Esta característica necesita ser usada en almenos por un túnel</string>
|
||||
<string name="privacy_policy">Ver Política de Privacidad</string>
|
||||
<string name="privacy_policy">Ver Política de Privacidad</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_on_ethernet">Túnel en ethernet</string>
|
||||
<string name="prominent_background_location_title">Divulgación de la ubicación en segundo plano</string>
|
||||
|
@ -157,4 +155,4 @@
|
|||
<string name="junk_packet_count">Recuento de paquetes basura</string>
|
||||
<string name="backend">Backend</string>
|
||||
<string name="junk_packet_minimum_size">Tamaño mínimo del paquete basura</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -56,8 +56,7 @@
|
|||
<string name="privacy_policy">Ver a Política de Privacidade</string>
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
|
||||
<string name="one_tunnel_required">Pelo menos um túnel é necessário para usar este recurso</string>
|
||||
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
||||
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
|
||||
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Envie o SSID</string>
|
||||
<string name="open_file">Abrir Arquivo</string>
|
||||
|
@ -119,7 +118,6 @@
|
|||
<string name="delete_tunnel">Apagar túnel</string>
|
||||
<string name="email_chooser">Enviar um email…</string>
|
||||
<string name="use_kernel">Usar o módulo do kernel</string>
|
||||
<string name="discord_description">Juntar-se à comunidade</string>
|
||||
<string name="docs_description">Ler a documentação</string>
|
||||
<string name="support_help_text">Se você enfrentar problemas, tiver ideias para melhorias ou apenas quiser participar, os seguintes recursos estão disponíveis:</string>
|
||||
<string name="error_root_denied">Shell Root negado</string>
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
<string name="tunnel_on_ethernet">Туннелировать через Ethernet</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="docs_description">Посмотреть документацию</string>
|
||||
<string name="discord_description">Присоединиться к сообществу</string>
|
||||
<string name="email_chooser">Отправить письмо…</string>
|
||||
<string name="add_trusted_ssid">Добавить доверенное имя сети Wi-Fi</string>
|
||||
<string name="included">включено</string>
|
||||
|
@ -88,8 +87,7 @@
|
|||
<string name="tunnels">Туннели</string>
|
||||
<string name="enable_auto_tunnel">Запустить авто-туннель</string>
|
||||
<string name="disable_auto_tunnel">Остановить авто-туннель</string>
|
||||
<string name="one_tunnel_required">Для использования этой функции нужно настроить хотя бы один туннель</string>
|
||||
<string name="okay">Хорошо</string>
|
||||
<string name="okay">Хорошо</string>
|
||||
<string name="prominent_background_location_title">Фоновая передача местоположения</string>
|
||||
<string name="thank_you">Благодарим Вас за использование WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Отправить SSID</string>
|
||||
|
@ -168,4 +166,4 @@
|
|||
<string name="getting_started_guide">руководство по началу работы</string>
|
||||
<string name="unsure_how">, если не уверены, что делать дальше</string>
|
||||
<string name="underload_packet_magic_header">Заголовок пакета под нагрузкой</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -21,8 +21,7 @@
|
|||
<string name="enable_auto_tunnel">Otomatik tünellemeyi başlat</string>
|
||||
<string name="disable_auto_tunnel">Otomatik tünellemeyi durdur</string>
|
||||
<string name="tunnel_mobile_data">Mobil veride tünel</string>
|
||||
<string name="one_tunnel_required">Bu özelliği kullanmak için en az bir tünel gerekli</string>
|
||||
<string name="privacy_policy">Gizlilik Politikasını Görüntüle</string>
|
||||
<string name="privacy_policy">Gizlilik Politikasını Görüntüle</string>
|
||||
<string name="okay">Tamam</string>
|
||||
<string name="tunnel_on_ethernet">Ethernet\'te tünel</string>
|
||||
<string name="prominent_background_location_message">Bu özellik, uygulama kapalıyken bile Wi-Fi SSID izlemesini etkinleştirmek için arka plan konum iznine ihtiyaç duyar. Daha fazla ayrıntı için lütfen Destek ekranında bağlantısı verilen Gizlilik Politikasına bakın.</string>
|
||||
|
@ -104,7 +103,6 @@
|
|||
<string name="email_chooser">E-posta gönder…</string>
|
||||
<string name="go">git</string>
|
||||
<string name="docs_description">Belgeleri oku</string>
|
||||
<string name="discord_description">Topluluğa katıl</string>
|
||||
<string name="email_description">Bana e-posta gönder</string>
|
||||
<string name="support_help_text">Sorun yaşıyorsanız, iyileştirme fikirleriniz varsa veya sadece iletişime geçmek istiyorsanız, aşağıdaki kaynaklar mevcuttur:</string>
|
||||
<string name="use_kernel">Kernel modülünü kullan</string>
|
||||
|
|
|
@ -56,8 +56,7 @@
|
|||
<string name="tunnel_start_title">VPN підключено</string>
|
||||
<string name="notification_permission_required">Потрібен дозвіл на відображення сповіщень.</string>
|
||||
<string name="tunnel_mobile_data">Тунелювати мобільні дані</string>
|
||||
<string name="one_tunnel_required">Для використання даної функції потрібно налаштувати мінімум один тунель</string>
|
||||
<string name="privacy_policy">Переглянути політику конфіденційності</string>
|
||||
<string name="privacy_policy">Переглянути політику конфіденційності</string>
|
||||
<string name="thank_you">Спасибі за використання WG Tunnel!</string>
|
||||
<string name="prominent_background_location_message">Дана функція потребує фоновий доступ до служби місцезнаходження для моніторингу назви мереж Wi-Fi навіть коли додаток закрито. Для отримання додаткової інформації прочитайте політику приватності на екрані Підтримки.</string>
|
||||
<string name="trusted_ssid_empty_description">Введіть SSID</string>
|
||||
|
@ -112,7 +111,6 @@
|
|||
<string name="go">вперед</string>
|
||||
<string name="docs_description">Переглянути документацію</string>
|
||||
<string name="email_description">Відправити email автору</string>
|
||||
<string name="discord_description">Приєднатися до спільноти</string>
|
||||
<string name="support_help_text">Якщо у вас виникли проблеми, є ідеї щодо покращення, чи бажання долучитися, скористайтесь наступними ресурсами:</string>
|
||||
<string name="use_kernel">Використовувати модуль режиму ядра</string>
|
||||
<string name="error_ssid_exists">SSID вже існує</string>
|
||||
|
|
|
@ -21,8 +21,7 @@
|
|||
<string name="enable_auto_tunnel">Start auto-tunneling</string>
|
||||
<string name="disable_auto_tunnel">Stop auto-tunneling</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="one_tunnel_required">At least one tunnel required to use this feature</string>
|
||||
<string name="privacy_policy">View Privacy Policy</string>
|
||||
<string name="privacy_policy">View Privacy Policy</string>
|
||||
<string name="okay">Okay</string>
|
||||
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
|
||||
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
||||
|
@ -104,8 +103,7 @@
|
|||
<string name="email_chooser">Send an email…</string>
|
||||
<string name="go">go</string>
|
||||
<string name="docs_description">Read the docs</string>
|
||||
<string name="discord_description">Join the community</string>
|
||||
<string name="email_description">Send me an email</string>
|
||||
<string name="email_description">Send me an email</string>
|
||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||
<string name="use_kernel">Use kernel module</string>
|
||||
<string name="error_ssid_exists">SSID already exists</string>
|
||||
|
@ -178,6 +176,14 @@
|
|||
<string name="error_file_format">Invalid tunnel config format</string>
|
||||
<string name="restart_at_boot">Restart on boot</string>
|
||||
<string name="vpn_denied_dialog_title">Permission Denied</string>
|
||||
<string name="vpn_denied_dialog_message">Permission to start the VPN has either been explicitly denied or is being blocked by the system.</string>
|
||||
<string name="vpn_denied_dialog_message2">If VPN permission is being blocked by the system, please confirm no other app is using the Always-on VPN feature and try again.</string>
|
||||
<string name="vpn_settings">VPN system settings</string>
|
||||
<string name="always_on_message">VPN connection permission has been denied. Please check the</string>
|
||||
<string name="always_on_message2">to make sure Always-on VPN is turned off for all other apps and try again</string>
|
||||
<string name="chat_description">Join the community</string>
|
||||
<string name="tunnel_required">Feature requires at least one tunnel</string>
|
||||
<string name="request_root">Request root</string>
|
||||
<string name="background_location_message">Allow all the time location permission and/or precise location is required for this feature. Please see</string>
|
||||
<string name="app_settings">app settings</string>
|
||||
<string name="background_location_message2">to make sure these permissions are enabled.</string>
|
||||
<string name="root_accepted">Root shell accepted</string>
|
||||
</resources>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
android:icon="@drawable/vpn_on"
|
||||
android:shortcutDisabledMessage="@string/vpn_on"
|
||||
android:shortcutId="defaultOn1"
|
||||
android:shortcutLongLabel="@string/default_vpn_on"
|
||||
android:shortcutLongLabel="@string/vpn_on"
|
||||
android:shortcutShortLabel="@string/vpn_on">
|
||||
<intent
|
||||
android:action="START"
|
||||
|
@ -21,7 +21,7 @@
|
|||
android:icon="@drawable/vpn_off"
|
||||
android:shortcutDisabledMessage="@string/vpn_off"
|
||||
android:shortcutId="defaultOff1"
|
||||
android:shortcutLongLabel="@string/default_vpn_off"
|
||||
android:shortcutLongLabel="@string/vpn_off"
|
||||
android:shortcutShortLabel="@string/vpn_off">
|
||||
<intent
|
||||
android:action="STOP"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
object Constants {
|
||||
const val VERSION_NAME = "3.4.8"
|
||||
const val VERSION_NAME = "3.4.9"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 34800
|
||||
const val VERSION_CODE = 34900
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
|
@ -19,5 +19,5 @@ object Constants {
|
|||
const val TYPE = "type"
|
||||
|
||||
const val NIGHTLY_CODE = 42
|
||||
const val PRERELEASE_CODE = 99
|
||||
const val PRERELEASE_CODE = 53
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import org.gradle.api.Project
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
accompanist = "0.34.0"
|
||||
activityCompose = "1.9.1"
|
||||
amneziawgAndroid = "1.2.0"
|
||||
amneziawgAndroid = "1.2.1"
|
||||
androidx-junit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
|
@ -10,7 +10,7 @@ coreKtx = "1.13.1"
|
|||
datastorePreferences = "1.1.1"
|
||||
desugar_jdk_libs = "2.0.4"
|
||||
espressoCore = "3.6.1"
|
||||
hiltAndroid = "2.51.1"
|
||||
hiltAndroid = "2.52"
|
||||
hiltNavigationCompose = "1.2.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.7.1"
|
||||
|
@ -21,10 +21,10 @@ navigationCompose = "2.7.7"
|
|||
pinLockCompose = "1.0.3"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.0.20230706"
|
||||
androidGradlePlugin = "8.5.1"
|
||||
kotlin = "2.0.0"
|
||||
ksp = "2.0.0-1.0.24"
|
||||
tunnel = "1.2.1"
|
||||
androidGradlePlugin = "8.6.0-rc01"
|
||||
kotlin = "2.0.10"
|
||||
ksp = "2.0.10-1.0.24"
|
||||
composeBom = "2024.06.00"
|
||||
compose = "1.6.8"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
|
@ -88,7 +88,7 @@ material-icons-extended = { module = "androidx.compose.material:material-icons-e
|
|||
|
||||
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
|
||||
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
|
||||
|
||||
zaneschepke-multifab = { module = "com.zaneschepke:multifab", version.ref = "multifabVersion" }
|
||||
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
|
|
Loading…
Reference in New Issue