coroutines: convert the rest
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
2fc0bb1a03
commit
bab70ab51e
@ -8,13 +8,10 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
|
||||||
import android.os.StrictMode.ThreadPolicy
|
import android.os.StrictMode.ThreadPolicy
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
@ -23,18 +20,18 @@ import com.wireguard.android.backend.GoBackend
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.configStore.FileConfigStore
|
import com.wireguard.android.configStore.FileConfigStore
|
||||||
import com.wireguard.android.model.TunnelManager
|
import com.wireguard.android.model.TunnelManager
|
||||||
import com.wireguard.android.util.AsyncWorker
|
|
||||||
import com.wireguard.android.util.ExceptionLoggers
|
|
||||||
import com.wireguard.android.util.ModuleLoader
|
import com.wireguard.android.util.ModuleLoader
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
|
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
|
||||||
private val futureBackend = CompletableFuture<Backend>()
|
private val futureBackend = CompletableDeferred<Backend>()
|
||||||
private lateinit var asyncWorker: AsyncWorker
|
|
||||||
private var backend: Backend? = null
|
private var backend: Backend? = null
|
||||||
private lateinit var moduleLoader: ModuleLoader
|
private lateinit var moduleLoader: ModuleLoader
|
||||||
private lateinit var rootShell: RootShell
|
private lateinit var rootShell: RootShell
|
||||||
@ -58,10 +55,37 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun determineBackend(): Backend {
|
||||||
|
var backend: Backend? = null
|
||||||
|
var didStartRootShell = false
|
||||||
|
if (!ModuleLoader.isModuleLoaded() && moduleLoader.moduleMightExist()) {
|
||||||
|
try {
|
||||||
|
rootShell.start()
|
||||||
|
didStartRootShell = true
|
||||||
|
moduleLoader.loadModule()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
|
||||||
|
try {
|
||||||
|
if (!didStartRootShell)
|
||||||
|
rootShell.start()
|
||||||
|
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
|
||||||
|
wgQuickBackend.setMultipleTunnels(sharedPreferences.getBoolean("multiple_tunnels", false))
|
||||||
|
backend = wgQuickBackend
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (backend == null) {
|
||||||
|
backend = GoBackend(applicationContext)
|
||||||
|
GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true) }
|
||||||
|
}
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
Log.i(TAG, USER_AGENT)
|
Log.i(TAG, USER_AGENT)
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper()))
|
|
||||||
rootShell = RootShell(applicationContext)
|
rootShell = RootShell(applicationContext)
|
||||||
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
||||||
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
|
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
|
||||||
@ -74,7 +98,14 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
|||||||
}
|
}
|
||||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||||
tunnelManager.onCreate()
|
tunnelManager.onCreate()
|
||||||
asyncWorker.supplyAsync(Companion::getBackend).thenAccept { futureBackend.complete(it) }
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
backend = determineBackend()
|
||||||
|
futureBackend.complete(backend!!)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,45 +130,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getAsyncWorker() = get().asyncWorker
|
suspend fun getBackend() = get().futureBackend.await()
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getBackend(): Backend {
|
|
||||||
val app = get()
|
|
||||||
synchronized(app.futureBackend) {
|
|
||||||
if (app.backend == null) {
|
|
||||||
var backend: Backend? = null
|
|
||||||
var didStartRootShell = false
|
|
||||||
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
|
|
||||||
try {
|
|
||||||
app.rootShell.start()
|
|
||||||
didStartRootShell = true
|
|
||||||
app.moduleLoader.loadModule()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
|
|
||||||
try {
|
|
||||||
if (!didStartRootShell)
|
|
||||||
app.rootShell.start()
|
|
||||||
val wgQuickBackend = WgQuickBackend(app.applicationContext, app.rootShell, app.toolsInstaller)
|
|
||||||
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false))
|
|
||||||
backend = wgQuickBackend
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (backend == null) {
|
|
||||||
backend = GoBackend(app.applicationContext)
|
|
||||||
GoBackend.setAlwaysOnCallback { get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D) }
|
|
||||||
}
|
|
||||||
app.backend = backend
|
|
||||||
}
|
|
||||||
return app.backend!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getBackendAsync() = get().futureBackend
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getModuleLoader() = get().moduleLoader
|
fun getModuleLoader() = get().moduleLoader
|
||||||
|
@ -8,19 +8,20 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.wireguard.android.backend.Backend
|
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.ExceptionLoggers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class BootShutdownReceiver : BroadcastReceiver() {
|
class BootShutdownReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (backend !is WgQuickBackend) return@thenAccept
|
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||||
val action = intent.action ?: return@thenAccept
|
val action = intent.action ?: return@launch
|
||||||
val tunnelManager = Application.getTunnelManager()
|
val tunnelManager = Application.getTunnelManager()
|
||||||
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||||
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||||
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D)
|
tunnelManager.restoreState(false)
|
||||||
} else if (Intent.ACTION_SHUTDOWN == action) {
|
} else if (Intent.ACTION_SHUTDOWN == action) {
|
||||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
||||||
tunnelManager.saveState()
|
tunnelManager.saveState()
|
||||||
|
@ -21,6 +21,9 @@ import com.wireguard.android.activity.TunnelToggleActivity
|
|||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
import com.wireguard.android.widget.SlashDrawable
|
import com.wireguard.android.widget.SlashDrawable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||||
@ -40,7 +43,7 @@ class QuickTileService : TileService() {
|
|||||||
var ret: IBinder? = null
|
var ret: IBinder? = null
|
||||||
try {
|
try {
|
||||||
ret = super.onBind(intent)
|
ret = super.onBind(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
Log.d(TAG, "Failed to bind to TileService", e)
|
Log.d(TAG, "Failed to bind to TileService", e)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
@ -54,11 +57,12 @@ class QuickTileService : TileService() {
|
|||||||
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
||||||
tile.updateTile()
|
tile.updateTile()
|
||||||
}
|
}
|
||||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (t == null) {
|
try {
|
||||||
|
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
|
||||||
updateTile()
|
updateTile()
|
||||||
} else {
|
} catch (_: Throwable) {
|
||||||
val toggleIntent = Intent(this, TunnelToggleActivity::class.java)
|
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(toggleIntent)
|
startActivity(toggleIntent)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@ import androidx.databinding.CallbackRegistry
|
|||||||
import androidx.databinding.CallbackRegistry.NotifierCallback
|
import androidx.databinding.CallbackRegistry.NotifierCallback
|
||||||
import com.wireguard.android.Application
|
import com.wireguard.android.Application
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for activities that need to remember the currently-selected tunnel.
|
* Base class for activities that need to remember the currently-selected tunnel.
|
||||||
@ -35,11 +38,8 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
|||||||
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (savedTunnelName != null) {
|
if (savedTunnelName != null)
|
||||||
Application.getTunnelManager()
|
GlobalScope.launch(Dispatchers.Main.immediate) { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
|
||||||
.tunnels
|
|
||||||
.thenAccept { selectedTunnel = it[savedTunnelName] }
|
|
||||||
}
|
|
||||||
|
|
||||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -51,6 +51,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
||||||
|
|
||||||
fun removeOnSelectedTunnelChangedListener(
|
fun removeOnSelectedTunnelChangedListener(
|
||||||
listener: OnSelectedTunnelChangedListener) {
|
listener: OnSelectedTunnelChangedListener) {
|
||||||
selectionChangeRegistry.remove(listener)
|
selectionChangeRegistry.remove(listener)
|
||||||
|
@ -42,6 +42,7 @@ import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
|||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -67,7 +68,7 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
private var rawLogLines = StringBuffer()
|
private var rawLogLines = StringBuffer()
|
||||||
private var recyclerView: RecyclerView? = null
|
private var recyclerView: RecyclerView? = null
|
||||||
private var saveButton: MenuItem? = null
|
private var saveButton: MenuItem? = null
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
private val logStreamingScope = CoroutineScope(Dispatchers.IO)
|
||||||
private val year by lazy {
|
private val year by lazy {
|
||||||
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
||||||
yearFormatter.format(Date())
|
yearFormatter.format(Date())
|
||||||
@ -114,7 +115,7 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope.launch { streamingLog() }
|
logStreamingScope.launch { streamingLog() }
|
||||||
|
|
||||||
binding.shareFab.setOnClickListener {
|
binding.shareFab.setOnClickListener {
|
||||||
revokeLastUri()
|
revokeLastUri()
|
||||||
@ -133,6 +134,11 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
logStreamingScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == SHARE_ACTIVITY_REQUEST) {
|
if (requestCode == SHARE_ACTIVITY_REQUEST) {
|
||||||
revokeLastUri()
|
revokeLastUri()
|
||||||
@ -153,27 +159,21 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.save_log -> {
|
R.id.save_log -> {
|
||||||
coroutineScope.launch { saveLog() }
|
GlobalScope.launch { saveLog() }
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
coroutineScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveLog() {
|
private suspend fun saveLog() {
|
||||||
val context = this
|
withContext(Dispatchers.Main.immediate) {
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
saveButton?.isEnabled = false
|
saveButton?.isEnabled = false
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var exception: Throwable? = null
|
var exception: Throwable? = null
|
||||||
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||||
try {
|
try {
|
||||||
outputFile = DownloadsFileSaver.save(context, "wireguard-log.txt", "text/plain", true)
|
outputFile = DownloadsFileSaver.save(this@LogViewerActivity, "wireguard-log.txt", "text/plain", true)
|
||||||
outputFile.outputStream.use {
|
outputFile.outputStream.use {
|
||||||
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
outputFile?.delete()
|
outputFile?.delete()
|
||||||
exception = e
|
exception = e
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
saveButton?.isEnabled = true
|
saveButton?.isEnabled = true
|
||||||
Snackbar.make(findViewById(android.R.id.content),
|
Snackbar.make(findViewById(android.R.id.content),
|
||||||
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
||||||
@ -212,7 +212,7 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
rawLogLines.append(line)
|
rawLogLines.append(line)
|
||||||
rawLogLines.append('\n')
|
rawLogLines.append('\n')
|
||||||
val logLine = parseLine(line)
|
val logLine = parseLine(line)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
if (logLine != null) {
|
if (logLine != null) {
|
||||||
recyclerView?.let {
|
recyclerView?.let {
|
||||||
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
||||||
@ -348,7 +348,7 @@ class LogViewerActivity : AppCompatActivity() {
|
|||||||
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||||
try {
|
try {
|
||||||
FileOutputStream(output.fileDescriptor).write(l!!)
|
FileOutputStream(output.fileDescriptor).write(l!!)
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ import com.wireguard.android.R
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.AdminKnobs
|
import com.wireguard.android.util.AdminKnobs
|
||||||
import com.wireguard.android.util.ModuleLoader
|
import com.wireguard.android.util.ModuleLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
@ -102,8 +102,8 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
|||||||
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
||||||
).filterNotNull()
|
).filterNotNull()
|
||||||
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
||||||
Application.getBackendAsync().thenAccept { backend ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (backend is WgQuickBackend) {
|
if (Application.getBackend() is WgQuickBackend) {
|
||||||
++preferenceScreen.initialExpandedChildrenCount
|
++preferenceScreen.initialExpandedChildrenCount
|
||||||
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
||||||
} else {
|
} else {
|
||||||
@ -121,11 +121,11 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
|||||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||||
} else {
|
} else {
|
||||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||||
moduleInstaller?.isVisible = true
|
moduleInstaller?.isVisible = true
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,27 +17,31 @@ import com.wireguard.android.QuickTileService
|
|||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
class TunnelToggleActivity : AppCompatActivity() {
|
class TunnelToggleActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
||||||
tunnel.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
|
try {
|
||||||
onToggleFinished(t)
|
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = getString(R.string.toggle_error, error)
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
|
||||||
|
finishAffinity()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||||
finishAffinity()
|
finishAffinity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onToggleFinished(throwable: Throwable?) {
|
|
||||||
if (throwable == null) return
|
|
||||||
val error = ErrorMessages[throwable]
|
|
||||||
val message = getString(R.string.toggle_error, error)
|
|
||||||
Log.e(TAG, message, throwable)
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "WireGuard/TunnelToggleActivity"
|
private const val TAG = "WireGuard/TunnelToggleActivity"
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ object BindingAdapters {
|
|||||||
return 0
|
return 0
|
||||||
return try {
|
return try {
|
||||||
Integer.parseInt(s)
|
Integer.parseInt(s)
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ import androidx.databinding.Observable
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.wireguard.android.Application
|
|
||||||
import com.wireguard.android.BR
|
import com.wireguard.android.BR
|
||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||||
@ -22,8 +21,8 @@ import com.wireguard.android.databinding.ObservableKeyedArrayList
|
|||||||
import com.wireguard.android.model.ApplicationData
|
import com.wireguard.android.model.ApplicationData
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
import com.wireguard.android.util.requireTargetFragment
|
import com.wireguard.android.util.requireTargetFragment
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ class AppListDialogFragment : DialogFragment() {
|
|||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val pm = activity.packageManager
|
val pm = activity.packageManager
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@ -57,12 +56,12 @@ class AppListDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
appData.clear()
|
appData.clear()
|
||||||
appData.addAll(applicationData)
|
appData.addAll(applicationData)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
val error = ErrorMessages[e]
|
val error = ErrorMessages[e]
|
||||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||||
|
@ -17,13 +17,15 @@ import com.wireguard.android.Application
|
|||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.activity.BaseActivity
|
import com.wireguard.android.activity.BaseActivity
|
||||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
||||||
import com.wireguard.android.backend.Backend
|
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||||
@ -70,14 +72,14 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
|||||||
is TunnelListItemBinding -> binding.item
|
is TunnelListItemBinding -> binding.item
|
||||||
else -> return
|
else -> return
|
||||||
} ?: return
|
} ?: return
|
||||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (backend is GoBackend) {
|
if (Application.getBackend() is GoBackend) {
|
||||||
val intent = GoBackend.VpnService.prepare(view.context)
|
val intent = GoBackend.VpnService.prepare(view.context)
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
pendingTunnel = tunnel
|
pendingTunnel = tunnel
|
||||||
pendingTunnelUp = checked
|
pendingTunnelUp = checked
|
||||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
|
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
|
||||||
return@thenAccept
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||||
@ -85,9 +87,11 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
||||||
tunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete { _, throwable ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (throwable == null) return@whenComplete
|
try {
|
||||||
val error = ErrorMessages[throwable]
|
tunnel.setStateAsync(Tunnel.State.of(checked))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||||
val message = requireContext().getString(messageResId, error)
|
val message = requireContext().getString(messageResId, error)
|
||||||
val view = view
|
val view = view
|
||||||
@ -97,7 +101,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
|||||||
.show()
|
.show()
|
||||||
else
|
else
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||||
Log.e(TAG, message, throwable)
|
Log.e(TAG, message, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,9 @@ import com.wireguard.android.R
|
|||||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
||||||
import com.wireguard.config.BadConfigException
|
import com.wireguard.config.BadConfigException
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@ -28,11 +31,12 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
|||||||
private fun createTunnelAndDismiss() {
|
private fun createTunnelAndDismiss() {
|
||||||
binding?.let {
|
binding?.let {
|
||||||
val name = it.tunnelNameText.text.toString()
|
val name = it.tunnelNameText.text.toString()
|
||||||
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (tunnel != null) {
|
try {
|
||||||
|
Application.getTunnelManager().create(name, config)
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} catch (e: Throwable) {
|
||||||
it.tunnelNameTextLayout.error = throwable.message
|
it.tunnelNameTextLayout.error = e.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +53,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
|||||||
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
||||||
config = try {
|
config = try {
|
||||||
Config.parse(ByteArrayInputStream(configBytes))
|
Config.parse(ByteArrayInputStream(configBytes))
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
||||||
else -> throw e
|
else -> throw e
|
||||||
|
@ -18,6 +18,9 @@ import com.wireguard.android.databinding.TunnelDetailPeerBinding
|
|||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import java.util.TimerTask
|
import java.util.TimerTask
|
||||||
|
|
||||||
@ -79,7 +82,13 @@ class TunnelDetailFragment : BaseFragment() {
|
|||||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||||
binding ?: return
|
binding ?: return
|
||||||
binding!!.tunnel = newTunnel
|
binding!!.tunnel = newTunnel
|
||||||
if (newTunnel == null) binding!!.config = null else newTunnel.configAsync.thenAccept { config -> binding!!.config = config }
|
if (newTunnel == null) binding!!.config = null else GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
binding!!.config = newTunnel.getConfigAsync()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
binding!!.config = null
|
||||||
|
}
|
||||||
|
}
|
||||||
lastState = Tunnel.State.TOGGLE
|
lastState = Tunnel.State.TOGGLE
|
||||||
updateStats()
|
updateStats()
|
||||||
}
|
}
|
||||||
@ -105,16 +114,9 @@ class TunnelDetailFragment : BaseFragment() {
|
|||||||
val state = tunnel.state
|
val state = tunnel.state
|
||||||
if (state != Tunnel.State.UP && lastState == state) return
|
if (state != Tunnel.State.UP && lastState == state) return
|
||||||
lastState = state
|
lastState = state
|
||||||
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
if (throwable != null) {
|
try {
|
||||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
val statistics = tunnel.getStatisticsAsync()
|
||||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
|
||||||
?: continue
|
|
||||||
peer.transferLabel.visibility = View.GONE
|
|
||||||
peer.transferText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
return@whenComplete
|
|
||||||
}
|
|
||||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||||
?: continue
|
?: continue
|
||||||
@ -130,6 +132,14 @@ class TunnelDetailFragment : BaseFragment() {
|
|||||||
peer.transferLabel.visibility = View.VISIBLE
|
peer.transferLabel.visibility = View.VISIBLE
|
||||||
peer.transferText.visibility = View.VISIBLE
|
peer.transferText.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||||
|
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||||
|
?: continue
|
||||||
|
peer.transferLabel.visibility = View.GONE
|
||||||
|
peer.transferText.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,16 @@ import com.wireguard.android.backend.Tunnel
|
|||||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||||
import com.wireguard.android.fragment.AppListDialogFragment.AppSelectionListener
|
import com.wireguard.android.fragment.AppListDialogFragment.AppSelectionListener
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
import com.wireguard.android.util.BiometricAuthenticator
|
|
||||||
import com.wireguard.android.util.AdminKnobs
|
import com.wireguard.android.util.AdminKnobs
|
||||||
|
import com.wireguard.android.util.BiometricAuthenticator
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
import com.wireguard.android.viewmodel.ConfigProxy
|
import com.wireguard.android.viewmodel.ConfigProxy
|
||||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment for editing a WireGuard configuration.
|
* Fragment for editing a WireGuard configuration.
|
||||||
@ -130,7 +133,7 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
|||||||
binding ?: return false
|
binding ?: return false
|
||||||
val newConfig = try {
|
val newConfig = try {
|
||||||
binding!!.config!!.resolve()
|
binding!!.config!!.resolve()
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
val error = ErrorMessages[e]
|
val error = ErrorMessages[e]
|
||||||
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
||||||
val message = getString(R.string.config_save_error, tunnelName, error)
|
val message = getString(R.string.config_save_error, tunnelName, error)
|
||||||
@ -138,20 +141,35 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
|||||||
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
when {
|
when {
|
||||||
tunnel == null -> {
|
tunnel == null -> {
|
||||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||||
val manager = Application.getTunnelManager()
|
val manager = Application.getTunnelManager()
|
||||||
manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
|
try {
|
||||||
|
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onTunnelCreated(null, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tunnel!!.name != binding!!.name -> {
|
tunnel!!.name != binding!!.name -> {
|
||||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||||
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
try {
|
||||||
|
tunnel!!.setNameAsync(binding!!.name!!)
|
||||||
|
onTunnelRenamed(tunnel!!, newConfig, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||||
|
try {
|
||||||
tunnel!!.setConfigAsync(newConfig)
|
tunnel!!.setConfigAsync(newConfig)
|
||||||
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
|
onConfigSaved(tunnel!!, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onConfigSaved(tunnel!!, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -187,13 +205,18 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
|||||||
binding!!.config = ConfigProxy()
|
binding!!.config = ConfigProxy()
|
||||||
if (tunnel != null) {
|
if (tunnel != null) {
|
||||||
binding!!.name = tunnel!!.name
|
binding!!.name = tunnel!!.name
|
||||||
tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
onConfigLoaded(tunnel!!.getConfigAsync())
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
binding!!.name = ""
|
binding!!.name = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
|
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||||
val message: String
|
val message: String
|
||||||
if (throwable == null) {
|
if (throwable == null) {
|
||||||
tunnel = newTunnel
|
tunnel = newTunnel
|
||||||
@ -219,7 +242,14 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
|||||||
Log.d(TAG, message)
|
Log.d(TAG, message)
|
||||||
// Now save the rest of configuration changes.
|
// Now save the rest of configuration changes.
|
||||||
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
|
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
|
||||||
renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
renamedTunnel.setConfigAsync(newConfig)
|
||||||
|
onConfigSaved(renamedTunnel, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onConfigSaved(renamedTunnel, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val error = ErrorMessages[throwable]
|
val error = ErrorMessages[throwable]
|
||||||
message = getString(R.string.tunnel_rename_error, error)
|
message = getString(R.string.tunnel_rename_error, error)
|
||||||
|
@ -36,7 +36,14 @@ import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
|||||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||||
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
@ -61,21 +68,22 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
|
|
||||||
// Config text is valid, now create the tunnel…
|
// Config text is valid, now create the tunnel…
|
||||||
newInstance(configText).show(parentFragmentManager, null)
|
newInstance(configText).show(parentFragmentManager, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importTunnel(uri: Uri?) {
|
private fun importTunnel(uri: Uri?) {
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val activity = activity
|
val activity = activity
|
||||||
if (activity == null || uri == null) {
|
if (activity == null || uri == null) {
|
||||||
return
|
return@withContext
|
||||||
}
|
}
|
||||||
val contentResolver = activity.contentResolver
|
val contentResolver = activity.contentResolver
|
||||||
|
val futureTunnels = ArrayList<Deferred<ObservableTunnel>>()
|
||||||
val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
|
|
||||||
val throwables = ArrayList<Throwable>()
|
val throwables = ArrayList<Throwable>()
|
||||||
Application.getAsyncWorker().supplyAsync {
|
try {
|
||||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||||
var name = ""
|
var name = ""
|
||||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||||
@ -119,21 +127,17 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Config.parse(reader)
|
Config.parse(reader)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
throwables.add(e)
|
throwables.add(e)
|
||||||
null
|
null
|
||||||
}?.let {
|
}?.let {
|
||||||
futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture())
|
val nameCopy = name
|
||||||
|
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(nameCopy, it) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
futureTunnels.add(
|
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(name, Config.parse(contentResolver.openInputStream(uri)!!)) })
|
||||||
Application.getTunnelManager().create(
|
|
||||||
name,
|
|
||||||
Config.parse(contentResolver.openInputStream(uri)!!)
|
|
||||||
).toCompletableFuture()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (futureTunnels.isEmpty()) {
|
if (futureTunnels.isEmpty()) {
|
||||||
@ -143,26 +147,17 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CompletableFuture.allOf(*futureTunnels.toTypedArray())
|
val tunnels = futureTunnels.mapNotNull {
|
||||||
}.whenComplete { future, exception ->
|
try {
|
||||||
if (exception != null) {
|
it.await()
|
||||||
onTunnelImportFinished(emptyList(), listOf(exception))
|
} catch (e: Throwable) {
|
||||||
} else {
|
|
||||||
future.whenComplete { _, _ ->
|
|
||||||
val tunnels = mutableListOf<ObservableTunnel>()
|
|
||||||
for (futureTunnel in futureTunnels) {
|
|
||||||
val tunnel: ObservableTunnel? = try {
|
|
||||||
futureTunnel.getNow(null)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throwables.add(e)
|
throwables.add(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tunnel != null) {
|
|
||||||
tunnels.add(tunnel)
|
|
||||||
}
|
}
|
||||||
}
|
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(tunnels, throwables) }
|
||||||
onTunnelImportFinished(tunnels, throwables)
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +221,8 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
|
|
||||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||||
binding ?: return
|
binding ?: return
|
||||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
|
if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
|
||||||
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
||||||
}
|
}
|
||||||
@ -268,11 +264,10 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
super.onViewStateRestored(savedInstanceState)
|
super.onViewStateRestored(savedInstanceState)
|
||||||
binding ?: return
|
binding ?: return
|
||||||
binding!!.fragment = this
|
binding!!.fragment = this
|
||||||
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
|
GlobalScope.launch(Dispatchers.Main.immediate) { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
|
||||||
val parent = this
|
|
||||||
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||||
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||||
binding.fragment = parent
|
binding.fragment = this@TunnelListFragment
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
if (actionMode == null) {
|
if (actionMode == null) {
|
||||||
selectedTunnel = item
|
selectedTunnel = item
|
||||||
@ -321,20 +316,24 @@ class TunnelListFragment : BaseFragment() {
|
|||||||
scaleX = 1f
|
scaleX = 1f
|
||||||
scaleY = 1f
|
scaleY = 1f
|
||||||
}
|
}
|
||||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||||
val futures = tunnelsToDelete.map { it.delete().toCompletableFuture() }.toTypedArray()
|
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
|
||||||
CompletableFuture.allOf(*futures)
|
onTunnelDeletionFinished(futures.awaitAll().size, null)
|
||||||
.thenApply { futures.size }
|
} catch (e: Throwable) {
|
||||||
.whenComplete(this@TunnelListFragment::onTunnelDeletionFinished)
|
onTunnelDeletionFinished(0, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
checkedItems.clear()
|
checkedItems.clear()
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.menu_action_select_all -> {
|
R.id.menu_action_select_all -> {
|
||||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
for (i in 0 until tunnels.size) {
|
for (i in 0 until tunnels.size) {
|
||||||
setItemChecked(i, true)
|
setItemChecked(i, true)
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.wireguard.android.model
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.databinding.BaseObservable
|
import androidx.databinding.BaseObservable
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.wireguard.android.BR
|
import com.wireguard.android.BR
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.databinding.Keyed
|
import com.wireguard.android.databinding.Keyed
|
||||||
import com.wireguard.android.util.ExceptionLoggers
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java9.util.concurrent.CompletionStage
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||||
@ -30,10 +32,12 @@ class ObservableTunnel internal constructor(
|
|||||||
@Bindable
|
@Bindable
|
||||||
override fun getName() = name
|
override fun getName() = name
|
||||||
|
|
||||||
fun setNameAsync(name: String): CompletionStage<String> = if (name != this.name)
|
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||||
manager.setTunnelName(this, name)
|
if (name != this@ObservableTunnel.name)
|
||||||
|
manager.setTunnelName(this@ObservableTunnel, name)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(this.name)
|
this@ObservableTunnel.name
|
||||||
|
}
|
||||||
|
|
||||||
fun onNameChanged(name: String): String {
|
fun onNameChanged(name: String): String {
|
||||||
this.name = name
|
this.name = name
|
||||||
@ -57,31 +61,42 @@ class ObservableTunnel internal constructor(
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStateAsync(state: Tunnel.State): CompletionStage<Tunnel.State> = if (state != this.state)
|
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
manager.setTunnelState(this, state)
|
if (state != this@ObservableTunnel.state)
|
||||||
|
manager.setTunnelState(this@ObservableTunnel, state)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(this.state)
|
this@ObservableTunnel.state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var config = config
|
var config = config
|
||||||
get() {
|
get() {
|
||||||
if (field == null)
|
if (field == null)
|
||||||
manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E)
|
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
manager.getTunnelConfig(this@ObservableTunnel)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val configAsync: CompletionStage<Config>
|
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
get() = if (config == null)
|
config ?: manager.getTunnelConfig(this@ObservableTunnel)
|
||||||
manager.getTunnelConfig(this)
|
}
|
||||||
else
|
|
||||||
CompletableFuture.completedFuture(config)
|
|
||||||
|
|
||||||
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
|
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
manager.setTunnelConfig(this, config)
|
this@ObservableTunnel.config.let {
|
||||||
|
if (config != it)
|
||||||
|
manager.setTunnelConfig(this@ObservableTunnel, config)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(this.config)
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onConfigChanged(config: Config?): Config? {
|
fun onConfigChanged(config: Config?): Config? {
|
||||||
this.config = config
|
this.config = config
|
||||||
@ -94,16 +109,26 @@ class ObservableTunnel internal constructor(
|
|||||||
var statistics: Statistics? = null
|
var statistics: Statistics? = null
|
||||||
get() {
|
get() {
|
||||||
if (field == null || field?.isStale != false)
|
if (field == null || field?.isStale != false)
|
||||||
manager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E)
|
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
try {
|
||||||
|
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val statisticsAsync: CompletionStage<Statistics>
|
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||||
get() = if (statistics == null || statistics?.isStale != false)
|
statistics.let {
|
||||||
manager.getTunnelStatistics(this)
|
if (it == null || it.isStale)
|
||||||
|
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||||
else
|
else
|
||||||
CompletableFuture.completedFuture(statistics)
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
|
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
|
||||||
this.statistics = statistics
|
this.statistics = statistics
|
||||||
@ -112,5 +137,10 @@ class ObservableTunnel internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun delete(): CompletionStage<Void> = manager.delete(this)
|
suspend fun deleteAsync() = manager.delete(this)
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/ObservableTunnel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.databinding.BaseObservable
|
import androidx.databinding.BaseObservable
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.wireguard.android.Application.Companion.get
|
import com.wireguard.android.Application.Companion.get
|
||||||
import com.wireguard.android.Application.Companion.getAsyncWorker
|
|
||||||
import com.wireguard.android.Application.Companion.getBackend
|
import com.wireguard.android.Application.Companion.getBackend
|
||||||
import com.wireguard.android.Application.Companion.getSharedPreferences
|
import com.wireguard.android.Application.Companion.getSharedPreferences
|
||||||
import com.wireguard.android.Application.Companion.getTunnelManager
|
import com.wireguard.android.Application.Companion.getTunnelManager
|
||||||
@ -22,60 +22,64 @@ import com.wireguard.android.backend.Statistics
|
|||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.configStore.ConfigStore
|
import com.wireguard.android.configStore.ConfigStore
|
||||||
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||||
import com.wireguard.android.util.ExceptionLoggers
|
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import java9.util.concurrent.CompletionStage
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.util.ArrayList
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||||
*/
|
*/
|
||||||
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||||
val tunnels = CompletableFuture<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||||
private val context: Context = get()
|
private val context: Context = get()
|
||||||
private val delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
|
|
||||||
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
||||||
private var haveLoaded = false
|
private var haveLoaded = false
|
||||||
|
|
||||||
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel? {
|
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
|
||||||
val tunnel = ObservableTunnel(this, name, config, state)
|
val tunnel = ObservableTunnel(this, name, config, state)
|
||||||
tunnelMap.add(tunnel)
|
tunnelMap.add(tunnel)
|
||||||
return tunnel
|
return tunnel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(name: String, config: Config?): CompletionStage<ObservableTunnel> {
|
suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
|
||||||
|
|
||||||
|
suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
|
||||||
if (Tunnel.isNameInvalid(name))
|
if (Tunnel.isNameInvalid(name))
|
||||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||||
if (tunnelMap.containsKey(name))
|
if (tunnelMap.containsKey(name))
|
||||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||||
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
|
addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(tunnel: ObservableTunnel): CompletionStage<Void> {
|
suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
|
||||||
val originalState = tunnel.state
|
val originalState = tunnel.state
|
||||||
val wasLastUsed = tunnel == lastUsedTunnel
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
// Make sure nothing touches the tunnel.
|
// Make sure nothing touches the tunnel.
|
||||||
if (wasLastUsed)
|
if (wasLastUsed)
|
||||||
lastUsedTunnel = null
|
lastUsedTunnel = null
|
||||||
tunnelMap.remove(tunnel)
|
tunnelMap.remove(tunnel)
|
||||||
return getAsyncWorker().runAsync {
|
|
||||||
if (originalState == Tunnel.State.UP)
|
|
||||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
|
||||||
try {
|
try {
|
||||||
configStore.delete(tunnel.name)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (originalState == Tunnel.State.UP)
|
if (originalState == Tunnel.State.UP)
|
||||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}.whenComplete { _, e ->
|
} catch (e: Throwable) {
|
||||||
if (e == null)
|
|
||||||
return@whenComplete
|
|
||||||
// Failure, put the tunnel back.
|
// Failure, put the tunnel back.
|
||||||
tunnelMap.add(tunnel)
|
tunnelMap.add(tunnel)
|
||||||
if (wasLastUsed)
|
if (wasLastUsed)
|
||||||
lastUsedTunnel = tunnel
|
lastUsedTunnel = tunnel
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,14 +96,18 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
|||||||
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
|
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
|
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
|
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
|
||||||
|
}
|
||||||
|
|
||||||
fun onCreate() {
|
fun onCreate() {
|
||||||
getAsyncWorker().supplyAsync { configStore.enumerate() }
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
|
try {
|
||||||
.whenComplete(ExceptionLoggers.E)
|
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||||
@ -108,42 +116,38 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
|||||||
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
|
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
|
||||||
if (lastUsedName != null)
|
if (lastUsedName != null)
|
||||||
lastUsedTunnel = tunnelMap[lastUsedName]
|
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||||
var toComplete: Array<CompletableFuture<Void>>
|
|
||||||
synchronized(delayedLoadRestoreTunnels) {
|
|
||||||
haveLoaded = true
|
haveLoaded = true
|
||||||
toComplete = delayedLoadRestoreTunnels.toTypedArray()
|
restoreState(true)
|
||||||
delayedLoadRestoreTunnels.clear()
|
|
||||||
}
|
|
||||||
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
|
|
||||||
for (f in toComplete) {
|
|
||||||
if (t == null)
|
|
||||||
f.complete(v)
|
|
||||||
else
|
|
||||||
f.completeExceptionally(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tunnels.complete(tunnelMap)
|
tunnels.complete(tunnelMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshTunnelStates() {
|
private fun refreshTunnelStates() {
|
||||||
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
|
try {
|
||||||
.whenComplete(ExceptionLoggers.E)
|
val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
|
||||||
|
for (tunnel in tunnelMap)
|
||||||
|
tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreState(force: Boolean): CompletionStage<Void> {
|
fun restoreState(force: Boolean) {
|
||||||
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
if (!haveLoaded || (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false)))
|
||||||
return CompletableFuture.completedFuture(null)
|
return
|
||||||
synchronized(delayedLoadRestoreTunnels) {
|
|
||||||
if (!haveLoaded) {
|
|
||||||
val f = CompletableFuture<Void>()
|
|
||||||
delayedLoadRestoreTunnels.add(f)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
|
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
|
||||||
?: return CompletableFuture.completedFuture(null)
|
?: return
|
||||||
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
|
if (previouslyRunning.isEmpty()) return
|
||||||
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
@SuppressLint("ApplySharedPref")
|
||||||
@ -151,16 +155,18 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
|||||||
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
|
getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet()).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
|
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
|
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
|
||||||
getBackend().setState(tunnel, tunnel.state, config)
|
getBackend().setState(tunnel, tunnel.state, config)
|
||||||
configStore.save(tunnel.name, config)
|
configStore.save(tunnel.name, config)
|
||||||
}.thenApply { tunnel.onConfigChanged(it) }
|
})!!
|
||||||
|
}
|
||||||
|
|
||||||
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
|
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||||
if (Tunnel.isNameInvalid(name))
|
if (Tunnel.isNameInvalid(name))
|
||||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)))
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||||
if (tunnelMap.containsKey(name)) {
|
if (tunnelMap.containsKey(name)) {
|
||||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||||
}
|
}
|
||||||
val originalState = tunnel.state
|
val originalState = tunnel.state
|
||||||
val wasLastUsed = tunnel == lastUsedTunnel
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
@ -168,33 +174,44 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
|||||||
if (wasLastUsed)
|
if (wasLastUsed)
|
||||||
lastUsedTunnel = null
|
lastUsedTunnel = null
|
||||||
tunnelMap.remove(tunnel)
|
tunnelMap.remove(tunnel)
|
||||||
return getAsyncWorker().supplyAsync {
|
var throwable: Throwable? = null
|
||||||
|
var newName: String? = null
|
||||||
|
try {
|
||||||
if (originalState == Tunnel.State.UP)
|
if (originalState == Tunnel.State.UP)
|
||||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||||
configStore.rename(tunnel.name, name)
|
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
|
||||||
val newName = tunnel.onNameChanged(name)
|
newName = tunnel.onNameChanged(name)
|
||||||
if (originalState == Tunnel.State.UP)
|
if (originalState == Tunnel.State.UP)
|
||||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||||
newName
|
} catch (e: Throwable) {
|
||||||
}.whenComplete { _, e ->
|
throwable = e
|
||||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||||
if (e != null)
|
|
||||||
getTunnelState(tunnel)
|
getTunnelState(tunnel)
|
||||||
|
}
|
||||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||||
tunnelMap.add(tunnel)
|
tunnelMap.add(tunnel)
|
||||||
if (wasLastUsed)
|
if (wasLastUsed)
|
||||||
lastUsedTunnel = tunnel
|
lastUsedTunnel = tunnel
|
||||||
}
|
if (throwable != null)
|
||||||
|
throw throwable
|
||||||
|
newName!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
|
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
|
var newState = tunnel.state
|
||||||
.whenComplete { newState, e ->
|
var throwable: Throwable? = null
|
||||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
try {
|
||||||
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
|
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
|
||||||
if (e == null && newState == Tunnel.State.UP)
|
if (newState == Tunnel.State.UP)
|
||||||
lastUsedTunnel = tunnel
|
lastUsedTunnel = tunnel
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throwable = e
|
||||||
|
}
|
||||||
|
tunnel.onStateChanged(newState)
|
||||||
saveState()
|
saveState()
|
||||||
|
if (throwable != null)
|
||||||
|
throw throwable
|
||||||
|
newState
|
||||||
}
|
}
|
||||||
|
|
||||||
class IntentReceiver : BroadcastReceiver() {
|
class IntentReceiver : BroadcastReceiver() {
|
||||||
@ -215,20 +232,25 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
|||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
val tunnelName = intent.getStringExtra("tunnel") ?: return
|
val tunnelName = intent.getStringExtra("tunnel") ?: return
|
||||||
manager.tunnels.thenAccept {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
val tunnel = it[tunnelName] ?: return@thenAccept
|
val tunnels = manager.getTunnels()
|
||||||
|
val tunnel = tunnels[tunnelName] ?: return@launch
|
||||||
manager.setTunnelState(tunnel, state)
|
manager.setTunnelState(tunnel, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
|
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
|
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
|
||||||
|
}
|
||||||
|
|
||||||
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
|
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||||
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
|
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/TunnelManager"
|
||||||
|
|
||||||
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
|
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
|
||||||
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
|
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
|
||||||
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
|
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
|
||||||
|
@ -8,22 +8,28 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import com.wireguard.android.Application
|
import com.wireguard.android.Application
|
||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.activity.SettingsActivity
|
import com.wireguard.android.activity.SettingsActivity
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||||
private var state = State.UNKNOWN
|
private var state = State.UNKNOWN
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
Application.getBackendAsync().thenAccept { backend ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
setState(if (backend is WgQuickBackend) State.ENABLED else State.DISABLED)
|
setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,17 +46,21 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
|||||||
setState(State.DISABLING)
|
setState(State.DISABLING)
|
||||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
|
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
|
||||||
}
|
}
|
||||||
Application.getAsyncWorker().runAsync {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
|
val observableTunnels = Application.getTunnelManager().getTunnels()
|
||||||
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
|
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
|
||||||
CompletableFuture.allOf(*downings).thenRun {
|
try {
|
||||||
|
downings.awaitAll()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
Application.get().startActivity(restartIntent)
|
Application.get().startActivity(restartIntent)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}.join()
|
} catch (e: Throwable) {
|
||||||
|
Log.println(Log.ERROR, TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,4 +79,8 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
|
|||||||
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
|
ENABLING(R.string.module_disabler_disabled_title, R.string.success_application_will_restart, false, true),
|
||||||
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
|
DISABLING(R.string.module_disabler_enabled_title, R.string.success_application_will_restart, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/KernelModuleDisablerPreference"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,14 @@ import com.wireguard.android.Application
|
|||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.activity.SettingsActivity
|
import com.wireguard.android.activity.SettingsActivity
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||||
private var state = State.INITIAL
|
private var state = State.INITIAL
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun getSummary() = context.getString(state.messageResourceId)
|
override fun getSummary() = context.getString(state.messageResourceId)
|
||||||
|
|
||||||
override fun getTitle() = context.getString(R.string.module_installer_title)
|
override fun getTitle() = context.getString(R.string.module_installer_title)
|
||||||
@ -32,14 +30,15 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
|
|||||||
@SuppressLint("ApplySharedPref")
|
@SuppressLint("ApplySharedPref")
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
setState(State.WORKING)
|
setState(State.WORKING)
|
||||||
coroutineScope.launch {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
try {
|
try {
|
||||||
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
|
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
|
||||||
OsConstants.ENOENT -> setState(State.NOTFOUND)
|
OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||||
OsConstants.EXIT_SUCCESS -> {
|
OsConstants.EXIT_SUCCESS -> {
|
||||||
setState(State.SUCCESS)
|
setState(State.SUCCESS)
|
||||||
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
|
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@ -47,9 +46,10 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
|
|||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> setState(State.FAILURE)
|
else -> setState(State.FAILURE)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
setState(State.FAILURE)
|
setState(State.FAILURE)
|
||||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ import androidx.preference.Preference
|
|||||||
import com.wireguard.android.Application
|
import com.wireguard.android.Application
|
||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ -21,15 +21,13 @@ import kotlinx.coroutines.withContext
|
|||||||
*/
|
*/
|
||||||
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||||
private var state = State.INITIAL
|
private var state = State.INITIAL
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun getSummary() = context.getString(state.messageResourceId)
|
override fun getSummary() = context.getString(state.messageResourceId)
|
||||||
|
|
||||||
override fun getTitle() = context.getString(R.string.tools_installer_title)
|
override fun getTitle() = context.getString(R.string.tools_installer_title)
|
||||||
|
|
||||||
override fun onAttached() {
|
override fun onAttached() {
|
||||||
super.onAttached()
|
super.onAttached()
|
||||||
coroutineScope.launch {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
try {
|
try {
|
||||||
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
|
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
|
||||||
when {
|
when {
|
||||||
@ -39,7 +37,7 @@ class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Prefere
|
|||||||
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
|
||||||
else -> setState(State.INITIAL)
|
else -> setState(State.INITIAL)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
setState(State.INITIAL)
|
setState(State.INITIAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,7 +45,7 @@ class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Prefere
|
|||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
setState(State.WORKING)
|
setState(State.WORKING)
|
||||||
coroutineScope.launch {
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
try {
|
try {
|
||||||
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
|
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
|
||||||
when {
|
when {
|
||||||
@ -55,7 +53,7 @@ class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Prefere
|
|||||||
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
|
||||||
else -> setState(State.FAILURE)
|
else -> setState(State.FAILURE)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
setState(State.FAILURE)
|
setState(State.FAILURE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@ import com.wireguard.android.R
|
|||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.GoBackend
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -47,16 +47,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Application.getBackendAsync().thenAccept { backend ->
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val backend = Application.getBackend()
|
||||||
versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
notifyChanged()
|
||||||
versionSummary = try {
|
versionSummary = try {
|
||||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
||||||
} catch (_: Exception) {
|
} catch (_: Throwable) {
|
||||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||||
}
|
}
|
||||||
notifyChanged()
|
notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -13,13 +13,18 @@ import androidx.preference.Preference
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.wireguard.android.Application
|
import com.wireguard.android.Application
|
||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.util.AdminKnobs
|
||||||
import com.wireguard.android.util.BiometricAuthenticator
|
import com.wireguard.android.util.BiometricAuthenticator
|
||||||
import com.wireguard.android.util.DownloadsFileSaver
|
import com.wireguard.android.util.DownloadsFileSaver
|
||||||
import com.wireguard.android.util.AdminKnobs
|
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
import com.wireguard.android.util.FragmentUtils
|
import com.wireguard.android.util.FragmentUtils
|
||||||
import java9.util.concurrent.CompletableFuture
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@ -29,52 +34,40 @@ import java.util.zip.ZipOutputStream
|
|||||||
*/
|
*/
|
||||||
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||||
private var exportedFilePath: String? = null
|
private var exportedFilePath: String? = null
|
||||||
|
|
||||||
private fun exportZip() {
|
private fun exportZip() {
|
||||||
Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
|
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
|
try {
|
||||||
|
exportedFilePath = withContext(Dispatchers.IO) {
|
||||||
|
val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
|
||||||
|
if (configs.isEmpty()) {
|
||||||
|
throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportZip(tunnels: List<ObservableTunnel>) {
|
|
||||||
val futureConfigs = tunnels.map { it.configAsync.toCompletableFuture() }.toTypedArray()
|
|
||||||
if (futureConfigs.isEmpty()) {
|
|
||||||
exportZipComplete(null, IllegalArgumentException(
|
|
||||||
context.getString(R.string.no_tunnels_error)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
CompletableFuture.allOf(*futureConfigs)
|
|
||||||
.whenComplete { _, exception ->
|
|
||||||
Application.getAsyncWorker().supplyAsync {
|
|
||||||
if (exception != null) throw exception
|
|
||||||
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
|
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
|
||||||
try {
|
try {
|
||||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||||
for (i in futureConfigs.indices) {
|
for (i in configs.indices) {
|
||||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||||
zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||||
}
|
}
|
||||||
zip.closeEntry()
|
zip.closeEntry()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
outputFile.delete()
|
outputFile.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
outputFile.fileName
|
outputFile.fileName
|
||||||
}.whenComplete(this::exportZipComplete)
|
|
||||||
}
|
}
|
||||||
}
|
notifyChanged()
|
||||||
|
} catch (e: Throwable) {
|
||||||
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
|
val error = ErrorMessages[e]
|
||||||
if (throwable != null) {
|
|
||||||
val error = ErrorMessages[throwable]
|
|
||||||
val message = context.getString(R.string.zip_export_error, error)
|
val message = context.getString(R.string.zip_export_error, error)
|
||||||
Log.e(TAG, message, throwable)
|
Log.e(TAG, message, e)
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
FragmentUtils.getPrefActivity(this@ZipExporterPreference).findViewById(android.R.id.content),
|
||||||
message, Snackbar.LENGTH_LONG).show()
|
message, Snackbar.LENGTH_LONG).show()
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
} else {
|
}
|
||||||
exportedFilePath = filePath
|
|
||||||
notifyChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
package com.wireguard.android.util
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import java9.util.concurrent.CompletableFuture
|
|
||||||
import java9.util.concurrent.CompletionStage
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class AsyncWorker(private val executor: Executor, private val handler: Handler) {
|
|
||||||
|
|
||||||
fun runAsync(run: () -> Unit): CompletionStage<Void> {
|
|
||||||
val future = CompletableFuture<Void>()
|
|
||||||
executor.execute {
|
|
||||||
try {
|
|
||||||
run()
|
|
||||||
handler.post { future.complete(null) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
handler.post { future.completeExceptionally(t) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> supplyAsync(get: () -> T?): CompletionStage<T> {
|
|
||||||
val future = CompletableFuture<T>()
|
|
||||||
executor.execute {
|
|
||||||
try {
|
|
||||||
val result = get()
|
|
||||||
handler.post { future.complete(result) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
handler.post { future.completeExceptionally(t) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
package com.wireguard.android.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import java9.util.function.BiConsumer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
|
|
||||||
* `CompletionStage.whenComplete()` at the end of an asynchronous future chain.
|
|
||||||
*/
|
|
||||||
enum class ExceptionLoggers(private val priority: Int) : BiConsumer<Any?, Throwable?> {
|
|
||||||
D(Log.DEBUG), E(Log.ERROR);
|
|
||||||
|
|
||||||
override fun accept(result: Any?, throwable: Throwable?) {
|
|
||||||
if (throwable != null)
|
|
||||||
Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable))
|
|
||||||
else if (priority <= Log.DEBUG)
|
|
||||||
Log.println(priority, TAG, "Future completed successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "WireGuard/ExceptionLoggers"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user