diff options
23 files changed, 569 insertions, 532 deletions
diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt index 5563fe62..8b3be8de 100644 --- a/ui/src/main/java/com/wireguard/android/Application.kt +++ b/ui/src/main/java/com/wireguard/android/Application.kt @@ -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 diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt index 41aff76d..70899a0c 100644 --- a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt +++ b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt @@ -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() diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt index 5099668e..5989499f 100644 --- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt +++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt @@ -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) } diff --git a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt index 14ab0bdb..e663c1f2 100644 --- a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt @@ -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) diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt index 87fdc236..e689f8ea 100644 --- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt @@ -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) { } } } diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt index 81548fe7..3abfe07c 100644 --- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt @@ -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) } } diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt index 44d81c01..004b10be 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt @@ -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" } diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt index 055c2f06..b4c27a63 100644 --- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt +++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt @@ -158,7 +158,7 @@ object BindingAdapters { return 0 return try { Integer.parseInt(s) - } catch (_: Exception) { + } catch (_: Throwable) { 0 } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt index 1a29c5e6..966ba7d1 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt @@ -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() diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt index 82802623..997c2221 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt @@ -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) + } } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt index d1b01944..12406df2 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt @@ -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 diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 2b5a4ba6..e81b4e6d 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -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 - peer.transferLabel.visibility = View.GONE - peer.transferText.visibility = View.GONE + 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 } - 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) { + } 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 - 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 } } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index cf39d052..5b556bc2 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -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) diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt index 7af5e06b..3250db65 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt @@ -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 - } - try { - Config.parse(reader) - } catch (e: Exception) { - throwables.add(e) - null - }?.let { - futureTunnels.add(Application.getTunnelManager().create(name, it).toCompletableFuture()) + require(throwables.isNotEmpty()) { resources.getString(R.string.no_configs_error) } } } - } - } 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) { + val tunnels = futureTunnels.mapNotNull { + try { + 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) } diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index f8691cbb..d9d09b87 100644 --- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt @@ -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" + } } diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index 5091ed3b..b06585e4 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -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) + 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): 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 + fun restoreState(force: Boolean) { + if (!haveLoaded || (!force && !getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))) + return + val previouslyRunning = getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null) + ?: 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)) + } } } - 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()) } @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" diff --git a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt index 1479d7b6..6c289073 100644 --- a/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleDisablerPreference.kt @@ -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" + } } diff --git a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt index adf0dc27..6960733c 100644 --- a/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.kt @@ -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() } diff --git a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt index e9c0dc36..f9edb6e1 100644 --- a/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt @@ -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) } } diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt index f944233b..e3eb0f95 100644 --- a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt @@ -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() } } } diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt index fe8d39a3..c1eaa9f6 100644 --- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt +++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt @@ -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 + } } } diff --git a/ui/src/main/java/com/wireguard/android/util/AsyncWorker.kt b/ui/src/main/java/com/wireguard/android/util/AsyncWorker.kt deleted file mode 100644 index a6e5d4be..00000000 --- a/ui/src/main/java/com/wireguard/android/util/AsyncWorker.kt +++ /dev/null @@ -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 - } -} diff --git a/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.kt b/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.kt deleted file mode 100644 index 4470134c..00000000 --- a/ui/src/main/java/com/wireguard/android/util/ExceptionLoggers.kt +++ /dev/null @@ -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" - } -} |