coroutines: convert the rest

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2020-09-14 19:46:49 +02:00
parent 2fc0bb1a03
commit bab70ab51e
23 changed files with 571 additions and 534 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)
} }

View File

@ -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)

View File

@ -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) {
} }
} }
} }

View File

@ -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)
} }
} }

View File

@ -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"
} }

View File

@ -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
} }
} }

View File

@ -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()

View File

@ -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)
}
} }
} }

View File

@ -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

View File

@ -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
}
}
} }
} }
} }

View File

@ -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)

View File

@ -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)
} }

View File

@ -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"
}
} }

View File

@ -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"

View File

@ -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"
}
} }

View File

@ -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()
} }

View File

@ -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)
} }
} }

View File

@ -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()
} }
} }
} }
}

View File

@ -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()
} }
} }

View File

@ -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
}
}

View File

@ -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"
}
}