Serviceless rewrite, part 1
Signed-off-by: Samuel Holland <samuel@sholland.org>
This commit is contained in:
parent
4c0869393e
commit
609194fae2
@ -31,6 +31,20 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.14.1'
|
||||
implementation 'com.android.databinding:library:1.3.3'
|
||||
implementation 'com.android.support:support-annotations:27.0.2'
|
||||
implementation 'com.commonsware.cwac:crossport:0.2.1'
|
||||
implementation 'com.gabrielittner.threetenbp:lazythreetenbp:0.2.0'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.google.dagger:dagger:2.14.1'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrofuture:1.6.0'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.6.0'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://s3.amazonaws.com/repo.commonsware.com"
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.wireguard.android"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:extractNativeLibs="true"
|
||||
android:name=".Application"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar">
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name=".AddActivity"
|
||||
android:label="@string/add_activity_title"
|
||||
android:parentActivityName=".ConfigActivity" />
|
||||
<activity android:name=".activity.MainActivity">
|
||||
|
||||
<activity android:name=".ConfigActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -32,17 +32,17 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".NotSupportedActivity"
|
||||
android:label="@string/not_supported"
|
||||
android:parentActivityName=".ConfigActivity" />
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".ConfigActivity" />
|
||||
android:name=".activity.TunnelCreatorActivity"
|
||||
android:label="@string/add_activity_title" />
|
||||
|
||||
<receiver android:name=".BootShutdownReceiver">
|
||||
|
||||
<receiver android:name=".BootCompletedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
@ -51,17 +51,14 @@
|
||||
android:name=".QuickTileService"
|
||||
android:icon="@drawable/ic_tile"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
android:value="false" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".backends.VpnService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -1,46 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.FragmentManager;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Standalone activity for creating configurations.
|
||||
*/
|
||||
|
||||
public class AddActivity extends BaseConfigActivity {
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.add_activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config oldConfig, final Config newConfig) {
|
||||
// Do nothing (this never happens).
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEditingStateChanged(final boolean isEditing) {
|
||||
// Go back to the main activity once the new configuration is created.
|
||||
if (!isEditing)
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceAvailable() {
|
||||
super.onServiceAvailable();
|
||||
final FragmentManager fm = getFragmentManager();
|
||||
ConfigEditFragment fragment = (ConfigEditFragment) fm.findFragmentById(R.id.master_fragment);
|
||||
if (fragment == null) {
|
||||
fragment = new ConfigEditFragment();
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
transaction.add(R.id.master_fragment, fragment);
|
||||
transaction.commit();
|
||||
}
|
||||
// Prime the state for the fragment to tell us it is finished.
|
||||
setIsEditing(true);
|
||||
}
|
||||
}
|
124
app/src/main/java/com/wireguard/android/Application.java
Normal file
124
app/src/main/java/com/wireguard/android/Application.java
Normal file
@ -0,0 +1,124 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.gabrielittner.threetenbp.LazyThreeTen;
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.configStore.ConfigStore;
|
||||
import com.wireguard.android.configStore.FileConfigStore;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import javax.inject.Scope;
|
||||
|
||||
import dagger.Component;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
/**
|
||||
* Base context for the WireGuard Android application. This class (instantiated once during the
|
||||
* application lifecycle) maintains and mediates access to the global state of the application.
|
||||
*/
|
||||
|
||||
public class Application extends android.app.Application {
|
||||
private static ApplicationComponent component;
|
||||
|
||||
public static ApplicationComponent getComponent() {
|
||||
if (component == null)
|
||||
throw new IllegalStateException("Application instance not yet created");
|
||||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
component = DaggerApplication_ApplicationComponent.builder()
|
||||
.applicationModule(new ApplicationModule(this))
|
||||
.build();
|
||||
component.getTunnelManager().onCreate();
|
||||
LazyThreeTen.init(this);
|
||||
}
|
||||
|
||||
@ApplicationScope
|
||||
@Component(modules = ApplicationModule.class)
|
||||
public interface ApplicationComponent {
|
||||
AsyncWorker getAsyncWorker();
|
||||
|
||||
SharedPreferences getPreferences();
|
||||
|
||||
TunnelManager getTunnelManager();
|
||||
}
|
||||
|
||||
@Qualifier
|
||||
public @interface ApplicationContext {
|
||||
}
|
||||
|
||||
@Qualifier
|
||||
public @interface ApplicationHandler {
|
||||
}
|
||||
|
||||
@Scope
|
||||
public @interface ApplicationScope {
|
||||
}
|
||||
|
||||
@Module
|
||||
public static final class ApplicationModule {
|
||||
private final Context context;
|
||||
|
||||
private ApplicationModule(final Application application) {
|
||||
context = application.getApplicationContext();
|
||||
}
|
||||
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public static Backend getBackend(final AsyncWorker asyncWorker,
|
||||
@ApplicationContext final Context context,
|
||||
final RootShell rootShell) {
|
||||
return new WgQuickBackend(asyncWorker, context, rootShell);
|
||||
}
|
||||
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public static ConfigStore getConfigStore(final AsyncWorker asyncWorker,
|
||||
@ApplicationContext final Context context) {
|
||||
return new FileConfigStore(asyncWorker, context);
|
||||
}
|
||||
|
||||
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public static Executor getExecutor() {
|
||||
return AsyncTask.SERIAL_EXECUTOR;
|
||||
}
|
||||
|
||||
@ApplicationHandler
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public static Handler getHandler() {
|
||||
return new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public static SharedPreferences getPreferences(@ApplicationContext final Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||
}
|
||||
|
||||
@ApplicationContext
|
||||
@ApplicationScope
|
||||
@Provides
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Base class for activities that need to remember the current configuration and wait for a service.
|
||||
*/
|
||||
|
||||
abstract class BaseConfigActivity extends Activity {
|
||||
protected static final String KEY_CURRENT_CONFIG = "currentConfig";
|
||||
protected static final String KEY_IS_EDITING = "isEditing";
|
||||
|
||||
private Config currentConfig;
|
||||
private String initialConfig;
|
||||
private boolean isEditing;
|
||||
private boolean wasEditing;
|
||||
|
||||
protected Config getCurrentConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
protected boolean isEditing() {
|
||||
return isEditing;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Restore the saved configuration if there is one; otherwise grab it from the intent.
|
||||
if (savedInstanceState != null) {
|
||||
initialConfig = savedInstanceState.getString(KEY_CURRENT_CONFIG);
|
||||
wasEditing = savedInstanceState.getBoolean(KEY_IS_EDITING, false);
|
||||
} else {
|
||||
final Intent intent = getIntent();
|
||||
initialConfig = intent.getStringExtra(KEY_CURRENT_CONFIG);
|
||||
wasEditing = intent.getBooleanExtra(KEY_IS_EDITING, false);
|
||||
}
|
||||
// Trigger starting the service as early as possible
|
||||
if (VpnService.getInstance() != null)
|
||||
onServiceAvailable();
|
||||
else
|
||||
bindService(new Intent(this, VpnService.class), new ServiceConnectionCallbacks(),
|
||||
Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
protected abstract void onCurrentConfigChanged(Config oldCconfig, Config newConfig);
|
||||
|
||||
protected abstract void onEditingStateChanged(boolean isEditing);
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (currentConfig != null)
|
||||
outState.putString(KEY_CURRENT_CONFIG, currentConfig.getName());
|
||||
outState.putBoolean(KEY_IS_EDITING, isEditing);
|
||||
}
|
||||
|
||||
protected void onServiceAvailable() {
|
||||
// Make sure the subclass activity is initialized before setting its config.
|
||||
if (initialConfig != null && currentConfig == null)
|
||||
setCurrentConfig(VpnService.getInstance().get(initialConfig));
|
||||
setIsEditing(wasEditing);
|
||||
}
|
||||
|
||||
public void setCurrentConfig(final Config config) {
|
||||
if (currentConfig == config)
|
||||
return;
|
||||
final Config oldConfig = currentConfig;
|
||||
currentConfig = config;
|
||||
onCurrentConfigChanged(oldConfig, config);
|
||||
}
|
||||
|
||||
public void setIsEditing(final boolean isEditing) {
|
||||
if (this.isEditing == isEditing)
|
||||
return;
|
||||
this.isEditing = isEditing;
|
||||
onEditingStateChanged(isEditing);
|
||||
}
|
||||
|
||||
private class ServiceConnectionCallbacks implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(final ComponentName component, final IBinder binder) {
|
||||
// We don't actually need a binding, only notification that the service is started.
|
||||
unbindService(this);
|
||||
onServiceAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(final ComponentName component) {
|
||||
// This can never happen; the service runs in the same thread as the activity.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to remember the current configuration.
|
||||
*/
|
||||
|
||||
abstract class BaseConfigFragment extends Fragment {
|
||||
private static final String KEY_CURRENT_CONFIG = "currentConfig";
|
||||
|
||||
private Config currentConfig;
|
||||
|
||||
protected Config getCurrentConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
protected abstract void onCurrentConfigChanged(Config config);
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Restore the saved configuration if there is one; otherwise grab it from the arguments.
|
||||
String initialConfig = null;
|
||||
if (savedInstanceState != null)
|
||||
initialConfig = savedInstanceState.getString(KEY_CURRENT_CONFIG);
|
||||
else if (getArguments() != null)
|
||||
initialConfig = getArguments().getString(KEY_CURRENT_CONFIG);
|
||||
if (initialConfig != null && currentConfig == null)
|
||||
setCurrentConfig(VpnService.getInstance().get(initialConfig));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (currentConfig != null)
|
||||
outState.putString(KEY_CURRENT_CONFIG, currentConfig.getName());
|
||||
}
|
||||
|
||||
public void setCurrentConfig(final Config config) {
|
||||
if (currentConfig == config)
|
||||
return;
|
||||
currentConfig = config;
|
||||
onCurrentConfigChanged(currentConfig);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
|
||||
public class BootCompletedReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED))
|
||||
return;
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
|
||||
public class BootShutdownReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = BootShutdownReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
final TunnelManager tunnelManager = Application.getComponent().getTunnelManager();
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
Log.d(TAG, "Broadcast receiver restoring state (boot)");
|
||||
tunnelManager.restoreState().whenComplete(ExceptionLoggers.D);
|
||||
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
|
||||
Log.d(TAG, "Broadcast receiver saving state (shutdown)");
|
||||
tunnelManager.saveState().whenComplete(ExceptionLoggers.D);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Intent;
|
||||
import android.databinding.Observable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Activity that allows creating/viewing/editing/deleting WireGuard configurations.
|
||||
*/
|
||||
|
||||
public class ConfigActivity extends BaseConfigActivity {
|
||||
private static final String KEY_EDITOR_STATE = "editorState";
|
||||
private static final String TAG_DETAIL = "detail";
|
||||
private static final String TAG_EDIT = "edit";
|
||||
private static final String TAG_LIST = "list";
|
||||
|
||||
private Fragment.SavedState editorState;
|
||||
private final FragmentManager fm = getFragmentManager();
|
||||
private final FragmentCache fragments = new FragmentCache(fm);
|
||||
private boolean isLayoutFinished;
|
||||
private boolean isServiceAvailable;
|
||||
private boolean isSingleLayout;
|
||||
private boolean isStateSaved;
|
||||
private int mainContainer;
|
||||
private Observable.OnPropertyChangedCallback nameChangeCallback;
|
||||
private String visibleFragmentTag;
|
||||
|
||||
/**
|
||||
* Updates the fragment visible in the UI.
|
||||
* Sets visibleFragmentTag.
|
||||
*
|
||||
* @param config The config that should be visible.
|
||||
* @param tag The tag of the fragment that should be visible.
|
||||
*/
|
||||
private void moveToFragment(final Config config, final String tag) {
|
||||
// Sanity check.
|
||||
if (tag == null && config != null)
|
||||
throw new IllegalArgumentException("Cannot set a config on a null fragment");
|
||||
if ((tag == null && isSingleLayout) || (TAG_LIST.equals(tag) && !isSingleLayout))
|
||||
throw new IllegalArgumentException("Requested tag " + tag + " does not match layout");
|
||||
// First tear down fragments as necessary.
|
||||
if (tag == null || TAG_LIST.equals(tag) || (TAG_DETAIL.equals(tag)
|
||||
&& TAG_EDIT.equals(visibleFragmentTag))) {
|
||||
while (visibleFragmentTag != null && !visibleFragmentTag.equals(tag) &&
|
||||
fm.getBackStackEntryCount() > 0) {
|
||||
final Fragment removedFragment = fm.findFragmentById(mainContainer);
|
||||
// The fragment *must* be removed first, or it will stay attached to the layout!
|
||||
fm.beginTransaction().remove(removedFragment).commit();
|
||||
fm.popBackStackImmediate();
|
||||
// Recompute the visible fragment.
|
||||
if (TAG_EDIT.equals(visibleFragmentTag))
|
||||
visibleFragmentTag = TAG_DETAIL;
|
||||
else if (isSingleLayout && TAG_DETAIL.equals(visibleFragmentTag))
|
||||
visibleFragmentTag = TAG_LIST;
|
||||
else
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
// Now build up intermediate entries in the back stack as necessary.
|
||||
if (TAG_EDIT.equals(tag) && !TAG_EDIT.equals(visibleFragmentTag) &&
|
||||
!TAG_DETAIL.equals(visibleFragmentTag))
|
||||
moveToFragment(config, TAG_DETAIL);
|
||||
// Finally, set the main container's content to the new top-level fragment.
|
||||
if (tag == null) {
|
||||
if (visibleFragmentTag != null) {
|
||||
final BaseConfigFragment fragment = fragments.get(visibleFragmentTag);
|
||||
fm.beginTransaction().remove(fragment).commit();
|
||||
fm.executePendingTransactions();
|
||||
visibleFragmentTag = null;
|
||||
}
|
||||
} else if (!TAG_LIST.equals(tag)) {
|
||||
final BaseConfigFragment fragment = fragments.get(tag);
|
||||
if (!tag.equals(visibleFragmentTag)) {
|
||||
// Restore any saved editor state the first time its fragment is added.
|
||||
if (TAG_EDIT.equals(tag) && editorState != null) {
|
||||
fragment.setInitialSavedState(editorState);
|
||||
editorState = null;
|
||||
}
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
if (TAG_EDIT.equals(tag) || (isSingleLayout && TAG_DETAIL.equals(tag))) {
|
||||
transaction.addToBackStack(null);
|
||||
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
|
||||
}
|
||||
transaction.replace(mainContainer, fragment, tag).commit();
|
||||
visibleFragmentTag = tag;
|
||||
}
|
||||
if (fragment.getCurrentConfig() != config)
|
||||
fragment.setCurrentConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition the state machine to the desired state, if possible.
|
||||
* Sets currentConfig and isEditing.
|
||||
*
|
||||
* @param config The desired config to show in the UI.
|
||||
* @param shouldBeEditing Whether or not the config should be in the editing state.
|
||||
*/
|
||||
private void moveToState(final Config config, final boolean shouldBeEditing) {
|
||||
// Update the saved state.
|
||||
setCurrentConfig(config);
|
||||
setIsEditing(shouldBeEditing);
|
||||
// Avoid performing fragment transactions when the app is not fully initialized.
|
||||
if (!isLayoutFinished || !isServiceAvailable || isStateSaved)
|
||||
return;
|
||||
// Ensure the list is present in the master pane. It will be restored on activity restarts!
|
||||
final BaseConfigFragment listFragment = fragments.get(TAG_LIST);
|
||||
if (fm.findFragmentById(R.id.master_fragment) == null)
|
||||
fm.beginTransaction().add(R.id.master_fragment, listFragment, TAG_LIST).commit();
|
||||
// In the single-pane layout, the main container starts holding the list fragment.
|
||||
if (isSingleLayout && visibleFragmentTag == null)
|
||||
visibleFragmentTag = TAG_LIST;
|
||||
// Forward any config changes to the list (they may have come from the intent or editing).
|
||||
listFragment.setCurrentConfig(config);
|
||||
// Ensure the correct main fragment is visible, adjusting the back stack as necessary.
|
||||
moveToFragment(config, shouldBeEditing ? TAG_EDIT :
|
||||
(config != null ? TAG_DETAIL : (isSingleLayout ? TAG_LIST : null)));
|
||||
// Show the current config as the title if the list of configurations is not visible.
|
||||
setTitle(isSingleLayout && config != null ? config.getName() : getString(R.string.app_name));
|
||||
// Show or hide the action bar back button if the back stack is not empty.
|
||||
if (getActionBar() != null) {
|
||||
getActionBar().setDisplayHomeAsUpEnabled(config != null &&
|
||||
(isSingleLayout || shouldBeEditing));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
final ConfigListFragment listFragment = (ConfigListFragment) fragments.get(TAG_LIST);
|
||||
if (listFragment.isVisible() && listFragment.tryCollapseMenu())
|
||||
return;
|
||||
super.onBackPressed();
|
||||
// The visible fragment is now the one that was on top of the back stack, if there was one.
|
||||
if (isEditing())
|
||||
visibleFragmentTag = TAG_DETAIL;
|
||||
else if (isSingleLayout && TAG_DETAIL.equals(visibleFragmentTag))
|
||||
visibleFragmentTag = TAG_LIST;
|
||||
// If the user went back from the detail screen to the list, clear the current config.
|
||||
moveToState(isEditing() ? getCurrentConfig() : null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null)
|
||||
editorState = savedInstanceState.getParcelable(KEY_EDITOR_STATE);
|
||||
setContentView(R.layout.config_activity);
|
||||
isSingleLayout = findViewById(R.id.detail_fragment) == null;
|
||||
mainContainer = isSingleLayout ? R.id.master_fragment : R.id.detail_fragment;
|
||||
if (isSingleLayout) {
|
||||
nameChangeCallback = new ConfigNameChangeCallback();
|
||||
if (getCurrentConfig() != null)
|
||||
getCurrentConfig().addOnPropertyChangedCallback(nameChangeCallback);
|
||||
}
|
||||
isLayoutFinished = true;
|
||||
moveToState(getCurrentConfig(), isEditing());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config oldConfig, final Config newConfig) {
|
||||
if (nameChangeCallback != null && oldConfig != null)
|
||||
oldConfig.removeOnPropertyChangedCallback(nameChangeCallback);
|
||||
// Abandon editing a config when the current config changes.
|
||||
moveToState(newConfig, false);
|
||||
if (nameChangeCallback != null && newConfig != null)
|
||||
newConfig.addOnPropertyChangedCallback(nameChangeCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEditingStateChanged(final boolean isEditing) {
|
||||
moveToState(getCurrentConfig(), isEditing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_action_edit:
|
||||
// Try to make the editing fragment visible.
|
||||
setIsEditing(true);
|
||||
return true;
|
||||
case R.id.menu_action_save:
|
||||
// This menu item is handled by the editing fragment.
|
||||
return false;
|
||||
case R.id.menu_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostResume() {
|
||||
super.onPostResume();
|
||||
// Allow changes to fragments.
|
||||
isStateSaved = false;
|
||||
moveToState(getCurrentConfig(), isEditing());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
// We cannot save fragments that might switch between containers if the layout changes.
|
||||
if (isLayoutFinished && isServiceAvailable && !isStateSaved) {
|
||||
// Save the editor state before destroying it.
|
||||
if (TAG_EDIT.equals(visibleFragmentTag)) {
|
||||
// For the case where the activity is resumed.
|
||||
editorState = fm.saveFragmentInstanceState(fragments.get(TAG_EDIT));
|
||||
// For the case where the activity is restarted.
|
||||
outState.putParcelable(KEY_EDITOR_STATE, editorState);
|
||||
}
|
||||
moveToFragment(null, isSingleLayout ? TAG_LIST : null);
|
||||
}
|
||||
// Prevent further changes to fragments.
|
||||
isStateSaved = true;
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceAvailable() {
|
||||
super.onServiceAvailable();
|
||||
// Allow creating fragments.
|
||||
isServiceAvailable = true;
|
||||
moveToState(getCurrentConfig(), isEditing());
|
||||
}
|
||||
|
||||
private class ConfigNameChangeCallback extends Observable.OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (sender != getCurrentConfig())
|
||||
sender.removeOnPropertyChangedCallback(this);
|
||||
if (propertyId != 0 && propertyId != BR.name)
|
||||
return;
|
||||
setTitle(getCurrentConfig().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static class FragmentCache {
|
||||
private ConfigDetailFragment detailFragment;
|
||||
private ConfigEditFragment editFragment;
|
||||
private final FragmentManager fm;
|
||||
private ConfigListFragment listFragment;
|
||||
|
||||
private FragmentCache(final FragmentManager fm) {
|
||||
this.fm = fm;
|
||||
}
|
||||
|
||||
private BaseConfigFragment get(final String tag) {
|
||||
switch (tag) {
|
||||
case TAG_DETAIL:
|
||||
if (detailFragment == null)
|
||||
detailFragment = (ConfigDetailFragment) fm.findFragmentByTag(tag);
|
||||
if (detailFragment == null)
|
||||
detailFragment = new ConfigDetailFragment();
|
||||
return detailFragment;
|
||||
case TAG_EDIT:
|
||||
if (editFragment == null)
|
||||
editFragment = (ConfigEditFragment) fm.findFragmentByTag(tag);
|
||||
if (editFragment == null)
|
||||
editFragment = new ConfigEditFragment();
|
||||
return editFragment;
|
||||
case TAG_LIST:
|
||||
if (listFragment == null)
|
||||
listFragment = (ConfigListFragment) fm.findFragmentByTag(tag);
|
||||
if (listFragment == null)
|
||||
listFragment = new ConfigListFragment();
|
||||
return listFragment;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.databinding.ConfigDetailFragmentBinding;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Fragment for viewing information about a WireGuard configuration.
|
||||
*/
|
||||
|
||||
public class ConfigDetailFragment extends BaseConfigFragment {
|
||||
private ConfigDetailFragmentBinding binding;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.config_detail, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
|
||||
final Bundle savedInstanceState) {
|
||||
binding = ConfigDetailFragmentBinding.inflate(inflater, parent, false);
|
||||
binding.setConfig(getCurrentConfig());
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config config) {
|
||||
if (binding != null)
|
||||
binding.setConfig(config);
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.android.databinding.ConfigEditFragmentBinding;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
|
||||
public class ConfigEditFragment extends BaseConfigFragment {
|
||||
private static final String KEY_MODIFIED_CONFIG = "modifiedConfig";
|
||||
private static final String KEY_ORIGINAL_NAME = "originalName";
|
||||
|
||||
public static void copyPublicKey(final Context context, final String publicKey) {
|
||||
if (publicKey == null || publicKey.isEmpty())
|
||||
return;
|
||||
final ClipboardManager clipboard =
|
||||
(ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
final String description =
|
||||
context.getResources().getString(R.string.public_key_description);
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(description, publicKey));
|
||||
final String message = context.getResources().getString(R.string.public_key_copied_message);
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private Config localConfig;
|
||||
private String originalName;
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config config) {
|
||||
// Only discard modifications when the config *they are based on* changes.
|
||||
if (config == null || config.getName().equals(originalName) || localConfig == null)
|
||||
return;
|
||||
localConfig.copyFrom(config);
|
||||
originalName = config.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Restore more saved information.
|
||||
if (savedInstanceState != null) {
|
||||
localConfig = savedInstanceState.getParcelable(KEY_MODIFIED_CONFIG);
|
||||
originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
|
||||
} else if (getArguments() != null) {
|
||||
final Bundle arguments = getArguments();
|
||||
localConfig = arguments.getParcelable(KEY_MODIFIED_CONFIG);
|
||||
originalName = arguments.getString(KEY_ORIGINAL_NAME);
|
||||
}
|
||||
if (localConfig == null) {
|
||||
localConfig = new Config();
|
||||
originalName = null;
|
||||
}
|
||||
onCurrentConfigChanged(getCurrentConfig());
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.config_edit, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
|
||||
final Bundle savedInstanceState) {
|
||||
final ConfigEditFragmentBinding binding =
|
||||
ConfigEditFragmentBinding.inflate(inflater, parent, false);
|
||||
binding.setConfig(localConfig);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Reset changes to the config when the user cancels editing. See also the comment below.
|
||||
if (isRemoving())
|
||||
localConfig.copyFrom(getCurrentConfig());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_save:
|
||||
saveConfig();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
// When ConfigActivity unwinds the back stack, isRemoving() is true, so localConfig will be
|
||||
// reset. Since outState is not serialized yet, it resets the saved config too. Avoid this
|
||||
// by copying the local config. originalName is fine because it is replaced, not modified.
|
||||
outState.putParcelable(KEY_MODIFIED_CONFIG, localConfig.copy());
|
||||
outState.putString(KEY_ORIGINAL_NAME, originalName);
|
||||
}
|
||||
|
||||
private void saveConfig() {
|
||||
final VpnService service = VpnService.getInstance();
|
||||
try {
|
||||
if (getCurrentConfig() != null)
|
||||
service.update(getCurrentConfig().getName(), localConfig);
|
||||
else
|
||||
service.add(localConfig);
|
||||
} catch (final IllegalArgumentException | IllegalStateException e) {
|
||||
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
final Activity activity = getActivity();
|
||||
final View focusedView = activity.getCurrentFocus();
|
||||
if (focusedView != null) {
|
||||
final InputMethodManager inputManager =
|
||||
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
((BaseConfigActivity) activity).setIsEditing(false);
|
||||
}
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.android.databinding.ObservableMapAdapter;
|
||||
import com.wireguard.android.databinding.ConfigListFragmentBinding;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Fragment containing the list of known WireGuard configurations.
|
||||
*/
|
||||
|
||||
public class ConfigListFragment extends BaseConfigFragment {
|
||||
private static final int REQUEST_IMPORT = 1;
|
||||
|
||||
private ConfigListFragmentBinding binding;
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
if (requestCode == REQUEST_IMPORT) {
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
VpnService.getInstance().importFrom(data.getData());
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
|
||||
final Bundle savedInstanceState) {
|
||||
binding = ConfigListFragmentBinding.inflate(inflater, parent, false);
|
||||
binding.setConfigs(VpnService.getInstance().getConfigs());
|
||||
binding.addFromFile.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQUEST_IMPORT);
|
||||
binding.addMenu.collapse();
|
||||
}
|
||||
});
|
||||
binding.addFromScratch.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
startActivity(new Intent(getActivity(), AddActivity.class));
|
||||
binding.addMenu.collapse();
|
||||
}
|
||||
});
|
||||
binding.configList.setMultiChoiceModeListener(new ConfigListModeListener());
|
||||
binding.configList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
final Config config = (Config) parent.getItemAtPosition(position);
|
||||
setCurrentConfig(config);
|
||||
}
|
||||
});
|
||||
binding.configList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
setConfigChecked(null);
|
||||
binding.configList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
binding.configList.setItemChecked(position, true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
binding.configList.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public boolean onTouch(final View view, final MotionEvent event) {
|
||||
binding.addMenu.collapse();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
binding.executePendingBindings();
|
||||
setConfigChecked(getCurrentConfig());
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config config) {
|
||||
final BaseConfigActivity activity = ((BaseConfigActivity) getActivity());
|
||||
if (activity != null)
|
||||
activity.setCurrentConfig(config);
|
||||
if (binding != null)
|
||||
setConfigChecked(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void setConfigChecked(final Config config) {
|
||||
if (config != null) {
|
||||
@SuppressWarnings("unchecked") final ObservableMapAdapter<String, Config> adapter =
|
||||
(ObservableMapAdapter<String, Config>) binding.configList.getAdapter();
|
||||
final int position = adapter.getPosition(config.getName());
|
||||
if (position >= 0)
|
||||
binding.configList.setItemChecked(position, true);
|
||||
} else {
|
||||
final int position = binding.configList.getCheckedItemPosition();
|
||||
if (position >= 0)
|
||||
binding.configList.setItemChecked(position, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean tryCollapseMenu() {
|
||||
if (binding != null && binding.addMenu.isExpanded()) {
|
||||
binding.addMenu.collapse();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ConfigListModeListener implements AbsListView.MultiChoiceModeListener {
|
||||
private final List<Config> configsToRemove = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_delete:
|
||||
// Ensure an unmanaged config is never the current config.
|
||||
if (configsToRemove.contains(getCurrentConfig()))
|
||||
setCurrentConfig(null);
|
||||
for (final Config config : configsToRemove)
|
||||
VpnService.getInstance().remove(config.getName());
|
||||
configsToRemove.clear();
|
||||
mode.finish();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(final ActionMode mode, final int position,
|
||||
final long id, final boolean checked) {
|
||||
if (checked)
|
||||
configsToRemove.add((Config) binding.configList.getItemAtPosition(position));
|
||||
else
|
||||
configsToRemove.remove(binding.configList.getItemAtPosition(position));
|
||||
final int count = configsToRemove.size();
|
||||
final Resources resources = binding.getRoot().getContext().getResources();
|
||||
mode.setTitle(resources.getQuantityString(R.plurals.list_delete_title, count, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.config_list_delete, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode mode) {
|
||||
configsToRemove.clear();
|
||||
binding.configList.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
binding.configList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
|
||||
// Restore the previous selection (before entering the action mode).
|
||||
setConfigChecked(getCurrentConfig());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
|
||||
configsToRemove.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.databinding.DataBindingUtil;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
|
||||
import com.wireguard.android.databinding.NotSupportedActivityBinding;
|
||||
|
||||
public class NotSupportedActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final NotSupportedActivityBinding binding =
|
||||
DataBindingUtil.setContentView(this, R.layout.not_supported_activity);
|
||||
final String messageHtml = getString(R.string.not_supported_message);
|
||||
final Spanned messageText;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
messageText = Html.fromHtml(messageHtml, Html.FROM_HTML_MODE_COMPACT);
|
||||
else
|
||||
messageText = Html.fromHtml(messageHtml);
|
||||
binding.notSupportedMessage.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
binding.notSupportedMessage.setText(messageText);
|
||||
}
|
||||
}
|
@ -1,40 +1,57 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.databinding.Observable;
|
||||
import android.databinding.Observable.OnPropertyChangedCallback;
|
||||
import android.databinding.ObservableMap.OnMapChangedCallback;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.android.Application.ApplicationComponent;
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.activity.SettingsActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.TunnelCollection;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
* system framework as necessary to update the appearance of the tile in the system UI, and to
|
||||
* forward click events to the application.
|
||||
*/
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class QuickTileService extends TileService {
|
||||
private Config config;
|
||||
public class QuickTileService extends TileService implements OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = QuickTileService.class.getSimpleName();
|
||||
|
||||
private final OnTunnelStateChangedCallback tunnelCallback = new OnTunnelStateChangedCallback();
|
||||
private final OnTunnelMapChangedCallback tunnelMapCallback = new OnTunnelMapChangedCallback();
|
||||
private SharedPreferences preferences;
|
||||
private VpnService service;
|
||||
private Tunnel tunnel;
|
||||
private TunnelManager tunnelManager;
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
if (service != null && config != null) {
|
||||
if (config.isEnabled())
|
||||
service.disable(config.getName());
|
||||
else
|
||||
service.enable(config.getName());
|
||||
if (tunnel != null) {
|
||||
tunnel.setState(State.TOGGLE).handle(this::onToggleFinished);
|
||||
} else {
|
||||
if (service != null && service.getConfigs().isEmpty()) {
|
||||
startActivityAndCollapse(new Intent(this, ConfigActivity.class));
|
||||
if (tunnelManager.getTunnels().isEmpty()) {
|
||||
// Prompt the user to create or import a tunnel configuration.
|
||||
startActivityAndCollapse(new Intent(this, MainActivity.class));
|
||||
} else {
|
||||
// Prompt the user to select a tunnel for use with the quick settings tile.
|
||||
final Intent intent = new Intent(this, SettingsActivity.class);
|
||||
intent.putExtra("showQuickTile", true);
|
||||
intent.putExtra(SettingsActivity.KEY_SHOW_QUICK_TILE_SETTINGS, true);
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
@ -42,50 +59,101 @@ public class QuickTileService extends TileService {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
service = VpnService.getInstance();
|
||||
if (service == null)
|
||||
bindService(new Intent(this, VpnService.class), new ServiceConnectionCallbacks(),
|
||||
Context.BIND_AUTO_CREATE);
|
||||
TileService.requestListeningState(this, new ComponentName(this, getClass()));
|
||||
super.onCreate();
|
||||
final ApplicationComponent component = Application.getComponent();
|
||||
preferences = component.getPreferences();
|
||||
tunnelManager = component.getTunnelManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences preferences, final String key) {
|
||||
if (!TunnelManager.KEY_PRIMARY_TUNNEL.equals(key))
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
// Since this is an active tile, this only gets called when we want to update the tile.
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
tunnelManager.getTunnels().addOnMapChangedCallback(tunnelMapCallback);
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(tunnelCallback);
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopListening() {
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
tunnelManager.getTunnels().removeOnMapChangedCallback(tunnelMapCallback);
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(tunnelCallback);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Void onToggleFinished(final State state, final Throwable throwable) {
|
||||
if (throwable == null)
|
||||
return null;
|
||||
Log.e(TAG, "Cannot toggle tunnel", throwable);
|
||||
final String message = "Cannot toggle tunnel: " + throwable.getCause().getMessage();
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateTile() {
|
||||
// Update the tunnel.
|
||||
final String currentName = tunnel != null ? tunnel.getName() : null;
|
||||
final String newName = preferences.getString(TunnelManager.KEY_PRIMARY_TUNNEL, null);
|
||||
if (!Objects.equals(currentName, newName)) {
|
||||
final TunnelCollection tunnels = tunnelManager.getTunnels();
|
||||
final Tunnel newTunnel = newName != null ? tunnels.get(newName) : null;
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(tunnelCallback);
|
||||
tunnel = newTunnel;
|
||||
if (tunnel != null)
|
||||
tunnel.addOnPropertyChangedCallback(tunnelCallback);
|
||||
}
|
||||
// Update the tile contents.
|
||||
final String label;
|
||||
final int state;
|
||||
final Tile tile = getQsTile();
|
||||
final String configName = preferences.getString(VpnService.KEY_PRIMARY_CONFIG, null);
|
||||
config = configName != null && service != null ? service.get(configName) : null;
|
||||
if (config != null) {
|
||||
tile.setLabel(config.getName());
|
||||
final int state = config.isEnabled() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
if (tunnel != null) {
|
||||
label = tunnel.getName();
|
||||
state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
} else {
|
||||
label = getString(R.string.app_name);
|
||||
state = Tile.STATE_INACTIVE;
|
||||
}
|
||||
tile.setLabel(label);
|
||||
if (tile.getState() != state) {
|
||||
// The icon must be changed every time the state changes, or the color won't change.
|
||||
final Integer iconResource = (state == Tile.STATE_ACTIVE) ?
|
||||
R.drawable.ic_tile : R.drawable.ic_tile_disabled;
|
||||
// The icon must be changed every time the state changes, or the shade will not change.
|
||||
final Integer iconResource = (state == Tile.STATE_ACTIVE)
|
||||
? R.drawable.ic_tile : R.drawable.ic_tile_disabled;
|
||||
tile.setIcon(Icon.createWithResource(this, iconResource));
|
||||
tile.setState(state);
|
||||
}
|
||||
} else {
|
||||
tile.setIcon(Icon.createWithResource(this, R.drawable.ic_tile_disabled));
|
||||
tile.setLabel(getString(R.string.app_name));
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
}
|
||||
tile.updateTile();
|
||||
}
|
||||
|
||||
private class ServiceConnectionCallbacks implements ServiceConnection {
|
||||
private final class OnTunnelMapChangedCallback
|
||||
extends OnMapChangedCallback<TunnelCollection, String, Tunnel> {
|
||||
@Override
|
||||
public void onServiceConnected(final ComponentName component, final IBinder binder) {
|
||||
// We don't actually need a binding, only notification that the service is started.
|
||||
unbindService(this);
|
||||
service = VpnService.getInstance();
|
||||
public void onMapChanged(final TunnelCollection sender, final String key) {
|
||||
if (!key.equals(preferences.getString(TunnelManager.KEY_PRIMARY_TUNNEL, null)))
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
private final class OnTunnelStateChangedCallback extends OnPropertyChangedCallback {
|
||||
@Override
|
||||
public void onServiceDisconnected(final ComponentName component) {
|
||||
// This can never happen; the service runs in the same thread as this service.
|
||||
throw new IllegalStateException();
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
if (!Objects.equals(sender, tunnel)) {
|
||||
sender.removeOnPropertyChangedCallback(this);
|
||||
return;
|
||||
}
|
||||
if (propertyId != 0 && propertyId != BR.state)
|
||||
return;
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.databinding.CallbackRegistry;
|
||||
import android.databinding.CallbackRegistry.NotifierCallback;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for activities that need to remember the currently-selected tunnel.
|
||||
*/
|
||||
|
||||
public abstract class BaseActivity extends Activity {
|
||||
private static final String TAG = BaseActivity.class.getSimpleName();
|
||||
|
||||
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
|
||||
private Tunnel selectedTunnel;
|
||||
|
||||
public void addOnSelectedTunnelChangedListener(
|
||||
final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.add(listener);
|
||||
}
|
||||
|
||||
public Tunnel getSelectedTunnel() {
|
||||
return selectedTunnel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
|
||||
String savedTunnelName = null;
|
||||
if (savedInstanceState != null)
|
||||
savedTunnelName = savedInstanceState.getString(TunnelManager.KEY_SELECTED_TUNNEL);
|
||||
else if (getIntent() != null)
|
||||
savedTunnelName = getIntent().getStringExtra(TunnelManager.KEY_SELECTED_TUNNEL);
|
||||
if (savedTunnelName != null) {
|
||||
final TunnelManager manager = Application.getComponent().getTunnelManager();
|
||||
selectedTunnel = manager.getTunnels().get(savedTunnelName);
|
||||
}
|
||||
// The selected tunnel must be set before the superclass method recreates fragments.
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
if (selectedTunnel != null)
|
||||
outState.putString(TunnelManager.KEY_SELECTED_TUNNEL, selectedTunnel.getName());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected abstract Tunnel onSelectedTunnelChanged(Tunnel oldTunnel, Tunnel newTunnel);
|
||||
|
||||
public void removeOnSelectedTunnelChangedListener(
|
||||
final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.remove(listener);
|
||||
}
|
||||
|
||||
public void setSelectedTunnel(final Tunnel tunnel) {
|
||||
final Tunnel oldTunnel = selectedTunnel;
|
||||
if (Objects.equals(oldTunnel, tunnel))
|
||||
return;
|
||||
// Give the activity a chance to override the tunnel change.
|
||||
selectedTunnel = onSelectedTunnelChanged(oldTunnel, tunnel);
|
||||
if (Objects.equals(oldTunnel, selectedTunnel))
|
||||
return;
|
||||
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, selectedTunnel);
|
||||
}
|
||||
|
||||
public interface OnSelectedTunnelChangedListener {
|
||||
void onSelectedTunnelChanged(Tunnel oldTunnel, Tunnel newTunnel);
|
||||
}
|
||||
|
||||
private static final class SelectionChangeNotifier
|
||||
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
@Override
|
||||
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
|
||||
final Tunnel oldTunnel, final int ignored,
|
||||
final Tunnel newTunnel) {
|
||||
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SelectionChangeRegistry
|
||||
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
private SelectionChangeRegistry() {
|
||||
super(new SelectionChangeNotifier());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.fragment.ConfigEditorFragment;
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment;
|
||||
import com.wireguard.android.fragment.TunnelListFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
|
||||
* WireGuard application, and contains several fragments for listing, viewing details of, and
|
||||
* editing the configuration and interface state of WireGuard tunnels.
|
||||
*/
|
||||
|
||||
public class MainActivity extends BaseActivity {
|
||||
private static final String KEY_STATE = "fragment_state";
|
||||
private static final String TAG = MainActivity.class.getSimpleName();
|
||||
private State state = State.EMPTY;
|
||||
|
||||
private boolean moveToState(final State nextState) {
|
||||
Log.i(TAG, "Moving from " + state.name() + " to " + nextState.name());
|
||||
if (nextState == state) {
|
||||
return false;
|
||||
} else if (nextState.layer > state.layer + 1) {
|
||||
moveToState(State.ofLayer(state.layer + 1));
|
||||
moveToState(nextState);
|
||||
return true;
|
||||
} else if (nextState.layer == state.layer + 1) {
|
||||
final Fragment fragment = Fragment.instantiate(this, nextState.fragment);
|
||||
final FragmentTransaction transaction = getFragmentManager().beginTransaction()
|
||||
.replace(R.id.master_fragment, fragment)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
|
||||
if (state.layer > 0)
|
||||
transaction.addToBackStack(null);
|
||||
transaction.commit();
|
||||
} else if (nextState.layer == state.layer - 1) {
|
||||
if (getFragmentManager().getBackStackEntryCount() == 0)
|
||||
return false;
|
||||
getFragmentManager().popBackStack();
|
||||
} else if (nextState.layer < state.layer - 1) {
|
||||
moveToState(State.ofLayer(state.layer - 1));
|
||||
moveToState(nextState);
|
||||
return true;
|
||||
}
|
||||
state = nextState;
|
||||
if (state.layer > 1) {
|
||||
if (getActionBar() != null)
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
} else {
|
||||
if (getActionBar() != null)
|
||||
getActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
setSelectedTunnel(null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!moveToState(State.ofLayer(state.layer - 1)))
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
if (savedInstanceState != null && savedInstanceState.getString(KEY_STATE) != null)
|
||||
state = State.valueOf(savedInstanceState.getString(KEY_STATE));
|
||||
if (state == State.EMPTY) {
|
||||
State initialState = getSelectedTunnel() != null ? State.DETAIL : State.LIST;
|
||||
if (getIntent() != null && getIntent().getStringExtra(KEY_STATE) != null)
|
||||
initialState = State.valueOf(getIntent().getStringExtra(KEY_STATE));
|
||||
moveToState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main_activity, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
moveToState(State.ofLayer(state.layer - 1));
|
||||
return true;
|
||||
case R.id.menu_action_edit:
|
||||
if (getSelectedTunnel() != null)
|
||||
moveToState(State.EDITOR);
|
||||
return true;
|
||||
case R.id.menu_action_save:
|
||||
// This menu item is handled by the editor fragment.
|
||||
return false;
|
||||
case R.id.menu_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
outState.putString(KEY_STATE, state.name());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Tunnel onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
|
||||
moveToState(newTunnel != null ? State.DETAIL : State.LIST);
|
||||
return newTunnel;
|
||||
}
|
||||
|
||||
private enum State {
|
||||
EMPTY(null, 0),
|
||||
LIST(TunnelListFragment.class, 1),
|
||||
DETAIL(TunnelDetailFragment.class, 2),
|
||||
EDITOR(ConfigEditorFragment.class, 3);
|
||||
|
||||
private final String fragment;
|
||||
private final int layer;
|
||||
|
||||
State(final Class<? extends Fragment> fragment, final int layer) {
|
||||
this.fragment = fragment != null ? fragment.getName() : null;
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
private static State ofLayer(final int layer) {
|
||||
return Stream.of(State.values()).filter(s -> s.layer == layer).findFirst().get();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,34 @@
|
||||
package com.wireguard.android;
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
import com.wireguard.android.backends.RootShell;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.preference.TunnelListPreference;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends Activity {
|
||||
public static final String KEY_SHOW_QUICK_TILE_SETTINGS = "show_quick_tile_settings";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final FragmentTransaction transaction = getFragmentManager().beginTransaction();
|
||||
final SettingsFragment fragment = new SettingsFragment();
|
||||
if (getFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
final Fragment fragment = new SettingsFragment();
|
||||
fragment.setArguments(getIntent().getExtras());
|
||||
transaction.replace(android.R.id.content, fragment).commit();
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
@ -25,44 +36,42 @@ public class SettingsActivity extends Activity {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
if (getArguments() != null && getArguments().getBoolean("showQuickTile"))
|
||||
((ConfigListPreference) findPreference("primary_config")).show();
|
||||
|
||||
final Preference installTools = findPreference("install_cmd_line_tools");
|
||||
installTools.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new ToolsInstaller(installTools).execute();
|
||||
installTools.setOnPreferenceClickListener(preference -> {
|
||||
new ToolsInstaller(preference).execute();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (getArguments() != null && getArguments().getBoolean(KEY_SHOW_QUICK_TILE_SETTINGS))
|
||||
((TunnelListPreference) findPreference("primary_config")).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ToolsInstaller extends AsyncTask<Void, Void, Integer> {
|
||||
Preference installTools;
|
||||
|
||||
public ToolsInstaller(Preference installTools) {
|
||||
this.installTools = installTools;
|
||||
installTools.setEnabled(false);
|
||||
installTools.setSummary(installTools.getContext().getString(R.string.install_cmd_line_tools_progress));
|
||||
}
|
||||
|
||||
private static final String[][] libraryNamedExecutables = {
|
||||
{ "libwg.so", "wg" },
|
||||
{ "libwg-quick.so", "wg-quick" }
|
||||
private static final class ToolsInstaller extends AsyncTask<Void, Void, Integer> {
|
||||
private static final String[][] LIBRARY_NAMED_EXECUTABLES = {
|
||||
{"libwg.so", "wg"},
|
||||
{"libwg-quick.so", "wg-quick"}
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
private final Preference preference;
|
||||
|
||||
private ToolsInstaller(final Preference preference) {
|
||||
context = preference.getContext();
|
||||
this.preference = preference;
|
||||
preference.setEnabled(false);
|
||||
preference.setSummary(context.getString(R.string.install_cmd_line_tools_progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(final Void... voids) {
|
||||
final Context context = installTools.getContext();
|
||||
final String libDir = context.getApplicationInfo().nativeLibraryDir;
|
||||
final StringBuilder cmd = new StringBuilder();
|
||||
|
||||
cmd.append("set -ex;");
|
||||
|
||||
for (final String[] libraryNamedExecutable : libraryNamedExecutables) {
|
||||
final String arg1 = "'" + libDir + "/" + libraryNamedExecutable[0] + "'";
|
||||
final String arg2 = "'/system/xbin/" + libraryNamedExecutable[1] + "'";
|
||||
for (final String[] libraryNamedExecutable : LIBRARY_NAMED_EXECUTABLES) {
|
||||
final String arg1 = '\'' + libDir + '/' + libraryNamedExecutable[0] + '\'';
|
||||
final String arg2 = "'/system/xbin/" + libraryNamedExecutable[1] + '\'';
|
||||
|
||||
cmd.append(String.format("cmp -s %s %s && ", arg1, arg2));
|
||||
}
|
||||
@ -71,9 +80,9 @@ public class SettingsActivity extends Activity {
|
||||
cmd.append("trap 'mount -o ro,remount /system' EXIT;");
|
||||
cmd.append("mount -o rw,remount /system;");
|
||||
|
||||
for (final String[] libraryNamedExecutable : libraryNamedExecutables) {
|
||||
final String arg1 = "'" + libDir + "/" + libraryNamedExecutable[0] + "'";
|
||||
final String arg2 = "'/system/xbin/" + libraryNamedExecutable[1] + "'";
|
||||
for (final String[] libraryNamedExecutable : LIBRARY_NAMED_EXECUTABLES) {
|
||||
final String arg1 = '\'' + libDir + '/' + libraryNamedExecutable[0] + '\'';
|
||||
final String arg2 = "'/system/xbin/" + libraryNamedExecutable[1] + '\'';
|
||||
cmd.append(String.format("cp %s %s; chmod 755 %s;", arg1, arg2, arg2));
|
||||
}
|
||||
|
||||
@ -82,8 +91,7 @@ public class SettingsActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Integer ret) {
|
||||
final Context context = installTools.getContext();
|
||||
String status;
|
||||
final String status;
|
||||
|
||||
switch (ret) {
|
||||
case 0:
|
||||
@ -96,8 +104,8 @@ public class SettingsActivity extends Activity {
|
||||
status = context.getString(R.string.install_cmd_line_tools_failure);
|
||||
break;
|
||||
}
|
||||
installTools.setSummary(status);
|
||||
installTools.setEnabled(true);
|
||||
preference.setSummary(status);
|
||||
preference.setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.wireguard.android.fragment.ConfigEditorFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* Created by samuel on 12/29/17.
|
||||
*/
|
||||
|
||||
public class TunnelCreatorActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, new ConfigEditorFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Tunnel onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
|
||||
finish();
|
||||
return null;
|
||||
}
|
||||
}
|
57
app/src/main/java/com/wireguard/android/backend/Backend.java
Normal file
57
app/src/main/java/com/wireguard/android/backend/Backend.java
Normal file
@ -0,0 +1,57 @@
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Interface for implementations of the WireGuard secure network tunnel.
|
||||
*/
|
||||
|
||||
public interface Backend {
|
||||
/**
|
||||
* Update the volatile configuration of a running tunnel, asynchronously, and return the
|
||||
* resulting configuration. If the tunnel is not up, return the configuration that would result
|
||||
* (if known), or else simply return the given configuration.
|
||||
*
|
||||
* @param tunnel The tunnel to apply the configuration to.
|
||||
* @param config The new configuration for this tunnel.
|
||||
* @return A future completed when the configuration of the tunnel has been updated, and the new
|
||||
* volatile configuration has been determined. This future will always be completed on the main
|
||||
* thread.
|
||||
*/
|
||||
CompletionStage<Config> applyConfig(Tunnel tunnel, Config config);
|
||||
|
||||
/**
|
||||
* Get the actual state of a tunnel, asynchronously.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return A future completed when the state of the tunnel has been determined. This future will
|
||||
* always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<State> getState(Tunnel tunnel);
|
||||
|
||||
/**
|
||||
* Get statistics about traffic and errors on this tunnel, asynchronously. If the tunnel is not
|
||||
* running, the statistics object will be filled with zero values.
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve statistics for.
|
||||
* @return A future completed when statistics for the tunnel are available. This future will
|
||||
* always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Statistics> getStatistics(Tunnel tunnel);
|
||||
|
||||
/**
|
||||
* Set the state of a tunnel, asynchronously.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @return A future completed when the state of the tunnel has changed, containing the new state
|
||||
* of the tunnel. This future will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<State> setState(Tunnel tunnel, State state);
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Created by samuel on 12/19/17.
|
||||
*/
|
||||
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = WgQuickBackend.class.getSimpleName();
|
||||
|
||||
private final AsyncWorker asyncWorker;
|
||||
private final Context context;
|
||||
private final RootShell rootShell;
|
||||
|
||||
public WgQuickBackend(final AsyncWorker asyncWorker, final Context context,
|
||||
final RootShell rootShell) {
|
||||
this.asyncWorker = asyncWorker;
|
||||
this.context = context;
|
||||
this.rootShell = rootShell;
|
||||
}
|
||||
|
||||
private static State resolveState(final State currentState, State requestedState) {
|
||||
if (requestedState == State.UNKNOWN)
|
||||
throw new IllegalArgumentException("Requested unknown state");
|
||||
if (requestedState == State.TOGGLE)
|
||||
requestedState = currentState == State.UP ? State.DOWN : State.UP;
|
||||
return requestedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Config> applyConfig(final Tunnel tunnel, final Config config) {
|
||||
if (tunnel.getState() == State.UP)
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException("stub"));
|
||||
return CompletableFuture.completedFuture(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<State> getState(final Tunnel tunnel) {
|
||||
Log.v(TAG, "Requested state for tunnel " + tunnel.getName());
|
||||
return asyncWorker.supplyAsync(() -> {
|
||||
final List<String> output = new LinkedList<>();
|
||||
final State state;
|
||||
if (rootShell.run(output, "wg show interfaces") != 0) {
|
||||
state = State.UNKNOWN;
|
||||
} else if (output.isEmpty()) {
|
||||
// There are no running interfaces.
|
||||
state = State.DOWN;
|
||||
} else {
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
final String[] names = output.get(0).split(" ");
|
||||
state = Arrays.asList(names).contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
Log.v(TAG, "Got state " + state + " for tunnel " + tunnel.getName());
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Statistics> getStatistics(final Tunnel tunnel) {
|
||||
return CompletableFuture.completedFuture(new Statistics());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<State> setState(final Tunnel tunnel, final State state) {
|
||||
Log.v(TAG, "Requested state change to " + state + " for tunnel " + tunnel.getName());
|
||||
return tunnel.getStateAsync().thenCompose(currentState -> asyncWorker.supplyAsync(() -> {
|
||||
final String stateName = resolveState(currentState, state).name().toLowerCase();
|
||||
final File file = new File(context.getFilesDir(), tunnel.getName() + ".conf");
|
||||
final String path = file.getAbsolutePath();
|
||||
// FIXME: Assumes file layout from FIleConfigStore. Use a temporary file.
|
||||
if (rootShell.run(null, String.format("wg-quick %s '%s'", stateName, path)) != 0)
|
||||
throw new IOException("wg-quick failed");
|
||||
return tunnel;
|
||||
})).thenCompose(this::getState);
|
||||
}
|
||||
}
|
@ -1,559 +0,0 @@
|
||||
package com.wireguard.android.backends;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.NotSupportedActivity;
|
||||
import com.wireguard.android.QuickTileService;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ObservableSortedMap;
|
||||
import com.wireguard.android.databinding.ObservableTreeMap;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.Peer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Service that handles config state coordination and all background processing for the application.
|
||||
*/
|
||||
|
||||
public class VpnService extends Service
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public static final String KEY_ENABLED_CONFIGS = "enabled_configs";
|
||||
public static final String KEY_PRIMARY_CONFIG = "primary_config";
|
||||
public static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
||||
private static final String TAG = "WireGuard/VpnService";
|
||||
|
||||
private static VpnService instance;
|
||||
private final IBinder binder = new Binder();
|
||||
private final ObservableTreeMap<String, Config> configurations = new ObservableTreeMap<>();
|
||||
private final Set<String> enabledConfigs = new HashSet<>();
|
||||
private SharedPreferences preferences;
|
||||
private String primaryName;
|
||||
private RootShell rootShell;
|
||||
|
||||
public static VpnService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new configuration to the set of known configurations. The configuration will initially
|
||||
* be disabled. The configuration's name must be unique within the set of known configurations.
|
||||
*
|
||||
* @param config The configuration to add.
|
||||
*/
|
||||
public void add(final Config config) {
|
||||
new ConfigUpdater(null, config, false).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to disable and tear down an interface for this configuration. The configuration's
|
||||
* enabled state will be updated the operation is successful. If this configuration is already
|
||||
* disconnected, or it is not a known configuration, no changes will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to disable.
|
||||
*/
|
||||
public void disable(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null || !config.isEnabled())
|
||||
return;
|
||||
new ConfigDisabler(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to set up and enable an interface for this configuration. The configuration's enabled
|
||||
* state will be updated if the operation is successful. If this configuration is already
|
||||
* enabled, or it is not a known configuration, no changes will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to enable.
|
||||
*/
|
||||
public void enable(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null || config.isEnabled())
|
||||
return;
|
||||
new ConfigEnabler(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a configuration known and managed by this service. The returned object must not be
|
||||
* modified directly.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to retrieve.
|
||||
* @return An object representing the configuration. This object must not be modified.
|
||||
*/
|
||||
public Config get(final String name) {
|
||||
return configurations.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the set of configurations known and managed by the service. Configurations in this
|
||||
* set must not be modified directly. If a configuration is to be updated, first create a copy
|
||||
* of it by calling getCopy().
|
||||
*
|
||||
* @return The set of known configurations.
|
||||
*/
|
||||
public ObservableSortedMap<String, Config> getConfigs() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public void importFrom(final Uri... uris) {
|
||||
new ConfigImporter().execute(uris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
instance = this;
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Ensure the service sticks around after being unbound. This only needs to happen once.
|
||||
startService(new Intent(this, getClass()));
|
||||
rootShell = new RootShell(this);
|
||||
new ConfigLoader().execute(getFilesDir().listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(final File dir, final String name) {
|
||||
return name.endsWith(".conf");
|
||||
}
|
||||
}));
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences preferences,
|
||||
final String key) {
|
||||
if (!KEY_PRIMARY_CONFIG.equals(key))
|
||||
return;
|
||||
boolean changed = false;
|
||||
final String newName = preferences.getString(key, null);
|
||||
if (primaryName != null && !primaryName.equals(newName)) {
|
||||
final Config oldConfig = configurations.get(primaryName);
|
||||
if (oldConfig != null)
|
||||
oldConfig.setIsPrimary(false);
|
||||
changed = true;
|
||||
}
|
||||
if (newName != null && !newName.equals(primaryName)) {
|
||||
final Config newConfig = configurations.get(newName);
|
||||
if (newConfig != null)
|
||||
newConfig.setIsPrimary(true);
|
||||
else
|
||||
preferences.edit().remove(KEY_PRIMARY_CONFIG).apply();
|
||||
changed = true;
|
||||
}
|
||||
primaryName = newName;
|
||||
if (changed)
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
instance = this;
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a configuration from being managed by the service. If it is currently enabled, the
|
||||
* the configuration will be disabled before removal. If successful, the configuration will be
|
||||
* removed from persistent storage. If the configuration is not known to the service, no changes
|
||||
* will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to remove.
|
||||
*/
|
||||
public void remove(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null)
|
||||
return;
|
||||
if (config.isEnabled())
|
||||
new ConfigDisabler(config).execute();
|
||||
new ConfigRemover(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attributes of the named configuration. If the configuration is currently enabled,
|
||||
* it will be disabled before the update, and the service will attempt to re-enable it
|
||||
* afterward. If successful, the updated configuration will be saved to persistent storage.
|
||||
*
|
||||
* @param name The name of an existing configuration to update.
|
||||
* @param config A copy of the configuration, with updated attributes.
|
||||
*/
|
||||
public void update(final String name, final Config config) {
|
||||
if (name == null)
|
||||
return;
|
||||
if (configurations.containsValue(config))
|
||||
throw new IllegalArgumentException("Config " + config.getName() + " modified directly");
|
||||
final Config oldConfig = configurations.get(name);
|
||||
if (oldConfig == null)
|
||||
return;
|
||||
final boolean wasEnabled = oldConfig.isEnabled();
|
||||
if (wasEnabled)
|
||||
new ConfigDisabler(oldConfig).execute();
|
||||
new ConfigUpdater(oldConfig, config, wasEnabled).execute();
|
||||
}
|
||||
|
||||
private void updateTile() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
return;
|
||||
Log.v(TAG, "Requesting quick tile update");
|
||||
TileService.requestListeningState(this, new ComponentName(this, QuickTileService.class));
|
||||
}
|
||||
|
||||
private class ConfigDisabler extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigDisabler(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, "Running wg-quick down for " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
return rootShell.run(null, "wg-quick down '" + configFile.getPath() + "'") == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
config.setIsEnabled(!result);
|
||||
if (!result) {
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.error_down),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
enabledConfigs.remove(config.getName());
|
||||
preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply();
|
||||
if (config.getName().equals(primaryName))
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigEnabler extends AsyncTask<Void, Void, Integer> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigEnabler(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(final Void... voids) {
|
||||
if (!new File("/sys/module/wireguard").exists())
|
||||
return -0xfff0001;
|
||||
if (!existsInPath("su"))
|
||||
return -0xfff0002;
|
||||
Log.i(TAG, "Running wg-quick up for " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
final int ret = rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'");
|
||||
if (ret == OsConstants.EACCES)
|
||||
return -0xfff0002;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private boolean existsInPath(final String file) {
|
||||
final String pathEnv = System.getenv("PATH");
|
||||
if (pathEnv == null)
|
||||
return false;
|
||||
final String[] paths = pathEnv.split(":");
|
||||
for (final String path : paths)
|
||||
if (new File(path, file).exists())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Integer ret) {
|
||||
config.setIsEnabled(ret == 0);
|
||||
if (ret != 0) {
|
||||
if (ret == -0xfff0001) {
|
||||
startActivity(new Intent(getApplicationContext(), NotSupportedActivity.class));
|
||||
} else if (ret == -0xfff0002) {
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.error_su),
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.error_up),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
enabledConfigs.add(config.getName());
|
||||
preferences.edit().putStringSet(KEY_ENABLED_CONFIGS, enabledConfigs).apply();
|
||||
if (config.getName().equals(primaryName))
|
||||
updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigImporter extends AsyncTask<Uri, String, List<File>> {
|
||||
@Override
|
||||
protected List<File> doInBackground(final Uri... uris) {
|
||||
final ContentResolver contentResolver = getContentResolver();
|
||||
final List<File> files = new ArrayList<>(uris.length);
|
||||
for (final Uri uri : uris) {
|
||||
if (isCancelled())
|
||||
return null;
|
||||
String name = null;
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
name = uri.getLastPathSegment();
|
||||
} else {
|
||||
final String[] columns = {OpenableColumns.DISPLAY_NAME};
|
||||
try (final Cursor cursor =
|
||||
getContentResolver().query(uri, columns, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
name = cursor.getString(0);
|
||||
Log.v(getClass().getSimpleName(), "Got name via cursor");
|
||||
}
|
||||
}
|
||||
if (name == null) {
|
||||
name = Uri.decode(uri.getLastPathSegment());
|
||||
if (name.indexOf('/') >= 0)
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
Log.v(getClass().getSimpleName(), "Got name from urlencoded path");
|
||||
}
|
||||
}
|
||||
if (!name.endsWith(".conf"))
|
||||
name = name + ".conf";
|
||||
if (!Config.isNameValid(name.substring(0, name.length() - 5))) {
|
||||
Log.v(getClass().getSimpleName(), "Detected name is not valid: " + name);
|
||||
publishProgress(name + ": Invalid config filename");
|
||||
continue;
|
||||
}
|
||||
Log.d(getClass().getSimpleName(), "Mapped URI " + uri + " to file name " + name);
|
||||
final File output = new File(getFilesDir(), name);
|
||||
if (output.exists()) {
|
||||
Log.w(getClass().getSimpleName(), "Config file " + name + " already exists");
|
||||
publishProgress(name + " already exists");
|
||||
continue;
|
||||
}
|
||||
try (final InputStream in = contentResolver.openInputStream(uri);
|
||||
final OutputStream out = new FileOutputStream(output, false)) {
|
||||
if (in == null)
|
||||
throw new IOException("Failed to open input");
|
||||
// FIXME: This is a rather arbitrary size.
|
||||
final byte[] buffer = new byte[4096];
|
||||
int bytes;
|
||||
while ((bytes = in.read(buffer)) != -1)
|
||||
out.write(buffer, 0, bytes);
|
||||
files.add(output);
|
||||
} catch (final IOException e) {
|
||||
Log.w(getClass().getSimpleName(), "Failed to import config from " + uri, e);
|
||||
publishProgress(name + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(final String... errors) {
|
||||
Toast.makeText(getApplicationContext(), errors[0], Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final List<File> files) {
|
||||
new ConfigLoader().execute(files.toArray(new File[files.size()]));
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigLoader extends AsyncTask<File, String, List<Config>> {
|
||||
@Override
|
||||
protected List<Config> doInBackground(final File... files) {
|
||||
final List<Config> configs = new LinkedList<>();
|
||||
final List<String> interfaces = new LinkedList<>();
|
||||
final String command = "wg show interfaces";
|
||||
if (rootShell.run(interfaces, command) == 0 && interfaces.size() == 1) {
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
final String nameList = interfaces.get(0);
|
||||
Collections.addAll(interfaces, nameList.split(" "));
|
||||
interfaces.remove(0);
|
||||
} else {
|
||||
interfaces.clear();
|
||||
Log.w(TAG, "No existing WireGuard interfaces found. Maybe they are all disabled?");
|
||||
}
|
||||
for (final File file : files) {
|
||||
if (isCancelled())
|
||||
return null;
|
||||
final String fileName = file.getName();
|
||||
final String configName = fileName.substring(0, fileName.length() - 5);
|
||||
Log.v(TAG, "Attempting to load config " + configName);
|
||||
try {
|
||||
final Config config = new Config();
|
||||
config.parseFrom(openFileInput(fileName));
|
||||
config.setIsEnabled(interfaces.contains(configName));
|
||||
config.setName(configName);
|
||||
configs.add(config);
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
if (!file.delete()) {
|
||||
Log.e(TAG, "Could not delete configuration for config " + configName);
|
||||
}
|
||||
Log.w(TAG, "Failed to load config from " + fileName, e);
|
||||
publishProgress(fileName + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(final String... errors) {
|
||||
Toast.makeText(getApplicationContext(), errors[0], Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final List<Config> configs) {
|
||||
if (configs == null)
|
||||
return;
|
||||
for (final Config config : configs)
|
||||
configurations.put(config.getName(), config);
|
||||
// Run the handler to avoid duplicating the code here.
|
||||
onSharedPreferenceChanged(preferences, KEY_PRIMARY_CONFIG);
|
||||
if (preferences.getBoolean(KEY_RESTORE_ON_BOOT, false)) {
|
||||
final Set<String> configsToEnable =
|
||||
preferences.getStringSet(KEY_ENABLED_CONFIGS, null);
|
||||
if (configsToEnable != null) {
|
||||
for (final String name : configsToEnable) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config != null && !config.isEnabled())
|
||||
new ConfigEnabler(config).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigRemover extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigRemover(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, "Removing config " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
if (configFile.delete()) {
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "Could not delete configuration for config " + config.getName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
configurations.remove(config.getName());
|
||||
if (config.getName().equals(primaryName)) {
|
||||
// This will get picked up by the preference change listener.
|
||||
preferences.edit().remove(KEY_PRIMARY_CONFIG).apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigUpdater extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config newConfig;
|
||||
private final String newName;
|
||||
private final String oldName;
|
||||
private final Boolean shouldConnect;
|
||||
private Config knownConfig;
|
||||
|
||||
private ConfigUpdater(final Config knownConfig, final Config newConfig,
|
||||
final Boolean shouldConnect) {
|
||||
this.knownConfig = knownConfig;
|
||||
this.newConfig = newConfig.copy();
|
||||
newName = newConfig.getName();
|
||||
// When adding a config, "old file" and "new file" are the same thing.
|
||||
oldName = knownConfig != null ? knownConfig.getName() : newName;
|
||||
this.shouldConnect = shouldConnect;
|
||||
if (newName == null || !Config.isNameValid(newName))
|
||||
throw new IllegalArgumentException("This configuration does not have a valid name");
|
||||
if (isAddOrRename() && configurations.containsKey(newName))
|
||||
throw new IllegalStateException("Configuration " + newName + " already exists");
|
||||
if (newConfig.getInterface().getPublicKey() == null)
|
||||
throw new IllegalArgumentException("This configuration needs a valid private key");
|
||||
for (final Peer peer : newConfig.getPeers())
|
||||
if (peer.getPublicKey() == null || peer.getPublicKey().isEmpty())
|
||||
throw new IllegalArgumentException("Each peer must have a valid public key");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, (knownConfig == null ? "Adding" : "Updating") + " config " + newName);
|
||||
final File newFile = new File(getFilesDir(), newName + ".conf");
|
||||
final File oldFile = new File(getFilesDir(), oldName + ".conf");
|
||||
if (isAddOrRename() && newFile.exists()) {
|
||||
Log.w(TAG, "Refusing to overwrite existing config configuration");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final FileOutputStream stream = openFileOutput(oldFile.getName(), MODE_PRIVATE);
|
||||
stream.write(newConfig.toString().getBytes(StandardCharsets.UTF_8));
|
||||
stream.close();
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Could not save configuration for config " + oldName, e);
|
||||
return false;
|
||||
}
|
||||
if (isRename() && !oldFile.renameTo(newFile)) {
|
||||
Log.e(TAG, "Could not rename " + oldFile.getName() + " to " + newFile.getName());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isAddOrRename() {
|
||||
return knownConfig == null || !newName.equals(oldName);
|
||||
}
|
||||
|
||||
private boolean isRename() {
|
||||
return knownConfig != null && !newName.equals(oldName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (knownConfig != null)
|
||||
configurations.remove(oldName);
|
||||
if (knownConfig == null)
|
||||
knownConfig = new Config();
|
||||
knownConfig.copyFrom(newConfig);
|
||||
knownConfig.setIsEnabled(false);
|
||||
knownConfig.setIsPrimary(oldName != null && oldName.equals(primaryName));
|
||||
configurations.put(newName, knownConfig);
|
||||
if (isRename() && oldName != null && oldName.equals(primaryName))
|
||||
preferences.edit().putString(KEY_PRIMARY_CONFIG, newName).apply();
|
||||
if (shouldConnect)
|
||||
new ConfigEnabler(knownConfig).execute();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.wireguard.android.configStore;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Interface for persistent storage providers for WireGuard configurations.
|
||||
*/
|
||||
|
||||
public interface ConfigStore {
|
||||
/**
|
||||
* Create a persistent tunnel, which must have a unique name within the persistent storage
|
||||
* medium.
|
||||
*
|
||||
* @param name The name of the tunnel to create.
|
||||
* @param config Configuration for the new tunnel.
|
||||
* @return A future completed when the tunnel and its configuration have been saved to
|
||||
* persistent storage. This future encapsulates the configuration that was actually saved to
|
||||
* persistent storage. This future will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Config> create(final String name, final Config config);
|
||||
|
||||
/**
|
||||
* Delete a persistent tunnel.
|
||||
*
|
||||
* @param name The name of the tunnel to delete.
|
||||
* @return A future completed when the tunnel and its configuration have been deleted. This
|
||||
* future will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Void> delete(final String name);
|
||||
|
||||
/**
|
||||
* Enumerate the names of tunnels present in persistent storage.
|
||||
*
|
||||
* @return A future completed when the set of present tunnel names is available. This future
|
||||
* will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Set<String>> enumerate();
|
||||
|
||||
/**
|
||||
* Load the configuration for the tunnel given by {@code name}.
|
||||
*
|
||||
* @param name The identifier for the configuration in persistent storage (i.e. the name of the
|
||||
* tunnel).
|
||||
* @return A future completed when an in-memory representation of the configuration is
|
||||
* available. This future encapsulates the configuration loaded from persistent storage. This
|
||||
* future will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Config> load(final String name);
|
||||
|
||||
/**
|
||||
* Save the configuration for an existing tunnel given by {@code name}.
|
||||
*
|
||||
* @param name The identifier for the configuration in persistent storage (i.e. the name of
|
||||
* the tunnel).
|
||||
* @param config An updated configuration object for the tunnel.
|
||||
* @return A future completed when the configuration has been saved to persistent storage. This
|
||||
* future encapsulates the configuration that was actually saved to persistent storage. This
|
||||
* future will always be completed on the main thread.
|
||||
*/
|
||||
CompletionStage<Config> save(final String name, final Config config);
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package com.wireguard.android.configStore;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application.ApplicationContext;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Created by samuel on 12/28/17.
|
||||
*/
|
||||
|
||||
public final class FileConfigStore implements ConfigStore {
|
||||
private static final String TAG = FileConfigStore.class.getSimpleName();
|
||||
|
||||
private final AsyncWorker asyncWorker;
|
||||
private final Context context;
|
||||
|
||||
public FileConfigStore(final AsyncWorker asyncWorker,
|
||||
@ApplicationContext final Context context) {
|
||||
this.asyncWorker = asyncWorker;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Config> create(final String name, final Config config) {
|
||||
return asyncWorker.supplyAsync(() -> {
|
||||
final File file = fileFor(name);
|
||||
if (!file.createNewFile()) {
|
||||
final String message = "Configuration file " + file.getName() + " already exists";
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
try (FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
return config;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Void> delete(final String name) {
|
||||
return asyncWorker.runAsync(() -> {
|
||||
final File file = fileFor(name);
|
||||
if (!file.delete())
|
||||
throw new IOException("Cannot delete configuration file " + file.getName());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Set<String>> enumerate() {
|
||||
return asyncWorker.supplyAsync(() -> Stream.of(context.fileList())
|
||||
.filter(name -> name.endsWith(".conf"))
|
||||
.map(name -> name.substring(0, name.length() - ".conf".length()))
|
||||
.collect(Collectors.toUnmodifiableSet()));
|
||||
}
|
||||
|
||||
private File fileFor(final String name) {
|
||||
return new File(context.getFilesDir(), name + ".conf");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Config> load(final String name) {
|
||||
return asyncWorker.supplyAsync(() -> {
|
||||
try (FileInputStream stream = new FileInputStream(fileFor(name))) {
|
||||
return Config.from(stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<Config> save(final String name, final Config config) {
|
||||
Log.d(TAG, "Requested save config for tunnel " + name);
|
||||
return asyncWorker.supplyAsync(() -> {
|
||||
final File file = fileFor(name);
|
||||
if (!file.isFile()) {
|
||||
final String message = "Configuration file " + file.getName() + " not found";
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
try (FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
Log.d(TAG, "Writing out config for tunnel " + name);
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
return config;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -12,13 +12,22 @@ import android.widget.TextView;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.widget.ToggleSwitch;
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.ZoneId;
|
||||
import org.threeten.bp.ZonedDateTime;
|
||||
import org.threeten.bp.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Static methods for use by generated code in the Android data binding library.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
public final class BindingAdapters {
|
||||
@BindingAdapter({"app:checked"})
|
||||
private BindingAdapters() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
@BindingAdapter({"checked"})
|
||||
public static void setChecked(final ToggleSwitch view, final boolean checked) {
|
||||
view.setCheckedInternal(checked);
|
||||
}
|
||||
@ -80,9 +89,9 @@ public final class BindingAdapters {
|
||||
|
||||
@BindingAdapter({"items", "layout"})
|
||||
public static <K extends Comparable<K>, V> void setItems(final ListView view,
|
||||
final ObservableSortedMap<K, V> oldMap,
|
||||
final ObservableNavigableMap<K, V> oldMap,
|
||||
final int oldLayoutId,
|
||||
final ObservableSortedMap<K, V> newMap,
|
||||
final ObservableNavigableMap<K, V> newMap,
|
||||
final int newLayoutId) {
|
||||
if (oldMap == newMap && oldLayoutId == newLayoutId)
|
||||
return;
|
||||
@ -105,19 +114,26 @@ public final class BindingAdapters {
|
||||
adapter.setMap(newMap);
|
||||
}
|
||||
|
||||
@BindingAdapter({"app:onBeforeCheckedChanged"})
|
||||
@BindingAdapter({"onBeforeCheckedChanged"})
|
||||
public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
|
||||
final ToggleSwitch.OnBeforeCheckedChangeListener
|
||||
listener) {
|
||||
view.setOnBeforeCheckedChangeListener(listener);
|
||||
}
|
||||
|
||||
@BindingAdapter({"android:text"})
|
||||
public static void setText(final TextView view, final Instant instant) {
|
||||
if (instant == null || Instant.EPOCH.equals(instant)) {
|
||||
view.setText(R.string.never);
|
||||
} else {
|
||||
final ZoneId defaultZone = ZoneId.systemDefault();
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, defaultZone);
|
||||
view.setText(zonedDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter({"android:textStyle"})
|
||||
public static void setTextStyle(final TextView view, final Typeface typeface) {
|
||||
view.setTypeface(typeface);
|
||||
}
|
||||
|
||||
private BindingAdapters() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ class ItemChangeListener<T> {
|
||||
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
|
||||
if (binding == null)
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
|
||||
binding.setVariable(BR.collection, list);
|
||||
binding.setVariable(BR.item, list.get(position));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
@ -49,7 +50,7 @@ class ItemChangeListener<T> {
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnListChangedCallback<T>
|
||||
private static final class OnListChangedCallback<T>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<T>> {
|
||||
|
||||
private final WeakReference<ItemChangeListener<T>> weakListener;
|
||||
|
@ -8,7 +8,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
|
||||
@ -18,7 +17,7 @@ import java.lang.ref.WeakReference;
|
||||
* A generic ListAdapter backed by an ObservableList.
|
||||
*/
|
||||
|
||||
class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
||||
class ObservableListAdapter<T> extends BaseAdapter {
|
||||
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
@ -54,6 +53,7 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
||||
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
|
||||
if (binding == null)
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
||||
binding.setVariable(BR.collection, list);
|
||||
binding.setVariable(BR.item, getItem(position));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
@ -74,7 +74,7 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class OnListChangedCallback<U>
|
||||
private static final class OnListChangedCallback<U>
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<U>> {
|
||||
|
||||
private final WeakReference<ObservableListAdapter<U>> weakAdapter;
|
||||
|
@ -8,28 +8,27 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A generic ListAdapter backed by a TreeMap that adds observability.
|
||||
*/
|
||||
|
||||
public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapter
|
||||
implements ListAdapter {
|
||||
public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapter {
|
||||
private final OnMapChangedCallback<K, V> callback = new OnMapChangedCallback<>(this);
|
||||
private ArrayList<K> keys;
|
||||
private final int layoutId;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private ObservableSortedMap<K, V> map;
|
||||
private List<K> keys;
|
||||
private ObservableNavigableMap<K, V> map;
|
||||
|
||||
ObservableMapAdapter(final Context context, final int layoutId,
|
||||
final ObservableSortedMap<K, V> map) {
|
||||
final ObservableNavigableMap<K, V> map) {
|
||||
this.layoutId = layoutId;
|
||||
layoutInflater = LayoutInflater.from(context);
|
||||
setMap(map);
|
||||
@ -51,14 +50,17 @@ public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapte
|
||||
public long getItemId(final int position) {
|
||||
if (map == null || position < 0 || position >= map.size())
|
||||
return -1;
|
||||
return getItem(position).hashCode();
|
||||
//final V item = getItem(position);
|
||||
//return item != null ? item.hashCode() : -1;
|
||||
final K key = getKey(position);
|
||||
return key.hashCode();
|
||||
}
|
||||
|
||||
private K getKey(final int position) {
|
||||
return getKeys().get(position);
|
||||
}
|
||||
|
||||
private ArrayList<K> getKeys() {
|
||||
private List<K> getKeys() {
|
||||
if (keys == null)
|
||||
keys = new ArrayList<>(map.keySet());
|
||||
return keys;
|
||||
@ -75,6 +77,7 @@ public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapte
|
||||
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
|
||||
if (binding == null)
|
||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
||||
binding.setVariable(BR.collection, map);
|
||||
binding.setVariable(BR.key, getKey(position));
|
||||
binding.setVariable(BR.item, getItem(position));
|
||||
binding.executePendingBindings();
|
||||
@ -86,7 +89,7 @@ public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapte
|
||||
return true;
|
||||
}
|
||||
|
||||
void setMap(final ObservableSortedMap<K, V> newMap) {
|
||||
void setMap(final ObservableNavigableMap<K, V> newMap) {
|
||||
if (map != null)
|
||||
map.removeOnMapChangedCallback(callback);
|
||||
keys = null;
|
||||
@ -97,8 +100,8 @@ public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapte
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class OnMapChangedCallback<K extends Comparable<K>, V>
|
||||
extends ObservableMap.OnMapChangedCallback<ObservableSortedMap<K, V>, K, V> {
|
||||
private static final class OnMapChangedCallback<K extends Comparable<K>, V>
|
||||
extends ObservableMap.OnMapChangedCallback<ObservableNavigableMap<K, V>, K, V> {
|
||||
|
||||
private final WeakReference<ObservableMapAdapter<K, V>> weakAdapter;
|
||||
|
||||
@ -107,7 +110,7 @@ public class ObservableMapAdapter<K extends Comparable<K>, V> extends BaseAdapte
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapChanged(final ObservableSortedMap<K, V> sender, final K key) {
|
||||
public void onMapChanged(final ObservableNavigableMap<K, V> sender, final K key) {
|
||||
final ObservableMapAdapter<K, V> adapter = weakAdapter.get();
|
||||
if (adapter != null) {
|
||||
adapter.keys = null;
|
||||
|
@ -2,12 +2,12 @@ package com.wireguard.android.databinding;
|
||||
|
||||
import android.databinding.ObservableMap;
|
||||
|
||||
import java.util.SortedMap;
|
||||
import java.util.NavigableMap;
|
||||
|
||||
/**
|
||||
* Interface for maps that are both observable and sorted.
|
||||
*/
|
||||
|
||||
public interface ObservableSortedMap<K, V> extends ObservableMap<K, V>, SortedMap<K, V> {
|
||||
public interface ObservableNavigableMap<K, V> extends NavigableMap<K, V>, ObservableMap<K, V> {
|
||||
// No additional methods.
|
||||
}
|
@ -12,15 +12,9 @@ import java.util.TreeMap;
|
||||
* views. This behavior is in line with that of ObservableArrayMap.
|
||||
*/
|
||||
|
||||
public class ObservableTreeMap<K, V> extends TreeMap<K, V> implements ObservableSortedMap<K, V> {
|
||||
public class ObservableTreeMap<K, V> extends TreeMap<K, V> implements ObservableNavigableMap<K, V> {
|
||||
private transient MapChangeRegistry listeners;
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
notifyChange(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOnMapChangedCallback(
|
||||
final OnMapChangedCallback<? extends ObservableMap<K, V>, K, V> listener) {
|
||||
@ -29,6 +23,12 @@ public class ObservableTreeMap<K, V> extends TreeMap<K, V> implements Observable
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
notifyChange(null);
|
||||
}
|
||||
|
||||
private void notifyChange(final K key) {
|
||||
if (listeners != null)
|
||||
listeners.notifyChange(this, key);
|
||||
@ -51,8 +51,7 @@ public class ObservableTreeMap<K, V> extends TreeMap<K, V> implements Observable
|
||||
@Override
|
||||
public V remove(final Object key) {
|
||||
final V oldValue = super.remove(key);
|
||||
@SuppressWarnings("unchecked")
|
||||
final K k = (K) key;
|
||||
@SuppressWarnings("unchecked") final K k = (K) key;
|
||||
notifyChange(k);
|
||||
return oldValue;
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
|
||||
import com.wireguard.android.activity.BaseActivity;
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||
* attached to a {@code BaseActivity}.
|
||||
*/
|
||||
|
||||
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
|
||||
private BaseActivity activity;
|
||||
|
||||
protected Tunnel getSelectedTunnel() {
|
||||
return activity != null ? activity.getSelectedTunnel() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof BaseActivity) {
|
||||
activity = (BaseActivity) context;
|
||||
activity.addOnSelectedTunnelChangedListener(this);
|
||||
} else {
|
||||
activity = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
if (activity != null)
|
||||
activity.removeOnSelectedTunnelChangedListener(this);
|
||||
activity = null;
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
protected void setSelectedTunnel(final Tunnel tunnel) {
|
||||
if (activity != null)
|
||||
activity.setSelectedTunnel(tunnel);
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.databinding.ObservableField;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.commonsware.cwac.crossport.design.widget.CoordinatorLayout;
|
||||
import com.commonsware.cwac.crossport.design.widget.Snackbar;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ConfigEditorFragmentBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
|
||||
public class ConfigEditorFragment extends BaseFragment {
|
||||
private static final String KEY_LOCAL_CONFIG = "local_config";
|
||||
private static final String KEY_LOCAL_NAME = "local_name";
|
||||
private static final String TAG = ConfigEditorFragment.class.getSimpleName();
|
||||
|
||||
private final ObservableField<String> localName = new ObservableField<>();
|
||||
private ConfigEditorFragmentBinding binding;
|
||||
private boolean isViewStateRestored;
|
||||
private Config localConfig = new Config();
|
||||
private String originalName;
|
||||
|
||||
private static <T extends Parcelable> T copyParcelable(final T original) {
|
||||
if (original == null)
|
||||
return null;
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
parcel.writeParcelable(original, 0);
|
||||
parcel.setDataPosition(0);
|
||||
final T copy = parcel.readParcelable(original.getClass().getClassLoader());
|
||||
parcel.recycle();
|
||||
return copy;
|
||||
}
|
||||
|
||||
private void onConfigCreated(final Tunnel tunnel, final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Cannot create tunnel", throwable);
|
||||
final String message = "Cannot create tunnel: "
|
||||
+ ExceptionLoggers.unwrap(throwable).getMessage();
|
||||
if (binding != null) {
|
||||
final CoordinatorLayout container = binding.mainContainer;
|
||||
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Successfully created tunnel " + tunnel.getName());
|
||||
onFinished(tunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private void onConfigLoaded(final Config config) {
|
||||
localConfig = copyParcelable(config);
|
||||
if (binding != null && isViewStateRestored)
|
||||
binding.setConfig(localConfig);
|
||||
}
|
||||
|
||||
private void onConfigSaved(final Config config, final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, "Cannot save configuration", throwable);
|
||||
final String message = "Cannot save configuration: "
|
||||
+ ExceptionLoggers.unwrap(throwable).getMessage();
|
||||
if (binding != null) {
|
||||
final CoordinatorLayout container = binding.mainContainer;
|
||||
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Successfully saved configuration for " + getSelectedTunnel().getName());
|
||||
onFinished(getSelectedTunnel());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
localConfig = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
|
||||
localName.set(savedInstanceState.getString(KEY_LOCAL_NAME));
|
||||
originalName = savedInstanceState.getString(TunnelManager.KEY_SELECTED_TUNNEL);
|
||||
}
|
||||
// Erase the remains of creating or editing a different tunnel.
|
||||
if (getSelectedTunnel() != null && !getSelectedTunnel().getName().equals(originalName)) {
|
||||
// The config must be loaded asynchronously since it's not an observable property.
|
||||
localConfig = null;
|
||||
getSelectedTunnel().getConfigAsync().thenAccept(this::onConfigLoaded);
|
||||
originalName = getSelectedTunnel().getName();
|
||||
localName.set(originalName);
|
||||
} else if (getSelectedTunnel() == null && originalName != null) {
|
||||
localConfig = new Config();
|
||||
originalName = null;
|
||||
localName.set(null);
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.config_editor, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = ConfigEditorFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void onFinished(final Tunnel tunnel) {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
final Activity activity = getActivity();
|
||||
final View focusedView = activity.getCurrentFocus();
|
||||
if (focusedView != null) {
|
||||
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
final InputMethodManager inputManager = (InputMethodManager) service;
|
||||
if (inputManager != null)
|
||||
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
getActivity().runOnUiThread(() -> {
|
||||
setSelectedTunnel(null);
|
||||
setSelectedTunnel(tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_save:
|
||||
if (getSelectedTunnel() != null) {
|
||||
Log.d(TAG, "Attempting to save config to " + getSelectedTunnel().getName());
|
||||
getSelectedTunnel().setConfig(localConfig)
|
||||
.whenComplete(this::onConfigSaved);
|
||||
} else {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + localName.get());
|
||||
final TunnelManager manager = Application.getComponent().getTunnelManager();
|
||||
manager.create(localName.get(), localConfig)
|
||||
.whenComplete(this::onConfigCreated);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
outState.putParcelable(KEY_LOCAL_CONFIG, localConfig);
|
||||
outState.putString(KEY_LOCAL_NAME, localName.get());
|
||||
outState.putString(TunnelManager.KEY_SELECTED_TUNNEL, originalName);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
|
||||
// Erase the remains of creating or editing a different tunnel.
|
||||
if (newTunnel != null) {
|
||||
// The config must be loaded asynchronously since it's not an observable property.
|
||||
localConfig = null;
|
||||
newTunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
|
||||
originalName = newTunnel.getName();
|
||||
} else {
|
||||
localConfig = new Config();
|
||||
if (binding != null && isViewStateRestored)
|
||||
binding.setConfig(localConfig);
|
||||
originalName = null;
|
||||
}
|
||||
localName.set(originalName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(final Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
binding.setConfig(localConfig);
|
||||
binding.setName(localName);
|
||||
// FIXME: Remove this when renaming works.
|
||||
binding.interfaceNameText.setEnabled(originalName == null);
|
||||
isViewStateRestored = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
*/
|
||||
|
||||
public class TunnelDetailFragment extends BaseFragment {
|
||||
private TunnelDetailFragmentBinding binding;
|
||||
private boolean isViewStateRestored;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tunnel_detail, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
|
||||
if (binding != null && isViewStateRestored)
|
||||
binding.setTunnel(newTunnel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(final Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
binding.setTunnel(getSelectedTunnel());
|
||||
isViewStateRestored = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AbsListView.MultiChoiceModeListener;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.AdapterView.OnItemLongClickListener;
|
||||
|
||||
import com.commonsware.cwac.crossport.design.widget.CoordinatorLayout;
|
||||
import com.commonsware.cwac.crossport.design.widget.Snackbar;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.Application.ApplicationComponent;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity;
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
import java9.util.function.Function;
|
||||
import java9.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
*/
|
||||
|
||||
public class TunnelListFragment extends BaseFragment {
|
||||
private static final int REQUEST_IMPORT = 1;
|
||||
private static final String TAG = TunnelListFragment.class.getSimpleName();
|
||||
|
||||
private final MultiChoiceModeListener actionModeListener = new ActionModeListener();
|
||||
private final ListViewCallbacks listViewCallbacks = new ListViewCallbacks();
|
||||
private ActionMode actionMode;
|
||||
private AsyncWorker asyncWorker;
|
||||
private TunnelListFragmentBinding binding;
|
||||
private TunnelManager tunnelManager;
|
||||
|
||||
private void importTunnel(final Uri uri) {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null)
|
||||
return;
|
||||
final ContentResolver contentResolver = activity.getContentResolver();
|
||||
final CompletionStage<String> nameFuture = asyncWorker.supplyAsync(() -> {
|
||||
final String[] columns = {OpenableColumns.DISPLAY_NAME};
|
||||
String name = null;
|
||||
try (final Cursor cursor = contentResolver.query(uri, columns, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
|
||||
name = cursor.getString(0);
|
||||
}
|
||||
if (name == null)
|
||||
name = Uri.decode(uri.getLastPathSegment());
|
||||
if (name.indexOf('/') >= 0)
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
if (name.endsWith(".conf"))
|
||||
name = name.substring(0, name.length() - ".conf".length());
|
||||
Log.d(TAG, "Import mapped URI " + uri + " to tunnel name " + name);
|
||||
return name;
|
||||
});
|
||||
asyncWorker.supplyAsync(() -> Config.from(contentResolver.openInputStream(uri)))
|
||||
.thenCombine(nameFuture, (config, name) -> tunnelManager.create(name, config))
|
||||
.thenCompose(Function.identity())
|
||||
.handle(this::onTunnelImportFinished);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_IMPORT:
|
||||
if (resultCode == Activity.RESULT_OK)
|
||||
importTunnel(data.getData());
|
||||
return;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final ApplicationComponent applicationComponent = Application.getComponent();
|
||||
asyncWorker = applicationComponent.getAsyncWorker();
|
||||
tunnelManager = applicationComponent.getTunnelManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
|
||||
binding.tunnelList.setMultiChoiceModeListener(actionModeListener);
|
||||
binding.tunnelList.setOnItemClickListener(listViewCallbacks);
|
||||
binding.tunnelList.setOnItemLongClickListener(listViewCallbacks);
|
||||
binding.tunnelList.setOnTouchListener(listViewCallbacks);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
|
||||
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
|
||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQUEST_IMPORT);
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(final Tunnel oldTunnel, final Tunnel newTunnel) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private Void onTunnelDeletionFinished(final Integer count, final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = "Successfully deleted " + count + " tunnels";
|
||||
} else {
|
||||
message = "Could not delete some tunnels: "
|
||||
+ ExceptionLoggers.unwrap(throwable).getMessage();
|
||||
Log.e(TAG, "Cannot delete tunnel", throwable);
|
||||
}
|
||||
if (binding != null) {
|
||||
final CoordinatorLayout container = binding.mainContainer;
|
||||
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Void onTunnelImportFinished(final Tunnel tunnel, final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = "Successfully imported tunnel '" + tunnel.getName() + '\'';
|
||||
} else {
|
||||
message = "Cannot import tunnel: "
|
||||
+ ExceptionLoggers.unwrap(throwable).getMessage();
|
||||
Log.e(TAG, "Cannot import tunnel", throwable);
|
||||
}
|
||||
if (binding != null) {
|
||||
final CoordinatorLayout container = binding.mainContainer;
|
||||
Snackbar.make(container, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(final Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
binding.setFragment(this);
|
||||
binding.setTunnels(tunnelManager.getTunnels());
|
||||
}
|
||||
|
||||
private final class ActionModeListener implements MultiChoiceModeListener {
|
||||
private Resources resources;
|
||||
private AbsListView tunnelList;
|
||||
|
||||
private IntStream getCheckedPositions() {
|
||||
final SparseBooleanArray checkedItemPositions = tunnelList.getCheckedItemPositions();
|
||||
return IntStream.range(0, checkedItemPositions.size())
|
||||
.filter(checkedItemPositions::valueAt)
|
||||
.map(checkedItemPositions::keyAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_delete:
|
||||
final CompletableFuture[] futures = getCheckedPositions()
|
||||
.mapToObj(pos -> (Tunnel) tunnelList.getItemAtPosition(pos))
|
||||
.map(tunnelManager::delete)
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures)
|
||||
.thenApply(x -> futures.length)
|
||||
.handle(TunnelListFragment.this::onTunnelDeletionFinished);
|
||||
mode.finish();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
|
||||
actionMode = mode;
|
||||
resources = getActivity().getResources();
|
||||
tunnelList = binding.tunnelList;
|
||||
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode mode) {
|
||||
actionMode = null;
|
||||
resources = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(final ActionMode mode, final int position,
|
||||
final long id, final boolean checked) {
|
||||
updateTitle(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
|
||||
updateTitle(mode);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateTitle(final ActionMode mode) {
|
||||
final int count = (int) getCheckedPositions().count();
|
||||
mode.setTitle(resources.getQuantityString(R.plurals.list_delete_title, count, count));
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListViewCallbacks
|
||||
implements OnItemClickListener, OnItemLongClickListener, OnTouchListener {
|
||||
@Override
|
||||
public void onItemClick(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
setSelectedTunnel((Tunnel) parent.getItemAtPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
if (actionMode != null)
|
||||
return false;
|
||||
binding.tunnelList.setItemChecked(position, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public boolean onTouch(final View view, final MotionEvent motionEvent) {
|
||||
binding.createMenu.collapse();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
166
app/src/main/java/com/wireguard/android/model/Tunnel.java
Normal file
166
app/src/main/java/com/wireguard/android/model/Tunnel.java
Normal file
@ -0,0 +1,166 @@
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.configStore.ConfigStore;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
*/
|
||||
|
||||
public class Tunnel extends BaseObservable implements Comparable<Tunnel> {
|
||||
public static final int NAME_MAX_LENGTH = 16;
|
||||
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,16}");
|
||||
private static final String TAG = Tunnel.class.getSimpleName();
|
||||
|
||||
private final Backend backend;
|
||||
private final ConfigStore configStore;
|
||||
private final String name;
|
||||
private Config config;
|
||||
private Instant lastStateChange = Instant.EPOCH;
|
||||
private State state = State.UNKNOWN;
|
||||
private Statistics statistics;
|
||||
|
||||
Tunnel(@NonNull final Backend backend, @NonNull final ConfigStore configStore,
|
||||
@NonNull final String name, @Nullable final Config config) {
|
||||
this.backend = backend;
|
||||
this.configStore = configStore;
|
||||
this.name = name;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public static boolean isNameValid(final CharSequence name) {
|
||||
return name != null && NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull final Tunnel tunnel) {
|
||||
return name.compareTo(tunnel.name);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Config getConfig() {
|
||||
if (config == null)
|
||||
getConfigAsync().whenComplete(ExceptionLoggers.D);
|
||||
return config;
|
||||
}
|
||||
|
||||
public CompletionStage<Config> getConfigAsync() {
|
||||
if (config == null)
|
||||
return configStore.load(name).thenApply(this::setConfigInternal);
|
||||
return CompletableFuture.completedFuture(config);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Instant getLastStateChange() {
|
||||
return lastStateChange;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public State getState() {
|
||||
if (state == State.UNKNOWN)
|
||||
getStateAsync().whenComplete(ExceptionLoggers.D);
|
||||
return state;
|
||||
}
|
||||
|
||||
public CompletionStage<State> getStateAsync() {
|
||||
if (state == State.UNKNOWN)
|
||||
return backend.getState(this).thenApply(this::setStateInternal);
|
||||
return CompletableFuture.completedFuture(state);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Statistics getStatistics() {
|
||||
// FIXME: Check age of statistics.
|
||||
if (statistics == null)
|
||||
getStatisticsAsync().whenComplete(ExceptionLoggers.D);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public CompletionStage<Statistics> getStatisticsAsync() {
|
||||
// FIXME: Check age of statistics.
|
||||
if (statistics == null)
|
||||
return backend.getStatistics(this).thenApply(this::setStatisticsInternal);
|
||||
return CompletableFuture.completedFuture(statistics);
|
||||
}
|
||||
|
||||
private void onStateChanged(final State oldState, final State newState) {
|
||||
if (oldState != State.UNKNOWN) {
|
||||
lastStateChange = Instant.now();
|
||||
notifyPropertyChanged(BR.lastStateChange);
|
||||
}
|
||||
if (newState != State.UP)
|
||||
setStatisticsInternal(null);
|
||||
}
|
||||
|
||||
public CompletionStage<Config> setConfig(@NonNull final Config config) {
|
||||
if (!config.equals(this.config)) {
|
||||
return backend.applyConfig(this, config)
|
||||
.thenCompose(cfg -> configStore.save(name, cfg))
|
||||
.thenApply(this::setConfigInternal);
|
||||
}
|
||||
return CompletableFuture.completedFuture(this.config);
|
||||
}
|
||||
|
||||
private Config setConfigInternal(final Config config) {
|
||||
if (Objects.equals(this.config, config))
|
||||
return config;
|
||||
this.config = config;
|
||||
notifyPropertyChanged(BR.config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public CompletionStage<State> setState(@NonNull final State state) {
|
||||
if (state != this.state)
|
||||
return backend.setState(this, state)
|
||||
.thenApply(this::setStateInternal);
|
||||
return CompletableFuture.completedFuture(this.state);
|
||||
}
|
||||
|
||||
private State setStateInternal(final State state) {
|
||||
if (Objects.equals(this.state, state))
|
||||
return state;
|
||||
onStateChanged(this.state, state);
|
||||
this.state = state;
|
||||
notifyPropertyChanged(BR.state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private Statistics setStatisticsInternal(final Statistics statistics) {
|
||||
if (Objects.equals(this.statistics, statistics))
|
||||
return statistics;
|
||||
this.statistics = statistics;
|
||||
notifyPropertyChanged(BR.statistics);
|
||||
return statistics;
|
||||
}
|
||||
|
||||
public enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UNKNOWN,
|
||||
UP
|
||||
}
|
||||
|
||||
public static class Statistics extends BaseObservable {
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import com.wireguard.android.databinding.ObservableTreeMap;
|
||||
|
||||
/**
|
||||
* Created by samuel on 12/19/17.
|
||||
*/
|
||||
|
||||
public class TunnelCollection extends ObservableTreeMap<String, Tunnel> {
|
||||
}
|
111
app/src/main/java/com/wireguard/android/model/TunnelManager.java
Normal file
111
app/src/main/java/com/wireguard/android/model/TunnelManager.java
Normal file
@ -0,0 +1,111 @@
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application.ApplicationScope;
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.configStore.ConfigStore;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||
*/
|
||||
|
||||
@ApplicationScope
|
||||
public final class TunnelManager {
|
||||
public static final String KEY_PRIMARY_TUNNEL = "primary_config";
|
||||
public static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
|
||||
private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
|
||||
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
|
||||
private static final String TAG = TunnelManager.class.getSimpleName();
|
||||
|
||||
private final Backend backend;
|
||||
private final ConfigStore configStore;
|
||||
private final SharedPreferences preferences;
|
||||
private final TunnelCollection tunnels = new TunnelCollection();
|
||||
|
||||
@Inject
|
||||
public TunnelManager(final Backend backend, final ConfigStore configStore,
|
||||
final SharedPreferences preferences) {
|
||||
this.backend = backend;
|
||||
this.configStore = configStore;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
private Tunnel add(final String name, final Config config) {
|
||||
final Tunnel tunnel = new Tunnel(backend, configStore, name, config);
|
||||
tunnels.put(name, tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
|
||||
private Tunnel add(final String name) {
|
||||
return add(name, null);
|
||||
}
|
||||
|
||||
public CompletionStage<Tunnel> create(final String name, final Config config) {
|
||||
Log.v(TAG, "Requested create tunnel " + name + " with config\n" + config);
|
||||
if (!Tunnel.isNameValid(name))
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
|
||||
if (tunnels.containsKey(name)) {
|
||||
final String message = "Tunnel " + name + " already exists";
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
|
||||
}
|
||||
return configStore.create(name, config).thenApply(savedConfig -> add(name, savedConfig));
|
||||
}
|
||||
|
||||
public CompletionStage<Void> delete(final Tunnel tunnel) {
|
||||
Log.v(TAG, "Requested delete tunnel " + tunnel.getName() + " state=" + tunnel.getState());
|
||||
return backend.setState(tunnel, State.DOWN)
|
||||
.thenCompose(x -> configStore.delete(tunnel.getName()))
|
||||
.thenAccept(x -> tunnels.remove(tunnel.getName()));
|
||||
}
|
||||
|
||||
public TunnelCollection getTunnels() {
|
||||
return tunnels;
|
||||
}
|
||||
|
||||
public void onCreate() {
|
||||
Log.v(TAG, "onCreate triggered");
|
||||
configStore.enumerate()
|
||||
.thenApply(names -> StreamSupport.stream(names)
|
||||
.map(this::add)
|
||||
.map(Tunnel::getStateAsync)
|
||||
.toArray(CompletableFuture[]::new))
|
||||
.thenCompose(CompletableFuture::allOf)
|
||||
.whenComplete(ExceptionLoggers.E);
|
||||
}
|
||||
|
||||
public CompletionStage<Void> restoreState() {
|
||||
if (!preferences.getBoolean(KEY_RESTORE_ON_BOOT, false))
|
||||
return CompletableFuture.completedFuture(null);
|
||||
final Set<String> tunnelsToEnable =
|
||||
preferences.getStringSet(KEY_RUNNING_TUNNELS, Collections.emptySet());
|
||||
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToEnable)
|
||||
.map(tunnels::get)
|
||||
.map(tunnel -> tunnel.setState(State.UP))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
return CompletableFuture.allOf(futures);
|
||||
}
|
||||
|
||||
public CompletionStage<Void> saveState() {
|
||||
final Set<String> runningTunnels = StreamSupport.stream(tunnels.values())
|
||||
.filter(tunnel -> tunnel.getState() == State.UP)
|
||||
.map(Tunnel::getName)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
preferences.edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package com.wireguard.android;
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wireguard.android.backends.VpnService;
|
||||
import com.wireguard.android.Application;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ -12,28 +12,30 @@ import java.util.Set;
|
||||
* ListPreference that is automatically filled with the list of configurations.
|
||||
*/
|
||||
|
||||
public class ConfigListPreference extends ListPreference {
|
||||
public ConfigListPreference(final Context context, final AttributeSet attrs,
|
||||
public class TunnelListPreference extends ListPreference {
|
||||
public TunnelListPreference(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr, final int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
final Set<String> entrySet = VpnService.getInstance().getConfigs().keySet();
|
||||
final Set<String> entrySet = Application.getComponent().getTunnelManager().getTunnels().keySet();
|
||||
final CharSequence[] entries = entrySet.toArray(new CharSequence[entrySet.size()]);
|
||||
setEntries(entries);
|
||||
setEntryValues(entries);
|
||||
}
|
||||
|
||||
public ConfigListPreference(final Context context, final AttributeSet attrs,
|
||||
public TunnelListPreference(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public ConfigListPreference(final Context context, final AttributeSet attrs) {
|
||||
public TunnelListPreference(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.dialogPreferenceStyle);
|
||||
}
|
||||
|
||||
public ConfigListPreference(final Context context) {
|
||||
public TunnelListPreference(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public void show() { showDialog(null); }
|
||||
public void show() {
|
||||
showDialog(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.wireguard.android.Application.ApplicationHandler;
|
||||
import com.wireguard.android.Application.ApplicationScope;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
|
||||
*/
|
||||
|
||||
@ApplicationScope
|
||||
public class AsyncWorker {
|
||||
private final Executor executor;
|
||||
private final Handler handler;
|
||||
|
||||
@Inject
|
||||
public AsyncWorker(final Executor executor, @ApplicationHandler final Handler handler) {
|
||||
this.executor = executor;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
handler.post(() -> future.complete(null));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
|
||||
final CompletableFuture<T> future = new CompletableFuture<>();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
final T result = supplier.get();
|
||||
handler.post(() -> future.complete(result));
|
||||
} catch (final Throwable t) {
|
||||
handler.post(() -> future.completeExceptionally(t));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncRunnable<E extends Throwable> {
|
||||
void run() throws E;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AsyncSupplier<T, E extends Throwable> {
|
||||
T get() throws E;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.commonsware.cwac.crossport.design.widget.Snackbar;
|
||||
|
||||
/**
|
||||
* Created by samuel on 12/30/17.
|
||||
*/
|
||||
|
||||
public final class ClipboardUtils {
|
||||
private ClipboardUtils() {
|
||||
}
|
||||
|
||||
public static void copyTextView(final View view) {
|
||||
if (!(view instanceof TextView))
|
||||
return;
|
||||
final CharSequence text = ((TextView) view).getText();
|
||||
if (text == null || text.length() == 0)
|
||||
return;
|
||||
final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (!(service instanceof ClipboardManager))
|
||||
return;
|
||||
final CharSequence description = view.getContentDescription();
|
||||
((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
|
||||
Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java9.util.concurrent.CompletionException;
|
||||
import java9.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Helpers for logging exceptions from asynchronous tasks. These can be passed to
|
||||
* {@code CompletionStage.handle()} at the end of an asynchronous future chain.
|
||||
*/
|
||||
|
||||
public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
|
||||
D(Log.DEBUG),
|
||||
E(Log.ERROR);
|
||||
|
||||
private static final String TAG = ExceptionLoggers.class.getSimpleName();
|
||||
|
||||
private final int priority;
|
||||
|
||||
ExceptionLoggers(final int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public static Throwable unwrap(final Throwable throwable) {
|
||||
if (throwable instanceof CompletionException)
|
||||
return throwable.getCause();
|
||||
return throwable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(final Object result, final 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");
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
package com.wireguard.android.backends;
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application.ApplicationContext;
|
||||
import com.wireguard.android.Application.ApplicationScope;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -11,29 +14,32 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Helper class for running commands as root.
|
||||
*/
|
||||
|
||||
@ApplicationScope
|
||||
public class RootShell {
|
||||
private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");
|
||||
/**
|
||||
* Setup commands that are run at the beginning of each root shell. The trap command ensures
|
||||
* access to the return value of the last command, since su itself always exits with 0.
|
||||
*/
|
||||
private static final String TAG = "WireGuard/RootShell";
|
||||
private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");
|
||||
|
||||
private static final String[][] libraryNamedExecutables = {
|
||||
{ "libwg.so", "wg" },
|
||||
{ "libwg-quick.so", "wg-quick" }
|
||||
{"libwg.so", "wg"},
|
||||
{"libwg-quick.so", "wg-quick"}
|
||||
};
|
||||
|
||||
private final String preamble;
|
||||
|
||||
public RootShell(final Context context) {
|
||||
@Inject
|
||||
public RootShell(@ApplicationContext final Context context) {
|
||||
final String binDir = context.getCacheDir().getPath() + "/bin";
|
||||
final String tmpDir = context.getCacheDir().getPath() + "/tmp";
|
||||
final String libDir = context.getApplicationInfo().nativeLibraryDir;
|
@ -1,4 +1,4 @@
|
||||
package com.wireguard.android;
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
@ -1,10 +1,10 @@
|
||||
package com.wireguard.android;
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
@ -28,8 +28,8 @@ public class NameInputFilter implements InputFilter {
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex < Config.NAME_MAX_LENGTH && isAllowed(c)) &&
|
||||
dLength + (sIndex - sStart) < Config.NAME_MAX_LENGTH) {
|
||||
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
@ -17,17 +17,15 @@
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Switch;
|
||||
|
||||
public class ToggleSwitch extends Switch {
|
||||
private boolean hasPendingStateChange;
|
||||
private boolean isRestoringState;
|
||||
private OnBeforeCheckedChangeListener listener;
|
||||
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
|
||||
}
|
||||
|
||||
public ToggleSwitch(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
@ -45,21 +43,25 @@ public class ToggleSwitch extends Switch {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
|
||||
this.listener = listener;
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
isRestoringState = true;
|
||||
super.onRestoreInstanceState(state);
|
||||
isRestoringState = false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
if (listener != null) {
|
||||
if (!isEnabled())
|
||||
return;
|
||||
setEnabled(false);
|
||||
hasPendingStateChange = true;
|
||||
listener.onBeforeCheckedChanged(this, checked);
|
||||
} else {
|
||||
if (isRestoringState || listener == null) {
|
||||
super.setChecked(checked);
|
||||
return;
|
||||
}
|
||||
if (hasPendingStateChange)
|
||||
return;
|
||||
hasPendingStateChange = true;
|
||||
setEnabled(false);
|
||||
listener.onBeforeCheckedChanged(this, checked);
|
||||
}
|
||||
|
||||
public void setCheckedInternal(final boolean checked) {
|
||||
@ -69,4 +71,12 @@ public class ToggleSwitch extends Switch {
|
||||
}
|
||||
super.setChecked(checked);
|
||||
}
|
||||
|
||||
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
|
||||
}
|
||||
}
|
||||
|
@ -21,38 +21,38 @@ enum Attribute {
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
|
||||
private static final Map<String, Attribute> map;
|
||||
private static final Map<String, Attribute> KEY_MAP;
|
||||
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
|
||||
|
||||
static {
|
||||
map = new HashMap<>(Attribute.values().length);
|
||||
for (final Attribute key : Attribute.values())
|
||||
map.put(key.getToken(), key);
|
||||
KEY_MAP = new HashMap<>(Attribute.values().length);
|
||||
for (final Attribute key : Attribute.values()) {
|
||||
KEY_MAP.put(key.token, key);
|
||||
}
|
||||
}
|
||||
|
||||
public static Attribute match(final String line) {
|
||||
return map.get(line.split("\\s|=")[0]);
|
||||
}
|
||||
|
||||
private final String token;
|
||||
private final Pattern pattern;
|
||||
private final String token;
|
||||
|
||||
Attribute(final String token) {
|
||||
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String composeWith(final String value) {
|
||||
return token + " = " + value + "\n";
|
||||
public static Attribute match(final CharSequence line) {
|
||||
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0]);
|
||||
}
|
||||
|
||||
public String composeWith(final Object value) {
|
||||
return String.format("%s = %s%n", token, value);
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String parseFrom(final String line) {
|
||||
public String parse(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
if (matcher.matches())
|
||||
return matcher.group(1);
|
||||
return null;
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,23 @@
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.Observable;
|
||||
import android.databinding.ObservableArrayList;
|
||||
import android.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents a wg-quick configuration file, its name, and its connection state.
|
||||
*/
|
||||
|
||||
public class Config extends BaseObservable
|
||||
implements Comparable<Config>, Copyable<Config>, Observable, Parcelable {
|
||||
public static final Parcelable.Creator<Config> CREATOR = new Parcelable.Creator<Config>() {
|
||||
public class Config extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Config> CREATOR = new Creator<Config>() {
|
||||
@Override
|
||||
public Config createFromParcel(final Parcel in) {
|
||||
return new Config(in);
|
||||
@ -37,104 +28,22 @@ public class Config extends BaseObservable
|
||||
return new Config[size];
|
||||
}
|
||||
};
|
||||
public static final int NAME_MAX_LENGTH = 16;
|
||||
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,16}$");
|
||||
|
||||
public static boolean isNameValid(final String name) {
|
||||
return name.length() <= NAME_MAX_LENGTH && PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
private final Interface iface;
|
||||
private boolean isEnabled;
|
||||
private boolean isPrimary;
|
||||
private String name;
|
||||
private final Interface interfaceSection;
|
||||
private final ObservableList<Peer> peers = new ObservableArrayList<>();
|
||||
|
||||
public Config() {
|
||||
iface = new Interface();
|
||||
interfaceSection = new Interface();
|
||||
}
|
||||
|
||||
protected Config(final Parcel in) {
|
||||
iface = in.readParcelable(Interface.class.getClassLoader());
|
||||
name = in.readString();
|
||||
// The flattened peers must be recreated to associate them with this config.
|
||||
final List<Peer> flattenedPeers = new LinkedList<>();
|
||||
in.readTypedList(flattenedPeers, Peer.CREATOR);
|
||||
for (final Peer peer : flattenedPeers)
|
||||
addPeer(peer);
|
||||
private Config(final Parcel in) {
|
||||
interfaceSection = in.readParcelable(Interface.class.getClassLoader());
|
||||
in.readTypedList(peers, Peer.CREATOR);
|
||||
}
|
||||
|
||||
public Peer addPeer() {
|
||||
final Peer peer = new Peer(this);
|
||||
peers.add(peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
private Peer addPeer(final Peer peer) {
|
||||
final Peer copy = peer.copy(this);
|
||||
peers.add(copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull final Config config) {
|
||||
return getName().compareTo(config.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config copy() {
|
||||
final Config copy = new Config();
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyFrom(final Config source) {
|
||||
if (source != null) {
|
||||
iface.copyFrom(source.iface);
|
||||
name = source.name;
|
||||
peers.clear();
|
||||
for (final Peer peer : source.peers)
|
||||
addPeer(peer);
|
||||
} else {
|
||||
iface.copyFrom(null);
|
||||
name = null;
|
||||
peers.clear();
|
||||
}
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Interface getInterface() {
|
||||
return iface;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public ObservableList<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isPrimary() {
|
||||
return isPrimary;
|
||||
}
|
||||
|
||||
public void parseFrom(final InputStream stream)
|
||||
public static Config from(final InputStream stream)
|
||||
throws IOException {
|
||||
peers.clear();
|
||||
final Config config = new Config();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||
Peer currentPeer = null;
|
||||
@ -147,10 +56,11 @@ public class Config extends BaseObservable
|
||||
currentPeer = null;
|
||||
inInterfaceSection = true;
|
||||
} else if ("[Peer]".equals(line)) {
|
||||
currentPeer = addPeer();
|
||||
currentPeer = new Peer();
|
||||
config.peers.add(currentPeer);
|
||||
inInterfaceSection = false;
|
||||
} else if (inInterfaceSection) {
|
||||
iface.parse(line);
|
||||
config.interfaceSection.parse(line);
|
||||
} else if (currentPeer != null) {
|
||||
currentPeer.parse(line);
|
||||
} else {
|
||||
@ -161,28 +71,25 @@ public class Config extends BaseObservable
|
||||
throw new IllegalArgumentException("Could not find any config information");
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setIsEnabled(final boolean isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
notifyPropertyChanged(BR.enabled);
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setIsPrimary(final boolean isPrimary) {
|
||||
this.isPrimary = isPrimary;
|
||||
notifyPropertyChanged(BR.primary);
|
||||
public Interface getInterface() {
|
||||
return interfaceSection;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
if (name != null && !name.isEmpty() && !isNameValid(name))
|
||||
throw new IllegalArgumentException();
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
public ObservableList<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append(iface);
|
||||
final StringBuilder sb = new StringBuilder().append(interfaceSection);
|
||||
for (final Peer peer : peers)
|
||||
sb.append('\n').append(peer);
|
||||
return sb.toString();
|
||||
@ -190,8 +97,7 @@ public class Config extends BaseObservable
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeParcelable(iface, flags);
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(interfaceSection, flags);
|
||||
dest.writeTypedList(peers);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package com.wireguard.config;
|
||||
|
||||
/**
|
||||
* Interface for classes that can perform a deep copy of their objects.
|
||||
*/
|
||||
|
||||
public interface Copyable<T> {
|
||||
T copy();
|
||||
void copyFrom(T source);
|
||||
}
|
@ -2,7 +2,6 @@ package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.Observable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
@ -14,10 +13,8 @@ import com.wireguard.crypto.Keypair;
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block).
|
||||
*/
|
||||
|
||||
public class Interface extends BaseObservable
|
||||
implements Copyable<Interface>, Observable, Parcelable {
|
||||
public static final Parcelable.Creator<Interface> CREATOR
|
||||
= new Parcelable.Creator<Interface>() {
|
||||
public class Interface extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Interface> CREATOR = new Creator<Interface>() {
|
||||
@Override
|
||||
public Interface createFromParcel(final Parcel in) {
|
||||
return new Interface(in);
|
||||
@ -31,8 +28,8 @@ public class Interface extends BaseObservable
|
||||
|
||||
private String address;
|
||||
private String dns;
|
||||
private String listenPort;
|
||||
private Keypair keypair;
|
||||
private String listenPort;
|
||||
private String mtu;
|
||||
private String privateKey;
|
||||
|
||||
@ -40,7 +37,7 @@ public class Interface extends BaseObservable
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
protected Interface(final Parcel in) {
|
||||
private Interface(final Parcel in) {
|
||||
address = in.readString();
|
||||
dns = in.readString();
|
||||
listenPort = in.readString();
|
||||
@ -48,31 +45,6 @@ public class Interface extends BaseObservable
|
||||
setPrivateKey(in.readString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interface copy() {
|
||||
final Interface copy = new Interface();
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyFrom(final Interface source) {
|
||||
if (source != null) {
|
||||
address = source.address;
|
||||
dns = source.dns;
|
||||
listenPort = source.listenPort;
|
||||
mtu = source.mtu;
|
||||
setPrivateKey(source.privateKey);
|
||||
} else {
|
||||
address = null;
|
||||
dns = null;
|
||||
listenPort = null;
|
||||
mtu = null;
|
||||
setPrivateKey(null);
|
||||
}
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
@ -118,15 +90,15 @@ public class Interface extends BaseObservable
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == Attribute.ADDRESS)
|
||||
setAddress(key.parseFrom(line));
|
||||
setAddress(key.parse(line));
|
||||
else if (key == Attribute.DNS)
|
||||
setDns(key.parseFrom(line));
|
||||
setDns(key.parse(line));
|
||||
else if (key == Attribute.LISTEN_PORT)
|
||||
setListenPort(key.parseFrom(line));
|
||||
setListenPort(key.parse(line));
|
||||
else if (key == Attribute.MTU)
|
||||
setMtu(key.parseFrom(line));
|
||||
setMtu(key.parse(line));
|
||||
else if (key == Attribute.PRIVATE_KEY)
|
||||
setPrivateKey(key.parseFrom(line));
|
||||
setPrivateKey(key.parse(line));
|
||||
else
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package com.wireguard.config;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.Observable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
@ -12,8 +11,8 @@ import com.android.databinding.library.baseAdapters.BR;
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block).
|
||||
*/
|
||||
|
||||
public class Peer extends BaseObservable implements Copyable<Peer>, Observable, Parcelable {
|
||||
public static final Parcelable.Creator<Peer> CREATOR = new Parcelable.Creator<Peer>() {
|
||||
public class Peer extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Peer> CREATOR = new Creator<Peer>() {
|
||||
@Override
|
||||
public Peer createFromParcel(final Parcel in) {
|
||||
return new Peer(in);
|
||||
@ -26,44 +25,25 @@ public class Peer extends BaseObservable implements Copyable<Peer>, Observable,
|
||||
};
|
||||
|
||||
private String allowedIPs;
|
||||
private final Config config;
|
||||
private String endpoint;
|
||||
private String persistentKeepalive;
|
||||
private String preSharedKey;
|
||||
private String publicKey;
|
||||
|
||||
public Peer(final Config config) {
|
||||
this.config = config;
|
||||
public Peer() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
protected Peer(final Parcel in) {
|
||||
private Peer(final Parcel in) {
|
||||
allowedIPs = in.readString();
|
||||
config = null;
|
||||
endpoint = in.readString();
|
||||
persistentKeepalive = in.readString();
|
||||
preSharedKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Peer copy() {
|
||||
return copy(config);
|
||||
}
|
||||
|
||||
public Peer copy(final Config config) {
|
||||
final Peer copy = new Peer(config);
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyFrom(final Peer source) {
|
||||
allowedIPs = source.allowedIPs;
|
||||
endpoint = source.endpoint;
|
||||
persistentKeepalive = source.persistentKeepalive;
|
||||
preSharedKey = source.preSharedKey;
|
||||
publicKey = source.publicKey;
|
||||
notifyChange();
|
||||
public static Peer newInstance() {
|
||||
return new Peer();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -99,24 +79,19 @@ public class Peer extends BaseObservable implements Copyable<Peer>, Observable,
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == Attribute.ALLOWED_IPS)
|
||||
setAllowedIPs(key.parseFrom(line));
|
||||
setAllowedIPs(key.parse(line));
|
||||
else if (key == Attribute.ENDPOINT)
|
||||
setEndpoint(key.parseFrom(line));
|
||||
setEndpoint(key.parse(line));
|
||||
else if (key == Attribute.PERSISTENT_KEEPALIVE)
|
||||
setPersistentKeepalive(key.parseFrom(line));
|
||||
setPersistentKeepalive(key.parse(line));
|
||||
else if (key == Attribute.PRESHARED_KEY)
|
||||
setPreSharedKey(key.parseFrom(line));
|
||||
setPreSharedKey(key.parse(line));
|
||||
else if (key == Attribute.PUBLIC_KEY)
|
||||
setPublicKey(key.parseFrom(line));
|
||||
setPublicKey(key.parse(line));
|
||||
else
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
|
||||
public void removeSelf() {
|
||||
if (!config.getPeers().remove(this))
|
||||
throw new IllegalStateException("This peer was already removed from its config");
|
||||
}
|
||||
|
||||
public void setAllowedIPs(String allowedIPs) {
|
||||
if (allowedIPs != null && allowedIPs.isEmpty())
|
||||
allowedIPs = null;
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/master_fragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/detail_fragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2"
|
||||
tools:ignore="InconsistentLayout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/placeholder_text" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/master_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="MergeRootFrame" />
|
@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.backends.VpnService" />
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:elevation="2dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_label"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_toStartOf="@+id/config_switch"
|
||||
android:text="@string/status" />
|
||||
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/config_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/status_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:checked="@{config.enabled}"
|
||||
app:onBeforeCheckedChanged="@{(v, checked) -> checked ? VpnService.instance.enable(config.name) : VpnService.instance.disable(config.name)}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/status_label"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{config.interface.publicKey}" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/config_detail_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
@ -1,219 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.ConfigEditFragment" />
|
||||
|
||||
<import type="com.wireguard.android.KeyInputFilter" />
|
||||
|
||||
<import type="com.wireguard.android.NameInputFilter" />
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:elevation="2dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/iface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_title"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/interface_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_label"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={config.name}"
|
||||
app:filter="@{NameInputFilter.newInstance()}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/private_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_text"
|
||||
android:labelFor="@+id/private_key_text"
|
||||
android:text="@string/private_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/private_key_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:layout_toStartOf="@+id/generate_private_key_button"
|
||||
android:inputType="textVisiblePassword"
|
||||
android:text="@={config.interface.privateKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/generate_private_key_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/private_key_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:onClick="@{() -> config.interface.generateKeypair()}"
|
||||
android:text="@string/generate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/private_key_text"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/editTextStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:ellipsize="end"
|
||||
android:focusable="false"
|
||||
android:hint="@string/hint_generated"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{(view) -> ConfigEditFragment.copyPublicKey(view.getContext(), config.interface.publicKey)}"
|
||||
android:text="@{config.interface.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_toStartOf="@+id/listen_port_label"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/addresses_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_label"
|
||||
android:layout_toStartOf="@+id/listen_port_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={config.interface.address}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/listen_port_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_random"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.listenPort}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_text"
|
||||
android:layout_toStartOf="@+id/mtu_label"
|
||||
android:labelFor="@+id/dns_servers_text"
|
||||
android:text="@string/dns_servers" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dns_servers_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/dns_servers_label"
|
||||
android:layout_toStartOf="@+id/mtu_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={config.interface.dns}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/mtu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_automatic"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.mtu}"
|
||||
android:textAlignment="center" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/config_edit_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
|
||||
<Button
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:onClick="@{() -> config.addPeer()}"
|
||||
android:text="@string/add_peer" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
233
app/src/main/res/layout/config_editor_fragment.xml
Normal file
233
app/src/main/res/layout/config_editor_fragment.xml
Normal file
@ -0,0 +1,233 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.util.ClipboardUtils" />
|
||||
|
||||
<import type="com.wireguard.android.widget.KeyInputFilter" />
|
||||
|
||||
<import type="com.wireguard.android.widget.NameInputFilter" />
|
||||
|
||||
<import type="com.wireguard.config.Peer" />
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config" />
|
||||
|
||||
<variable
|
||||
name="name"
|
||||
type="android.databinding.ObservableField<String>" />
|
||||
</data>
|
||||
|
||||
<com.commonsware.cwac.crossport.design.widget.CoordinatorLayout
|
||||
android:id="@+id/main_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:elevation="2dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/iface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/interface_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_label"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={name}"
|
||||
app:filter="@{NameInputFilter.newInstance()}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/private_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_text"
|
||||
android:labelFor="@+id/private_key_text"
|
||||
android:text="@string/private_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/private_key_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:layout_toStartOf="@+id/generate_private_key_button"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:inputType="textVisiblePassword"
|
||||
android:text="@={config.interface.privateKey}"
|
||||
app:filter="@{KeyInputFilter.newInstance()}" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/generate_private_key_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/private_key_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:onClick="@{() -> config.interface.generateKeypair()}"
|
||||
android:text="@string/generate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/private_key_text"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/editTextStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:ellipsize="end"
|
||||
android:focusable="false"
|
||||
android:hint="@string/hint_generated"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addresses_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_toStartOf="@+id/listen_port_label"
|
||||
android:labelFor="@+id/addresses_text"
|
||||
android:text="@string/addresses" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/addresses_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_label"
|
||||
android:layout_toStartOf="@+id/listen_port_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={config.interface.address}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/listen_port_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/addresses_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_random"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.listenPort}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_servers_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/addresses_text"
|
||||
android:layout_toStartOf="@+id/mtu_label"
|
||||
android:labelFor="@+id/dns_servers_text"
|
||||
android:text="@string/dns_servers" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dns_servers_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/dns_servers_label"
|
||||
android:layout_toStartOf="@+id/mtu_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text="@={config.interface.dns}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/mtu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_servers_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:hint="@string/hint_automatic"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.mtu}"
|
||||
android:textAlignment="center" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{config.peers}"
|
||||
app:layout="@{@layout/config_editor_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
|
||||
<Button
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:onClick="@{() -> config.peers.add(Peer.newInstance())}"
|
||||
android:text="@string/add_peer" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</com.commonsware.cwac.crossport.design.widget.CoordinatorLayout>
|
||||
</layout>
|
@ -4,7 +4,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.KeyInputFilter" />
|
||||
<import type="com.wireguard.android.widget.KeyInputFilter" />
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="android.databinding.ObservableList<com.wireguard.config.Peer>" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
@ -41,7 +45,7 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/delete"
|
||||
android:onClick="@{() -> item.removeSelf()}"
|
||||
android:onClick="@{() -> collection.remove(item)}"
|
||||
android:src="@drawable/ic_action_delete_black" />
|
||||
|
||||
<TextView
|
@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<variable
|
||||
name="configs"
|
||||
type="com.wireguard.android.databinding.ObservableSortedMap<String, com.wireguard.config.Config>" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/config_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="singleChoice"
|
||||
app:items="@{configs}"
|
||||
app:layout="@{@layout/config_list_item}" />
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionsMenu
|
||||
android:id="@+id/add_menu"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:fab_labelStyle="@style/fab_label"
|
||||
app:fab_labelsPosition="left">
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/add_from_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_icon="@drawable/ic_action_open"
|
||||
app:fab_size="mini"
|
||||
app:fab_title="@string/add_from_file" />
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/add_from_scratch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_icon="@drawable/ic_action_edit"
|
||||
app:fab_size="mini"
|
||||
app:fab_title="@string/add_from_scratch" />
|
||||
</com.getbase.floatingactionbutton.FloatingActionsMenu>
|
||||
|
||||
</RelativeLayout>
|
||||
</layout>
|
@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/master_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="MergeRootFrame" />
|
||||
android:layout_height="match_parent" />
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/not_supported_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="32dp"
|
||||
android:textAppearance="@android:style/TextAppearance.Material.Subhead" />
|
||||
</ScrollView>
|
||||
</layout>
|
142
app/src/main/res/layout/tunnel_detail_fragment.xml
Normal file
142
app/src/main/res/layout/tunnel_detail_fragment.xml
Normal file
@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.model.Tunnel.State" />
|
||||
|
||||
<import type="com.wireguard.android.util.ClipboardUtils" />
|
||||
|
||||
<variable
|
||||
name="tunnel"
|
||||
type="com.wireguard.android.model.Tunnel" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:elevation="2dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_title"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:text="@string/iface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/interface_name_text"
|
||||
android:text="@string/name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interface_name_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_label"
|
||||
android:text="@{tunnel.name}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/interface_name_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/status_text"
|
||||
android:text="@string/status" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/status_label"
|
||||
android:layout_toStartOf="@+id/tunnel_switch"
|
||||
android:text="@{tunnel.state.name}" />
|
||||
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/tunnel_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/status_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:enabled="@{tunnel.state != State.UNKNOWN}"
|
||||
app:checked="@{tunnel.state == State.UP}"
|
||||
app:onBeforeCheckedChanged="@{() -> tunnel.setState(State.TOGGLE)}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_change_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/status_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/last_change_text"
|
||||
android:text="@string/last_change" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_change_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/last_change_label"
|
||||
android:text="@{tunnel.lastStateChange}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/last_change_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:contentDescription="@string/public_key_description"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{tunnel.config.interface.publicKey}" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:divider="@null"
|
||||
android:orientation="vertical"
|
||||
app:items="@{tunnel.config.peers}"
|
||||
app:layout="@{@layout/tunnel_detail_peer}"
|
||||
tools:ignore="UselessLeaf" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
@ -3,6 +3,10 @@
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="collection"
|
||||
type="android.databinding.ObservableList<com.wireguard.config.Peer>" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.wireguard.config.Peer" />
|
||||
@ -25,7 +29,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/peer" />
|
||||
|
||||
<TextView
|
||||
@ -33,6 +36,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/peer_title"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
@ -51,6 +55,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/allowed_ips_text"
|
||||
android:text="@string/allowed_ips" />
|
||||
|
||||
@ -67,6 +72,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/allowed_ips_text"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/endpoint_text"
|
||||
android:text="@string/endpoint" />
|
||||
|
59
app/src/main/res/layout/tunnel_list_fragment.xml
Normal file
59
app/src/main/res/layout/tunnel_list_fragment.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.TunnelListFragment" />
|
||||
|
||||
<variable
|
||||
name="tunnels"
|
||||
type="com.wireguard.android.model.TunnelCollection" />
|
||||
</data>
|
||||
|
||||
<com.commonsware.cwac.crossport.design.widget.CoordinatorLayout
|
||||
android:id="@+id/main_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/tunnel_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="multipleChoiceModal"
|
||||
app:items="@{tunnels}"
|
||||
app:layout="@{@layout/tunnel_list_item}" />
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionsMenu
|
||||
android:id="@+id/create_menu"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="8dp"
|
||||
app:fab_labelStyle="@style/fab_label"
|
||||
app:fab_labelsPosition="left"
|
||||
app:layout_dodgeInsetEdges="bottom">
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestCreateConfig}"
|
||||
app:fab_icon="@drawable/ic_action_edit"
|
||||
app:fab_size="mini"
|
||||
app:fab_title="@string/create_empty" />
|
||||
|
||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_from_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestImportConfig}"
|
||||
app:fab_icon="@drawable/ic_action_open"
|
||||
app:fab_size="mini"
|
||||
app:fab_title="@string/create_from_file" />
|
||||
</com.getbase.floatingactionbutton.FloatingActionsMenu>
|
||||
</com.commonsware.cwac.crossport.design.widget.CoordinatorLayout>
|
||||
</layout>
|
@ -4,9 +4,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.graphics.Typeface" />
|
||||
<import type="com.wireguard.android.model.Tunnel.State" />
|
||||
|
||||
<import type="com.wireguard.android.backends.VpnService" />
|
||||
<variable
|
||||
name="collection"
|
||||
type="com.wireguard.android.model.TunnelCollection" />
|
||||
|
||||
<variable
|
||||
name="key"
|
||||
@ -14,7 +16,7 @@
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.wireguard.config.Config" />
|
||||
type="com.wireguard.android.model.Tunnel" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
@ -25,24 +27,25 @@
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/config_name"
|
||||
android:id="@+id/tunnel_name"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@+id/config_switch"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toStartOf="@+id/tunnel_switch"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{key}"
|
||||
android:textStyle="@{item.primary ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT}" />
|
||||
android:text="@{key}" />
|
||||
|
||||
<com.wireguard.android.widget.ToggleSwitch
|
||||
android:id="@+id/config_switch"
|
||||
android:id="@+id/tunnel_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/config_name"
|
||||
android:layout_alignBaseline="@+id/tunnel_name"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:checked="@{item.enabled}"
|
||||
app:onBeforeCheckedChanged="@{(v, checked) -> checked ? VpnService.instance.enable(item.name) : VpnService.instance.disable(item.name)}" />
|
||||
android:enabled="@{item.state != State.UNKNOWN}"
|
||||
app:checked="@{item.state == State.UP}"
|
||||
app:onBeforeCheckedChanged="@{() -> item.setState(State.TOGGLE)}" />
|
||||
</RelativeLayout>
|
||||
</layout>
|
@ -5,8 +5,8 @@
|
||||
<item quantity="other">%d configurations selected</item>
|
||||
</plurals>
|
||||
<string name="add_activity_title">New WireGuard configuration</string>
|
||||
<string name="add_from_file">Add from file</string>
|
||||
<string name="add_from_scratch">Add from scratch</string>
|
||||
<string name="create_from_file">Add from file</string>
|
||||
<string name="create_empty">Add from scratch</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
@ -57,7 +57,7 @@
|
||||
<string name="private_key">Private key</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="public_key_copied_message">Public key copied to clipboard</string>
|
||||
<string name="public_key_description">WireGuard public key</string>
|
||||
<string name="public_key_description">Public key</string>
|
||||
<string name="restore_on_boot">Restore on boot</string>
|
||||
<string name="restore_on_boot_summary">Restore previously enabled configurations on boot</string>
|
||||
<string name="install_cmd_line_tools">Install command line tools</string>
|
||||
@ -70,4 +70,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="status">Status</string>
|
||||
<string name="toggle">Toggle</string>
|
||||
<string name="last_change">Last change</string>
|
||||
<string name="never">never</string>
|
||||
</resources>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<com.wireguard.android.ConfigListPreference
|
||||
<com.wireguard.android.preference.TunnelListPreference
|
||||
android:key="primary_config"
|
||||
android:summary="@string/primary_config_summary"
|
||||
android:title="@string/primary_config" />
|
||||
|
Loading…
Reference in New Issue
Block a user