global: supply backend asynchronously

We can't block for IO, so move everything to async workers or to
callbacks.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2018-06-14 04:59:24 +02:00
parent 0f128f99a1
commit e8891d775b
6 changed files with 98 additions and 59 deletions

View File

@ -24,6 +24,8 @@ import com.wireguard.android.util.ToolsInstaller;
import java.io.File; import java.io.File;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
public class Application extends android.app.Application { public class Application extends android.app.Application {
@ -34,6 +36,10 @@ public class Application extends android.app.Application {
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private ToolsInstaller toolsInstaller; private ToolsInstaller toolsInstaller;
private TunnelManager tunnelManager; private TunnelManager tunnelManager;
private Handler handler;
private List<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private Object haveBackendCallbacksLock = new Object();
public Application() { public Application() {
weakSelf = new WeakReference<>(this); weakSelf = new WeakReference<>(this);
} }
@ -47,11 +53,40 @@ public class Application extends android.app.Application {
} }
public static Backend getBackend() { public static Backend getBackend() {
return get().backend; final Application app = get();
synchronized(app) {
if (app.backend == null) {
if (new File("/sys/module/wireguard").exists()) {
try {
app.rootShell.start();
app.backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) { }
}
if (app.backend == null)
app.backend = new GoBackend(app.getApplicationContext());
synchronized (app.haveBackendCallbacksLock) {
for (final BackendCallback callback : app.haveBackendCallbacks)
app.handler.post(() -> callback.callback(app.backend));
app.haveBackendCallbacks = null;
}
}
return app.backend;
}
} }
public static Class getBackendType() { @FunctionalInterface
return get().backend.getClass(); public interface BackendCallback {
void callback(final Backend backend);
}
public static void onHaveBackend(final BackendCallback callback) {
final Application app = get();
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks == null)
callback.callback(app.backend);
else
app.haveBackendCallbacks.add(callback);
}
} }
public static RootShell getRootShell() { public static RootShell getRootShell() {
@ -74,8 +109,8 @@ public class Application extends android.app.Application {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
handler = new Handler(Looper.getMainLooper());
final Executor executor = AsyncTask.SERIAL_EXECUTOR; final Executor executor = AsyncTask.SERIAL_EXECUTOR;
final Handler handler = new Handler(Looper.getMainLooper());
final ConfigStore configStore = new FileConfigStore(getApplicationContext()); final ConfigStore configStore = new FileConfigStore(getApplicationContext());
asyncWorker = new AsyncWorker(executor, handler); asyncWorker = new AsyncWorker(executor, handler);
@ -87,16 +122,8 @@ public class Application extends android.app.Application {
sharedPreferences.getBoolean("dark_theme", false) ? sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
if (new File("/sys/module/wireguard").exists()) { tunnelManager = new TunnelManager(configStore);
try { asyncWorker.runAsync(Application::getBackend);
rootShell.start();
backend = new WgQuickBackend(getApplicationContext());
} catch (final Exception ignored) { }
}
if (backend == null)
backend = new GoBackend(getApplicationContext());
tunnelManager = new TunnelManager(backend, configStore);
tunnelManager.onCreate(); tunnelManager.onCreate();
} }
} }

View File

@ -20,18 +20,20 @@ public class BootShutdownReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(final Context context, final Intent intent) { public void onReceive(final Context context, final Intent intent) {
if (Application.getBackendType() != WgQuickBackend.class) Application.onHaveBackend(backend -> {
return; if (backend.getClass() != WgQuickBackend.class)
final String action = intent.getAction(); return;
if (action == null) final String action = intent.getAction();
return; if (action == null)
final TunnelManager tunnelManager = Application.getTunnelManager(); return;
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { final TunnelManager tunnelManager = Application.getTunnelManager();
Log.i(TAG, "Broadcast receiver restoring state (boot)"); if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D); Log.i(TAG, "Broadcast receiver restoring state (boot)");
} else if (Intent.ACTION_SHUTDOWN.equals(action)) { tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
Log.i(TAG, "Broadcast receiver saving state (shutdown)"); } else if (Intent.ACTION_SHUTDOWN.equals(action)) {
tunnelManager.saveState(); Log.i(TAG, "Broadcast receiver saving state (shutdown)");
} tunnelManager.saveState();
}
});
} }
} }

View File

@ -54,11 +54,13 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
// The selected tunnel must be set before the superclass method recreates fragments. // The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Application.getBackendType() == GoBackend.class) { Application.onHaveBackend(backend -> {
final Intent intent = GoBackend.VpnService.prepare(this); if (backend.getClass() == GoBackend.class) {
if (intent != null) final Intent intent = GoBackend.VpnService.prepare(this);
startActivityForResult(intent, 0); if (intent != null)
} startActivityForResult(intent, 0);
}
});
} }
@Override @Override

View File

@ -16,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat; import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import android.view.MenuItem; import android.view.MenuItem;
import com.wireguard.android.Application; import com.wireguard.android.Application;
@ -95,12 +96,21 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
@Override @Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) { public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
if (Application.getBackendType() != WgQuickBackend.class) { final Preference wgQuickOnlyPrefs[] = {
Preference pref = getPreferenceManager().findPreference("tools_installer"); getPreferenceManager().findPreference("tools_installer"),
getPreferenceScreen().removePreference(pref); getPreferenceManager().findPreference("restore_on_boot")
pref = getPreferenceManager().findPreference("restore_on_boot"); };
getPreferenceScreen().removePreference(pref); for (final Preference pref : wgQuickOnlyPrefs)
} pref.setVisible(false);
final PreferenceScreen screen = getPreferenceScreen();
Application.onHaveBackend(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend.getClass() == WgQuickBackend.class)
pref.setVisible(true);
else
screen.removePreference(pref);
}
});
} }
} }
} }

