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.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
@ -23,18 +20,18 @@ import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.configStore.FileConfigStore
|
||||
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.RootShell
|
||||
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.util.Locale
|
||||
|
||||
class Application : android.app.Application(), OnSharedPreferenceChangeListener {
|
||||
private val futureBackend = CompletableFuture<Backend>()
|
||||
private lateinit var asyncWorker: AsyncWorker
|
||||
private val futureBackend = CompletableDeferred<Backend>()
|
||||
private var backend: Backend? = null
|
||||
private lateinit var moduleLoader: ModuleLoader
|
||||
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() {
|
||||
Log.i(TAG, USER_AGENT)
|
||||
super.onCreate()
|
||||
asyncWorker = AsyncWorker(AsyncTask.SERIAL_EXECUTOR, Handler(Looper.getMainLooper()))
|
||||
rootShell = RootShell(applicationContext)
|
||||
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
||||
moduleLoader = ModuleLoader(applicationContext, rootShell, USER_AGENT)
|
||||
@ -74,7 +98,14 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
||||
}
|
||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||
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)
|
||||
}
|
||||
|
||||
@ -99,45 +130,7 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAsyncWorker() = get().asyncWorker
|
||||
|
||||
@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
|
||||
suspend fun getBackend() = get().futureBackend.await()
|
||||
|
||||
@JvmStatic
|
||||
fun getModuleLoader() = get().moduleLoader
|
||||
|
@ -8,19 +8,20 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.wireguard.android.backend.Backend
|
||||
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() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend !is WgQuickBackend) return@thenAccept
|
||||
val action = intent.action ?: return@thenAccept
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||
val action = intent.action ?: return@launch
|
||||
val tunnelManager = Application.getTunnelManager()
|
||||
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D)
|
||||
tunnelManager.restoreState(false)
|
||||
} else if (Intent.ACTION_SHUTDOWN == action) {
|
||||
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
||||
tunnelManager.saveState()
|
||||
|
@ -21,6 +21,9 @@ import com.wireguard.android.activity.TunnelToggleActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
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
|
||||
@ -40,7 +43,7 @@ class QuickTileService : TileService() {
|
||||
var ret: IBinder? = null
|
||||
try {
|
||||
ret = super.onBind(intent)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
Log.d(TAG, "Failed to bind to TileService", e)
|
||||
}
|
||||
return ret
|
||||
@ -54,11 +57,12 @@ class QuickTileService : TileService() {
|
||||
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
||||
tile.updateTile()
|
||||
}
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
||||
if (t == null) {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
|
||||
updateTile()
|
||||
} else {
|
||||
val toggleIntent = Intent(this, TunnelToggleActivity::class.java)
|
||||
} catch (_: Throwable) {
|
||||
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(toggleIntent)
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ import androidx.databinding.CallbackRegistry
|
||||
import androidx.databinding.CallbackRegistry.NotifierCallback
|
||||
import com.wireguard.android.Application
|
||||
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.
|
||||
@ -35,11 +38,8 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
||||
else -> null
|
||||
}
|
||||
if (savedTunnelName != null) {
|
||||
Application.getTunnelManager()
|
||||
.tunnels
|
||||
.thenAccept { selectedTunnel = it[savedTunnelName] }
|
||||
}
|
||||
if (savedTunnelName != null)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] }
|
||||
|
||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -51,6 +51,7 @@ abstract class BaseActivity : ThemeChangeAwareActivity() {
|
||||
}
|
||||
|
||||
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
||||
|
||||
fun removeOnSelectedTunnelChangedListener(
|
||||
listener: OnSelectedTunnelChangedListener) {
|
||||
selectionChangeRegistry.remove(listener)
|
||||
|
@ -42,6 +42,7 @@ import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -67,7 +68,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
private var rawLogLines = StringBuffer()
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var saveButton: MenuItem? = null
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
private val logStreamingScope = CoroutineScope(Dispatchers.IO)
|
||||
private val year by lazy {
|
||||
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
||||
yearFormatter.format(Date())
|
||||
@ -114,7 +115,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||
}
|
||||
|
||||
coroutineScope.launch { streamingLog() }
|
||||
logStreamingScope.launch { streamingLog() }
|
||||
|
||||
binding.shareFab.setOnClickListener {
|
||||
revokeLastUri()
|
||||
@ -133,6 +134,11 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
logStreamingScope.cancel()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SHARE_ACTIVITY_REQUEST) {
|
||||
revokeLastUri()
|
||||
@ -153,27 +159,21 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
true
|
||||
}
|
||||
R.id.save_log -> {
|
||||
coroutineScope.launch { saveLog() }
|
||||
GlobalScope.launch { saveLog() }
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
private suspend fun saveLog() {
|
||||
val context = this
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
saveButton?.isEnabled = false
|
||||
withContext(Dispatchers.IO) {
|
||||
var exception: Throwable? = null
|
||||
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||
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 {
|
||||
it.write(rawLogLines.toString().toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
@ -181,7 +181,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
outputFile?.delete()
|
||||
exception = e
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
saveButton?.isEnabled = true
|
||||
Snackbar.make(findViewById(android.R.id.content),
|
||||
if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
|
||||
@ -212,7 +212,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
rawLogLines.append(line)
|
||||
rawLogLines.append('\n')
|
||||
val logLine = parseLine(line)
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (logLine != null) {
|
||||
recyclerView?.let {
|
||||
val shouldScroll = haveScrolled && !it.canScrollVertically(1)
|
||||
@ -348,7 +348,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||
try {
|
||||
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.util.AdminKnobs
|
||||
import com.wireguard.android.util.ModuleLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.ArrayList
|
||||
@ -102,8 +102,8 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
||||
).filterNotNull()
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
if (backend is WgQuickBackend) {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
if (Application.getBackend() is WgQuickBackend) {
|
||||
++preferenceScreen.initialExpandedChildrenCount
|
||||
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
||||
} else {
|
||||
@ -121,11 +121,11 @@ class SettingsActivity : ThemeChangeAwareActivity() {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
} else {
|
||||
kernelModuleDisabler?.parent?.removePreference(kernelModuleDisabler)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||
moduleInstaller?.isVisible = true
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
moduleInstaller?.parent?.removePreference(moduleInstaller)
|
||||
}
|
||||
}
|
||||
|
@ -17,27 +17,31 @@ import com.wireguard.android.QuickTileService
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TunnelToggleActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE).whenComplete { _, t ->
|
||||
TileService.requestListeningState(this, ComponentName(this, QuickTileService::class.java))
|
||||
onToggleFinished(t)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
private const val TAG = "WireGuard/TunnelToggleActivity"
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ object BindingAdapters {
|
||||
return 0
|
||||
return try {
|
||||
Integer.parseInt(s)
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import androidx.databinding.Observable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.R
|
||||
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.util.ErrorMessages
|
||||
import com.wireguard.android.util.requireTargetFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ -37,7 +36,7 @@ class AppListDialogFragment : DialogFragment() {
|
||||
private fun loadData() {
|
||||
val activity = activity ?: return
|
||||
val pm = activity.packageManager
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
try {
|
||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||
withContext(Dispatchers.IO) {
|
||||
@ -57,12 +56,12 @@ class AppListDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
appData.clear()
|
||||
appData.addAll(applicationData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||
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.activity.BaseActivity
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
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
|
||||
@ -70,14 +72,14 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
is TunnelListItemBinding -> binding.item
|
||||
else -> return
|
||||
} ?: return
|
||||
Application.getBackendAsync().thenAccept { backend: Backend? ->
|
||||
if (backend is GoBackend) {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
if (Application.getBackend() is GoBackend) {
|
||||
val intent = GoBackend.VpnService.prepare(view.context)
|
||||
if (intent != null) {
|
||||
pendingTunnel = tunnel
|
||||
pendingTunnelUp = checked
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
|
||||
return@thenAccept
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||
@ -85,19 +87,22 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
}
|
||||
|
||||
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
||||
tunnel.setStateAsync(Tunnel.State.of(checked)).whenComplete { _, throwable ->
|
||||
if (throwable == null) return@whenComplete
|
||||
val error = ErrorMessages[throwable]
|
||||
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||
val message = requireContext().getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById<View>(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, throwable)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
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 message = requireContext().getString(messageResId, error)
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById<View>(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,9 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
@ -28,11 +31,12 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
private fun createTunnelAndDismiss() {
|
||||
binding?.let {
|
||||
val name = it.tunnelNameText.text.toString()
|
||||
Application.getTunnelManager().create(name, config).whenComplete { tunnel, throwable ->
|
||||
if (tunnel != null) {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
Application.getTunnelManager().create(name, config)
|
||||
dismiss()
|
||||
} else {
|
||||
it.tunnelNameTextLayout.error = throwable.message
|
||||
} catch (e: Throwable) {
|
||||
it.tunnelNameTextLayout.error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,7 +53,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
||||
config = try {
|
||||
Config.parse(ByteArrayInputStream(configBytes))
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
when (e) {
|
||||
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
||||
else -> throw e
|
||||
|
@ -18,6 +18,9 @@ import com.wireguard.android.databinding.TunnelDetailPeerBinding
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
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.TimerTask
|
||||
|
||||
@ -79,7 +82,13 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||
binding ?: return
|
||||
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
|
||||
updateStats()
|
||||
}
|
||||
@ -105,30 +114,31 @@ class TunnelDetailFragment : BaseFragment() {
|
||||
val state = tunnel.state
|
||||
if (state != Tunnel.State.UP && lastState == state) return
|
||||
lastState = state
|
||||
tunnel.statisticsAsync.whenComplete { statistics, throwable ->
|
||||
if (throwable != null) {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
val tx = statistics.peerTx(publicKey)
|
||||
if (rx == 0L && tx == 0L) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferLabel.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
|
||||
}
|
||||
return@whenComplete
|
||||
}
|
||||
for (i in 0 until binding!!.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val rx = statistics.peerRx(publicKey)
|
||||
val tx = statistics.peerTx(publicKey)
|
||||
if (rx == 0L && tx == 0L) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
continue
|
||||
}
|
||||
peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,16 @@ import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppSelectionListener
|
||||
import com.wireguard.android.model.ObservableTunnel
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.BiometricAuthenticator
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.viewmodel.ConfigProxy
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
|
||||
import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
@ -130,7 +133,7 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
binding ?: return false
|
||||
val newConfig = try {
|
||||
binding!!.config!!.resolve()
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
||||
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()
|
||||
return false
|
||||
}
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
tunnel!!.setConfigAsync(newConfig)
|
||||
.whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
when {
|
||||
tunnel == null -> {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||
val manager = Application.getTunnelManager()
|
||||
try {
|
||||
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelCreated(null, e)
|
||||
}
|
||||
}
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
try {
|
||||
tunnel!!.setNameAsync(binding!!.name!!)
|
||||
onTunnelRenamed(tunnel!!, newConfig, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
try {
|
||||
tunnel!!.setConfigAsync(newConfig)
|
||||
onConfigSaved(tunnel!!, null)
|
||||
} catch (e: Throwable) {
|
||||
onConfigSaved(tunnel!!, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
@ -187,13 +205,18 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
binding!!.config = ConfigProxy()
|
||||
if (tunnel != null) {
|
||||
binding!!.name = tunnel!!.name
|
||||
tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
onConfigLoaded(tunnel!!.getConfigAsync())
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding!!.name = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
|
||||
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||
val message: String
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel
|
||||
@ -219,7 +242,14 @@ class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||
Log.d(TAG, message)
|
||||
// Now save the rest of configuration changes.
|
||||
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 {
|
||||
val error = ErrorMessages[throwable]
|
||||
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.MultiselectableRelativeLayout
|
||||
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.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
@ -61,108 +68,96 @@ class TunnelListFragment : BaseFragment() {
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
newInstance(configText).show(parentFragmentManager, null)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun importTunnel(uri: Uri?) {
|
||||
val activity = activity
|
||||
if (activity == null || uri == null) {
|
||||
return
|
||||
}
|
||||
val contentResolver = activity.contentResolver
|
||||
|
||||
val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
|
||||
val throwables = ArrayList<Throwable>()
|
||||
Application.getAsyncWorker().supplyAsync {
|
||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
var name = ""
|
||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val activity = activity
|
||||
if (activity == null || uri == null) {
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = Uri.decode(uri.lastPathSegment)
|
||||
}
|
||||
var idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
|
||||
name = name.substring(idx + 1)
|
||||
}
|
||||
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
require(isZip) { resources.getString(R.string.bad_extension_error) }
|
||||
}
|
||||
val contentResolver = activity.contentResolver
|
||||
val futureTunnels = ArrayList<Deferred<ObservableTunnel>>()
|
||||
val throwables = ArrayList<Throwable>()
|
||||
try {
|
||||
val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
var name = ""
|
||||
contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0)
|
||||
}
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
name = Uri.decode(uri.lastPathSegment)
|
||||
}
|
||||
var idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
require(idx < name.length - 1) { resources.getString(R.string.illegal_filename_error, name) }
|
||||
name = name.substring(idx + 1)
|
||||
}
|
||||
val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
require(isZip) { resources.getString(R.string.bad_extension_error) }
|
||||
}
|
||||
|
||||
if (isZip) {
|
||||
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
|
||||
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
|
||||
var entry: ZipEntry?
|
||||
while (true) {
|
||||
entry = zip.nextEntry ?: break
|
||||
name = entry.name
|
||||
idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length - 1) {
|
||||
continue
|
||||
if (isZip) {
|
||||
ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
|
||||
val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
|
||||
var entry: ZipEntry?
|
||||
while (true) {
|
||||
entry = zip.nextEntry ?: break
|
||||
name = entry.name
|
||||
idx = name.lastIndexOf('/')
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length - 1) {
|
||||
continue
|
||||
}
|
||||
name = name.substring(name.lastIndexOf('/') + 1)
|
||||
}
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
Config.parse(reader)
|
||||
} catch (e: Throwable) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}?.let {
|
||||
val nameCopy = name
|
||||
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(nameCopy, it) })
|
||||
}
|
||||
}
|
||||
name = name.substring(name.lastIndexOf('/') + 1)
|
||||
}
|
||||
if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
|
||||
name = name.substring(0, name.length - ".conf".length)
|
||||
} else {
|
||||
futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(name, Config.parse(contentResolver.openInputStream(uri)!!)) })
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size == 1) {
|
||||
throw throwables[0]
|
||||
} else {
|
||||
continue
|
||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
val tunnels = futureTunnels.mapNotNull {
|
||||
try {
|
||||
Config.parse(reader)
|
||||
} catch (e: Exception) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}?.let {
|
||||
futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
futureTunnels.add(
|
||||
Application.getTunnelManager().create(
|
||||
name,
|
||||
Config.parse(contentResolver.openInputStream(uri)!!)
|
||||
).toCompletableFuture()
|
||||
)
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size == 1) {
|
||||
throw throwables[0]
|
||||
} else {
|
||||
require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) }
|
||||
}
|
||||
}
|
||||
CompletableFuture.allOf(*futureTunnels.toTypedArray())
|
||||
}.whenComplete { future, exception ->
|
||||
if (exception != null) {
|
||||
onTunnelImportFinished(emptyList(), listOf(exception))
|
||||
} else {
|
||||
future.whenComplete { _, _ ->
|
||||
val tunnels = mutableListOf<ObservableTunnel>()
|
||||
for (futureTunnel in futureTunnels) {
|
||||
val tunnel: ObservableTunnel? = try {
|
||||
futureTunnel.getNow(null)
|
||||
} catch (e: Exception) {
|
||||
it.await()
|
||||
} catch (e: Throwable) {
|
||||
throwables.add(e)
|
||||
null
|
||||
}
|
||||
|
||||
if (tunnel != null) {
|
||||
tunnels.add(tunnel)
|
||||
}
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables)
|
||||
withContext(Dispatchers.Main.immediate) { 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?) {
|
||||
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 (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
|
||||
}
|
||||
@ -268,11 +264,10 @@ class TunnelListFragment : BaseFragment() {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
binding ?: return
|
||||
binding!!.fragment = this
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels -> binding!!.tunnels = tunnels }
|
||||
val parent = this
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
|
||||
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||
binding.fragment = parent
|
||||
binding.fragment = this@TunnelListFragment
|
||||
binding.root.setOnClickListener {
|
||||
if (actionMode == null) {
|
||||
selectedTunnel = item
|
||||
@ -321,20 +316,24 @@ class TunnelListFragment : BaseFragment() {
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
}
|
||||
Application.getTunnelManager().tunnels.thenAccept { tunnels ->
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { it.delete().toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*futures)
|
||||
.thenApply { futures.size }
|
||||
.whenComplete(this@TunnelListFragment::onTunnelDeletionFinished)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
|
||||
onTunnelDeletionFinished(futures.awaitAll().size, null)
|
||||
} catch (e: Throwable) {
|
||||
onTunnelDeletionFinished(0, e)
|
||||
}
|
||||
}
|
||||
checkedItems.clear()
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
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) {
|
||||
setItemChecked(i, true)
|
||||
}
|
||||
|
@ -4,16 +4,18 @@
|
||||
*/
|
||||
package com.wireguard.android.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
import com.wireguard.android.BR
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.databinding.Keyed
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
@ -30,10 +32,12 @@ class ObservableTunnel internal constructor(
|
||||
@Bindable
|
||||
override fun getName() = name
|
||||
|
||||
fun setNameAsync(name: String): CompletionStage<String> = if (name != this.name)
|
||||
manager.setTunnelName(this, name)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.name)
|
||||
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
if (name != this@ObservableTunnel.name)
|
||||
manager.setTunnelName(this@ObservableTunnel, name)
|
||||
else
|
||||
this@ObservableTunnel.name
|
||||
}
|
||||
|
||||
fun onNameChanged(name: String): String {
|
||||
this.name = name
|
||||
@ -57,31 +61,42 @@ class ObservableTunnel internal constructor(
|
||||
return state
|
||||
}
|
||||
|
||||
fun setStateAsync(state: Tunnel.State): CompletionStage<Tunnel.State> = if (state != this.state)
|
||||
manager.setTunnelState(this, state)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.state)
|
||||
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
if (state != this@ObservableTunnel.state)
|
||||
manager.setTunnelState(this@ObservableTunnel, state)
|
||||
else
|
||||
this@ObservableTunnel.state
|
||||
}
|
||||
|
||||
|
||||
@get:Bindable
|
||||
var config = config
|
||||
get() {
|
||||
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
|
||||
}
|
||||
private set
|
||||
|
||||
val configAsync: CompletionStage<Config>
|
||||
get() = if (config == null)
|
||||
manager.getTunnelConfig(this)
|
||||
else
|
||||
CompletableFuture.completedFuture(config)
|
||||
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
|
||||
config ?: manager.getTunnelConfig(this@ObservableTunnel)
|
||||
}
|
||||
|
||||
fun setConfigAsync(config: Config): CompletionStage<Config> = if (config != this.config)
|
||||
manager.setTunnelConfig(this, config)
|
||||
else
|
||||
CompletableFuture.completedFuture(this.config)
|
||||
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
this@ObservableTunnel.config.let {
|
||||
if (config != it)
|
||||
manager.setTunnelConfig(this@ObservableTunnel, config)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfigChanged(config: Config?): Config? {
|
||||
this.config = config
|
||||
@ -94,16 +109,26 @@ class ObservableTunnel internal constructor(
|
||||
var statistics: Statistics? = null
|
||||
get() {
|
||||
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
|
||||
}
|
||||
private set
|
||||
|
||||
val statisticsAsync: CompletionStage<Statistics>
|
||||
get() = if (statistics == null || statistics?.isStale != false)
|
||||
manager.getTunnelStatistics(this)
|
||||
else
|
||||
CompletableFuture.completedFuture(statistics)
|
||||
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
statistics.let {
|
||||
if (it == null || it.isStale)
|
||||
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||
else
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
fun onStatisticsChanged(statistics: 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.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.databinding.Bindable
|
||||
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.getSharedPreferences
|
||||
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.configStore.ConfigStore
|
||||
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||
import com.wireguard.android.util.ExceptionLoggers
|
||||
import com.wireguard.config.Config
|
||||
import java9.util.concurrent.CompletableFuture
|
||||
import java9.util.concurrent.CompletionStage
|
||||
import java.util.ArrayList
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
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
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
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 delayedLoadRestoreTunnels = ArrayList<CompletableFuture<Void>>()
|
||||
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
||||
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)
|
||||
tunnelMap.add(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))
|
||||
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))
|
||||
return CompletableFuture.failedFuture(IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name)))
|
||||
return getAsyncWorker().supplyAsync { configStore.create(name, config!!) }.thenApply { addToList(name, it, Tunnel.State.DOWN) }
|
||||
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||
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 wasLastUsed = tunnel == lastUsedTunnel
|
||||
// Make sure nothing touches the tunnel.
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
return getAsyncWorker().runAsync {
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
try {
|
||||
configStore.delete(tunnel.name)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
|
||||
} catch (e: Throwable) {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
throw e
|
||||
}
|
||||
}.whenComplete { _, e ->
|
||||
if (e == null)
|
||||
return@whenComplete
|
||||
} catch (e: Throwable) {
|
||||
// Failure, put the tunnel back.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,14 +96,18 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).commit()
|
||||
}
|
||||
|
||||
fun getTunnelConfig(tunnel: ObservableTunnel): CompletionStage<Config> = getAsyncWorker()
|
||||
.supplyAsync { configStore.load(tunnel.name) }.thenApply(tunnel::onConfigChanged)
|
||||
|
||||
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
|
||||
}
|
||||
|
||||
fun onCreate() {
|
||||
getAsyncWorker().supplyAsync { configStore.enumerate() }
|
||||
.thenAcceptBoth(getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }, this::onTunnelsLoaded)
|
||||
.whenComplete(ExceptionLoggers.E)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
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>) {
|
||||
@ -108,42 +116,38 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
val lastUsedName = getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null)
|
||||
if (lastUsedName != null)
|
||||
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||
var toComplete: Array<CompletableFuture<Void>>
|
||||
synchronized(delayedLoadRestoreTunnels) {
|
||||
haveLoaded = true
|
||||
toComplete = delayedLoadRestoreTunnels.toTypedArray()
|
||||
delayedLoadRestoreTunnels.clear()
|
||||
}
|
||||
restoreState(true).whenComplete { v: Void?, t: Throwable? ->
|
||||
for (f in toComplete) {
|
||||
if (t == null)
|
||||
f.complete(v)
|
||||
else
|
||||
f.completeExceptionally(t)
|
||||
}
|
||||
}
|
||||
haveLoaded = true
|
||||
restoreState(true)
|
||||
tunnels.complete(tunnelMap)
|
||||
}
|
||||
|
||||
fun refreshTunnelStates() {
|
||||
getAsyncWorker().supplyAsync { getBackend().runningTunnelNames }
|
||||
.thenAccept { running: Set<String> -> for (tunnel in tunnelMap) tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN) }
|
||||
.whenComplete(ExceptionLoggers.E)
|
||||
}
|
||||
|
||||
fun restoreState(force: Boolean): CompletionStage<Void> {
|
||||
if (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||
return CompletableFuture.completedFuture(null)
|
||||
synchronized(delayedLoadRestoreTunnels) {
|
||||
if (!haveLoaded) {
|
||||
val f = CompletableFuture<Void>()
|
||||
delayedLoadRestoreTunnels.add(f)
|
||||
return f
|
||||
private fun refreshTunnelStates() {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
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) {
|
||||
if (!haveLoaded || (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false)))
|
||||
return
|
||||
val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null)
|
||||
?: return CompletableFuture.completedFuture(null)
|
||||
return CompletableFuture.allOf(*tunnelMap.filter { previouslyRunning.contains(it.name) }.map { setTunnelState(it, Tunnel.State.UP).toCompletableFuture() }.toTypedArray())
|
||||
?: return
|
||||
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")
|
||||
@ -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()
|
||||
}
|
||||
|
||||
fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): CompletionStage<Config> = getAsyncWorker().supplyAsync {
|
||||
getBackend().setState(tunnel, tunnel.state, config)
|
||||
configStore.save(tunnel.name, config)
|
||||
}.thenApply { tunnel.onConfigChanged(it) }
|
||||
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
|
||||
getBackend().setState(tunnel, tunnel.state, config)
|
||||
configStore.save(tunnel.name, config)
|
||||
})!!
|
||||
}
|
||||
|
||||
fun setTunnelName(tunnel: ObservableTunnel, name: String): CompletionStage<String> {
|
||||
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||
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)) {
|
||||
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 wasLastUsed = tunnel == lastUsedTunnel
|
||||
@ -168,34 +174,45 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = null
|
||||
tunnelMap.remove(tunnel)
|
||||
return getAsyncWorker().supplyAsync {
|
||||
var throwable: Throwable? = null
|
||||
var newName: String? = null
|
||||
try {
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.DOWN, null)
|
||||
configStore.rename(tunnel.name, name)
|
||||
val newName = tunnel.onNameChanged(name)
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
|
||||
newName = tunnel.onNameChanged(name)
|
||||
if (originalState == Tunnel.State.UP)
|
||||
getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config)
|
||||
newName
|
||||
}.whenComplete { _, e ->
|
||||
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||
if (e != null)
|
||||
getTunnelState(tunnel)
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
getTunnelState(tunnel)
|
||||
}
|
||||
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||
tunnelMap.add(tunnel)
|
||||
if (wasLastUsed)
|
||||
lastUsedTunnel = tunnel
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newName!!
|
||||
}
|
||||
|
||||
fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): CompletionStage<Tunnel.State> = tunnel.configAsync
|
||||
.thenCompose { getAsyncWorker().supplyAsync { getBackend().setState(tunnel, state, it) } }
|
||||
.whenComplete { newState, e ->
|
||||
// Ensure onStateChanged is always called (failure or not), and with the correct state.
|
||||
tunnel.onStateChanged(if (e == null) newState else tunnel.state)
|
||||
if (e == null && newState == Tunnel.State.UP)
|
||||
lastUsedTunnel = tunnel
|
||||
saveState()
|
||||
}
|
||||
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
var newState = tunnel.state
|
||||
var throwable: Throwable? = null
|
||||
try {
|
||||
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
|
||||
if (newState == Tunnel.State.UP)
|
||||
lastUsedTunnel = tunnel
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
}
|
||||
tunnel.onStateChanged(newState)
|
||||
saveState()
|
||||
if (throwable != null)
|
||||
throw throwable
|
||||
newState
|
||||
}
|
||||
|
||||
class IntentReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
@ -215,20 +232,25 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
else -> return
|
||||
}
|
||||
val tunnelName = intent.getStringExtra("tunnel") ?: return
|
||||
manager.tunnels.thenAccept {
|
||||
val tunnel = it[tunnelName] ?: return@thenAccept
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
val tunnels = manager.getTunnels()
|
||||
val tunnel = tunnels[tunnelName] ?: return@launch
|
||||
manager.setTunnelState(tunnel, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTunnelState(tunnel: ObservableTunnel): CompletionStage<Tunnel.State> = getAsyncWorker()
|
||||
.supplyAsync { getBackend().getState(tunnel) }.thenApply(tunnel::onStateChanged)
|
||||
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
|
||||
}
|
||||
|
||||
fun getTunnelStatistics(tunnel: ObservableTunnel): CompletionStage<Statistics> = getAsyncWorker()
|
||||
.supplyAsync { getBackend().getStatistics(tunnel) }.thenApply(tunnel::onStatisticsChanged)
|
||||
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/TunnelManager"
|
||||
|
||||
private const val KEY_LAST_USED_TUNNEL = "last_used_tunnel"
|
||||
private const val KEY_RESTORE_ON_BOOT = "restore_on_boot"
|
||||
private const val KEY_RUNNING_TUNNELS = "enabled_configs"
|
||||
|
@ -8,22 +8,28 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.SettingsActivity
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
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
|
||||
|
||||
class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.UNKNOWN
|
||||
|
||||
init {
|
||||
isVisible = false
|
||||
Application.getBackendAsync().thenAccept { backend ->
|
||||
setState(if (backend is WgQuickBackend) State.ENABLED else State.DISABLED)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
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)
|
||||
Application.getSharedPreferences().edit().putBoolean("disable_kernel_module", true).commit()
|
||||
}
|
||||
Application.getAsyncWorker().runAsync {
|
||||
Application.getTunnelManager().tunnels.thenApply { observableTunnels ->
|
||||
val downings = observableTunnels.map { it.setStateAsync(Tunnel.State.DOWN).toCompletableFuture() }.toTypedArray()
|
||||
CompletableFuture.allOf(*downings).thenRun {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
val observableTunnels = Application.getTunnelManager().getTunnels()
|
||||
val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
|
||||
try {
|
||||
downings.awaitAll()
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
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),
|
||||
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.activity.SettingsActivity
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.module_installer_title)
|
||||
@ -32,24 +30,26 @@ class ModuleDownloaderPreference(context: Context, attrs: AttributeSet?) : Prefe
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
coroutineScope.launch {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
when (withContext(Dispatchers.IO) { Application.getModuleLoader().download() }) {
|
||||
OsConstants.ENOENT -> setState(State.NOTFOUND)
|
||||
OsConstants.EXIT_SUCCESS -> {
|
||||
setState(State.SUCCESS)
|
||||
Application.getSharedPreferences().edit().remove("disable_kernel_module").commit()
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val restartIntent = Intent(context, SettingsActivity::class.java)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
Application.get().startActivity(restartIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
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.R
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ -21,15 +21,13 @@ import kotlinx.coroutines.withContext
|
||||
*/
|
||||
class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var state = State.INITIAL
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
override fun getSummary() = context.getString(state.messageResourceId)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.tools_installer_title)
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
coroutineScope.launch {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
|
||||
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)
|
||||
else -> setState(State.INITIAL)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
setState(State.INITIAL)
|
||||
}
|
||||
}
|
||||
@ -47,7 +45,7 @@ class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Prefere
|
||||
|
||||
override fun onClick() {
|
||||
setState(State.WORKING)
|
||||
coroutineScope.launch {
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
|
||||
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)
|
||||
else -> setState(State.FAILURE)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} catch (_: Throwable) {
|
||||
setState(State.FAILURE)
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ import com.wireguard.android.R
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
@ -47,16 +47,16 @@ class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(con
|
||||
}
|
||||
|
||||
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))
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
versionSummary = try {
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
||||
} catch (_: Exception) {
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
}
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
versionSummary = try {
|
||||
getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
|
||||
} catch (_: Throwable) {
|
||||
getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).toLowerCase(Locale.ENGLISH))
|
||||
}
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,18 @@ import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.Application
|
||||
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.DownloadsFileSaver
|
||||
import com.wireguard.android.util.AdminKnobs
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
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.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
@ -29,52 +34,40 @@ import java.util.zip.ZipOutputStream
|
||||
*/
|
||||
class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
private var exportedFilePath: String? = null
|
||||
|
||||
private fun exportZip() {
|
||||
Application.getTunnelManager().tunnels.thenAccept(this::exportZip)
|
||||
}
|
||||
|
||||
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)
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in futureConfigs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(futureConfigs[i].getNow(null)!!.toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
zip.closeEntry()
|
||||
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))
|
||||
}
|
||||
val outputFile = DownloadsFileSaver.save(context, "wireguard-export.zip", "application/zip", true)
|
||||
try {
|
||||
ZipOutputStream(outputFile.outputStream).use { zip ->
|
||||
for (i in configs.indices) {
|
||||
zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
|
||||
zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
zip.closeEntry()
|
||||
}
|
||||
outputFile.fileName
|
||||
}.whenComplete(this::exportZipComplete)
|
||||
} catch (e: Throwable) {
|
||||
outputFile.delete()
|
||||
throw e
|
||||
}
|
||||
outputFile.fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportZipComplete(filePath: String?, throwable: Throwable?) {
|
||||
if (throwable != null) {
|
||||
val error = ErrorMessages[throwable]
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, throwable)
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
} else {
|
||||
exportedFilePath = filePath
|
||||
notifyChanged()
|
||||
notifyChanged()
|
||||
} catch (e: Throwable) {
|
||||
val error = ErrorMessages[e]
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this@ZipExporterPreference).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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