diff --git a/README.md b/README.md index 3d35ae1..8759ef2 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,8 @@ and on while on different networks. This app was created to offer a free solutio ## Docs (WIP) -Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html). +Basic documentation of the feature and behaviors of this app can be +found [here](https://zaneschepke.com/wgtunnel-docs/overview.html). The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs). @@ -74,6 +75,7 @@ $ cd wgtunnel ``` And then build the app: + ``` $ ./gradlew assembleDebug ``` diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/7.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/7.json new file mode 100644 index 0000000..33bbe18 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/7.json @@ -0,0 +1,176 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "e65e4e7cf01f50fb03196d47b54288b1", + "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)", + "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" + } + ], + "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)", + "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" + } + ], + "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, 'e65e4e7cf01f50fb03196d47b54288b1')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt index e461527..c15965d 100644 --- a/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt +++ b/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/MigrationTest.kt @@ -23,13 +23,13 @@ class MigrationTest { @Test @Throws(IOException::class) - fun migrate4To5() { - helper.createDatabase(dbName, 4).apply { + fun migrate6To7() { + helper.createDatabase(dbName, 6).apply { // Database has schema version 1. Insert some data using SQL queries. // You can't use DAO classes because they expect the latest schema. execSQL(Queries.createDefaultSettings()) execSQL( - "INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')", + Queries.createTunnelConfig(), ) // Prepare for the next version. close() @@ -37,7 +37,7 @@ class MigrationTest { // Re-open the database with version 2 and provide // MIGRATION_1_2 as the migration process. - helper.runMigrationsAndValidate(dbName, 5, true) + helper.runMigrationsAndValidate(dbName, 7, true) // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa70b78..58fea87 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + + + + + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 328742b..2ccb894 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -5,6 +5,7 @@ import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.service.quicksettings.TileService +import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import dagger.hilt.android.HiltAndroidApp @@ -19,6 +20,7 @@ class WireGuardAutoTunnel : Application() { if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree()) PinManager.initialize(this) } + companion object { lateinit var instance: WireGuardAutoTunnel private set @@ -27,11 +29,18 @@ class WireGuardAutoTunnel : Application() { return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) } - fun requestTileServiceStateUpdate(context : Context) { + fun requestTunnelTileServiceStateUpdate(context: Context) { TileService.requestListeningState( context, ComponentName(instance, TunnelControlTile::class.java), ) } + + fun requestAutoTunnelTileServiceUpdate(context: Context) { + TileService.requestListeningState( + context, + ComponentName(instance, AutoTunnelControlTile::class.java), + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 29fdeef..3cd52c2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -2,31 +2,38 @@ package com.zaneschepke.wireguardautotunnel.data import androidx.room.AutoMigration import androidx.room.Database +import androidx.room.DeleteColumn import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig @Database( entities = [Settings::class, TunnelConfig::class], - version = 6, + version = 7, autoMigrations = - [ - AutoMigration(from = 1, to = 2), - AutoMigration(from = 2, to = 3), - AutoMigration( - from = 3, - to = 4, - ), - AutoMigration( - from = 4, - to = 5, - ), - AutoMigration( - from = 5, - to = 6, - ), - ], + [ + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3), + AutoMigration( + from = 3, + to = 4, + ), + AutoMigration( + from = 4, + to = 5, + ), + AutoMigration( + from = 5, + to = 6, + ), + AutoMigration( + from = 6, + to = 7, + spec = RemoveLegacySettingColumnsMigration::class, + ), + ], exportSchema = true, ) @TypeConverters(DatabaseListConverters::class) @@ -35,3 +42,13 @@ abstract class AppDatabase : RoomDatabase() { abstract fun tunnelConfigDoa(): TunnelConfigDao } + +@DeleteColumn( + tableName = "Settings", + columnName = "default_tunnel", +) +@DeleteColumn( + tableName = "Settings", + columnName = "is_battery_saver_enabled", +) +class RemoveLegacySettingColumnsMigration : AutoMigrationSpec diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt index a620dcd..1f8a9a2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseCallback.kt @@ -12,7 +12,7 @@ class DatabaseCallback : RoomDatabase.Callback() { execSQL(Queries.createDefaultSettings()) Timber.i("Bootstrapping settings data") setTransactionSuccessful() - } catch (e : Exception) { + } catch (e: Exception) { Timber.e(e) } finally { endTransaction() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt index dc44129..8d76569 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/DatabaseListConverters.kt @@ -12,7 +12,7 @@ class DatabaseListConverters { @TypeConverter fun stringToList(value: String): MutableList { - if (value.isEmpty()) return mutableListOf() + if (value.isBlank() || value.isEmpty()) return mutableListOf() return try { Json.decodeFromString>(value) } catch (e: Exception) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt index 8a77cb2..833fc00 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/Queries.kt @@ -1,16 +1,14 @@ package com.zaneschepke.wireguardautotunnel.data object Queries { - fun createDefaultSettings() : String { + fun createDefaultSettings(): String { return """ INSERT INTO Settings (is_tunnel_enabled, is_tunnel_on_mobile_data_enabled, trusted_network_ssids, - default_tunnel, is_always_on_vpn_enabled, is_tunnel_on_ethernet_enabled, is_shortcuts_enabled, - is_battery_saver_enabled, is_tunnel_on_wifi_enabled, is_kernel_enabled, is_restore_on_boot_enabled, @@ -19,8 +17,6 @@ object Queries { ('false', 'false', 'sampleSSID1,sampleSSID2', - NULL, - 'false', 'false', 'false', 'false', @@ -30,4 +26,10 @@ object Queries { 'false') """.trimIndent() } + + fun createTunnelConfig(): String { + return """ + INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test') + """.trimIndent() + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt index 77061ae..ffd1574 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt @@ -10,19 +10,27 @@ import kotlinx.coroutines.flow.Flow @Dao interface SettingsDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(t: Settings) - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAll(t: List) - @Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings? + @Query("SELECT * FROM settings WHERE id=:id") + suspend fun getById(id: Long): Settings? - @Query("SELECT * FROM settings") suspend fun getAll(): List + @Query("SELECT * FROM settings") + suspend fun getAll(): List - @Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow + @Query("SELECT * FROM settings LIMIT 1") + fun getSettingsFlow(): Flow - @Query("SELECT * FROM settings") fun getAllFlow(): Flow> + @Query("SELECT * FROM settings") + fun getAllFlow(): Flow> - @Delete suspend fun delete(t: Settings) + @Delete + suspend fun delete(t: Settings) - @Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long + @Query("SELECT COUNT('id') FROM settings") + suspend fun count(): Long } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index 930c261..96b740d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -6,21 +6,44 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow @Dao interface TunnelConfigDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(t: TunnelConfig) - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAll(t: TunnelConfigs) - @Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig? + @Query("SELECT * FROM TunnelConfig WHERE id=:id") + suspend fun getById(id: Long): TunnelConfig? - @Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List + @Query("SELECT * FROM TunnelConfig") + suspend fun getAll(): TunnelConfigs - @Delete suspend fun delete(t: TunnelConfig) + @Delete + suspend fun delete(t: TunnelConfig) - @Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long + @Query("SELECT COUNT('id') FROM TunnelConfig") + suspend fun count(): Long - @Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow> + @Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'") + suspend fun findByTunnelNetworkName(name: String): TunnelConfigs + + @Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1") + fun resetPrimaryTunnel() + + @Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1") + fun resetMobileDataTunnel() + + @Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1") + suspend fun findByPrimary(): TunnelConfigs + + @Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1") + suspend fun findByMobileDataTunnel(): TunnelConfigs + + @Query("SELECT * FROM tunnelconfig") + fun getAllFlow(): Flow> } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt index 2f959fe..eb8725c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/datastore/DataStoreManager.kt @@ -4,39 +4,66 @@ import android.content.Context import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.io.IOException class DataStoreManager(private val context: Context) { 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 CURRENT_SSID = stringPreferencesKey("CURRENT_SSID") } // preferences private val preferencesKey = "preferences" private val Context.dataStore by - preferencesDataStore( - name = preferencesKey, - ) + preferencesDataStore( + name = preferencesKey, + ) suspend fun init() { - context.dataStore.data.first() + try { + context.dataStore.data.first() + } catch (e: IOException) { + Timber.e(e) + } + } + + suspend fun saveToDataStore(key: Preferences.Key, value: T) { + try { + context.dataStore.edit { it[key] = value } + } catch (e: IOException) { + Timber.e(e) + } catch (e: Exception) { + Timber.e(e) + } } - suspend fun saveToDataStore(key: Preferences.Key, value: T) = - context.dataStore.edit { it[key] = value } fun getFromStoreFlow(key: Preferences.Key) = context.dataStore.data.map { it[key] } - suspend fun getFromStore(key: Preferences.Key) = - context.dataStore.data.map{ it[key] }.first() + suspend fun getFromStore(key: Preferences.Key): T? { + return try { + context.dataStore.data.map { it[key] }.first() + } catch (e: IOException) { + Timber.e(e) + null + } + } + fun getFromStoreBlocking(key: Preferences.Key) = runBlocking { - context.dataStore.data.map{ it[key] }.first() + context.dataStore.data.map { it[key] }.first() } val preferencesFlow: Flow = context.dataStore.data diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt new file mode 100644 index 0000000..17a0893 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt @@ -0,0 +1,14 @@ +package com.zaneschepke.wireguardautotunnel.data.model + +data class GeneralState( + val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, + val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, + val activeTunnelId: 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 + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt index 2a8b595..80b0761 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt @@ -7,62 +7,47 @@ import androidx.room.PrimaryKey @Entity data class Settings( @PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false, + @ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false, @ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") - var isTunnelOnMobileDataEnabled: Boolean = false, + val isTunnelOnMobileDataEnabled: Boolean = false, @ColumnInfo(name = "trusted_network_ssids") - var trustedNetworkSSIDs: MutableList = mutableListOf(), - @ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null, - @ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false, + val trustedNetworkSSIDs: MutableList = mutableListOf(), + @ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false, @ColumnInfo(name = "is_tunnel_on_ethernet_enabled") - var isTunnelOnEthernetEnabled: Boolean = false, + val isTunnelOnEthernetEnabled: Boolean = false, @ColumnInfo( name = "is_shortcuts_enabled", defaultValue = "false", ) - var isShortcutsEnabled: Boolean = false, - @ColumnInfo( - name = "is_battery_saver_enabled", - defaultValue = "false", - ) - var isBatterySaverEnabled: Boolean = false, + val isShortcutsEnabled: Boolean = false, @ColumnInfo( name = "is_tunnel_on_wifi_enabled", defaultValue = "false", ) - var isTunnelOnWifiEnabled: Boolean = false, + val isTunnelOnWifiEnabled: Boolean = false, @ColumnInfo( name = "is_kernel_enabled", defaultValue = "false", ) - var isKernelEnabled: Boolean = false, + val isKernelEnabled: Boolean = false, @ColumnInfo( name = "is_restore_on_boot_enabled", defaultValue = "false", ) - var isRestoreOnBootEnabled: Boolean = false, + val isRestoreOnBootEnabled: Boolean = false, @ColumnInfo( name = "is_multi_tunnel_enabled", defaultValue = "false", ) - var isMultiTunnelEnabled: Boolean = false, + val isMultiTunnelEnabled: Boolean = false, @ColumnInfo( name = "is_auto_tunnel_paused", defaultValue = "false", ) - var isAutoTunnelPaused: Boolean = false, + val isAutoTunnelPaused: Boolean = false, @ColumnInfo( name = "is_ping_enabled", defaultValue = "false", ) - var isPingEnabled: Boolean = false, -) { - fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean { - return if (defaultTunnel != null) { - val defaultConfig = TunnelConfig.from(defaultTunnel!!) - (tunnelConfig.id == defaultConfig.id) - } else { - false - } - } -} + val isPingEnabled: Boolean = false, +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt index acb348e..b16b372 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt @@ -5,26 +5,30 @@ import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey import com.wireguard.config.Config -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import java.io.InputStream @Entity(indices = [Index(value = ["name"], unique = true)]) -@Serializable data class TunnelConfig( @PrimaryKey(autoGenerate = true) val id: Int = 0, - @ColumnInfo(name = "name") var name: String, - @ColumnInfo(name = "wg_quick") var wgQuick: String + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "wg_quick") val wgQuick: String, + @ColumnInfo( + name = "tunnel_networks", + defaultValue = "", + ) + val tunnelNetworks: MutableList = mutableListOf(), + @ColumnInfo( + name = "is_mobile_data_tunnel", + defaultValue = "false", + ) + val isMobileDataTunnel: Boolean = false, + @ColumnInfo( + name = "is_primary_tunnel", + defaultValue = "false", + ) + val isPrimaryTunnel: Boolean = false, ) { - override fun toString(): String { - return Json.encodeToString(serializer(), this) - } - companion object { - fun from(string: String): TunnelConfig { - return Json.decodeFromString(string) - } - fun configFromQuick(wgQuick: String): Config { val inputStream: InputStream = wgQuick.byteInputStream() val reader = inputStream.bufferedReader(Charsets.UTF_8) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt new file mode 100644 index 0000000..9054036 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt @@ -0,0 +1,14 @@ +package com.zaneschepke.wireguardautotunnel.data.repository + +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig + +interface AppDataRepository { + suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? + suspend fun getStartTunnelConfig(): TunnelConfig? + + suspend fun toggleWatcherServicePause() + + val settings: SettingsRepository + val tunnels: TunnelConfigRepository + val appState: AppStateRepository +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt new file mode 100644 index 0000000..650322a --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt @@ -0,0 +1,34 @@ +package com.zaneschepke.wireguardautotunnel.data.repository + +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import javax.inject.Inject + +class AppDataRoomRepository @Inject constructor( + override val settings: SettingsRepository, + override val tunnels: TunnelConfigRepository, + override val appState: AppStateRepository +) : AppDataRepository { + override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? { + return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull() + } + + 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, + ), + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt new file mode 100644 index 0000000..9b42e1c --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt @@ -0,0 +1,26 @@ +package com.zaneschepke.wireguardautotunnel.data.repository + +import com.zaneschepke.wireguardautotunnel.data.model.GeneralState +import kotlinx.coroutines.flow.Flow + +interface AppStateRepository { + suspend fun isLocationDisclosureShown(): Boolean + suspend fun setLocationDisclosureShown(shown: Boolean) + + suspend fun isBatteryOptimizationDisableShown(): Boolean + suspend fun setBatteryOptimizationDisableShown(shown: Boolean) + + suspend fun isTunnelRunningFromManualStart(): Boolean + suspend fun setTunnelRunningFromManualStart(id: Int) + + suspend fun setManualStop() + + suspend fun getActiveTunnelId(): Int? + + suspend fun getCurrentSsid(): String? + + suspend fun setCurrentSsid(ssid: String) + + val generalStateFlow: Flow + +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt new file mode 100644 index 0000000..4d1bb74 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -0,0 +1,81 @@ +package com.zaneschepke.wireguardautotunnel.data.repository + +import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager +import com.zaneschepke.wireguardautotunnel.data.model.GeneralState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) : + AppStateRepository { + override suspend fun isLocationDisclosureShown(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT + } + + override suspend fun setLocationDisclosureShown(shown: Boolean) { + dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) + } + + override suspend fun isBatteryOptimizationDisableShown(): Boolean { + return 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) + } + + override suspend fun isTunnelRunningFromManualStart(): Boolean { + return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START) + ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT + } + + 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 getCurrentSsid(): String? { + return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) + } + + override suspend fun setCurrentSsid(ssid: String) { + dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) + } + + override val generalStateFlow: Flow = + dataStoreManager.preferencesFlow.map { prefs -> + prefs?.let { pref -> + try { + GeneralState( + locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN] + ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT, + batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN] + ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, + tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START] + ?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT, + ) + } catch (e: IllegalArgumentException) { + Timber.e(e) + GeneralState() + } + } ?: GeneralState() + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt similarity index 91% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt index abdadd9..ff26ac8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt @@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.model.Settings import kotlinx.coroutines.flow.Flow -class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository { +class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { override suspend fun save(settings: Settings) { settingsDoa.save(settings) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt new file mode 100644 index 0000000..199079a --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt @@ -0,0 +1,68 @@ +package com.zaneschepke.wireguardautotunnel.data.repository + +import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs +import kotlinx.coroutines.flow.Flow + +class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) : + TunnelConfigRepository { + override fun getTunnelConfigsFlow(): Flow { + return tunnelConfigDao.getAllFlow() + } + + override suspend fun getAll(): TunnelConfigs { + return tunnelConfigDao.getAll() + } + + override suspend fun save(tunnelConfig: TunnelConfig) { + tunnelConfigDao.save(tunnelConfig) + } + + override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) { + tunnelConfigDao.resetPrimaryTunnel() + tunnelConfig?.let { + save( + it.copy( + isPrimaryTunnel = true, + ), + ) + } + + } + + override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) { + tunnelConfigDao.resetMobileDataTunnel() + tunnelConfig?.let { + save( + it.copy( + isMobileDataTunnel = true, + ), + ) + } + } + + override suspend fun delete(tunnelConfig: TunnelConfig) { + tunnelConfigDao.delete(tunnelConfig) + } + + override suspend fun getById(id: Int): TunnelConfig? { + return tunnelConfigDao.getById(id.toLong()) + } + + override suspend fun count(): Int { + return tunnelConfigDao.count().toInt() + } + + override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs { + return tunnelConfigDao.findByTunnelNetworkName(name) + } + + override suspend fun findByMobileDataTunnel(): TunnelConfigs { + return tunnelConfigDao.findByMobileDataTunnel() + } + + override suspend fun findPrimary(): TunnelConfigs { + return tunnelConfigDao.findByPrimary() + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index 1a12215..505815e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -12,7 +12,19 @@ interface TunnelConfigRepository { suspend fun save(tunnelConfig: TunnelConfig) + suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) + + suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) + suspend fun delete(tunnelConfig: TunnelConfig) + suspend fun getById(id: Int): TunnelConfig? + suspend fun count(): Int + + suspend fun findByTunnelNetworksName(name: String): TunnelConfigs + + suspend fun findByMobileDataTunnel(): TunnelConfigs + + suspend fun findPrimary(): TunnelConfigs } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt deleted file mode 100644 index b681ac1..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepositoryImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.data.repository - -import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs -import kotlinx.coroutines.flow.Flow - -class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) : - TunnelConfigRepository { - override fun getTunnelConfigsFlow(): Flow { - return tunnelConfigDao.getAllFlow() - } - - override suspend fun getAll(): TunnelConfigs { - return tunnelConfigDao.getAll() - } - - override suspend fun save(tunnelConfig: TunnelConfig) { - tunnelConfigDao.save(tunnelConfig) - } - - override suspend fun delete(tunnelConfig: TunnelConfig) { - tunnelConfigDao.delete(tunnelConfig) - } - - override suspend fun count(): Int { - return tunnelConfigDao.count().toInt() - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt index c0f4617..be87f9e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/DatabaseModule.kt @@ -19,10 +19,10 @@ class DatabaseModule { @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder( - context, - AppDatabase::class.java, - context.getString(R.string.db_name), - ) + context, + AppDatabase::class.java, + context.getString(R.string.db_name), + ) .fallbackToDestructiveMigration() .addCallback(DatabaseCallback()) .build() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt index c4b0d2d..a763c09 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Kernel.kt @@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module import javax.inject.Qualifier -@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Kernel diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt index 453607a..f786045 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt @@ -5,10 +5,14 @@ import com.zaneschepke.wireguardautotunnel.data.AppDatabase import com.zaneschepke.wireguardautotunnel.data.SettingsDao import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository +import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository +import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository +import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,13 +38,13 @@ class RepositoryModule { @Singleton @Provides fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository { - return TunnelConfigRepositoryImpl(tunnelConfigDao) + return RoomTunnelConfigRepository(tunnelConfigDao) } @Singleton @Provides fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository { - return SettingsRepositoryImpl(settingsDao) + return RoomSettingsRepository(settingsDao) } @Singleton @@ -48,4 +52,22 @@ class RepositoryModule { fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager { return DataStoreManager(context) } + + @Provides + @Singleton + fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository { + return DataStoreAppStateRepository(dataStoreManager) + } + + @Provides + @Singleton + fun provideAppDataRepository( + settingsRepository: SettingsRepository, + tunnelConfigRepository: TunnelConfigRepository, + appStateRepository: AppStateRepository + ): AppDataRepository { + return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository) + } + + } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt index bb490f6..1d01659 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -6,7 +6,8 @@ import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.util.RootShell import com.wireguard.android.util.ToolsInstaller -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +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.WireGuardTunnel import dagger.Module @@ -44,8 +45,14 @@ class TunnelModule { fun provideVpnService( @Userspace userspaceBackend: Backend, @Kernel kernelBackend: Backend, - settingsRepository: SettingsRepository + appDataRepository: AppDataRepository ): VpnService { - return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository) + return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository) + } + + @Provides + @Singleton + fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager { + return ServiceManager(appDataRepository) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt index f064aab..8a85a7d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/Userspace.kt @@ -2,4 +2,6 @@ package com.zaneschepke.wireguardautotunnel.module import javax.inject.Qualifier -@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Userspace diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt index 320fb72..589aa8e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -3,8 +3,7 @@ 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.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.util.goAsync import dagger.hilt.android.AndroidEntryPoint @@ -14,19 +13,33 @@ import javax.inject.Inject @AndroidEntryPoint class BootReceiver : BroadcastReceiver() { - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository + @Inject + lateinit var serviceManager: ServiceManager override fun onReceive(context: Context?, intent: Intent?) = goAsync { if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync - val settings = settingsRepository.getSettings() - if (settings.isAutoTunnelEnabled) { - Timber.i("Starting watcher service from boot") - ServiceManager.startWatcherServiceForeground(context!!) - } else if(settings.isAlwaysOnVpnEnabled) { - Timber.i("Starting tunnel from boot") - ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull()) + context?.run { + val settings = appDataRepository.settings.getSettings() + 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, + ) + } + } + if (settings.isAlwaysOnVpnEnabled) { + Timber.i("Starting vpn service from boot AOVPN") + serviceManager.startVpnServiceForeground(context) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt index 4ba46b6..bcfb90f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -10,20 +10,25 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint class NotificationActionReceiver : BroadcastReceiver() { - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var settingsRepository: SettingsRepository + + @Inject + lateinit var serviceManager: ServiceManager override fun onReceive(context: Context, intent: Intent?) = goAsync { try { - val settings = settingsRepository.getSettings() - if (settings.defaultTunnel != null) { - ServiceManager.stopVpnService(context) - delay(Constants.TOGGLE_TUNNEL_DELAY) - ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString()) - } + //TODO fix for manual start changes when enabled + serviceManager.stopVpnService(context) + delay(Constants.TOGGLE_TUNNEL_DELAY) + serviceManager.startVpnServiceForeground(context) + } catch (e: Exception) { + Timber.e(e) } finally { cancel() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt index 8135fc4..06accde 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import android.os.IBinder import androidx.lifecycle.LifecycleService +import com.zaneschepke.wireguardautotunnel.util.Constants import timber.log.Timber open class ForegroundService : LifecycleService() { @@ -23,11 +24,13 @@ open class ForegroundService : LifecycleService() { when (action) { Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras) + Action.STOP.name -> stopService(intent.extras) - "android.net.VpnService" -> { - Timber.d("Always-on VPN starting service") + Constants.ALWAYS_ON_VPN_ACTION -> { + Timber.i("Always-on VPN starting service") startService(intent.extras) } + else -> Timber.d("This should never happen. No action in the received intent") } } else { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt index 15e9d72..5bf7700 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -3,33 +3,17 @@ package com.zaneschepke.wireguardautotunnel.service.foreground import android.app.Service import android.content.Context import android.content.Intent -import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.util.Constants import timber.log.Timber -object ServiceManager { - - // private - // fun Context.isServiceRunning(service: Class) = - // (getSystemService(ACTIVITY_SERVICE) as ActivityManager) - // .runningAppProcesses.any { - // it.processName == service.name - // } - // - // fun getServiceState( - // context: Context, - // cls: Class - // ): ServiceState { - // val isServiceRunning = context.isServiceRunning(cls) - // return if (isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED - // } +class ServiceManager(private val appDataRepository: AppDataRepository) { private fun actionOnService( action: Action, context: Context, cls: Class, - extras: Map? = null + extras: Map? = null ) { val intent = Intent(context, cls).also { @@ -42,9 +26,11 @@ object ServiceManager { Action.START_FOREGROUND -> { context.startForegroundService(intent) } + Action.START -> { context.startService(intent) } + Action.STOP -> context.startService(intent) } } catch (e: Exception) { @@ -52,16 +38,22 @@ object ServiceManager { } } - fun startVpnService(context: Context, tunnelConfig: String) { + suspend fun startVpnService( + context: Context, + tunnelId: Int? = null, + isManualStart: Boolean = false + ) { + if (isManualStart) onManualStart(tunnelId) actionOnService( Action.START, context, WireGuardTunnelService::class.java, - mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig), + tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, ) } - fun stopVpnService(context: Context) { + suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { + if (isManualStop) onManualStop() Timber.d("Stopping vpn service action") actionOnService( Action.STOP, @@ -70,24 +62,30 @@ object ServiceManager { ) } - fun startVpnServiceForeground(context: Context, tunnelConfig: String) { + 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 + ) { + if (isManualStart) onManualStart(tunnelId) actionOnService( Action.START_FOREGROUND, context, WireGuardTunnelService::class.java, - mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig), + tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) }, ) } - fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) { - if(settings.defaultTunnel != null) { - return startVpnServiceForeground(context, settings.defaultTunnel!!) - } - if(fallbackTunnel != null) { - startVpnServiceForeground(context, fallbackTunnel.toString()) - } - } - fun startWatcherServiceForeground( context: Context, ) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt new file mode 100644 index 0000000..4e22360 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt @@ -0,0 +1,84 @@ +package com.zaneschepke.wireguardautotunnel.service.foreground + +import com.wireguard.android.backend.Tunnel +import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig + +data class WatcherState( + val isWifiConnected: Boolean = false, + val config: TunnelConfig? = null, + val vpnStatus: Tunnel.State = Tunnel.State.DOWN, + val isEthernetConnected: Boolean = false, + val isMobileDataConnected: Boolean = false, + val currentNetworkSSID: String = "", + val settings: Settings = Settings() +) { + + private fun isVpnConnected() = vpnStatus == Tunnel.State.UP + fun isEthernetConditionMet(): Boolean { + return (isEthernetConnected && + settings.isTunnelOnEthernetEnabled && + !isVpnConnected()) + } + + fun isMobileDataConditionMet(): Boolean { + return (!isEthernetConnected && + settings.isTunnelOnMobileDataEnabled && + !isWifiConnected && + isMobileDataConnected && + !isVpnConnected()) + } + + fun isTunnelNotMobileDataPreferredConditionMet(): Boolean { + return (!isEthernetConnected && + settings.isTunnelOnMobileDataEnabled && + !isWifiConnected && + isMobileDataConnected && + config?.isMobileDataTunnel == false && isVpnConnected()) + } + + fun isTunnelOffOnMobileDataConditionMet(): Boolean { + return (!isEthernetConnected && + !settings.isTunnelOnMobileDataEnabled && + isMobileDataConnected && + !isWifiConnected && + isVpnConnected()) + } + + fun isUntrustedWifiConditionMet(): Boolean { + return (!isEthernetConnected && + isWifiConnected && + !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && + settings.isTunnelOnWifiEnabled + && !isVpnConnected()) + } + + fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean { + return (!isEthernetConnected && + isWifiConnected && + !settings.trustedNetworkSSIDs.contains(currentNetworkSSID) && + settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected()) + } + + fun isTrustedWifiConditionMet(): Boolean { + return (!isEthernetConnected && + (isWifiConnected && + settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) && + (isVpnConnected())) + } + + fun isTunnelOffOnWifiConditionMet(): Boolean { + return (!isEthernetConnected && + (isWifiConnected && + !settings.isTunnelOnWifiEnabled && + (isVpnConnected()))) + } + + fun isTunnelOffOnNoConnectivityMet(): Boolean { + return (!isEthernetConnected && + !isWifiConnected && + !isMobileDataConnected && + (isVpnConnected())) + } +} + diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 98894d3..90cc3a0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -7,8 +7,8 @@ import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService @@ -33,29 +33,29 @@ import javax.inject.Inject class WireGuardConnectivityWatcherService : ForegroundService() { private val foregroundId = 122 - @Inject lateinit var wifiService: NetworkService + @Inject + lateinit var wifiService: NetworkService - @Inject lateinit var mobileDataService: NetworkService + @Inject + lateinit var mobileDataService: NetworkService - @Inject lateinit var ethernetService: NetworkService + @Inject + lateinit var ethernetService: NetworkService - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject lateinit var notificationService: NotificationService + @Inject + lateinit var notificationService: NotificationService - @Inject lateinit var vpnService: VpnService + @Inject + lateinit var vpnService: VpnService + + @Inject + lateinit var serviceManager: ServiceManager private val networkEventsFlow = MutableStateFlow(WatcherState()) - data class WatcherState( - val isWifiConnected: Boolean = false, - val isVpnConnected: Boolean = false, - val isEthernetConnected: Boolean = false, - val isMobileDataConnected: Boolean = false, - val currentNetworkSSID: String = "", - val settings: Settings = Settings() - ) - private lateinit var watcherJob: Job private var wakeLock: PowerManager.WakeLock? = null @@ -65,7 +65,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { super.onCreate() lifecycleScope.launch(Dispatchers.Main) { try { - if (settingsRepository.getSettings().isAutoTunnelPaused) { + if (appDataRepository.settings.getSettings().isAutoTunnelPaused) { launchWatcherPausedNotification() } else launchWatcherNotification() } catch (e: Exception) { @@ -121,16 +121,16 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private fun initWakeLock() { wakeLock = - (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { - try { - Timber.i("Initiating wakelock with 10 min timeout") - acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) - } finally { - release() + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { + try { + Timber.i("Initiating wakelock with 10 min timeout") + acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) + } finally { + release() + } } } - } } private fun cancelWatcherJob() { @@ -142,7 +142,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private fun startWatcherJob() { watcherJob = lifecycleScope.launch(Dispatchers.IO) { - val setting = settingsRepository.getSettings() + val setting = appDataRepository.settings.getSettings() launch { Timber.i("Starting wifi watcher") watchForWifiConnectivityChanges() @@ -167,7 +167,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { Timber.i("Starting settings watcher") watchForSettingsChanges() } - if(setting.isPingEnabled) { + if (setting.isPingEnabled) { launch { Timber.i("Starting ping watcher") watchForPingFailure() @@ -191,6 +191,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { isMobileDataConnected = true, ) } + is NetworkStatus.CapabilitiesChanged -> { networkEventsFlow.value = networkEventsFlow.value.copy( @@ -198,6 +199,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { ) Timber.i("Mobile data capabilities changed") } + is NetworkStatus.Unavailable -> { networkEventsFlow.value = networkEventsFlow.value.copy( @@ -212,39 +214,39 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private suspend fun watchForPingFailure() { try { do { - if(vpnService.vpnState.value.status == Tunnel.State.UP) { - val config = vpnService.vpnState.value.config - config?.let { - val results = it.peers.map { peer -> - val host = if(peer.endpoint.isPresent && + if (vpnService.vpnState.value.status == Tunnel.State.UP) { + val tunnelConfig = vpnService.vpnState.value.tunnelConfig + tunnelConfig?.let { + val config = TunnelConfig.configFromQuick(it.wgQuick) + val results = config.peers.map { peer -> + val host = if (peer.endpoint.isPresent && peer.endpoint.get().resolved.isPresent) peer.endpoint.get().resolved.get().host else Constants.BACKUP_PING_HOST Timber.i("Checking reachability of: $host") - val reachable = InetAddress.getByName(host).isReachable(Constants.PING_TIMEOUT.toInt()) + val reachable = InetAddress.getByName(host) + .isReachable(Constants.PING_TIMEOUT.toInt()) Timber.i("Result: reachable - $reachable") reachable } - if(results.contains(false)) { + if (results.contains(false)) { Timber.i("Restarting VPN for ping failure") - ServiceManager.stopVpnService(this) + serviceManager.stopVpnService(this) delay(Constants.VPN_RESTART_DELAY) - val tunnel = networkEventsFlow.value.settings.defaultTunnel - ServiceManager.startVpnServiceForeground(this, tunnel!!) + serviceManager.startVpnServiceForeground(this) delay(Constants.PING_COOLDOWN) } } } delay(Constants.PING_INTERVAL) } while (true) - } catch (e: Exception) { Timber.e(e) } } private suspend fun watchForSettingsChanges() { - settingsRepository.getSettingsFlow().collect { + appDataRepository.settings.getSettingsFlow().collect { if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) { when (it.isAutoTunnelPaused) { true -> launchWatcherPausedNotification() @@ -260,19 +262,11 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private suspend fun watchForVpnConnectivityChanges() { vpnService.vpnState.collect { - when (it.status) { - Tunnel.State.DOWN -> - networkEventsFlow.value = - networkEventsFlow.value.copy( - isVpnConnected = false, - ) - Tunnel.State.UP -> - networkEventsFlow.value = - networkEventsFlow.value.copy( - isVpnConnected = true, - ) - else -> {} - } + networkEventsFlow.value = + networkEventsFlow.value.copy( + vpnStatus = it.status, + config = it.tunnelConfig, + ) } } @@ -286,6 +280,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { isEthernetConnected = true, ) } + is NetworkStatus.CapabilitiesChanged -> { Timber.i("Ethernet capabilities changed") networkEventsFlow.value = @@ -293,6 +288,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() { isEthernetConnected = true, ) } + is NetworkStatus.Unavailable -> { networkEventsFlow.value = networkEventsFlow.value.copy( @@ -314,19 +310,24 @@ class WireGuardConnectivityWatcherService : ForegroundService() { isWifiConnected = true, ) } + is NetworkStatus.CapabilitiesChanged -> { Timber.i("Wifi capabilities changed") networkEventsFlow.value = networkEventsFlow.value.copy( isWifiConnected = true, ) - val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" - Timber.i("Detected SSID: $ssid") - networkEventsFlow.value = - networkEventsFlow.value.copy( - currentNetworkSSID = ssid, - ) + val ssid = wifiService.getNetworkName(it.networkCapabilities) + ssid?.let { + Timber.i("Detected SSID: $ssid") + appDataRepository.appState.setCurrentSsid(ssid) + networkEventsFlow.value = + networkEventsFlow.value.copy( + currentNetworkSSID = ssid, + ) + } ?: Timber.w("Failed to read ssid") } + is NetworkStatus.Unavailable -> { networkEventsFlow.value = networkEventsFlow.value.copy( @@ -338,64 +339,84 @@ class WireGuardConnectivityWatcherService : ForegroundService() { } } + private suspend fun getMobileDataTunnel(): TunnelConfig? { + return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() + } + + private suspend fun getSsidTunnel(ssid: String): TunnelConfig? { + return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull() + } + private suspend fun manageVpn() { - networkEventsFlow.collectLatest { + networkEventsFlow.collectLatest { watcherState -> val autoTunnel = "Auto-tunnel watcher" - if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) { - delay(Constants.TOGGLE_TUNNEL_DELAY) + if (!watcherState.settings.isAutoTunnelPaused) { + //delay for rapid network state changes and then collect latest + delay(Constants.WATCHER_COLLECTION_DELAY) when { - ((it.isEthernetConnected && - it.settings.isTunnelOnEthernetEnabled && - !it.isVpnConnected)) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("$autoTunnel condition 1 met") + watcherState.isEthernetConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on ethernet condition met") + serviceManager.startVpnServiceForeground(this) } - (!it.isEthernetConnected && - it.settings.isTunnelOnMobileDataEnabled && - !it.isWifiConnected && - it.isMobileDataConnected && - !it.isVpnConnected) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("$autoTunnel condition 2 met") + + watcherState.isMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel on on mobile data condition met") + serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id) } - (!it.isEthernetConnected && - !it.settings.isTunnelOnMobileDataEnabled && - !it.isWifiConnected && - it.isVpnConnected) -> { - ServiceManager.stopVpnService(this) - Timber.i("$autoTunnel condition 3 met") + + watcherState.isTunnelNotMobileDataPreferredConditionMet() -> { + getMobileDataTunnel()?.let { + Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred") + serviceManager.startVpnServiceForeground( + this, + getMobileDataTunnel()?.id, + ) + } } - (!it.isEthernetConnected && - it.isWifiConnected && - !it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) && - it.settings.isTunnelOnWifiEnabled && - (!it.isVpnConnected)) -> { - ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!) - Timber.i("$autoTunnel condition 4 met") + + watcherState.isTunnelOffOnMobileDataConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") + serviceManager.stopVpnService(this) } - (!it.isEthernetConnected && - (it.isWifiConnected && - it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) && - (it.isVpnConnected)) -> { - ServiceManager.stopVpnService(this) - Timber.i("$autoTunnel condition 5 met") + + watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> { + 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") + serviceManager.startVpnServiceForeground(this, it.id) + } ?: suspend { + Timber.i("No tunnel associated with this SSID, using defaults") + if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) { + serviceManager.startVpnServiceForeground(this) + } + }.invoke() } - (!it.isEthernetConnected && - (it.isWifiConnected && - !it.settings.isTunnelOnWifiEnabled && - (it.isVpnConnected))) -> { - ServiceManager.stopVpnService(this) - Timber.i("$autoTunnel condition 6 met") + + watcherState.isUntrustedWifiConditionMet() -> { + Timber.i("$autoTunnel - tunnel on untrusted wifi condition met") + serviceManager.startVpnServiceForeground( + this, + getSsidTunnel(watcherState.currentNetworkSSID)?.id, + ) } - (!it.isEthernetConnected && - !it.isWifiConnected && - !it.isMobileDataConnected && - (it.isVpnConnected)) -> { - ServiceManager.stopVpnService(this) - Timber.i("$autoTunnel condition 7 met") + + watcherState.isTrustedWifiConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") + serviceManager.stopVpnService(this) } + + watcherState.isTunnelOffOnWifiConditionMet() -> { + Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") + serviceManager.stopVpnService(this) + } + + watcherState.isTunnelOffOnNoConnectivityMet() -> { + Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") + serviceManager.stopVpnService(this) + } + else -> { - Timber.i("$autoTunnel no condition met") + Timber.i("$autoTunnel - no condition met") } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index d1ca34a..3dd987f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -5,10 +5,9 @@ import android.content.Intent import android.os.Bundle import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope +import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus @@ -21,30 +20,30 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint class WireGuardTunnelService : ForegroundService() { private val foregroundId = 123 - @Inject lateinit var vpnService: VpnService + @Inject + lateinit var vpnService: VpnService - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository - - @Inject lateinit var notificationService: NotificationService + @Inject + lateinit var notificationService: NotificationService private lateinit var job: Job - private var tunnelName: String = "" private var didShowConnected = false override fun onCreate() { super.onCreate() lifecycleScope.launch(Dispatchers.Main) { - if (tunnelConfigRepository.getAll().isNotEmpty()) { + //TODO fix this to not launch if AOVPN + if (appDataRepository.tunnels.count() != 0) { launchVpnNotification() } } @@ -53,74 +52,69 @@ class WireGuardTunnelService : ForegroundService() { override fun startService(extras: Bundle?) { super.startService(extras) cancelJob() - val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) - val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) } - tunnelName = tunnelConfig?.name ?: "" job = lifecycleScope.launch(Dispatchers.IO) { launch { - if (tunnelConfig != null) { - try { - tunnelName = tunnelConfig.name - vpnService.startTunnel(tunnelConfig) - } catch (e: Exception) { - Timber.e("Problem starting tunnel: ${e.message}") - stopService(extras) - } - } else { - Timber.i("Tunnel config null, starting default tunnel or first") - val settings = settingsRepository.getSettings() - val tunnels = tunnelConfigRepository.getAll() - if (settings.isAlwaysOnVpnEnabled) { - val tunnel = - if (settings.defaultTunnel != null) { - TunnelConfig.from(settings.defaultTunnel!!) - } else if (tunnels.isNotEmpty()) { - tunnels.first() - } else { - null - } - if (tunnel != null) { - tunnelName = tunnel.name - vpnService.startTunnel(tunnel) - } - } else { - launchAlwaysOnDisabledNotification() - } + val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) + if (vpnService.getState() == Tunnel.State.UP) { + vpnService.stopTunnel() } + vpnService.startTunnel( + tunnelId?.let { + appDataRepository.tunnels.getById(it) + }, + ) } - // TODO add failed to connect notification launch { - 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) - 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 -> {} - } - } - } + handshakeNotifications() } } } + //TODO improve tunnel notifications + private suspend fun handshakeNotifications() { + 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 == Tunnel.State.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)) + launchVpnNotification( + title = this.getString(R.string.vpn_connection_failed), + description = this.getString(R.string.always_on_disabled), + ) } override fun stopService(extras: Bundle?) { @@ -161,12 +155,12 @@ class WireGuardTunnelService : ForegroundService() { 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, - ), + 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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt index 56be188..7bbff4d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt @@ -53,6 +53,7 @@ abstract class BaseNetworkService>( } } } + else -> { object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { @@ -117,7 +118,7 @@ inline fun Flow.map( crossinline onUnavailable: suspend (network: Network) -> Result, crossinline onAvailable: suspend (network: Network) -> Result, crossinline onCapabilitiesChanged: - suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result + suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result ): Flow = map { status -> when (status) { is NetworkStatus.Unavailable -> onUnavailable(status.network) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt index c61fda2..eb6c951 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt @@ -45,10 +45,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val ): Notification { val channel = NotificationChannel( - channelId, - channelName, - importance, - ) + channelId, + channelName, + importance, + ) .let { it.description = title it.enableLights(lights) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index f5d1e0f..ffa3525 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -1,80 +1,66 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut import android.os.Bundle -import android.view.View import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository 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 dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint class ShortcutsActivity : ComponentActivity() { - @Inject lateinit var settingsRepository: SettingsRepository - @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository + @Inject + lateinit var appDataRepository: AppDataRepository - private suspend fun toggleWatcherServicePause() { - val settings = settingsRepository.getSettings() - if (settings.isAutoTunnelEnabled) { - val pauseAutoTunnel = !settings.isAutoTunnelPaused - settingsRepository.save( - settings.copy( - isAutoTunnelPaused = pauseAutoTunnel, - ), - ) - } - } + @Inject + lateinit var serviceManager: ServiceManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(View(this)) - if ( - intent - .getStringExtra(CLASS_NAME_EXTRA_KEY) - .equals(WireGuardTunnelService::class.java.simpleName) - ) { - lifecycleScope.launch(Dispatchers.Main) { - val settings = settingsRepository.getSettings() - if (settings.isShortcutsEnabled) { - try { + lifecycleScope.launch(Dispatchers.Main) { + val settings = appDataRepository.settings.getSettings() + if (settings.isShortcutsEnabled) { + when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { + WireGuardTunnelService::class.java.simpleName -> { val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY) - val tunnelConfig = - if (tunnelName != null) { - tunnelConfigRepository.getAll().firstOrNull { - it.name == tunnelName - } - } else { - if (settings.defaultTunnel == null) { - tunnelConfigRepository.getAll().first() - } else { - TunnelConfig.from(settings.defaultTunnel!!) - } + val tunnelConfig = tunnelName?.let { + appDataRepository.tunnels.getAll().firstOrNull { + it.name == tunnelName } - tunnelConfig ?: return@launch - toggleWatcherServicePause() - when (intent.action) { - Action.STOP.name -> - ServiceManager.stopVpnService( - this@ShortcutsActivity, - ) - Action.START.name -> - ServiceManager.startVpnServiceForeground( - this@ShortcutsActivity, - tunnelConfig.toString(), - ) } - } catch (e: Exception) { - Timber.e(e.message) - finish() + when (intent.action) { + Action.START.name -> serviceManager.startVpnServiceForeground( + this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true, + ) + + Action.STOP.name -> serviceManager.stopVpnService( + this@ShortcutsActivity, + isManualStop = true, + ) + } + } + + WireGuardConnectivityWatcherService::class.java.simpleName -> { + when (intent.action) { + Action.START.name -> appDataRepository.settings.save( + settings.copy( + isAutoTunnelPaused = false, + ), + ) + + Action.STOP.name -> appDataRepository.settings.save( + settings.copy( + isAutoTunnelPaused = true, + ), + ) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt new file mode 100644 index 0000000..a25c866 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt @@ -0,0 +1,106 @@ +package com.zaneschepke.wireguardautotunnel.service.tile + +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class AutoTunnelControlTile : TileService() { + + @Inject + lateinit var appDataRepository: AppDataRepository + + @Inject + lateinit var serviceManager: ServiceManager + + private val scope = CoroutineScope(Dispatchers.IO) + + private var manualStartConfig: TunnelConfig? = null + + override fun onStartListening() { + super.onStartListening() + scope.launch { + appDataRepository.settings.getSettingsFlow().collectLatest { + 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() + } + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + + override fun onTileRemoved() { + super.onTileRemoved() + scope.cancel() + } + + override fun onClick() { + super.onClick() + unlockAndRun { + scope.launch { + try { + appDataRepository.toggleWatcherServicePause() + } catch (e: Exception) { + Timber.e(e.message) + } finally { + cancel() + } + } + } + } + + private fun setActive() { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } + + private fun setInactive() { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } + + private fun setUnavailable() { + manualStartConfig = null + qsTile.state = Tile.STATE_UNAVAILABLE + qsTile.updateTile() + } + + private fun setTileDescription(description: String) { + 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() + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index 63d8142..8109ad7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -5,8 +5,7 @@ import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint @@ -18,40 +17,44 @@ import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class TunnelControlTile() : TileService() { +class TunnelControlTile : TileService() { - @Inject lateinit var tunnelConfigRepository: TunnelConfigRepository + @Inject + lateinit var appDataRepository: AppDataRepository - @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var vpnService: VpnService - @Inject lateinit var vpnService: VpnService + @Inject + lateinit var serviceManager: ServiceManager private val scope = CoroutineScope(Dispatchers.IO) - private var tunnelName: String? = null + private var manualStartConfig: TunnelConfig? = null override fun onStartListening() { super.onStartListening() Timber.d("On start listening called") scope.launch { - vpnService.vpnState.collect { + vpnService.vpnState.collect { it -> when (it.status) { - Tunnel.State.UP -> setActive() - Tunnel.State.DOWN -> setInactive() + Tunnel.State.UP -> { + setActive() + it.tunnelConfig?.name?.let { name -> setTileDescription(name) } + } + + Tunnel.State.DOWN -> { + setInactive() + val config = appDataRepository.getStartTunnelConfig()?.also { config -> + manualStartConfig = config + } ?: appDataRepository.getPrimaryOrFirstTunnel() + config?.let { + setTileDescription(it.name) + } ?: setUnavailable() + } + else -> setInactive() } - val tunnels = tunnelConfigRepository.getAll() - if (tunnels.isEmpty()) { - setUnavailable() - return@collect - } - tunnelName = it.name.run { - val settings = settingsRepository.getSettings() - if (settings.defaultTunnel != null) { - TunnelConfig.from(settings.defaultTunnel!!).name - } else tunnels.firstOrNull()?.name - } - setTileDescription(tunnelName ?: "") } } } @@ -71,18 +74,11 @@ class TunnelControlTile() : TileService() { unlockAndRun { scope.launch { try { - val defaultTunnel = settingsRepository.getSettings().defaultTunnel - val config = defaultTunnel ?: run { - val tunnelConfigs = tunnelConfigRepository.getAll() - return@run tunnelConfigs.find { it.name == tunnelName } - } - toggleWatcherServicePause() if (vpnService.getState() == Tunnel.State.UP) { - ServiceManager.stopVpnService(this@TunnelControlTile) + serviceManager.stopVpnService(this@TunnelControlTile, isManualStop = true) } else { - ServiceManager.startVpnServiceForeground( - this@TunnelControlTile, - config.toString(), + serviceManager.startVpnServiceForeground( + this@TunnelControlTile, manualStartConfig?.id, isManualStart = true, ) } } catch (e: Exception) { @@ -94,20 +90,6 @@ class TunnelControlTile() : TileService() { } } - private fun toggleWatcherServicePause() { - scope.launch { - val settings = settingsRepository.getSettings() - if (settings.isAutoTunnelEnabled) { - val pauseAutoTunnel = !settings.isAutoTunnelPaused - settingsRepository.save( - settings.copy( - isAutoTunnelPaused = pauseAutoTunnel, - ), - ) - } - } - } - private fun setActive() { qsTile.state = Tile.STATE_ACTIVE qsTile.updateTile() @@ -119,6 +101,7 @@ class TunnelControlTile() : TileService() { } private fun setUnavailable() { + manualStartConfig = null qsTile.state = Tile.STATE_UNAVAILABLE qsTile.updateTile() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt index 90e5ae1..285ba40 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt @@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import kotlinx.coroutines.flow.StateFlow interface VpnService : Tunnel { - suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State + suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State suspend fun stopTunnel() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt index 03be54d..e409980 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -2,12 +2,10 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel -import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig data class VpnState( val status: Tunnel.State = Tunnel.State.DOWN, - val name: String = "", - val config: Config? = null, + val tunnelConfig: TunnelConfig? = null, val statistics: Statistics? = null ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index cadcd4e..9eb841c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -4,10 +4,9 @@ import com.wireguard.android.backend.Backend import com.wireguard.android.backend.BackendException import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel.State -import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.util.Constants @@ -25,9 +24,9 @@ import javax.inject.Inject class WireGuardTunnel @Inject constructor( - @Userspace private val userspaceBackend : Backend, + @Userspace private val userspaceBackend: Backend, @Kernel private val kernelBackend: Backend, - private val settingsRepository: SettingsRepository + private val appDataRepository: AppDataRepository, ) : VpnService { private val _vpnState = MutableStateFlow(VpnState()) override val vpnState: StateFlow = _vpnState.asStateFlow() @@ -36,15 +35,13 @@ constructor( private lateinit var statsJob: Job - private var config: Config? = null - private var backend: Backend = userspaceBackend private var backendIsUserspace = true init { scope.launch { - settingsRepository.getSettingsFlow().collect { + appDataRepository.settings.getSettingsFlow().collect { if (it.isKernelEnabled && backendIsUserspace) { Timber.d("Setting kernel backend") backend = kernelBackend @@ -58,20 +55,21 @@ constructor( } } - override suspend fun startTunnel(tunnelConfig: TunnelConfig): State { + override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State { return try { - stopTunnelOnConfigChange(tunnelConfig) - emitTunnelName(tunnelConfig.name) - config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - emitTunnelConfig(config) - val state = - backend.setState( - this, - State.UP, - config, - ) - emitTunnelState(state) - state + //TODO we need better error handling here + val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() + if (config != null) { + emitTunnelConfig(config) + val wgConfig = TunnelConfig.configFromQuick(config.wgQuick) + val state = + backend.setState( + this, + State.UP, + wgConfig, + ) + state + } else throw Exception("No tunnels") } catch (e: BackendException) { Timber.e("Failed to start tunnel with error: ${e.message}") State.DOWN @@ -94,32 +92,14 @@ constructor( ) } - private suspend fun emitTunnelName(name: String) { + private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) { _vpnState.emit( _vpnState.value.copy( - name = name, + tunnelConfig = tunnelConfig, ), ) } - private suspend fun emitTunnelConfig(config : Config?) { - _vpnState.emit( - _vpnState.value.copy( - config = config, - ), - ) - } - - private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) { - if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) { - stopTunnel() - } - } - - override fun getName(): String { - return _vpnState.value.name - } - override suspend fun stopTunnel() { try { if (getState() == State.UP) { @@ -135,10 +115,14 @@ constructor( return backend.getState(this) } + override fun getName(): String { + return _vpnState.value.tunnelConfig?.name ?: "" + } + override fun onStateChange(state: State) { val tunnel = this emitTunnelState(state) - WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) if (state == State.UP) { statsJob = scope.launch { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 8111743..667df97 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -13,7 +13,6 @@ import com.zaneschepke.logcatter.Logcatter import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.FileUtils import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,7 +20,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.time.Instant import javax.inject.Inject @@ -32,30 +30,33 @@ class AppViewModel constructor( private val application: Application, ) : ViewModel() { - + val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) - private val _appUiState = MutableStateFlow(AppUiState( - vpnPermissionAccepted = vpnIntent == null - )) + private val _appUiState = MutableStateFlow( + AppUiState( + vpnPermissionAccepted = vpnIntent == null, + ), + ) val appUiState = _appUiState.asStateFlow() - - - fun isRequiredPermissionGranted() : Boolean { - val allAccepted = (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted) - if(!allAccepted) requestPermissions() + + + fun isRequiredPermissionGranted(): Boolean { + val allAccepted = + (_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted) + if (!allAccepted) requestPermissions() return allAccepted } - + private fun requestPermissions() { _appUiState.value = _appUiState.value.copy( - requestPermissions = true + requestPermissions = true, ) } fun permissionsRequested() { _appUiState.value = _appUiState.value.copy( - requestPermissions = false + requestPermissions = false, ) } @@ -71,10 +72,10 @@ constructor( showSnackbarMessage(application.getString(R.string.no_browser_detected)) } } - + fun onVpnPermissionAccepted() { _appUiState.value = _appUiState.value.copy( - vpnPermissionAccepted = true + vpnPermissionAccepted = true, ) } @@ -90,37 +91,41 @@ constructor( application.startActivity( Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) + }, + ) } catch (e: ActivityNotFoundException) { Timber.e(e) showSnackbarMessage(application.getString(R.string.no_email_detected)) } } - fun showSnackbarMessage(message : String) { + + fun showSnackbarMessage(message: String) { _appUiState.value = _appUiState.value.copy( snackbarMessage = message, - snackbarMessageConsumed = false + snackbarMessageConsumed = false, ) } fun snackbarMessageConsumed() { _appUiState.value = _appUiState.value.copy( snackbarMessage = "", - snackbarMessageConsumed = true + snackbarMessageConsumed = true, ) } + val logs = mutableStateListOf() - fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) { - launch { - Logcatter.logs { - logs.add(it) - if (logs.size > Constants.LOG_BUFFER_SIZE) { - logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) + fun readLogCatOutput() = + viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) { + launch { + Logcatter.logs { + logs.add(it) + if (logs.size > Constants.LOG_BUFFER_SIZE) { + logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) + } } } } - } fun clearLogs() { logs.clear() @@ -131,12 +136,13 @@ constructor( val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" val content = logs.joinToString(separator = "\n") FileUtils.saveFileToDownloads(application.applicationContext, content, fileName) - Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show() + Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT) + .show() } fun setNotificationPermissionAccepted(accepted: Boolean) { _appUiState.value = _appUiState.value.copy( - notificationPermissionAccepted = accepted + notificationPermissionAccepted = accepted, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index 210d429..3f00705 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -19,14 +19,10 @@ 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.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties @@ -52,6 +48,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen +import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen @@ -61,7 +58,6 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import xyz.teamgravity.pin_lock_compose.PinLock import xyz.teamgravity.pin_lock_compose.PinManager import javax.inject.Inject @@ -74,6 +70,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var serviceManager: ServiceManager + @OptIn( ExperimentalPermissionsApi::class, ) @@ -85,10 +84,10 @@ class MainActivity : AppCompatActivity() { // load preferences into memory and init data lifecycleScope.launch { dataStoreManager.init() - WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity) val settings = settingsRepository.getSettings() if (settings.isAutoTunnelEnabled) { - ServiceManager.startWatcherService(application.applicationContext) + serviceManager.startWatcherService(application.applicationContext) } } setContent { @@ -101,7 +100,6 @@ class MainActivity : AppCompatActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null - val focusRequester = remember { FocusRequester() } val snackbarHostState = remember { SnackbarHostState() } val vpnActivityResultState = @@ -150,7 +148,7 @@ class MainActivity : AppCompatActivity() { appViewModel.setNotificationPermissionAccepted( notificationPermissionState?.status?.isGranted ?: true, ) - if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput() + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput() } LaunchedEffect(appUiState.snackbarMessageConsumed) { @@ -160,6 +158,8 @@ class MainActivity : AppCompatActivity() { } } + val focusRequester = remember { FocusRequester() } + Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) { snackbarData: SnackbarData -> @@ -176,19 +176,21 @@ class MainActivity : AppCompatActivity() { //TODO refactor modifier = Modifier .focusable() - .focusProperties { when(navBackStackEntry?.destination?.route) { - Screen.Lock.route -> Unit - else -> up = focusRequester } - }, + .focusProperties { + when (navBackStackEntry?.destination?.route) { + Screen.Lock.route -> Unit + else -> up = focusRequester + } + }, bottomBar = { - BottomNavBar( - navController, - listOf( - Screen.Main.navItem, - Screen.Settings.navItem, - Screen.Support.navItem, - ), - ) + BottomNavBar( + navController, + listOf( + Screen.Main.navItem, + Screen.Settings.navItem, + Screen.Support.navItem, + ), + ) }, ) { padding -> NavHost( @@ -215,7 +217,7 @@ class MainActivity : AppCompatActivity() { SettingsScreen( appViewModel = appViewModel, navController = navController, - focusRequester = focusRequester + focusRequester = focusRequester, ) } composable( @@ -235,14 +237,28 @@ class MainActivity : AppCompatActivity() { if (!id.isNullOrBlank()) { ConfigScreen( navController = navController, - id = id, + tunnelId = id, + appViewModel = appViewModel, + focusRequester = focusRequester, + ) + } + } + composable("${Screen.Option.route}/{id}") { + val id = it.arguments?.getString("id") + if (!id.isNullOrBlank()) { + OptionsScreen( + navController = navController, + tunnelId = id, appViewModel = appViewModel, focusRequester = focusRequester, ) } } composable(Screen.Lock.route) { - PinLockScreen(navController = navController, appViewModel = appViewModel) + PinLockScreen( + navController = navController, + appViewModel = appViewModel, + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt index 9dedd02..f49c9f2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Screen.kt @@ -32,9 +32,12 @@ sealed class Screen(val route: String) { route = route, icon = Icons.Rounded.QuestionMark, ) + data object Logs : Screen("support/logs") } data object Config : Screen("config") data object Lock : Screen("lock") + + data object Option : Screen("option") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt index 3115574..3e33f78 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -10,8 +10,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import com.zaneschepke.wireguardautotunnel.R @Composable fun ClickableIconButton( @@ -31,7 +29,10 @@ fun ClickableIconButton( imageVector = icon, contentDescription = icon.name, modifier = - Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable { + Modifier + .size(ButtonDefaults.IconSize) + .weight(1f, false) + .clickable { if (enabled) { onIconClick() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index 54cae1e..87964b2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -34,16 +34,19 @@ fun RowListItem( ) { Box( modifier = - Modifier.animateContentSize() - .clip(RoundedCornerShape(30.dp)) - .combinedClickable( - onClick = { onClick() }, - onLongClick = { onHold() }, - ), + Modifier + .animateContentSize() + .clip(RoundedCornerShape(30.dp)) + .combinedClickable( + onClick = { onClick() }, + onLongClick = { onHold() }, + ), ) { Column { Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -59,8 +62,9 @@ fun RowListItem( statistics?.peers()?.forEach { Row( modifier = - Modifier.fillMaxWidth() - .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), + Modifier + .fillMaxWidth() + .padding(end = 10.dp, bottom = 10.dp, start = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt index 95ade9e..e4302f5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt @@ -63,17 +63,18 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) { }, maxLines = 1, colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - ), + TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ), placeholder = { Text(text = stringResource(R.string.hint_search_packages)) }, textStyle = MaterialTheme.typography.bodySmall, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), modifier = - Modifier.fillMaxWidth() - .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape), + Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.background, shape = RectangleShape), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt index 1226162..616eabd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationTextBox.kt @@ -27,10 +27,10 @@ fun ConfigurationTextBox( maxLines = 1, placeholder = { Text(hint) }, keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - imeAction = ImeAction.Done, - ), + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), keyboardActions = keyboardActions, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt index 80146c0..e65f755 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/config/ConfigurationToggle.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @Composable @@ -21,11 +22,13 @@ fun ConfigurationToggle( modifier: Modifier = Modifier ) { Row( - modifier = Modifier.fillMaxWidth().padding(padding), + modifier = Modifier + .fillMaxWidth() + .padding(padding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(label) + Text(label, textAlign = TextAlign.Start) Switch( modifier = modifier, enabled = enabled, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt index cac6b57..700d24d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -30,9 +30,9 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List + if (showBottomBar) bottomNavItems.forEach { item -> val selected = item.route == backStackEntry.value?.destination?.route NavigationBarItem( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt index bf25174..23540c5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/AuthorizationPrompt.kt @@ -21,26 +21,32 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: ( onError("Biometrics not available") false } + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { onError("Biometrics not created") false } + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> { onError("Biometric hardware not found") false } + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> { onError("Biometric security update required") false } + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { onError("Biometrics not supported") false } + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> { onError("Biometrics status unknown") false } + BiometricManager.BIOMETRIC_SUCCESS -> true else -> false } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt index 50d1216..d8811c5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/prompt/CustomSnackbar.kt @@ -20,10 +20,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel @Composable @@ -35,17 +33,20 @@ fun CustomSnackBar( Snackbar( containerColor = containerColor, modifier = - Modifier.fillMaxWidth( - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f, - ) - .padding(bottom = 100.dp), + Modifier + .fillMaxWidth( + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f, + ) + .padding(bottom = 100.dp), shape = RoundedCornerShape(16.dp), ) { CompositionLocalProvider( LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr, ) { Row( - modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min), + modifier = Modifier + .width(IntrinsicSize.Max) + .height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt index ed72273..97fb5de 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/screen/LoadingScreen.kt @@ -16,7 +16,10 @@ fun LoadingScreen() { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxSize().focusable().padding(), + modifier = Modifier + .fillMaxSize() + .focusable() + .padding(), ) { Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt index 2ddd7d4..334df0d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/LogTypeLabel.kt @@ -12,9 +12,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable -fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) { +fun LogTypeLabel(color: Color, content: @Composable () -> Unit) { Box( - modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) { + modifier = Modifier + .size(20.dp) + .clip(RoundedCornerShape(2.dp)) + .background(color), + contentAlignment = Alignment.Center, + ) { content() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt index 2d56e66..79caabb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/text/SectionTitle.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp fun SectionTitle(title: String, padding: Dp) { Text( title, - textAlign = TextAlign.Center, + textAlign = TextAlign.Start, style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold), modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp), ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt index caf8b7f..76189a5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt @@ -18,11 +18,11 @@ data class InterfaceProxy( addresses = i.addresses.joinToString(", ").trim(), dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), listenPort = - if (i.listenPort.isPresent) { - i.listenPort.get().toString().trim() - } else { - "" - }, + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt index ad5c3cb..abf78d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt @@ -14,23 +14,23 @@ data class PeerProxy( return PeerProxy( publicKey = peer.publicKey.toBase64(), preSharedKey = - if (peer.preSharedKey.isPresent) { - peer.preSharedKey.get().toBase64().trim() - } else { - "" - }, + if (peer.preSharedKey.isPresent) { + peer.preSharedKey.get().toBase64().trim() + } else { + "" + }, persistentKeepalive = - if (peer.persistentKeepalive.isPresent) { - peer.persistentKeepalive.get().toString().trim() - } else { - "" - }, + if (peer.persistentKeepalive.isPresent) { + peer.persistentKeepalive.get().toString().trim() + } else { + "" + }, endpoint = - if (peer.endpoint.isPresent) { - peer.endpoint.get().toString().trim() - } else { - "" - }, + if (peer.endpoint.isPresent) { + peer.endpoint.get().toString().trim() + } else { + "" + }, allowedIps = peer.allowedIps.joinToString(", ").trim(), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index 0e7b035..ead04ad 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -94,7 +94,7 @@ fun ConfigScreen( focusRequester: FocusRequester, navController: NavController, appViewModel: AppViewModel, - id: String + tunnelId: String ) { val context = LocalContext.current val clipboardManager: ClipboardManager = LocalClipboardManager.current @@ -105,7 +105,7 @@ fun ConfigScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { viewModel.init(id) } + LaunchedEffect(Unit) { viewModel.init(tunnelId) } LaunchedEffect(uiState.loading) { if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) { @@ -130,14 +130,14 @@ fun ConfigScreen( val applicationButtonText = buildAnnotatedString { append(stringResource(id = R.string.tunneling_apps)) append(": ") - if (uiState.isAllApplicationsEnabled) { - append(stringResource(id = R.string.all)) - } else { - append("${uiState.checkedPackageNames.size} ") - (if (uiState.include) append(stringResource(id = R.string.included)) else append( - stringResource(id = R.string.excluded), - )) - } + if (uiState.isAllApplicationsEnabled) { + append(stringResource(id = R.string.all)) + } else { + append("${uiState.checkedPackageNames.size} ") + (if (uiState.include) append(stringResource(id = R.string.included)) else append( + stringResource(id = R.string.excluded), + )) + } } if (showAuthPrompt) { @@ -173,8 +173,10 @@ fun ConfigScreen( .fillMaxWidth() .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), ) { - Column(modifier = Modifier - .fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth(), + ) { Row( modifier = Modifier @@ -273,7 +275,7 @@ fun ConfigScreen( modifier = Modifier.fillMaxSize(), checked = (uiState.checkedPackageNames.contains( - pack.packageName + pack.packageName, )), onCheckedChange = { if (it) { @@ -311,11 +313,11 @@ fun ConfigScreen( var fobColor by remember { mutableStateOf(secondaryColor) } FloatingActionButton( modifier = - Modifier.onFocusChanged { - if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - fobColor = if (it.isFocused) hoverColor else secondaryColor - } - }, + Modifier.onFocusChanged { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + fobColor = if (it.isFocused) hoverColor else secondaryColor + } + }, onClick = { viewModel.onSaveAllChanges().let { when (it) { @@ -323,6 +325,7 @@ fun ConfigScreen( appViewModel.showSnackbarMessage(it.data.message) navController.navigate(Screen.Main.route) } + is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) } } @@ -354,14 +357,14 @@ fun ConfigScreen( shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - } else { - Modifier.fillMaxWidth(fillMaxWidth) - }) - .padding(bottom = 10.dp), + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .fillMaxHeight(fillMaxHeight) + .fillMaxWidth(fillMaxWidth) + } else { + Modifier.fillMaxWidth(fillMaxWidth) + }) + .padding(bottom = 10.dp), ) { Column( horizontalAlignment = Alignment.Start, @@ -390,10 +393,10 @@ fun ConfigScreen( .clickable { showAuthPrompt = true }, value = uiState.interfaceProxy.privateKey, visualTransformation = - if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) - VisualTransformation.None - else PasswordVisualTransformation(), - enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, + if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated) + VisualTransformation.None + else PasswordVisualTransformation(), + enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated, onValueChange = { value -> viewModel.onPrivateKeyChange(value) }, trailingIcon = { IconButton( @@ -503,14 +506,14 @@ fun ConfigScreen( shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .fillMaxHeight(fillMaxHeight) - .fillMaxWidth(fillMaxWidth) - } else { - Modifier.fillMaxWidth(fillMaxWidth) - }) - .padding(top = 10.dp, bottom = 10.dp), + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .fillMaxHeight(fillMaxHeight) + .fillMaxWidth(fillMaxWidth) + } else { + Modifier.fillMaxWidth(fillMaxWidth) + }) + .padding(top = 10.dp, bottom = 10.dp), ) { Column( horizontalAlignment = Alignment.Start, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index ff30fa3..b7a4fdc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -7,17 +7,14 @@ import android.content.pm.PackageManager import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wireguard.config.BadConfigException import com.wireguard.config.Config import com.wireguard.config.Interface -import com.wireguard.config.ParseException import com.wireguard.config.Peer import com.wireguard.crypto.Key import com.wireguard.crypto.KeyPair import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy import com.zaneschepke.wireguardautotunnel.util.Constants @@ -30,7 +27,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -40,8 +36,7 @@ class ConfigViewModel @Inject constructor( private val application: Application, - private val tunnelConfigRepository: TunnelConfigRepository, - private val settingsRepository: SettingsRepository, + private val appDataRepository: AppDataRepository ) : ViewModel() { private val packageManager = application.packageManager @@ -55,7 +50,8 @@ constructor( val state = if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) { val tunnelConfig = - tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId } + appDataRepository.tunnels.getAll() + .firstOrNull { it.id.toString() == tunnelId } if (tunnelConfig != null) { val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) val proxyPeers = config.peers.map { PeerProxy.from(it) } @@ -103,7 +99,7 @@ constructor( fun onAddCheckedPackage(packageName: String) { _uiState.value = _uiState.value.copy( - checkedPackageNames = _uiState.value.checkedPackageNames + packageName + checkedPackageNames = _uiState.value.checkedPackageNames + packageName, ) } @@ -114,7 +110,7 @@ constructor( fun onRemoveCheckedPackage(packageName: String) { _uiState.value = _uiState.value.copy( - checkedPackageNames = _uiState.value.checkedPackageNames - packageName + checkedPackageNames = _uiState.value.checkedPackageNames - packageName, ) } @@ -148,26 +144,16 @@ constructor( } private fun saveConfig(tunnelConfig: TunnelConfig) = - viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) } + viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) } private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) = viewModelScope.launch { if (tunnelConfig != null) { saveConfig(tunnelConfig).join() - WireGuardAutoTunnel.requestTileServiceStateUpdate(application) - updateSettingsDefaultTunnel(tunnelConfig) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) } } - private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) { - val settings = settingsRepository.getSettingsFlow().first() - if (settings.defaultTunnel != null) { - if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) { - settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString())) - } - } - } - private fun buildPeerListFromProxyPeers(): List { return _uiState.value.proxyPeers.map { val builder = Peer.Builder() @@ -209,8 +195,12 @@ constructor( val peerList = buildPeerListFromProxyPeers() val wgInterface = buildInterfaceListFromProxyInterface() val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() - val tunnelConfig = when(uiState.value.tunnel) { - null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString()) + val tunnelConfig = when (uiState.value.tunnel) { + null -> TunnelConfig( + name = _uiState.value.tunnelName, + wgQuick = config.toWgQuickString(), + ) + else -> uiState.value.tunnel!!.copy( name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString(), @@ -229,10 +219,10 @@ constructor( _uiState.value = _uiState.value.copy( proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(publicKey = value), - ), + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(publicKey = value), + ), ) } @@ -240,10 +230,10 @@ constructor( _uiState.value = _uiState.value.copy( proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(preSharedKey = value), - ), + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(preSharedKey = value), + ), ) } @@ -251,10 +241,10 @@ constructor( _uiState.value = _uiState.value.copy( proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(endpoint = value), - ), + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(endpoint = value), + ), ) } @@ -262,10 +252,10 @@ constructor( _uiState.value = _uiState.value.copy( proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(allowedIps = value), - ), + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(allowedIps = value), + ), ) } @@ -273,10 +263,10 @@ constructor( _uiState.value = _uiState.value.copy( proxyPeers = - _uiState.value.proxyPeers.update( - index, - _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), - ), + _uiState.value.proxyPeers.update( + index, + _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), + ), ) } @@ -296,31 +286,31 @@ constructor( _uiState.value = _uiState.value.copy( interfaceProxy = - _uiState.value.interfaceProxy.copy( - privateKey = keyPair.privateKey.toBase64(), - publicKey = keyPair.publicKey.toBase64(), - ), + _uiState.value.interfaceProxy.copy( + privateKey = keyPair.privateKey.toBase64(), + publicKey = keyPair.publicKey.toBase64(), + ), ) } fun onAddressesChanged(value: String) { _uiState.value = _uiState.value.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), ) } fun onListenPortChanged(value: String) { _uiState.value = _uiState.value.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), ) } fun onDnsServersChanged(value: String) { _uiState.value = _uiState.value.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), ) } @@ -332,14 +322,14 @@ constructor( private fun onInterfacePublicKeyChange(value: String) { _uiState.value = _uiState.value.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), ) } fun onPrivateKeyChange(value: String) { _uiState.value = _uiState.value.copy( - interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value) + interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), ) if (NumberUtils.isValidKey(value)) { val pair = KeyPair(Key.fromBase64(value)) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 32f1c73..a2282fb 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -38,8 +38,9 @@ import androidx.compose.material.icons.rounded.Bolt import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.CopyAll import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Edit 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.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api @@ -123,7 +124,6 @@ fun MainScreen( val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { mutableStateOf(false) } - var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var selectedTunnel by remember { mutableStateOf(null) } val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -197,30 +197,6 @@ fun MainScreen( }, ) - AnimatedVisibility(showPrimaryChangeAlertDialog) { - AlertDialog( - onDismissRequest = { showPrimaryChangeAlertDialog = false }, - confirmButton = { - TextButton( - onClick = { - viewModel.onDefaultTunnelChange(selectedTunnel) - showPrimaryChangeAlertDialog = false - selectedTunnel = null - }, - ) { - Text(text = stringResource(R.string.okay)) - } - }, - dismissButton = { - TextButton(onClick = { showPrimaryChangeAlertDialog = false }) { - Text(text = stringResource(R.string.cancel)) - } - }, - title = { Text(text = stringResource(R.string.primary_tunnel_change)) }, - text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) }, - ) - } - AnimatedVisibility(showDeleteTunnelAlertDialog) { AlertDialog( onDismissRequest = { showDeleteTunnelAlertDialog = false }, @@ -246,12 +222,12 @@ fun MainScreen( } fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { - if(appViewModel.isRequiredPermissionGranted()) { + if (appViewModel.isRequiredPermissionGranted()) { if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() } } - if(uiState.loading) { + if (uiState.loading) { return LoadingScreen() } @@ -426,7 +402,7 @@ fun MainScreen( icon, icon.name, modifier = Modifier - .padding(end = 10.dp) + .padding(end = 8.5.dp) .size(25.dp), tint = if (uiState.settings.isAutoTunnelPaused) Color.Gray @@ -462,7 +438,7 @@ fun MainScreen( ) { tunnel -> val leadingIconColor = (if ( - uiState.vpnState.name == tunnel.name && + uiState.vpnState.tunnelConfig?.name == tunnel.name && uiState.vpnState.status == Tunnel.State.UP ) { uiState.vpnState.statistics @@ -486,31 +462,31 @@ fun MainScreen( val expanded = remember { mutableStateOf(false) } RowListItem( icon = { - if (uiState.settings.isTunnelConfigDefault(tunnel)) { - Icon( - Icons.Rounded.Star, - stringResource(R.string.status), - tint = leadingIconColor, - modifier = Modifier - .padding(end = 10.dp) - .size(20.dp), - ) + val circleIcon = Icons.Rounded.Circle + val icon = if (tunnel.isPrimaryTunnel) { + Icons.Rounded.Star + } else if (tunnel.isMobileDataTunnel) { + Icons.Rounded.Smartphone } else { - Icon( - Icons.Rounded.Circle, - stringResource(R.string.status), - tint = leadingIconColor, - modifier = Modifier - .padding(end = 15.dp) - .size(15.dp), - ) + circleIcon } + Icon( + icon, + icon.name, + tint = leadingIconColor, + modifier = Modifier + .padding( + end = if (icon == circleIcon) 12.5.dp else 10.dp, + start = if (icon == circleIcon) 2.5.dp else 0.dp, + ) + .size(if (icon == circleIcon) 15.dp else 20.dp), + ) }, - text = tunnel.name.truncateWithEllipsis(15), + text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH), onHold = { if ( (uiState.vpnState.status == Tunnel.State.UP) && - (tunnel.name == uiState.vpnState.name) + (tunnel.name == uiState.vpnState.tunnelConfig?.name) ) { appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) return@RowListItem @@ -522,7 +498,7 @@ fun MainScreen( if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if ( uiState.vpnState.status == Tunnel.State.UP && - (uiState.vpnState.name == tunnel.name) + (uiState.vpnState.tunnelConfig?.name == tunnel.name) ) { expanded.value = !expanded.value } @@ -539,48 +515,27 @@ fun MainScreen( !WireGuardAutoTunnel.isRunningOnAndroidTv() ) { Row { - if (!uiState.settings.isTunnelConfigDefault(tunnel)) { - IconButton( - onClick = { - if ( - uiState.settings.isAutoTunnelEnabled && - !uiState.settings.isAutoTunnelPaused - ) { - appViewModel.showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message, - ) - } else { - showPrimaryChangeAlertDialog = true - } - }, - ) { - val icon = Icons.Rounded.Star - Icon( - icon, - icon.name, - ) - } - } IconButton( onClick = { if ( uiState.settings.isAutoTunnelEnabled && - uiState.settings.isTunnelConfigDefault( - tunnel, - ) && !uiState.settings.isAutoTunnelPaused ) { appViewModel.showSnackbarMessage( Event.Message.AutoTunnelOffAction.message, ) - } else + } else { navController.navigate( - "${Screen.Config.route}/${selectedTunnel?.id}", + "${Screen.Option.route}/${selectedTunnel?.id}", ) + } }, ) { - val icon = Icons.Rounded.Edit - Icon(icon, icon.name) + val icon = Icons.Rounded.Settings + Icon( + icon, + icon.name, + ) } IconButton( modifier = Modifier.focusable(), @@ -601,7 +556,7 @@ fun MainScreen( val checked by remember { derivedStateOf { (uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name) + tunnel.name == uiState.vpnState.tunnelConfig?.name) } } if (!checked) expanded.value = false @@ -618,32 +573,32 @@ fun MainScreen( ) if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Row { - if (!uiState.settings.isTunnelConfigDefault(tunnel)) { - IconButton( - onClick = { - if (uiState.settings.isAutoTunnelEnabled) { - appViewModel.showSnackbarMessage( - Event.Message.AutoTunnelOffAction.message, - ) - } else { - selectedTunnel = tunnel - showPrimaryChangeAlertDialog = true - } - }, - ) { - val icon = Icons.Rounded.Star - Icon( - icon, - icon.name, - ) - } + IconButton( + onClick = { + if (uiState.settings.isAutoTunnelEnabled) { + appViewModel.showSnackbarMessage( + Event.Message.AutoTunnelOffAction.message, + ) + } else { + selectedTunnel = tunnel + navController.navigate( + "${Screen.Option.route}/${selectedTunnel?.id}", + ) + } + }, + ) { + val icon = Icons.Rounded.Settings + Icon( + icon, + icon.name, + ) } IconButton( modifier = Modifier.focusRequester(focusRequester), onClick = { if ( uiState.vpnState.status == Tunnel.State.UP && - (uiState.vpnState.name == tunnel.name) + (uiState.vpnState.tunnelConfig?.name == tunnel.name) ) { expanded.value = !expanded.value } else { @@ -656,25 +611,6 @@ fun MainScreen( val icon = Icons.Rounded.Info Icon(icon, icon.name) } - IconButton( - onClick = { - if ( - uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name - ) { - appViewModel.showSnackbarMessage( - Event.Message.TunnelOffAction.message, - ) - } else { - navController.navigate( - "${Screen.Config.route}/${tunnel.id}", - ) - } - }, - ) { - val icon = Icons.Rounded.Edit - Icon(icon, icon.name) - } IconButton( onClick = { viewModel.onCopyTunnel(tunnel) }, ) { @@ -685,7 +621,7 @@ fun MainScreen( onClick = { if ( uiState.vpnState.status == Tunnel.State.UP && - tunnel.name == uiState.vpnState.name + tunnel.name == uiState.vpnState.tunnelConfig?.name ) { appViewModel.showSnackbarMessage( Event.Message.TunnelOffAction.message, @@ -699,7 +635,7 @@ fun MainScreen( val icon = Icons.Rounded.Delete Icon( icon, - icon.name + icon.name, ) } TunnelSwitch() diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index 52d041b..c18a73f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -7,13 +7,11 @@ import android.net.Uri import android.provider.OpenableColumns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.zxing.common.StringUtils import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.model.Settings import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +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.util.Constants @@ -22,8 +20,6 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.Result import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -39,19 +35,19 @@ class MainViewModel @Inject constructor( private val application: Application, - private val tunnelConfigRepository: TunnelConfigRepository, - private val settingsRepository: SettingsRepository, - private val vpnService: VpnService + private val appDataRepository: AppDataRepository, + private val serviceManager: ServiceManager, + val vpnService: VpnService ) : ViewModel() { val uiState = combine( - settingsRepository.getSettingsFlow(), - tunnelConfigRepository.getTunnelConfigsFlow(), - vpnService.vpnState, - ) { settings, tunnels, vpnState -> - MainUiState(settings, tunnels, vpnState, false) - } + appDataRepository.settings.getSettingsFlow(), + appDataRepository.tunnels.getTunnelConfigsFlow(), + vpnService.vpnState, + ) { settings, tunnels, vpnState -> + MainUiState(settings, tunnels, vpnState, false) + } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), @@ -60,48 +56,46 @@ constructor( private fun stopWatcherService() = viewModelScope.launch(Dispatchers.IO) { - ServiceManager.stopWatcherService(application.applicationContext) + serviceManager.stopWatcherService(application.applicationContext) } fun onDelete(tunnel: TunnelConfig) { viewModelScope.launch(Dispatchers.IO) { - val settings = settingsRepository.getSettings() - val isDefault = settings.isTunnelConfigDefault(tunnel) - if (tunnelConfigRepository.count() == 1 || isDefault) { + val settings = appDataRepository.settings.getSettings() + val isPrimary = tunnel.isPrimaryTunnel + if (appDataRepository.tunnels.count() == 1 || isPrimary) { stopWatcherService() - settings.defaultTunnel = null - settings.isAutoTunnelEnabled = false - settings.isAlwaysOnVpnEnabled = false - saveSettings(settings) + resetTunnelSetting(settings) } - tunnelConfigRepository.delete(tunnel) - WireGuardAutoTunnel.requestTileServiceStateUpdate(application) + appDataRepository.tunnels.delete(tunnel) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) } } + private fun resetTunnelSetting(settings: Settings) { + saveSettings( + settings.copy( + isAutoTunnelEnabled = false, + isAlwaysOnVpnEnabled = false, + ), + ) + } + fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch(Dispatchers.IO) { Timber.d("On start called!") - stopActiveTunnel().await() - startTunnel(tunnelConfig) + serviceManager.startVpnService( + application.applicationContext, + tunnelConfig.id, + isManualStart = true, + ) } - private fun startTunnel(tunnelConfig: TunnelConfig) = - viewModelScope.launch(Dispatchers.IO) { - Timber.d("Start tunnel via manager") - ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) - } - - private fun stopActiveTunnel() = - viewModelScope.async(Dispatchers.IO) { - onTunnelStop() - delay(Constants.TOGGLE_TUNNEL_DELAY) - } fun onTunnelStop() = viewModelScope.launch(Dispatchers.IO) { Timber.i("Stopping active tunnel") - ServiceManager.stopVpnService(application.applicationContext) + serviceManager.stopVpnService(application.applicationContext, isManualStop = true) } private fun validateConfigString(config: String) { @@ -145,6 +139,7 @@ constructor( is Result.Success -> return it } } + Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri) else -> return Result.Error(Event.Error.InvalidFileExtension) } @@ -186,23 +181,25 @@ constructor( } private suspend fun addTunnel(tunnelConfig: TunnelConfig) { - val firstTunnel = tunnelConfigRepository.count() == 0 + val firstTunnel = appDataRepository.tunnels.count() == 0 saveTunnel(tunnelConfig) - if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application) + if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application) } fun pauseAutoTunneling() = viewModelScope.launch { - settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) + appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true)) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) } fun resumeAutoTunneling() = viewModelScope.launch { - settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) + appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false)) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) } private suspend fun saveTunnel(tunnelConfig: TunnelConfig) { - tunnelConfigRepository.save(tunnelConfig) + appDataRepository.tunnels.save(tunnelConfig) } private fun getFileNameByCursor(context: Context, uri: Uri): String? { @@ -252,20 +249,17 @@ constructor( } private fun saveSettings(settings: Settings) = - viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) } + viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) } - fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) = - viewModelScope.launch { - if (selectedTunnel != null) { - saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString())) - .join() - WireGuardAutoTunnel.requestTileServiceStateUpdate(application) - } - } fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch { tunnel?.let { - saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick)) + saveTunnel( + TunnelConfig( + name = it.name.plus(NumberUtils.randomThree()), + wgQuick = it.wgQuick, + ), + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt new file mode 100644 index 0000000..bfcd161 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsScreen.kt @@ -0,0 +1,287 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.options + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +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.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.ui.common.ClickableIconButton +import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle +import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle +import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.Result +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun OptionsScreen( + optionsViewModel: OptionsViewModel = hiltViewModel(), + navController: NavController, + appViewModel: AppViewModel, + focusRequester: FocusRequester, + tunnelId: String +) { + val scrollState = rememberScrollState() + val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle() + val interactionSource = remember { MutableInteractionSource() } + val scope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + val screenPadding = 5.dp + val fillMaxWidth = .85f + + var currentText by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + optionsViewModel.init(tunnelId) + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + delay(Constants.FOCUS_REQUEST_DELAY) + focusRequester.requestFocus() + } + } + + fun saveTrustedSSID() { + if (currentText.isNotEmpty()) { + scope.launch { + optionsViewModel.onSaveRunSSID(currentText).let { + when (it) { + is Result.Success -> currentText = "" + is Result.Error -> appViewModel.showSnackbarMessage(it.error.message) + } + } + } + } + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + }) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.general), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.set_primary_tunnel), + enabled = true, + checked = uiState.isDefaultTunnel, + modifier = Modifier + .focusRequester(focusRequester), + padding = screenPadding, + onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() }, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + onClick = { + navController.navigate( + "${Screen.Config.route}/${tunnelId}", + ) + }, + ) { + Text(stringResource(R.string.edit_tunnel)) + } + } + } + } + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + }) + .padding(bottom = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), + ) { + SectionTitle( + title = stringResource(id = R.string.auto_tunneling), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.mobile_data_tunnel), + enabled = true, + checked = uiState.tunnel?.isMobileDataTunnel == true, + padding = screenPadding, + onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() }, + ) + Column { + FlowRow( + modifier = Modifier + .padding(screenPadding) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + uiState.tunnel?.tunnelNetworks?.forEach { ssid -> + ClickableIconButton( + onClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + } + }, + onIconClick = { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus() + optionsViewModel.onDeleteRunSSID(ssid) + + }, + text = ssid, + icon = Icons.Filled.Close, + enabled = true, + ) + } + if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) { + Text( + stringResource(R.string.no_wifi_names_configured), + fontStyle = FontStyle.Italic, + color = Color.Gray, + ) + } + } + OutlinedTextField( + enabled = true, + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, + modifier = + Modifier + .padding( + start = screenPadding, + top = 5.dp, + bottom = 10.dp, + ), + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }), + trailingIcon = { + if (currentText != "") { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = + if (currentText == "") { + stringResource( + id = + R.string + .trusted_ssid_empty_description, + ) + } else { + stringResource( + id = + R.string + .trusted_ssid_value_description, + ) + }, + tint = MaterialTheme.colorScheme.primary, + ) + } + } + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt new file mode 100644 index 0000000..ded1d5f --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt @@ -0,0 +1,9 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.options + +import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig + +data class OptionsUiState( + val id: String? = null, + val tunnel: TunnelConfig? = null, + val isDefaultTunnel: Boolean = false +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt new file mode 100644 index 0000000..8d874ed --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -0,0 +1,101 @@ +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.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.util.Event +import com.zaneschepke.wireguardautotunnel.util.Result +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class OptionsViewModel @Inject +constructor( + private val appDataRepository: AppDataRepository +) : ViewModel() { + + private val _optionState = MutableStateFlow(OptionsUiState()) + + val uiState = combine( + appDataRepository.tunnels.getTunnelConfigsFlow(), + _optionState, + ) { tunnels, optionState -> + if (optionState.id != null) { + val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id } + val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true + OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel) + } else OptionsUiState() + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), + OptionsUiState(), + ) + + fun init(tunnelId: String) { + _optionState.value = _optionState.value.copy( + id = tunnelId, + ) + } + + fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) { + uiState.value.tunnel?.let { + appDataRepository.tunnels.save( + tunnelConfig = it.copy( + tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(), + ), + ) + } + } + + private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) { + tunnelConfig?.let { + appDataRepository.tunnels.save(it) + } + } + + suspend fun onSaveRunSSID(ssid: String): Result { + val trimmed = ssid.trim() + val tunnelsWithName = withContext(viewModelScope.coroutineContext) { + appDataRepository.tunnels.findByTunnelNetworksName(trimmed) + } + return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true && + tunnelsWithName.isEmpty()) { + uiState.value.tunnel?.tunnelNetworks?.add(trimmed) + saveTunnel(uiState.value.tunnel) + Result.Success(Unit) + } else { + Result.Error(Event.Error.SsidConflict) + } + } + + fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) { + uiState.value.tunnel?.let { + if (it.isMobileDataTunnel) { + appDataRepository.tunnels.updateMobileDataTunnel(null) + } else appDataRepository.tunnels.updateMobileDataTunnel(it) + } + } + + fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) { + if (uiState.value.tunnel != null) { + appDataRepository.tunnels.updatePrimaryTunnel( + when (uiState.value.isDefaultTunnel) { + true -> null + false -> uiState.value.tunnel + }, + ) + WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt index 54f9a7c..1e51da3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/pinlock/PinLockScreen.kt @@ -27,11 +27,11 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) { color = MaterialTheme.colorScheme.surface, onPinCorrect = { // pin is correct, navigate or hide pin lock - if(WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { navController.navigate(Screen.Main.route) } else { val isPopped = navController.popBackStack() - if(!isPopped) { + if (!isPopped) { navController.navigate(Screen.Main.route) } } @@ -39,11 +39,15 @@ fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) { }, onPinIncorrect = { // pin is incorrect, show error - appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.incorrect_pin).asString(context)) + appViewModel.showSnackbarMessage( + StringValue.StringResource(R.string.incorrect_pin).asString(context), + ) }, onPinCreated = { // pin created for the first time, navigate or hide pin lock - appViewModel.showSnackbarMessage(StringValue.StringResource(R.string.pin_created).asString(context)) + appViewModel.showSnackbarMessage( + StringValue.StringResource(R.string.pin_created).asString(context), + ) }, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 8b4c839..21c5180 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -160,7 +160,7 @@ fun SettingsScreen( fun handleAutoTunnelToggle() { if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) { if (appViewModel.isRequiredPermissionGranted()) { - viewModel.toggleAutoTunnel() + viewModel.onToggleAutoTunnel() } } else { requestBatteryOptimizationsDisabled() @@ -247,15 +247,15 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState), + .fillMaxSize() + .verticalScroll(scrollState), ) { Icon( Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier - .padding(30.dp) - .size(128.dp), + .padding(30.dp) + .size(128.dp), ) Text( stringResource(R.string.prominent_background_location_title), @@ -273,12 +273,12 @@ fun SettingsScreen( modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Modifier - .fillMaxWidth() - .padding(10.dp) + .fillMaxWidth() + .padding(10.dp) } else { Modifier - .fillMaxWidth() - .padding(30.dp) + .fillMaxWidth() + .padding(30.dp) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, @@ -336,14 +336,14 @@ fun SettingsScreen( verticalArrangement = Arrangement.Top, modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .clickable( - indication = null, - interactionSource = interactionSource, - ) { - focusManager.clearFocus() - }, + .fillMaxSize() + .verticalScroll(scrollState) + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusManager.clearFocus() + }, ) { Surface( tonalElevation = 2.dp, @@ -353,13 +353,13 @@ fun SettingsScreen( modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) } else { Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) }) .padding(bottom = 10.dp), ) { @@ -390,8 +390,8 @@ fun SettingsScreen( Column { FlowRow( modifier = Modifier - .padding(screenPadding) - .fillMaxWidth(), + .padding(screenPadding) + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(5.dp), ) { uiState.settings.trustedNetworkSSIDs.forEach { ssid -> @@ -503,10 +503,10 @@ fun SettingsScreen( (if (!uiState.settings.isAutoTunnelEnabled) Modifier else Modifier.focusRequester( - focusRequester, + focusRequester, )) - .fillMaxSize() - .padding(top = 5.dp), + .fillMaxSize() + .padding(top = 5.dp), horizontalArrangement = Arrangement.Center, ) { TextButton( @@ -557,8 +557,8 @@ fun SettingsScreen( shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, modifier = Modifier - .fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp), + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp), ) { Column( horizontalAlignment = Alignment.Start, @@ -596,9 +596,9 @@ fun SettingsScreen( color = MaterialTheme.colorScheme.surface, modifier = Modifier - .fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp) - .padding(bottom = 140.dp), + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp) + .padding(bottom = 140.dp), ) { Column( horizontalAlignment = Alignment.Start, @@ -609,7 +609,7 @@ fun SettingsScreen( title = stringResource(id = R.string.other), padding = screenPadding, ) - if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { ConfigurationToggle( stringResource(R.string.always_on_vpn_support), enabled = !uiState.settings.isAutoTunnelEnabled, @@ -639,7 +639,7 @@ fun SettingsScreen( } }, ) - if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index fff235b..d71ce51 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -7,10 +7,9 @@ import androidx.core.location.LocationManagerCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.android.util.RootShell -import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager +import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository -import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository +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.util.Constants @@ -29,28 +28,27 @@ class SettingsViewModel @Inject constructor( private val application: Application, - private val tunnelConfigRepository: TunnelConfigRepository, - private val settingsRepository: SettingsRepository, - private val dataStoreManager: DataStoreManager, + private val appDataRepository: AppDataRepository, + private val serviceManager: ServiceManager, private val rootShell: RootShell, - private val vpnService: VpnService + vpnService: VpnService ) : ViewModel() { val uiState = combine( - settingsRepository.getSettingsFlow(), - tunnelConfigRepository.getTunnelConfigsFlow(), - vpnService.vpnState, - dataStoreManager.preferencesFlow, - ) { settings, tunnels, tunnelState, preferences -> - SettingsUiState( - settings, - tunnels, - tunnelState, - preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false, - preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false, - ) - } + appDataRepository.settings.getSettingsFlow(), + appDataRepository.tunnels.getTunnelConfigsFlow(), + vpnService.vpnState, + appDataRepository.appState.generalStateFlow, + ) { settings, tunnels, tunnelState, generalState -> + SettingsUiState( + settings, + tunnels, + tunnelState, + generalState.locationDisclosureShown, + generalState.batteryOptimizationDisableShown, + ) + } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), @@ -70,12 +68,12 @@ constructor( fun setLocationDisclosureShown() = viewModelScope.launch { - dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true) + appDataRepository.appState.setLocationDisclosureShown(true) } fun setBatteryOptimizeDisableShown() = viewModelScope.launch { - dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true) + appDataRepository.appState.setBatteryOptimizationDisableShown(true) } fun onToggleTunnelOnMobileData() { @@ -90,48 +88,42 @@ constructor( saveSettings( uiState.value.settings.copy( trustedNetworkSSIDs = - (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(), + (uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(), ), ) } - private suspend fun getDefaultTunnelOrFirst(): String { - return uiState.value.settings.defaultTunnel - ?: tunnelConfigRepository.getAll().first().toString() - } - - fun toggleAutoTunnel() = + fun onToggleAutoTunnel() = viewModelScope.launch { val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused if (isAutoTunnelEnabled) { - ServiceManager.stopWatcherService(application) + serviceManager.stopWatcherService(application) } else { - ServiceManager.startWatcherService(application) + serviceManager.startWatcherService(application) isAutoTunnelPaused = false } saveSettings( uiState.value.settings.copy( isAutoTunnelEnabled = !isAutoTunnelEnabled, isAutoTunnelPaused = isAutoTunnelPaused, - defaultTunnel = getDefaultTunnelOrFirst(), ), ) + WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application) } fun onToggleAlwaysOnVPN() = viewModelScope.launch { - val updatedSettings = + saveSettings( uiState.value.settings.copy( isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled, - defaultTunnel = getDefaultTunnelOrFirst(), - ) - saveSettings(updatedSettings) + ), + ) } private fun saveSettings(settings: Settings) = - viewModelScope.launch { settingsRepository.save(settings) } + viewModelScope.launch { appDataRepository.settings.save(settings) } fun onToggleTunnelOnEthernet() { saveSettings( @@ -154,14 +146,6 @@ constructor( ) } - fun onToggleBatterySaver() { - saveSettings( - uiState.value.settings.copy( - isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled, - ), - ) - } - private fun saveKernelMode(on: Boolean) { saveSettings( uiState.value.settings.copy( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index 45cf1b9..c95ceb6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -50,7 +50,6 @@ 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.screen.LoadingScreen @Composable fun SupportScreen( @@ -71,7 +70,7 @@ fun SupportScreen( Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .focusable() + .focusable(), ) { Surface( tonalElevation = 2.dp, @@ -79,17 +78,17 @@ fun SupportScreen( shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surface, modifier = - (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { - Modifier - .height(IntrinsicSize.Min) - .fillMaxWidth(fillMaxWidth) - .padding(top = 10.dp) - } else { - Modifier - .fillMaxWidth(fillMaxWidth) - .padding(top = 20.dp) - }) - .padding(bottom = 25.dp), + (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth(fillMaxWidth) + .padding(top = 10.dp) + } else { + Modifier + .fillMaxWidth(fillMaxWidth) + .padding(top = 20.dp) + }) + .padding(bottom = 25.dp), ) { Column(modifier = Modifier.padding(20.dp)) { val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward @@ -128,13 +127,13 @@ fun SupportScreen( } Icon( forwardIcon, - forwardIcon.name + forwardIcon.name, ) } } HorizontalDivider( thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ) TextButton( onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) }, @@ -160,13 +159,13 @@ fun SupportScreen( } Icon( forwardIcon, - forwardIcon.name + forwardIcon.name, ) } } HorizontalDivider( thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ) TextButton( onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) }, @@ -192,13 +191,13 @@ fun SupportScreen( } Icon( forwardIcon, - forwardIcon.name + forwardIcon.name, ) } } HorizontalDivider( thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ) TextButton( onClick = { appViewModel.launchEmail() }, @@ -220,14 +219,14 @@ fun SupportScreen( } Icon( forwardIcon, - forwardIcon.name + forwardIcon.name, ) } } - if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { HorizontalDivider( thickness = 0.5.dp, - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ) TextButton( onClick = { navController.navigate(Screen.Support.Logs.route) }, @@ -249,7 +248,7 @@ fun SupportScreen( } Icon( Icons.AutoMirrored.Rounded.ArrowForward, - stringResource(id = R.string.go) + stringResource(id = R.string.go), ) } } @@ -262,9 +261,9 @@ fun SupportScreen( style = TextStyle(textDecoration = TextDecoration.Underline), fontSize = 16.sp, modifier = - Modifier.clickable { - appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url)) - }, + Modifier.clickable { + appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url)) + }, ) Row( horizontalArrangement = Arrangement.spacedBy(25.dp), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt index 8b36ae4..7b14d9a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportViewModel.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel -class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) : +class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) : ViewModel() { val uiState = diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index 905ad54..0de0ba3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.text.AnnotatedString 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 com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel import kotlinx.coroutines.launch @@ -48,7 +47,7 @@ fun LogsScreen(appViewModel: AppViewModel) { val clipboardManager: ClipboardManager = LocalClipboardManager.current val scope = rememberCoroutineScope() - LaunchedEffect(logs.size){ + LaunchedEffect(logs.size) { scope.launch { lazyColumnListState.animateScrollToItem(logs.size) } @@ -61,16 +60,16 @@ fun LogsScreen(appViewModel: AppViewModel) { appViewModel.saveLogsToFile() }, shape = RoundedCornerShape(16.dp), - containerColor = MaterialTheme.colorScheme.primary + containerColor = MaterialTheme.colorScheme.primary, ) { val icon = Icons.Filled.Save Icon( imageVector = icon, contentDescription = icon.name, - tint = MaterialTheme.colorScheme.onPrimary + tint = MaterialTheme.colorScheme.onPrimary, ) } - } + }, ) { LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, @@ -78,9 +77,12 @@ fun LogsScreen(appViewModel: AppViewModel) { state = lazyColumnListState, modifier = Modifier .fillMaxSize() - .padding(horizontal = 24.dp)) { + .padding(horizontal = 24.dp), + ) { items(logs) { - Row(horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top, + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), + verticalAlignment = Alignment.Top, modifier = Modifier .fillMaxSize() .clickable( @@ -88,13 +90,17 @@ fun LogsScreen(appViewModel: AppViewModel) { indication = null, onClick = { clipboardManager.setText(annotatedString = AnnotatedString(it.toString())) - } - ) + }, + ), ) { val fontSize = 10.sp Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) LogTypeLabel(color = Color(it.level.color())) { - Text(text = it.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize) + Text( + text = it.level.signifier, + textAlign = TextAlign.Center, + fontSize = fontSize, + ) } Text("${it.message} - ${it.time}", fontSize = fontSize) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt index 05b3514..51f23a8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt @@ -57,6 +57,7 @@ fun WireguardAutoTunnelTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } + darkTheme -> DarkColorScheme else -> LightColorScheme } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt index 9eac359..6fad95b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt @@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp val Typography = Typography( bodyLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index 3361b09..ea1d356 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -10,6 +10,7 @@ object Constants { 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" const val URI_CONTENT_SCHEME = "content" @@ -17,6 +18,7 @@ object Constants { const val TEXT_MIME_TYPE = "text/plain" 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 EMAIL_MIME_TYPE = "message/rfc822" const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 @@ -29,4 +31,8 @@ object Constants { const val PING_INTERVAL = 60_000L const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour + const val ALLOWED_DISPLAY_NAME_LENGTH = 20 + + const val TUNNEL_EXTRA_KEY = "tunnelId" + } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt index 92e626c..d4019c6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Event.kt @@ -18,7 +18,7 @@ sealed class Event { get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists) } - data class ConfigParseError(val appendedMessage : String) : Error() { + data class ConfigParseError(val appendedMessage: String) : Error() { override val message: String = WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + ( if (appendedMessage != "") ": ${appendedMessage.trim()}" else "") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index 9648477..e82ea9f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -31,8 +31,8 @@ fun BroadcastReceiver.goAsync( } } -fun String.truncateWithEllipsis(allowedLength : Int) : String { - return if(this.length > allowedLength + 3) { +fun String.truncateWithEllipsis(allowedLength: Int): String { + return if (this.length > allowedLength + 3) { this.substring(0, allowedLength) + "***" } else this } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt index 2e389f8..8afd01a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/FileUtils.kt @@ -16,6 +16,7 @@ import java.util.zip.ZipOutputStream object FileUtils { private const val ZIP_FILE_MIME_TYPE = "application/zip" + //TODO issue with android 9 private fun createDownloadsFileOutputStream( context: Context, fileName: String, @@ -45,11 +46,11 @@ object FileUtils { } fun saveFileToDownloads(context: Context, content: String, fileName: String) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + put(MediaColumns.DISPLAY_NAME, fileName) + put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE) + put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } val resolver = context.contentResolver val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) @@ -61,7 +62,7 @@ object FileUtils { } else { val target = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName + fileName, ) FileOutputStream(target).use { output -> output.write(content.toByteArray()) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt index d60f510..0b3eaa6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -22,11 +22,11 @@ object NumberUtils { return "tunnel${randomFive()}" } - private fun randomFive() : Int { + private fun randomFive(): Int { return (Math.random() * 100000).toInt() } - fun randomThree() : Int { + fun randomThree(): Int { return (Math.random() * 1000).toInt() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt index cf5f729..2cd1fd6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/ReleaseTree.kt @@ -1,13 +1,17 @@ package com.zaneschepke.wireguardautotunnel.util -import android.util.Log import timber.log.Timber -class ReleaseTree : Timber.Tree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - when(priority) { - Log.DEBUG -> return - } - super.log(priority,tag,message,t) +class ReleaseTree : Timber.DebugTree() { + override fun d(t: Throwable?) { + return + } + + override fun d(t: Throwable?, message: String?, vararg args: Any?) { + return + } + + override fun d(message: String?, vararg args: Any?) { + return } } diff --git a/app/src/main/res/drawable-anydpi/ic_launcher.xml b/app/src/main/res/drawable-anydpi/ic_launcher.xml index 05b0d57..f4b91bc 100644 --- a/app/src/main/res/drawable-anydpi/ic_launcher.xml +++ b/app/src/main/res/drawable-anydpi/ic_launcher.xml @@ -1,17 +1,18 @@ - - - + android:viewportHeight="640"> + + + diff --git a/app/src/main/res/drawable/auto_pause.xml b/app/src/main/res/drawable/auto_pause.xml new file mode 100644 index 0000000..caeb3e9 --- /dev/null +++ b/app/src/main/res/drawable/auto_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/auto_play.xml b/app/src/main/res/drawable/auto_play.xml new file mode 100644 index 0000000..4c36150 --- /dev/null +++ b/app/src/main/res/drawable/auto_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_banner_foreground.xml b/app/src/main/res/drawable/ic_banner_foreground.xml index 15e85a8..df2a545 100644 --- a/app/src/main/res/drawable/ic_banner_foreground.xml +++ b/app/src/main/res/drawable/ic_banner_foreground.xml @@ -3,43 +3,54 @@ android:height="180dp" android:viewportWidth="640" android:viewportHeight="640"> - - - - + + + + - - - - - - - - - - - + + + + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 5873138..13a9911 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -3,14 +3,15 @@ android:height="108dp" android:viewportWidth="640" android:viewportHeight="640"> - - - + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml index a0a0dec..de18eab 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 1801bbc..8b192e0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,7 +1,7 @@ - - + + - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 1801bbc..8b192e0 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,7 +1,7 @@ - - + + - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a086e68..da44abe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ WG Tunnel - tunnelConfig VPN Channel VPN Notification Channel Watcher Channel @@ -17,8 +16,8 @@ Watcher Service Monitoring network state changes: active Monitoring network state changes: paused - VPN Connected - Connected to tunnel - + VPN connected + Connected to tunnel VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again. Notifications permission required. Open Settings @@ -103,7 +102,7 @@ Primary VPN on Primary VPN off Create from scratch - Action requires auto-tunnel disabled + Action requires auto-tunnel disabled or paused Action requires active tunnel Add peer Done @@ -121,8 +120,6 @@ seconds Persistent keepalive Cancel - Primary tunnel change - Would you like to make this your primary tunnel? Authentication failed Failed to authorize Enable app shortcuts @@ -133,7 +130,6 @@ Precise location required Unknown error occurred Exported configs to downloads - status Tunnel on untrusted wifi support@zaneschepke.com WG Tunnel Support @@ -179,5 +175,17 @@ Enter your pin Create pin Enabled app lock - Restart on ping fail + Restart on ping fail (beta) + Set as mobile data tunnel + Set as primary tunnel + Use tunnel on wifi name + No wifi names configured for this tunnel + General + Edit tunnel + Tunnel + disabled + Resume auto tun + Pause auto tun + Resume auto-tunnel + Pause auto-tunnel \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 9414a53..b703e9d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,6 @@ + diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml index 4d47265..f6f7d72 100644 --- a/app/src/main/res/xml/shortcuts.xml +++ b/app/src/main/res/xml/shortcuts.xml @@ -33,4 +33,36 @@ + + + + + + + + + + + + diff --git a/buildSrc/src/main/kotlin/BuildHelper.kt b/buildSrc/src/main/kotlin/BuildHelper.kt index f0b0894..546a645 100644 --- a/buildSrc/src/main/kotlin/BuildHelper.kt +++ b/buildSrc/src/main/kotlin/BuildHelper.kt @@ -26,9 +26,10 @@ object BuildHelper { val properties = java.util.Properties() val localProperties = File(file) if (localProperties.isFile) { - java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader -> - properties.load(reader) - } + java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8) + .use { reader -> + properties.load(reader) + } } else return null return properties.getProperty(key) } diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 54ca2d9..3ba8f1f 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,12 +1,12 @@ object Constants { - const val VERSION_NAME = "3.3.9" + const val VERSION_NAME = "3.4.0" const val JVM_TARGET = "17" - const val VERSION_CODE = 33900 + const val VERSION_CODE = 34000 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_NAME = "wgtunnel" - const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.10" + const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.11" const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD" diff --git a/fastlane/metadata/android/en-US/changelogs/34000.txt b/fastlane/metadata/android/en-US/changelogs/34000.txt new file mode 100644 index 0000000..8a2a1ba --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/34000.txt @@ -0,0 +1,6 @@ +What's new: +- Auto tunnel to specific tunnels by wifi name +- Auto tunnel control from tile and shortcuts +- Auto start manual tunnel if it was on before reboot +- Various bug fixes and performance improvements + diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png index 86080ea..515499b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/config_screen.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png index 0eac9dd..bf94bc8 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png index 31dd0bb..b9d5266 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png index 714d08f..8970125 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/support_screen.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a88715..6dffeea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,15 +21,15 @@ roomVersion = "2.6.1" timber = "5.0.1" tunnel = "1.0.20230706" androidGradlePlugin = "8.3.1" -kotlin = "1.9.22" -ksp = "1.9.22-1.0.17" -composeBom = "2024.02.02" -compose = "1.6.3" +kotlin = "1.9.23" +ksp = "1.9.23-1.0.19" +composeBom = "2024.03.00" +compose = "1.6.4" zxingAndroidEmbedded = "4.3.0" zxingCore = "3.5.3" #plugins -gradlePlugins-kotlinxSerialization = "1.8.21" +gradlePlugins-kotlinxSerialization = "1.9.23" material = "1.11.0" diff --git a/logcatter/build.gradle.kts b/logcatter/build.gradle.kts index 68d4c7c..bbe4e01 100644 --- a/logcatter/build.gradle.kts +++ b/logcatter/build.gradle.kts @@ -19,7 +19,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt index ac50c86..e427db5 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogLevel.kt @@ -1,40 +1,42 @@ package com.zaneschepke.logcatter.model + enum class LogLevel(val signifier: String) { DEBUG("D") { - override fun color(): Long { - return 0xFF2196F3 - } + override fun color(): Long { + return 0xFF2196F3 + } }, - INFO("I"){ + INFO("I") { override fun color(): Long { return 0xFF4CAF50 } - }, - ASSERT("A"){ + }, + ASSERT("A") { override fun color(): Long { return 0xFF9C27B0 } }, - WARNING("W"){ + WARNING("W") { override fun color(): Long { return 0xFFFFC107 } - }, - ERROR("E"){ + }, + ERROR("E") { override fun color(): Long { return 0xFFF44336 } - }, - VERBOSE("V"){ + }, + VERBOSE("V") { override fun color(): Long { return 0xFF000000 } }; - abstract fun color() : Long + abstract fun color(): Long + companion object { - fun fromSignifier(signifier: String) : LogLevel { - return when(signifier) { + fun fromSignifier(signifier: String): LogLevel { + return when (signifier) { DEBUG.signifier -> DEBUG INFO.signifier -> INFO WARNING.signifier -> WARNING diff --git a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt index 77156c2..90ef507 100644 --- a/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt +++ b/logcatter/src/main/java/com/zaneschepke/logcatter/model/LogMessage.kt @@ -6,22 +6,37 @@ data class LogMessage( val time: Instant, val pid: String, val tid: String, - val level : LogLevel, + val level: LogLevel, val tag: String, val message: String ) { override fun toString(): String { return "$time $pid $tid $level $tag message= $message" } + companion object { - fun from(logcatLine : String) : LogMessage { - return if(logcatLine.contains("---------")) LogMessage(Instant.now(), "0","0",LogLevel.VERBOSE,"System", logcatLine) + fun from(logcatLine: String): LogMessage { + return if (logcatLine.contains("---------")) LogMessage( + Instant.now(), + "0", + "0", + LogLevel.VERBOSE, + "System", + logcatLine, + ) else { //TODO improve this val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() } val epochParts = parts[0].split(".").map { it.toLong() } val message = parts.subList(5, parts.size).joinToString(" ") - LogMessage(Instant.ofEpochSecond(epochParts[0], epochParts[1]), parts[1], parts[2], LogLevel.fromSignifier(parts[3]), parts[4], message) + LogMessage( + Instant.ofEpochSecond(epochParts[0], epochParts[1]), + parts[1], + parts[2], + LogLevel.fromSignifier(parts[3]), + parts[4], + message, + ) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index da702fd..0d52c4f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,7 +11,6 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenLocal() - maven("https://gitea.zaneschepke.com/api/packages/zane/maven") google() mavenCentral() } @@ -21,9 +20,10 @@ fun getLocalProperty(key: String, file: String = "local.properties"): String? { val properties = java.util.Properties() val localProperties = File(file) if (localProperties.isFile) { - java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader -> - properties.load(reader) - } + java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8) + .use { reader -> + properties.load(reader) + } } else return null return properties.getProperty(key) }