View File

@ -15,7 +15,6 @@ import android.support.annotation.NonNull;
import com.wireguard.android.Application; import com.wireguard.android.Application;
import com.wireguard.android.BR; import com.wireguard.android.BR;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.configStore.ConfigStore; import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State; import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics; import com.wireguard.android.model.Tunnel.Statistics;
@ -47,7 +46,6 @@ public final class TunnelManager extends BaseObservable {
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot"; private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
private static final String KEY_RUNNING_TUNNELS = "enabled_configs"; private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final Backend backend;
private final ConfigStore configStore; private final ConfigStore configStore;
private final ObservableSortedKeyedList<String, Tunnel> tunnels = private final ObservableSortedKeyedList<String, Tunnel> tunnels =
new ObservableSortedKeyedArrayList<>(COMPARATOR); new ObservableSortedKeyedArrayList<>(COMPARATOR);
@ -55,8 +53,7 @@ public final class TunnelManager extends BaseObservable {
private boolean haveLoaded; private boolean haveLoaded;
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>(); private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
public TunnelManager(final Backend backend, final ConfigStore configStore) { public TunnelManager(final ConfigStore configStore) {
this.backend = backend;
this.configStore = configStore; this.configStore = configStore;
} }
@ -86,12 +83,12 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel); tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> { return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP) if (originalState == State.UP)
backend.setState(tunnel, State.DOWN); Application.getBackend().setState(tunnel, State.DOWN);
try { try {
configStore.delete(tunnel.getName()); configStore.delete(tunnel.getName());
} catch (final Exception e) { } catch (final Exception e) {
if (originalState == State.UP) if (originalState == State.UP)
backend.setState(tunnel, State.UP); Application.getBackend().setState(tunnel, State.UP);
// Re-throw the exception to fail the completion. // Re-throw the exception to fail the completion.
throw e; throw e;
} }
@ -116,12 +113,12 @@ public final class TunnelManager extends BaseObservable {
} }
CompletionStage<State> getTunnelState(final Tunnel tunnel) { CompletionStage<State> getTunnelState(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> backend.getState(tunnel)) return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged); .thenApply(tunnel::onStateChanged);
} }
CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) { CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> backend.getStatistics(tunnel)) return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged); .thenApply(tunnel::onStatisticsChanged);
} }
@ -131,7 +128,7 @@ public final class TunnelManager extends BaseObservable {
public void onCreate() { public void onCreate() {
Application.getAsyncWorker().supplyAsync(configStore::enumerate) Application.getAsyncWorker().supplyAsync(configStore::enumerate)
.thenAcceptBoth(Application.getAsyncWorker().supplyAsync(backend::enumerate), this::onTunnelsLoaded) .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded)
.whenComplete(ExceptionLoggers.E); .whenComplete(ExceptionLoggers.E);
} }
@ -159,7 +156,7 @@ public final class TunnelManager extends BaseObservable {
} }
public void refreshTunnelStates() { public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(backend::enumerate) Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
.thenAccept(running -> { .thenAccept(running -> {
for (final Tunnel tunnel : tunnels) for (final Tunnel tunnel : tunnels)
tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN); tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
@ -207,7 +204,7 @@ public final class TunnelManager extends BaseObservable {
CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) { CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
return Application.getAsyncWorker().supplyAsync(() -> { return Application.getAsyncWorker().supplyAsync(() -> {
final Config appliedConfig = backend.applyConfig(tunnel, config); final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
return configStore.save(tunnel.getName(), appliedConfig); return configStore.save(tunnel.getName(), appliedConfig);
}).thenApply(tunnel::onConfigChanged); }).thenApply(tunnel::onConfigChanged);
} }
@ -227,11 +224,11 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel); tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> { return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP) if (originalState == State.UP)
backend.setState(tunnel, State.DOWN); Application.getBackend().setState(tunnel, State.DOWN);
configStore.rename(tunnel.getName(), name); configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name); final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP) if (originalState == State.UP)
backend.setState(tunnel, State.UP); Application.getBackend().setState(tunnel, State.UP);
return newName; return newName;
}).whenComplete((newName, e) -> { }).whenComplete((newName, e) -> {
// On failure, we don't know what state the tunnel might be in. Fix that. // On failure, we don't know what state the tunnel might be in. Fix that.
@ -247,7 +244,7 @@ public final class TunnelManager extends BaseObservable {
CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) { CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
// Ensure the configuration is loaded before trying to use it. // Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x -> return tunnel.getConfigAsync().thenCompose(x ->
Application.getAsyncWorker().supplyAsync(() -> backend.setState(tunnel, state)) Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
).whenComplete((newState, e) -> { ).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state. // Ensure onStateChanged is always called (failure or not), and with the correct state.
tunnel.onStateChanged(e == null ? newState : tunnel.getState()); tunnel.onStateChanged(e == null ? newState : tunnel.getState());

View File

@ -27,13 +27,14 @@ public class VersionPreference extends Preference {
public VersionPreference(final Context context, final AttributeSet attrs) { public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
final Backend backend = Application.getBackend(); Application.onHaveBackend(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase()); versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> { Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null versionSummary = exception == null
? getContext().getString(R.string.version_summary, backend.getTypeName(), version) ? getContext().getString(R.string.version_summary, backend.getTypeName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase()); : getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase());
notifyChanged(); notifyChanged();
});
}); });
} }