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

View File

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

View File

@ -16,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import android.view.MenuItem;
import com.wireguard.android.Application;
@ -95,12 +96,21 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
addPreferencesFromResource(R.xml.preferences);
if (Application.getBackendType() != WgQuickBackend.class) {
Preference pref = getPreferenceManager().findPreference("tools_installer");
getPreferenceScreen().removePreference(pref);
pref = getPreferenceManager().findPreference("restore_on_boot");
getPreferenceScreen().removePreference(pref);
}
final Preference wgQuickOnlyPrefs[] = {
getPreferenceManager().findPreference("tools_installer"),
getPreferenceManager().findPreference("restore_on_boot")
};
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.BR;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
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_RUNNING_TUNNELS = "enabled_configs";
private final Backend backend;
private final ConfigStore configStore;
private final ObservableSortedKeyedList<String, Tunnel> tunnels =
new ObservableSortedKeyedArrayList<>(COMPARATOR);
@ -55,8 +53,7 @@ public final class TunnelManager extends BaseObservable {
private boolean haveLoaded;
private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
public TunnelManager(final Backend backend, final ConfigStore configStore) {
this.backend = backend;
public TunnelManager(final ConfigStore configStore) {
this.configStore = configStore;
}
@ -86,12 +83,12 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel);
return Application.getAsyncWorker().runAsync(() -> {
if (originalState == State.UP)
backend.setState(tunnel, State.DOWN);
Application.getBackend().setState(tunnel, State.DOWN);
try {
configStore.delete(tunnel.getName());
} catch (final Exception e) {
if (originalState == State.UP)
backend.setState(tunnel, State.UP);
Application.getBackend().setState(tunnel, State.UP);
// Re-throw the exception to fail the completion.
throw e;
}
@ -116,12 +113,12 @@ public final class TunnelManager extends BaseObservable {
}
CompletionStage<State> getTunnelState(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> backend.getState(tunnel))
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
.thenApply(tunnel::onStateChanged);
}
CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
return Application.getAsyncWorker().supplyAsync(() -> backend.getStatistics(tunnel))
return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
.thenApply(tunnel::onStatisticsChanged);
}
@ -131,7 +128,7 @@ public final class TunnelManager extends BaseObservable {
public void onCreate() {
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);
}
@ -159,7 +156,7 @@ public final class TunnelManager extends BaseObservable {
}
public void refreshTunnelStates() {
Application.getAsyncWorker().supplyAsync(backend::enumerate)
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
.thenAccept(running -> {
for (final Tunnel tunnel : tunnels)
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) {
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);
}).thenApply(tunnel::onConfigChanged);
}
@ -227,11 +224,11 @@ public final class TunnelManager extends BaseObservable {
tunnels.remove(tunnel);
return Application.getAsyncWorker().supplyAsync(() -> {
if (originalState == State.UP)
backend.setState(tunnel, State.DOWN);
Application.getBackend().setState(tunnel, State.DOWN);
configStore.rename(tunnel.getName(), name);
final String newName = tunnel.onNameChanged(name);
if (originalState == State.UP)
backend.setState(tunnel, State.UP);
Application.getBackend().setState(tunnel, State.UP);
return newName;
}).whenComplete((newName, e) -> {
// 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) {
// Ensure the configuration is loaded before trying to use it.
return tunnel.getConfigAsync().thenCompose(x ->
Application.getAsyncWorker().supplyAsync(() -> backend.setState(tunnel, state))
Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
).whenComplete((newState, e) -> {
// Ensure onStateChanged is always called (failure or not), and with the correct state.
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) {
super(context, attrs);
final Backend backend = Application.getBackend();
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
? getContext().getString(R.string.version_summary, backend.getTypeName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase());
notifyChanged();
Application.onHaveBackend(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
? getContext().getString(R.string.version_summary, backend.getTypeName(), version)
: getContext().getString(R.string.version_summary_unknown, backend.getTypeName().toLowerCase());
notifyChanged();
});
});
}