summaryrefslogtreecommitdiffhomepage
path: root/app/src/main/java/com/wireguard/android
diff options
context:
space:
mode:
authorSamuel Holland <samuel@sholland.org>2017-08-13 07:24:03 -0500
committerSamuel Holland <samuel@sholland.org>2017-08-13 07:24:03 -0500
commit5e55d196be092f4a4dcb212cf09d7a1bdab70e00 (patch)
treeeb765a1b961fefdaa7ddc3cfae9cb83a09e0c031 /app/src/main/java/com/wireguard/android
parentc72d30a1af8114ef506a137e3e7274ac33d82bd1 (diff)
Major renaming and refactoring in activity and service
Apparently "configuration" is the proper term, not "profile". Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src/main/java/com/wireguard/android')
-rw-r--r--app/src/main/java/com/wireguard/android/BaseConfigActivity.java85
-rw-r--r--app/src/main/java/com/wireguard/android/BaseConfigFragment.java47
-rw-r--r--app/src/main/java/com/wireguard/android/BindingAdapters.java15
-rw-r--r--app/src/main/java/com/wireguard/android/BootCompletedReceiver.java5
-rw-r--r--app/src/main/java/com/wireguard/android/ConfigActivity.java158
-rw-r--r--app/src/main/java/com/wireguard/android/ConfigDetailFragment.java44
-rw-r--r--app/src/main/java/com/wireguard/android/ConfigEditFragment.java74
-rw-r--r--app/src/main/java/com/wireguard/android/ConfigListFragment.java61
-rw-r--r--app/src/main/java/com/wireguard/android/ObservableArrayMapAdapter.java30
-rw-r--r--app/src/main/java/com/wireguard/android/ObservableListAdapter.java32
-rw-r--r--app/src/main/java/com/wireguard/android/PlaceholderFragment.java3
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileActivity.java69
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileDetailActivity.java39
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileDetailFragment.java43
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileEditActivity.java36
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileEditFragment.java59
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileFragment.java61
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileListActivity.java124
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileListFragment.java61
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileService.java290
-rw-r--r--app/src/main/java/com/wireguard/android/ProfileServiceInterface.java70
-rw-r--r--app/src/main/java/com/wireguard/android/RootShell.java10
-rw-r--r--app/src/main/java/com/wireguard/android/ServiceClientActivity.java75
-rw-r--r--app/src/main/java/com/wireguard/android/ServiceClientFragment.java64
-rw-r--r--app/src/main/java/com/wireguard/android/ServiceConnectionListener.java11
-rw-r--r--app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java13
-rw-r--r--app/src/main/java/com/wireguard/android/SettingsActivity.java5
-rw-r--r--app/src/main/java/com/wireguard/android/VpnService.java339
28 files changed, 864 insertions, 1059 deletions
diff --git a/app/src/main/java/com/wireguard/android/BaseConfigActivity.java b/app/src/main/java/com/wireguard/android/BaseConfigActivity.java
new file mode 100644
index 00000000..8359c34a
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/BaseConfigActivity.java
@@ -0,0 +1,85 @@
+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 android.view.Menu;
+
+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 TAG_DETAIL = "detail";
+ protected static final String TAG_EDIT = "edit";
+ protected static final String TAG_LIST = "list";
+ protected static final String TAG_PLACEHOLDER = "placeholder";
+
+ private final ServiceConnection callbacks = new ServiceConnectionCallbacks();
+ private Config currentConfig;
+ private String initialConfig;
+
+ protected Config getCurrentConfig() {
+ return currentConfig;
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Trigger starting the service as early as possible
+ bindService(new Intent(this, VpnService.class), callbacks, Context.BIND_AUTO_CREATE);
+ // Restore the saved configuration if there is one; otherwise grab it from the intent.
+ if (savedInstanceState != null)
+ initialConfig = savedInstanceState.getString(KEY_CURRENT_CONFIG);
+ else
+ initialConfig = getIntent().getStringExtra(KEY_CURRENT_CONFIG);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ protected abstract void onCurrentConfigChanged(Config config);
+
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (currentConfig != null)
+ outState.putString(KEY_CURRENT_CONFIG, currentConfig.getName());
+ }
+
+ protected abstract void onServiceAvailable();
+
+ public void setCurrentConfig(final Config config) {
+ currentConfig = config;
+ onCurrentConfigChanged(currentConfig);
+ }
+
+ 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(callbacks);
+ // Tell the subclass that it is now safe to use the service.
+ onServiceAvailable();
+ // Make sure the subclass activity is initialized before setting its config.
+ if (initialConfig != null && currentConfig == null)
+ setCurrentConfig(VpnService.getInstance().get(initialConfig));
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName component) {
+ // This can never happen; the service runs in the same thread as the activity.
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/BaseConfigFragment.java b/app/src/main/java/com/wireguard/android/BaseConfigFragment.java
new file mode 100644
index 00000000..4a754b63
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/BaseConfigFragment.java
@@ -0,0 +1,47 @@
+package com.wireguard.android;
+
+import android.app.Fragment;
+import android.os.Bundle;
+
+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) {
+ currentConfig = config;
+ onCurrentConfigChanged(currentConfig);
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/BindingAdapters.java b/app/src/main/java/com/wireguard/android/BindingAdapters.java
index 77c6f657..6cd4a70f 100644
--- a/app/src/main/java/com/wireguard/android/BindingAdapters.java
+++ b/app/src/main/java/com/wireguard/android/BindingAdapters.java
@@ -9,11 +9,14 @@ import android.widget.ListView;
* Static methods for use by generated code in the Android data binding library.
*/
+@SuppressWarnings("unused")
public final class BindingAdapters {
@BindingAdapter({"items", "layout"})
- public static <K, V> void arrayMapBinding(ListView view, ObservableArrayMap<K, V> oldMap,
- int oldLayoutId, ObservableArrayMap<K, V> newMap,
- int newLayoutId) {
+ public static <K, V> void arrayMapBinding(final ListView view,
+ final ObservableArrayMap<K, V> oldMap,
+ final int oldLayoutId,
+ final ObservableArrayMap<K, V> newMap,
+ final int newLayoutId) {
// Remove any existing binding when there is no new map.
if (newMap == null) {
view.setAdapter(null);
@@ -37,8 +40,9 @@ public final class BindingAdapters {
}
@BindingAdapter({"items", "layout"})
- public static <T> void listBinding(ListView view, ObservableList<T> oldList, int oldLayoutId,
- ObservableList<T> newList, int newLayoutId) {
+ public static <T> void listBinding(final ListView view,
+ final ObservableList<T> oldList, final int oldLayoutId,
+ final ObservableList<T> newList, final int newLayoutId) {
// Remove any existing binding when there is no new list.
if (newList == null) {
view.setAdapter(null);
@@ -61,5 +65,6 @@ public final class BindingAdapters {
}
private BindingAdapters() {
+ // Prevent instantiation.
}
}
diff --git a/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java b/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
index 64887369..68cb5f1f 100644
--- a/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
+++ b/app/src/main/java/com/wireguard/android/BootCompletedReceiver.java
@@ -7,10 +7,9 @@ import android.content.Intent;
public class BootCompletedReceiver extends BroadcastReceiver {
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(final Context context, final Intent intent) {
if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED))
return;
- Intent startServiceIntent = new Intent(context, ProfileService.class);
- context.startService(startServiceIntent);
+ context.startService(new Intent(context, VpnService.class));
}
}
diff --git a/app/src/main/java/com/wireguard/android/ConfigActivity.java b/app/src/main/java/com/wireguard/android/ConfigActivity.java
new file mode 100644
index 00000000..f672c594
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/ConfigActivity.java
@@ -0,0 +1,158 @@
+package com.wireguard.android;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+
+import com.wireguard.config.Config;
+
+/**
+ * Activity that allows creating/viewing/editing/deleting WireGuard configurations.
+ */
+
+public class ConfigActivity extends BaseConfigActivity {
+ private boolean canAddFragments;
+ private int containerId;
+ private final FragmentManager fm = getFragmentManager();
+ private boolean isEditing;
+ private boolean isSplitLayout;
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ // Make sure the current config is cleared when going back to the list.
+ if (isEditing)
+ isEditing = false;
+ else
+ setCurrentConfig(null);
+ }
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.config_activity);
+ isSplitLayout = findViewById(R.id.detail_fragment) != null;
+ if (isSplitLayout)
+ containerId = R.id.detail_fragment;
+ else
+ containerId = R.id.master_fragment;
+ }
+
+ @Override
+ protected void onCurrentConfigChanged(final Config config) {
+ if (!canAddFragments)
+ return;
+ final Fragment currentFragment = fm.findFragmentById(containerId);
+ Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" +
+ (config != null ? config.getName() : null) + " fragment=" + currentFragment);
+ if (currentFragment instanceof ConfigDetailFragment) {
+ // Handle the case when the split layout is switching from one config to another.
+ final ConfigDetailFragment detailFragment = (ConfigDetailFragment) currentFragment;
+ if (detailFragment.getCurrentConfig() != config)
+ detailFragment.setCurrentConfig(config);
+ } else if (currentFragment instanceof ConfigEditFragment) {
+ // Handle the case when ConfigEditFragment is finished updating a config.
+ fm.popBackStack();
+ isEditing = false;
+ final ConfigDetailFragment detailFragment =
+ (ConfigDetailFragment) fm.findFragmentByTag(TAG_DETAIL);
+ if (detailFragment.getCurrentConfig() != config)
+ detailFragment.setCurrentConfig(config);
+ } else if (config != null) {
+ // Handle the single-fragment-layout case and the case when a placeholder is replaced.
+ ConfigDetailFragment detailFragment =
+ (ConfigDetailFragment) fm.findFragmentByTag(TAG_DETAIL);
+ if (detailFragment != null) {
+ detailFragment.setCurrentConfig(config);
+ } else {
+ detailFragment = new ConfigDetailFragment();
+ final Bundle arguments = new Bundle();
+ arguments.putString(KEY_CURRENT_CONFIG, config.getName());
+ detailFragment.setArguments(arguments);
+ }
+ final FragmentTransaction transaction = fm.beginTransaction();
+ if (!isSplitLayout)
+ transaction.addToBackStack(TAG_DETAIL);
+ transaction.replace(containerId, detailFragment, TAG_DETAIL);
+ transaction.commit();
+ } else {
+ if (isSplitLayout) {
+ // Handle the split layout case when there is no config, so a placeholder is shown.
+ PlaceholderFragment placeholderFragment =
+ (PlaceholderFragment) fm.findFragmentByTag(TAG_PLACEHOLDER);
+ if (placeholderFragment == null)
+ placeholderFragment = new PlaceholderFragment();
+ final FragmentTransaction transaction = fm.beginTransaction();
+ transaction.replace(containerId, placeholderFragment, TAG_PLACEHOLDER);
+ transaction.commit();
+ }
+ }
+ // If the config change came from the intent or ConfigEditFragment, forward it to the list.
+ ConfigListFragment listFragment = (ConfigListFragment) fm.findFragmentByTag(TAG_LIST);
+ if (listFragment == null) {
+ listFragment = new ConfigListFragment();
+ final FragmentTransaction transaction = fm.beginTransaction();
+ transaction.replace(R.id.master_fragment, listFragment, TAG_LIST);
+ transaction.commit();
+ }
+ if (listFragment.getCurrentConfig() != config)
+ listFragment.setCurrentConfig(config);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_action_edit:
+ ConfigEditFragment editFragment =
+ (ConfigEditFragment) fm.findFragmentByTag(TAG_EDIT);
+ if (editFragment != null) {
+ editFragment.setCurrentConfig(getCurrentConfig());
+ } else {
+ editFragment = new ConfigEditFragment();
+ final Bundle arguments = new Bundle();
+ arguments.putString(KEY_CURRENT_CONFIG, getCurrentConfig().getName());
+ editFragment.setArguments(arguments);
+ }
+ final FragmentTransaction transaction = fm.beginTransaction();
+ transaction.addToBackStack(TAG_EDIT);
+ transaction.replace(containerId, editFragment, TAG_EDIT);
+ transaction.commit();
+ isEditing = true;
+ return true;
+ case R.id.menu_action_save:
+ // This menu item is handled by the current fragment.
+ return false;
+ case R.id.menu_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ // We cannot save fragments that might switch between containers if the layout changes.
+ if (fm.getBackStackEntryCount() > 0) {
+ final int bottomEntryId = fm.getBackStackEntryAt(0).getId();
+ fm.popBackStackImmediate(bottomEntryId, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ if (isSplitLayout) {
+ final Fragment oldFragment = fm.findFragmentById(containerId);
+ if (oldFragment != null)
+ fm.beginTransaction().remove(oldFragment).commit();
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onServiceAvailable() {
+ // Create the initial fragment set.
+ canAddFragments = true;
+ onCurrentConfigChanged(getCurrentConfig());
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/ConfigDetailFragment.java b/app/src/main/java/com/wireguard/android/ConfigDetailFragment.java
new file mode 100644
index 00000000..f7f6e6e2
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/ConfigDetailFragment.java
@@ -0,0 +1,44 @@
+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
+ protected void onCurrentConfigChanged(final Config config) {
+ if (binding != null)
+ binding.setConfig(config);
+ }
+
+ @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();
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/ConfigEditFragment.java b/app/src/main/java/com/wireguard/android/ConfigEditFragment.java
new file mode 100644
index 00000000..395358f6
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/ConfigEditFragment.java
@@ -0,0 +1,74 @@
+package com.wireguard.android;
+
+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 com.wireguard.android.databinding.ConfigEditFragmentBinding;
+import com.wireguard.config.Config;
+
+/**
+ * Fragment for editing a WireGuard configuration.
+ */
+
+public class ConfigEditFragment extends BaseConfigFragment {
+ private final Config localConfig = new Config();
+
+ @Override
+ protected void onCurrentConfigChanged(final Config config) {
+ localConfig.copyFrom(config);
+ }
+
+ @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_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 boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_action_save:
+ saveConfig();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void saveConfig() {
+ // FIXME: validate input
+ VpnService.getInstance().update(getCurrentConfig().getName(), localConfig);
+ // Hide the keyboard; it rarely goes away on its own.
+ final BaseConfigActivity activity = (BaseConfigActivity) 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 go back to the detail view.
+ activity.setCurrentConfig(localConfig);
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/ConfigListFragment.java b/app/src/main/java/com/wireguard/android/ConfigListFragment.java
new file mode 100644
index 00000000..870453d7
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/ConfigListFragment.java
@@ -0,0 +1,61 @@
+package com.wireguard.android;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.wireguard.android.databinding.ConfigListFragmentBinding;
+import com.wireguard.config.Config;
+
+/**
+ * Fragment containing the list of known WireGuard configurations.
+ */
+
+public class ConfigListFragment extends BaseConfigFragment {
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
+ final Bundle savedInstanceState) {
+ final ConfigListFragmentBinding binding =
+ ConfigListFragmentBinding.inflate(inflater, parent, false);
+ binding.setConfigs(VpnService.getInstance().getConfigs());
+ final ListView listView = (ListView) binding.getRoot();
+ listView.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);
+ }
+ });
+ listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(final AdapterView<?> parent, final View view,
+ final int position, final long id) {
+ final Config config = (Config) parent.getItemAtPosition(position);
+ final VpnService service = VpnService.getInstance();
+ if (config == null || service == null)
+ return false;
+ if (config.isEnabled())
+ service.disable(config.getName());
+ else
+ service.enable(config.getName());
+ return true;
+ }
+ });
+ return binding.getRoot();
+ }
+
+ @Override
+ protected void onCurrentConfigChanged(final Config config) {
+ Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" +
+ (config != null ? config.getName() : null));
+ final BaseConfigActivity activity = ((BaseConfigActivity) getActivity());
+ if (activity != null && activity.getCurrentConfig() != config)
+ activity.setCurrentConfig(config);
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/ObservableArrayMapAdapter.java b/app/src/main/java/com/wireguard/android/ObservableArrayMapAdapter.java
index d2a5a4cc..dd3a380f 100644
--- a/app/src/main/java/com/wireguard/android/ObservableArrayMapAdapter.java
+++ b/app/src/main/java/com/wireguard/android/ObservableArrayMapAdapter.java
@@ -14,7 +14,7 @@ import android.widget.ListAdapter;
import java.lang.ref.WeakReference;
/**
- * A generic ListAdapter backed by an ObservableMap.
+ * A generic ListAdapter backed by an ObservableArrayMap.
*/
class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter {
@@ -23,8 +23,10 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
private ObservableArrayMap<K, V> map;
private final OnMapChangedCallback<K, V> callback = new OnMapChangedCallback<>(this);
- ObservableArrayMapAdapter(Context context, int layoutId, ObservableArrayMap<K, V> map) {
- this.layoutInflater = LayoutInflater.from(context);
+ ObservableArrayMapAdapter(final Context context, final int layoutId,
+ final ObservableArrayMap<K, V> map) {
+ super();
+ layoutInflater = LayoutInflater.from(context);
this.layoutId = layoutId;
setMap(map);
}
@@ -35,17 +37,17 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
}
@Override
- public V getItem(int position) {
- return map != null ? map.get(map.keyAt(position)) : null;
+ public V getItem(final int position) {
+ return map != null ? map.valueAt(position) : null;
}
@Override
- public long getItemId(int position) {
- return position;
+ public long getItemId(final int position) {
+ return getItem(position) != null ? getItem(position).hashCode() : -1;
}
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
if (binding == null)
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
@@ -54,7 +56,12 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
return binding.getRoot();
}
- public void setMap(ObservableArrayMap<K, V> newMap) {
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public void setMap(final ObservableArrayMap<K, V> newMap) {
if (map != null)
map.removeOnMapChangedCallback(callback);
map = newMap;
@@ -68,12 +75,13 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
private final WeakReference<ObservableArrayMapAdapter<K, V>> weakAdapter;
- private OnMapChangedCallback(ObservableArrayMapAdapter<K, V> adapter) {
+ private OnMapChangedCallback(final ObservableArrayMapAdapter<K, V> adapter) {
+ super();
weakAdapter = new WeakReference<>(adapter);
}
@Override
- public void onMapChanged(ObservableMap<K, V> sender, K key) {
+ public void onMapChanged(final ObservableMap<K, V> sender, final K key) {
final ObservableArrayMapAdapter<K, V> adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
diff --git a/app/src/main/java/com/wireguard/android/ObservableListAdapter.java b/app/src/main/java/com/wireguard/android/ObservableListAdapter.java
index 475bafbf..66cb957d 100644
--- a/app/src/main/java/com/wireguard/android/ObservableListAdapter.java
+++ b/app/src/main/java/com/wireguard/android/ObservableListAdapter.java
@@ -22,8 +22,9 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
private ObservableList<T> list;
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
- ObservableListAdapter(Context context, int layoutId, ObservableList<T> list) {
- this.layoutInflater = LayoutInflater.from(context);
+ ObservableListAdapter(final Context context, final int layoutId, final ObservableList<T> list) {
+ super();
+ layoutInflater = LayoutInflater.from(context);
this.layoutId = layoutId;
setList(list);
}
@@ -34,17 +35,17 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
}
@Override
- public T getItem(int position) {
+ public T getItem(final int position) {
return list != null ? list.get(position) : null;
}
@Override
- public long getItemId(int position) {
+ public long getItemId(final int position) {
return position;
}
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
if (binding == null)
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
@@ -53,7 +54,7 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
return binding.getRoot();
}
- public void setList(ObservableList<T> newList) {
+ public void setList(final ObservableList<T> newList) {
if (list != null)
list.removeOnListChangedCallback(callback);
list = newList;
@@ -67,12 +68,13 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
private final WeakReference<ObservableListAdapter<U>> weakAdapter;
- private OnListChangedCallback(ObservableListAdapter<U> adapter) {
+ private OnListChangedCallback(final ObservableListAdapter<U> adapter) {
+ super();
weakAdapter = new WeakReference<>(adapter);
}
@Override
- public void onChanged(ObservableList<U> sender) {
+ public void onChanged(final ObservableList<U> sender) {
final ObservableListAdapter<U> adapter = weakAdapter.get();
if (adapter != null)
adapter.notifyDataSetChanged();
@@ -81,24 +83,26 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
}
@Override
- public void onItemRangeChanged(ObservableList<U> sender, int positionStart, int itemCount) {
+ public void onItemRangeChanged(final ObservableList<U> sender, final int positionStart,
+ final int itemCount) {
onChanged(sender);
}
@Override
- public void onItemRangeInserted(ObservableList<U> sender, int positionStart,
- int itemCount) {
+ public void onItemRangeInserted(final ObservableList<U> sender, final int positionStart,
+ final int itemCount) {
onChanged(sender);
}
@Override
- public void onItemRangeMoved(ObservableList<U> sender, int fromPosition, int toPosition,
- int itemCount) {
+ public void onItemRangeMoved(final ObservableList<U> sender, final int fromPosition,
+ final int toPosition, final int itemCount) {
onChanged(sender);
}
@Override
- public void onItemRangeRemoved(ObservableList<U> sender, int positionStart, int itemCount) {
+ public void onItemRangeRemoved(final ObservableList<U> sender, final int positionStart,
+ final int itemCount) {
onChanged(sender);
}
}
diff --git a/app/src/main/java/com/wireguard/android/PlaceholderFragment.java b/app/src/main/java/com/wireguard/android/PlaceholderFragment.java
index e17aac03..db5d7b33 100644
--- a/app/src/main/java/com/wireguard/android/PlaceholderFragment.java
+++ b/app/src/main/java/com/wireguard/android/PlaceholderFragment.java
@@ -12,7 +12,8 @@ import android.view.ViewGroup;
public class PlaceholderFragment extends Fragment {
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup parent,
+ final Bundle savedInstanceState) {
return inflater.inflate(R.layout.placeholder_fragment, parent, false);
}
}
diff --git a/app/src/main/java/com/wireguard/android/ProfileActivity.java b/app/src/main/java/com/wireguard/android/ProfileActivity.java
deleted file mode 100644
index 29d249d4..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileActivity.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.wireguard.android;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-/**
- * Base class for activities that use ProfileListFragment and ProfileDetailFragment.
- */
-
-abstract class ProfileActivity extends ServiceClientActivity<ProfileServiceInterface> {
- public static final String KEY_IS_EDITING = "is_editing";
- public static final String KEY_PROFILE_NAME = "profile_name";
- protected static final String TAG_DETAIL = "detail";
- protected static final String TAG_EDIT = "edit";
- protected static final String TAG_LIST = "list";
- protected static final String TAG_PLACEHOLDER = "placeholder";
-
- private String currentProfile;
- private boolean isEditing;
-
- public ProfileActivity() {
- super(ProfileService.class);
- }
-
- protected String getCurrentProfile() {
- return currentProfile;
- }
-
- protected boolean isEditing() {
- return isEditing;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // Restore the saved profile if there is one; otherwise grab it from the intent.
- if (savedInstanceState != null) {
- currentProfile = savedInstanceState.getString(KEY_PROFILE_NAME);
- isEditing = savedInstanceState.getBoolean(KEY_IS_EDITING, false);
- } else {
- final Intent intent = getIntent();
- currentProfile = intent.getStringExtra(KEY_PROFILE_NAME);
- isEditing = intent.getBooleanExtra(KEY_IS_EDITING, false);
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.main, menu);
- return true;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean(KEY_IS_EDITING, isEditing);
- outState.putString(KEY_PROFILE_NAME, currentProfile);
- }
-
- protected void setCurrentProfile(String profile) {
- currentProfile = profile;
- }
-
- protected void setIsEditing(boolean isEditing) {
- this.isEditing = isEditing;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileDetailActivity.java b/app/src/main/java/com/wireguard/android/ProfileDetailActivity.java
deleted file mode 100644
index 3e70de93..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileDetailActivity.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.wireguard.android;
-
-import android.app.Fragment;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MenuItem;
-
-/**
- * Activity that allows viewing information about a single WireGuard profile.
- */
-
-public class ProfileDetailActivity extends ProfileActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.profile_detail_activity);
- setTitle(getCurrentProfile());
- Fragment detailFragment = getFragmentManager().findFragmentByTag(TAG_DETAIL);
- ((ProfileDetailFragment) detailFragment).setProfile(getCurrentProfile());
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_edit:
- final Intent intent = new Intent(this, ProfileEditActivity.class);
- intent.putExtra(KEY_PROFILE_NAME, getCurrentProfile());
- startActivity(intent);
- return true;
- case R.id.menu_action_save:
- throw new IllegalStateException();
- case R.id.menu_settings:
- startActivity(new Intent(this, SettingsActivity.class));
- return true;
- default:
- return false;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java b/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java
deleted file mode 100644
index 0fed7708..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileDetailFragment.java
+++ /dev/null
@@ -1,43 +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.ProfileDetailFragmentBinding;
-import com.wireguard.config.Profile;
-
-/**
- * Fragment for viewing information about a WireGuard profile.
- */
-
-public class ProfileDetailFragment extends ProfileFragment {
- private ProfileDetailFragmentBinding binding;
-
- @Override
- protected void onCachedProfileChanged(Profile cachedProfile) {
- if (binding != null)
- binding.setProfile(cachedProfile);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.profile_detail, menu);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
- binding = ProfileDetailFragmentBinding.inflate(inflater, parent, false);
- binding.setProfile(getCachedProfile());
- return binding.getRoot();
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileEditActivity.java b/app/src/main/java/com/wireguard/android/ProfileEditActivity.java
deleted file mode 100644
index 34620279..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileEditActivity.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.wireguard.android;
-
-import android.app.Fragment;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MenuItem;
-
-/**
- * Activity that allows editing a single WireGuard profile.
- */
-
-public class ProfileEditActivity extends ProfileActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.profile_edit_activity);
- Fragment editFragment = getFragmentManager().findFragmentByTag(TAG_EDIT);
- ((ProfileEditFragment) editFragment).setProfile(getCurrentProfile());
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_edit:
- throw new IllegalStateException();
- case R.id.menu_action_save:
- finish();
- return false;
- case R.id.menu_settings:
- startActivity(new Intent(this, SettingsActivity.class));
- return true;
- default:
- return false;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileEditFragment.java b/app/src/main/java/com/wireguard/android/ProfileEditFragment.java
deleted file mode 100644
index 2249a8a7..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileEditFragment.java
+++ /dev/null
@@ -1,59 +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.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.wireguard.android.databinding.ProfileEditFragmentBinding;
-import com.wireguard.config.Profile;
-
-/**
- * Fragment for editing a WireGuard profile.
- */
-
-public class ProfileEditFragment extends ProfileFragment {
- private ProfileEditFragmentBinding binding;
- private Profile copy;
-
- @Override
- protected void onCachedProfileChanged(Profile cachedProfile) {
- copy = cachedProfile != null ? cachedProfile.copy() : null;
- if (binding != null)
- binding.setProfile(copy);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.profile_edit, menu);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
- binding = ProfileEditFragmentBinding.inflate(inflater, parent, false);
- binding.setProfile(copy);
- return binding.getRoot();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_save:
- final ProfileServiceInterface service = getService();
- if (service != null)
- service.saveProfile(getProfile(), copy);
- return true;
- default:
- return false;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileFragment.java b/app/src/main/java/com/wireguard/android/ProfileFragment.java
deleted file mode 100644
index 0e9092ec..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileFragment.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.wireguard.android;
-
-import android.os.Bundle;
-
-import com.wireguard.config.Profile;
-
-/**
- * Base class for fragments that need to remember which profile they belong to.
- */
-
-abstract class ProfileFragment extends ServiceClientFragment<ProfileServiceInterface> {
- private Profile cachedProfile;
- private String profile;
-
- protected Profile getCachedProfile() {
- return cachedProfile;
- }
-
- public String getProfile() {
- return profile;
- }
-
- protected void onCachedProfileChanged(Profile cachedProfile) {
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // Restore the saved profile if there is one; otherwise grab it from the arguments.
- if (savedInstanceState != null)
- setProfile(savedInstanceState.getString(ProfileActivity.KEY_PROFILE_NAME));
- else if (getArguments() != null)
- setProfile(getArguments().getString(ProfileActivity.KEY_PROFILE_NAME));
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(ProfileActivity.KEY_PROFILE_NAME, profile);
- }
-
- @Override
- public void onServiceConnected(ProfileServiceInterface service) {
- super.onServiceConnected(service);
- updateCachedProfile(service);
- }
-
- public void setProfile(String profile) {
- this.profile = profile;
- updateCachedProfile(getService());
- }
-
- private void updateCachedProfile(ProfileServiceInterface service) {
- final Profile newCachedProfile = service != null
- ? service.getProfiles().get(profile) : null;
- if (newCachedProfile != cachedProfile) {
- cachedProfile = newCachedProfile;
- onCachedProfileChanged(newCachedProfile);
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileListActivity.java b/app/src/main/java/com/wireguard/android/ProfileListActivity.java
deleted file mode 100644
index 49651201..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileListActivity.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.wireguard.android;
-
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MenuItem;
-
-/**
- * Activity that allows creating/viewing/editing/deleting WireGuard profiles.
- */
-
-public class ProfileListActivity extends ProfileActivity {
- private boolean isSplitLayout;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.profile_list_activity);
- isSplitLayout = findViewById(R.id.fragment_container) != null;
- final FragmentTransaction transaction = getFragmentManager().beginTransaction();
- final Fragment listFragment = getFragmentManager().findFragmentByTag(TAG_LIST);
- if (listFragment instanceof ProfileListFragment) {
- ((ProfileListFragment) listFragment).setIsSplitLayout(isSplitLayout);
- } else {
- final ProfileListFragment newListFragment = new ProfileListFragment();
- newListFragment.setIsSplitLayout(isSplitLayout);
- transaction.add(R.id.list_container, newListFragment, TAG_LIST);
- }
- if (!isSplitLayout) {
- // Avoid ProfileDetailFragment adding its menu when it is not in the view hierarchy.
- final Fragment detailFragment = getFragmentManager().findFragmentByTag(TAG_DETAIL);
- if (detailFragment != null)
- transaction.remove(detailFragment);
- }
- transaction.commit();
- onProfileSelected(getCurrentProfile());
- if (isEditing())
- startEditing();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_edit:
- startEditing();
- return true;
- case R.id.menu_action_save:
- getFragmentManager().popBackStack();
- return false;
- case R.id.menu_settings:
- startActivity(new Intent(this, SettingsActivity.class));
- return true;
- default:
- return false;
- }
- }
-
- public void onProfileSelected(String profile) {
- if (isSplitLayout) {
- if (isEditing())
- getFragmentManager().popBackStack();
- setIsEditing(false);
- setCurrentProfile(profile);
- updateLayout();
- } else if (profile != null) {
- final Intent intent = new Intent(this, ProfileDetailActivity.class);
- intent.putExtra(KEY_PROFILE_NAME, profile);
- startActivity(intent);
- setCurrentProfile(null);
- }
- }
-
- private void startEditing() {
- if (isSplitLayout) {
- setIsEditing(true);
- updateLayout();
- } else if (getCurrentProfile() != null) {
- final Intent intent = new Intent(this, ProfileEditActivity.class);
- intent.putExtra(KEY_PROFILE_NAME, getCurrentProfile());
- startActivity(intent);
- setCurrentProfile(null);
- setIsEditing(false);
- }
- }
-
- private void updateLayout() {
- final Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container);
- final String profile = getCurrentProfile();
- if (isEditing()) {
- if (fragment instanceof ProfileEditFragment) {
- final ProfileEditFragment editFragment = (ProfileEditFragment) fragment;
- if (!profile.equals(editFragment.getProfile()))
- editFragment.setProfile(profile);
- } else {
- final ProfileEditFragment editFragment = new ProfileEditFragment();
- editFragment.setProfile(profile);
- final FragmentTransaction transaction = getFragmentManager().beginTransaction();
- transaction.addToBackStack(null);
- transaction.replace(R.id.fragment_container, editFragment, TAG_EDIT);
- transaction.commit();
- }
- } else if (profile != null) {
- if (fragment instanceof ProfileDetailFragment) {
- final ProfileDetailFragment detailFragment = (ProfileDetailFragment) fragment;
- if (!profile.equals(detailFragment.getProfile()))
- detailFragment.setProfile(profile);
- } else {
- final ProfileDetailFragment detailFragment = new ProfileDetailFragment();
- detailFragment.setProfile(profile);
- final FragmentTransaction transaction = getFragmentManager().beginTransaction();
- transaction.replace(R.id.fragment_container, detailFragment, TAG_DETAIL);
- transaction.commit();
- }
- } else {
- if (!(fragment instanceof PlaceholderFragment)) {
- final PlaceholderFragment placeholderFragment = new PlaceholderFragment();
- final FragmentTransaction transaction = getFragmentManager().beginTransaction();
- transaction.replace(R.id.fragment_container, placeholderFragment, TAG_PLACEHOLDER);
- transaction.commit();
- }
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileListFragment.java b/app/src/main/java/com/wireguard/android/ProfileListFragment.java
deleted file mode 100644
index be1358a4..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileListFragment.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.wireguard.android;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import com.wireguard.android.databinding.ProfileListFragmentBinding;
-import com.wireguard.config.Profile;
-
-/**
- * Fragment containing the list of available WireGuard profiles.
- */
-
-public class ProfileListFragment extends ServiceClientFragment<ProfileServiceInterface> {
- private ProfileListFragmentBinding binding;
- private boolean isSplitLayout;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
- binding = ProfileListFragmentBinding.inflate(inflater, parent, false);
- final ListView listView = (ListView) binding.getRoot();
- listView.setChoiceMode(isSplitLayout
- ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE);
- listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- final Profile profile = (Profile) parent.getItemAtPosition(position);
- ((ProfileListActivity) getActivity()).onProfileSelected(profile.getName());
- }
- });
- listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
- long id) {
- final Profile profile = (Profile) parent.getItemAtPosition(position);
- final ProfileServiceInterface service = getService();
- if (profile == null || service == null)
- return false;
- if (profile.getIsConnected())
- service.disconnectProfile(profile.getName());
- else
- service.connectProfile(profile.getName());
- return true;
- }
- });
- return binding.getRoot();
- }
-
- @Override
- public void onServiceConnected(ProfileServiceInterface service) {
- super.onServiceConnected(service);
- binding.setProfiles(service.getProfiles());
- }
-
- public void setIsSplitLayout(boolean isSplitLayout) {
- this.isSplitLayout = isSplitLayout;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileService.java b/app/src/main/java/com/wireguard/android/ProfileService.java
deleted file mode 100644
index 984cf4b1..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileService.java
+++ /dev/null
@@ -1,290 +0,0 @@
-package com.wireguard.android;
-
-import android.app.Service;
-import android.content.Intent;
-import android.databinding.ObservableArrayMap;
-import android.os.AsyncTask;
-import android.os.Binder;
-import android.os.IBinder;
-import android.util.Log;
-
-import com.wireguard.config.Profile;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Service that handles profile state coordination and all background processing for the app.
- */
-
-public class ProfileService extends Service {
- private static final String TAG = "ProfileService";
-
- private final IBinder binder = new ProfileServiceBinder();
- private final ObservableArrayMap<String, Profile> profiles = new ObservableArrayMap<>();
- private RootShell rootShell;
-
- @Override
- public IBinder onBind(Intent intent) {
- return binder;
- }
-
- @Override
- public void onCreate() {
- rootShell = new RootShell(this);
- // Ensure the service sticks around after being unbound. This only needs to happen once.
- final Intent intent = new Intent(this, ProfileService.class);
- startService(intent);
- new ProfileLoader().execute(getFilesDir().listFiles(new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- return name.endsWith(".conf");
- }
- }));
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- return START_STICKY;
- }
-
- private class ProfileConnecter extends AsyncTask<Void, Void, Boolean> {
- private final Profile profile;
-
- private ProfileConnecter(Profile profile) {
- super();
- this.profile = profile;
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- Log.i(TAG, "Running wg-quick up for profile " + profile.getName());
- final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
- return rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'") == 0;
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- if (!result)
- return;
- profile.setIsConnected(true);
- }
- }
-
- private class ProfileDisconnecter extends AsyncTask<Void, Void, Boolean> {
- private final Profile profile;
-
- private ProfileDisconnecter(Profile profile) {
- super();
- this.profile = profile;
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- Log.i(TAG, "Running wg-quick down for profile " + profile.getName());
- final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
- return rootShell.run(null, "wg-quick down '" + configFile.getPath() + "'") == 0;
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- if (!result)
- return;
- profile.setIsConnected(false);
- }
- }
-
- private class ProfileLoader extends AsyncTask<File, Void, List<Profile>> {
- @Override
- protected List<Profile> doInBackground(File... files) {
- final List<String> interfaceNames = new LinkedList<>();
- final List<Profile> loadedProfiles = new LinkedList<>();
- final String command = "wg show interfaces";
- if (rootShell.run(interfaceNames, command) == 0 && interfaceNames.size() == 1) {
- // wg puts all interface names on the same line. Split them into separate elements.
- final String nameList = interfaceNames.get(0);
- Collections.addAll(interfaceNames, nameList.split(" "));
- interfaceNames.remove(0);
- } else {
- interfaceNames.clear();
- Log.w(TAG, "Can't enumerate network interfaces. All profiles will appear down.");
- }
- for (File file : files) {
- if (isCancelled())
- return null;
- final String fileName = file.getName();
- final String profileName = fileName.substring(0, fileName.length() - 5);
- final Profile profile = new Profile(profileName);
- Log.v(TAG, "Attempting to load profile " + profileName);
- try {
- profile.parseFrom(openFileInput(fileName));
- profile.setIsConnected(interfaceNames.contains(profileName));
- loadedProfiles.add(profile);
- } catch (IOException | IndexOutOfBoundsException e) {
- Log.w(TAG, "Failed to load profile from " + fileName, e);
- }
- }
- return loadedProfiles;
- }
-
- @Override
- protected void onPostExecute(List<Profile> loadedProfiles) {
- if (loadedProfiles == null)
- return;
- for (Profile profile : loadedProfiles)
- profiles.put(profile.getName(), profile);
- }
- }
-
- private class ProfileRemover extends AsyncTask<Void, Void, Boolean> {
- private final Profile profile;
-
- private ProfileRemover(Profile profile) {
- super();
- this.profile = profile;
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- Log.i(TAG, "Removing profile " + profile.getName());
- final File configFile = new File(getFilesDir(), profile.getName() + ".conf");
- if (configFile.delete()) {
- return true;
- } else {
- Log.e(TAG, "Could not delete configuration for profile " + profile.getName());
- return false;
- }
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- if (!result)
- return;
- profiles.remove(profile.getName());
- }
- }
-
- private class ProfileUpdater extends AsyncTask<Void, Void, Boolean> {
- private final String newName;
- private Profile newProfile;
- private final String oldName;
- private final Boolean shouldConnect;
-
- private ProfileUpdater(String oldName, Profile newProfile, Boolean shouldConnect) {
- super();
- this.newName = newProfile.getName();
- this.newProfile = newProfile;
- this.oldName = oldName;
- this.shouldConnect = shouldConnect;
- if (profiles.values().contains(newProfile))
- throw new IllegalArgumentException("Profile " + newName + " modified directly");
- if (!newName.equals(oldName) && profiles.get(newName) != null)
- throw new IllegalStateException("Profile " + newName + " already exists");
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
- Log.i(TAG, (oldName == null ? "Adding" : "Updating") + " profile " + newName);
- final File newFile = new File(getFilesDir(), newName + ".conf");
- final File oldFile = new File(getFilesDir(), oldName + ".conf");
- if (!newName.equals(oldName) && newFile.exists()) {
- Log.w(TAG, "Refusing to overwrite existing profile configuration");
- return false;
- }
- try {
- final FileOutputStream stream = openFileOutput(oldFile.getName(), MODE_PRIVATE);
- stream.write(newProfile.toString().getBytes(StandardCharsets.UTF_8));
- stream.close();
- } catch (IOException e) {
- Log.e(TAG, "Could not save configuration for profile " + oldName, e);
- return false;
- }
- if (!newName.equals(oldName) && !oldFile.renameTo(newFile)) {
- Log.e(TAG, "Could not rename " + oldFile.getName() + " to " + newFile.getName());
- return false;
- }
- return true;
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- if (!result)
- return;
- final Profile oldProfile = profiles.remove(oldName);
- if (oldProfile != null) {
- try {
- oldProfile.parseFrom(newProfile);
- oldProfile.setName(newName);
- newProfile = oldProfile;
- } catch (IOException e) {
- Log.e(TAG, "Could not replace profile " + oldName + " with " + newName, e);
- return;
- }
- }
- newProfile.setIsConnected(false);
- profiles.put(newName, newProfile);
- if (shouldConnect)
- new ProfileConnecter(newProfile).execute();
- }
- }
-
- private class ProfileServiceBinder extends Binder implements ProfileServiceInterface {
- @Override
- public void connectProfile(String name) {
- final Profile profile = profiles.get(name);
- if (profile == null || profile.getIsConnected())
- return;
- new ProfileConnecter(profile).execute();
- }
-
- @Override
- public Profile copyProfileForEditing(String name) {
- final Profile profile = profiles.get(name);
- return profile != null ? profile.copy() : null;
- }
-
- @Override
- public void disconnectProfile(String name) {
- final Profile profile = profiles.get(name);
- if (profile == null || !profile.getIsConnected())
- return;
- new ProfileDisconnecter(profile).execute();
- }
-
- @Override
- public ObservableArrayMap<String, Profile> getProfiles() {
- return profiles;
- }
-
- @Override
- public void removeProfile(String name) {
- final Profile profile = profiles.get(name);
- if (profile == null)
- return;
- if (profile.getIsConnected())
- new ProfileDisconnecter(profile).execute();
- new ProfileRemover(profile).execute();
- }
-
- @Override
- public void saveProfile(String oldName, Profile newProfile) {
- if (oldName != null) {
- final Profile oldProfile = profiles.get(oldName);
- if (oldProfile == null)
- return;
- final boolean wasConnected = oldProfile.getIsConnected();
- if (wasConnected)
- new ProfileDisconnecter(oldProfile).execute();
- new ProfileUpdater(oldName, newProfile, wasConnected).execute();
- } else {
- new ProfileUpdater(null, newProfile, false).execute();
- }
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java b/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java
deleted file mode 100644
index 65dc27a0..00000000
--- a/app/src/main/java/com/wireguard/android/ProfileServiceInterface.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.wireguard.android;
-
-import android.databinding.ObservableArrayMap;
-
-import com.wireguard.config.Profile;
-
-/**
- * Interface for the background connection service.
- */
-
-interface ProfileServiceInterface {
- /**
- * Attempt to set up and enable an interface for this profile. The profile's connection state
- * will be updated if connection is successful. If this profile is already connected, or it is
- * not a known profile, no changes will be made.
- *
- * @param name The profile (in the list of known profiles) to use for this connection.
- */
- void connectProfile(String name);
-
- /**
- * Creates a deep copy of an existing profile that can be modified and then passed to
- * saveProfile. If the given profile is not a known profile, or the profile cannot be copied,
- * this function returns null.
- *
- * @param name The existing profile (in the list of known profiles) to copy.
- * @return A copy of the profile that can be freely modified.
- */
- Profile copyProfileForEditing(String name);
-
- /**
- * Attempt to disable and tear down an interface for this profile. The profile's connection
- * state will be updated if disconnection is successful. If this profile is already
- * disconnected, or it is not a known profile, no changes will be made.
- *
- * @param name The profile (in the list of known profiles) to disconnect.
- */
- void disconnectProfile(String name);
-
- /**
- * Retrieve the set of profiles known and managed by this service. Profiles in this list must
- * not be modified directly. If a profile is to be updated, first create a copy of it by calling
- * copyProfileForEditing().
- *
- * @return The set of known profiles.
- */
- ObservableArrayMap<String, Profile> getProfiles();
-
- /**
- * Remove a profile from being managed by this service. If the profile is currently connected,
- * it will be disconnected before it is removed. If successful, configuration for this profile
- * will be removed from persistent storage. If the profile is not a known profile, no changes
- * will be made.
- *
- * @param name The profile (in the list of known profiles) to remove.
- */
- void removeProfile(String name);
-
- /**
- * Replace the given profile, or add a new profile if oldProfile is null.
- * If the profile exists and is currently connected, it will be disconnected before the
- * replacement, and the service will attempt to reconnect it afterward. If the profile is new,
- * it will be set to the disconnected state. If successful, configuration for this profile will
- * be saved to persistent storage.
- *
- * @param oldName The existing profile to replace, or null to add the new profile.
- * @param newProfile The profile to add, or a copy of the profile to replace.
- */
- void saveProfile(String oldName, Profile newProfile);
-}
diff --git a/app/src/main/java/com/wireguard/android/RootShell.java b/app/src/main/java/com/wireguard/android/RootShell.java
index f5879f00..973a5d0c 100644
--- a/app/src/main/java/com/wireguard/android/RootShell.java
+++ b/app/src/main/java/com/wireguard/android/RootShell.java
@@ -23,14 +23,14 @@ class RootShell {
private static final String SETUP_TEMPLATE = "export TMPDIR=%s\ntrap 'echo $?' EXIT\n";
private static final String TAG = "RootShell";
- private final byte setupCommands[];
+ private final byte[] setupCommands;
private final String shell;
- RootShell(Context context) {
+ RootShell(final Context context) {
this(context, "su");
}
- RootShell(Context context, String shell) {
+ RootShell(final Context context, final String shell) {
final String tmpdir = context.getCacheDir().getPath();
setupCommands = String.format(SETUP_TEMPLATE, tmpdir).getBytes(StandardCharsets.UTF_8);
this.shell = shell;
@@ -45,7 +45,7 @@ class RootShell {
* @param commands One or more commands to run as root (each element is a separate line).
* @return The exit value of the last command run, or -1 if there was an internal error.
*/
- int run(List<String> output, String... commands) {
+ int run(final List<String> output, final String... commands) {
if (commands.length < 1)
throw new IndexOutOfBoundsException("At least one command must be supplied");
int exitValue = -1;
@@ -54,7 +54,7 @@ class RootShell {
final Process process = builder.command(shell).start();
final OutputStream stdin = process.getOutputStream();
stdin.write(setupCommands);
- for (String command : commands)
+ for (final String command : commands)
stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8));
stdin.close();
Log.d(TAG, "Sent " + commands.length + " command(s), now reading output");
diff --git a/app/src/main/java/com/wireguard/android/ServiceClientActivity.java b/app/src/main/java/com/wireguard/android/ServiceClientActivity.java
deleted file mode 100644
index 263e012c..00000000
--- a/app/src/main/java/com/wireguard/android/ServiceClientActivity.java
+++ /dev/null
@@ -1,75 +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.IBinder;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Base class for activities that maintain a connection to a background service.
- */
-
-abstract class ServiceClientActivity<T> extends Activity implements ServiceConnectionProvider<T> {
- private final ServiceConnectionCallbacks callbacks = new ServiceConnectionCallbacks();
- private final List<ServiceConnectionListener<T>> listeners = new ArrayList<>();
- private T service;
- private final Class<?> serviceClass;
-
- protected ServiceClientActivity(Class<?> serviceClass) {
- this.serviceClass = serviceClass;
- }
-
- @Override
- public void addServiceConnectionListener(ServiceConnectionListener<T> listener) {
- listeners.add(listener);
- }
-
- public T getService() {
- return service;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- bindService(new Intent(this, serviceClass), callbacks, Context.BIND_AUTO_CREATE);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (service != null) {
- service = null;
- unbindService(callbacks);
- for (ServiceConnectionListener listener : listeners)
- listener.onServiceDisconnected();
- }
- }
-
- @Override
- public void removeServiceConnectionListener(ServiceConnectionListener<T> listener) {
- listeners.remove(listener);
- }
-
- private class ServiceConnectionCallbacks implements ServiceConnection {
- @Override
- public void onServiceConnected(ComponentName component, IBinder binder) {
- @SuppressWarnings("unchecked")
- final T localBinder = (T) binder;
- service = localBinder;
- for (ServiceConnectionListener<T> listener : listeners)
- listener.onServiceConnected(service);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName component) {
- service = null;
- for (ServiceConnectionListener<T> listener : listeners)
- listener.onServiceDisconnected();
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ServiceClientFragment.java b/app/src/main/java/com/wireguard/android/ServiceClientFragment.java
deleted file mode 100644
index 7377151c..00000000
--- a/app/src/main/java/com/wireguard/android/ServiceClientFragment.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.wireguard.android;
-
-import android.app.Fragment;
-import android.content.Context;
-
-/**
- * Base class for fragments in activities that maintain a connection to a background service.
- */
-
-abstract class ServiceClientFragment<T> extends Fragment implements ServiceConnectionListener<T> {
- private ServiceConnectionProvider<T> provider;
- private T service;
-
- protected T getService() {
- return service;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- @SuppressWarnings("unchecked")
- final ServiceConnectionProvider<T> localContext = (ServiceConnectionProvider<T>) context;
- provider = localContext;
- service = provider.getService();
- if (service != null)
- onServiceConnected(service);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- provider = null;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- provider.addServiceConnectionListener(this);
- // Run the handler if the connection state changed while we were not paying attention.
- final T localService = provider.getService();
- if (localService != service) {
- if (localService != null)
- onServiceConnected(localService);
- else
- onServiceDisconnected();
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
- provider.removeServiceConnectionListener(this);
- }
-
- @Override
- public void onServiceConnected(T service) {
- this.service = service;
- }
-
- @Override
- public void onServiceDisconnected() {
- service = null;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java b/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java
deleted file mode 100644
index c77fe931..00000000
--- a/app/src/main/java/com/wireguard/android/ServiceConnectionListener.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.wireguard.android;
-
-/**
- * Interface for fragments that need notification about service connection changes.
- */
-
-interface ServiceConnectionListener<T> {
- void onServiceConnected(T service);
-
- void onServiceDisconnected();
-}
diff --git a/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java b/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java
deleted file mode 100644
index 79a0efde..00000000
--- a/app/src/main/java/com/wireguard/android/ServiceConnectionProvider.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.wireguard.android;
-
-/**
- * Interface for activities that provide a connection to a service.
- */
-
-interface ServiceConnectionProvider<T> {
- void addServiceConnectionListener(ServiceConnectionListener<T> listener);
-
- T getService();
-
- void removeServiceConnectionListener(ServiceConnectionListener<T> listener);
-}
diff --git a/app/src/main/java/com/wireguard/android/SettingsActivity.java b/app/src/main/java/com/wireguard/android/SettingsActivity.java
index c91a1bd2..44e9b9b3 100644
--- a/app/src/main/java/com/wireguard/android/SettingsActivity.java
+++ b/app/src/main/java/com/wireguard/android/SettingsActivity.java
@@ -1,11 +1,6 @@
package com.wireguard.android;
import android.app.Activity;
-import android.os.Bundle;
public class SettingsActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
}
diff --git a/app/src/main/java/com/wireguard/android/VpnService.java b/app/src/main/java/com/wireguard/android/VpnService.java
new file mode 100644
index 00000000..2f3d97c8
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/VpnService.java
@@ -0,0 +1,339 @@
+package com.wireguard.android;
+
+import android.app.Service;
+import android.content.Intent;
+import android.databinding.ObservableArrayMap;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.wireguard.config.Config;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Service that handles config state coordination and all background processing for the application.
+ */
+
+public class VpnService extends Service {
+ private static final String TAG = "VpnService";
+
+ private static VpnService instance;
+
+ public static VpnService getInstance() {
+ return instance;
+ }
+
+ private final IBinder binder = new Binder();
+ private final ObservableArrayMap<String, Config> configurations = new ObservableArrayMap<>();
+ private RootShell rootShell;
+
+ /**
+ * 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 ObservableArrayMap<String, Config> getConfigs() {
+ return configurations;
+ }
+
+ @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");
+ }
+ }));
+ }
+
+ @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 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) {
+ if (!result)
+ return;
+ config.setEnabled(false);
+ }
+ }
+
+ private class ConfigEnabler extends AsyncTask<Void, Void, Boolean> {
+ private final Config config;
+
+ private ConfigEnabler(final Config config) {
+ this.config = config;
+ }
+
+ @Override
+ protected Boolean doInBackground(final Void... voids) {
+ Log.i(TAG, "Running wg-quick up for " + config.getName());
+ final File configFile = new File(getFilesDir(), config.getName() + ".conf");
+ return rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'") == 0;
+ }
+
+ @Override
+ protected void onPostExecute(final Boolean result) {
+ if (!result)
+ return;
+ config.setEnabled(true);
+ }
+ }
+
+ private class ConfigLoader extends AsyncTask<File, Void, 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.setEnabled(interfaces.contains(configName));
+ config.setName(configName);
+ configs.add(config);
+ } catch (IllegalArgumentException | IOException e) {
+ Log.w(TAG, "Failed to load config from " + fileName, e);
+ }
+ }
+ return configs;
+ }
+
+ @Override
+ protected void onPostExecute(final List<Config> configs) {
+ if (configs == null)
+ return;
+ for (final Config config : configs)
+ configurations.put(config.getName(), config);
+ }
+ }
+
+ 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());
+ }
+ }
+
+ private class ConfigUpdater extends AsyncTask<Void, Void, Boolean> {
+ private Config newConfig;
+ private final String newName;
+ private final Config oldConfig;
+ private final String oldName;
+ private final Boolean shouldConnect;
+
+ private ConfigUpdater(final Config oldConfig, final Config newConfig,
+ final Boolean shouldConnect) {
+ super();
+ this.newConfig = newConfig;
+ this.oldConfig = oldConfig;
+ this.shouldConnect = shouldConnect;
+ newName = newConfig.getName();
+ oldName = oldConfig.getName();
+ if (isRename() && configurations.containsKey(newName))
+ throw new IllegalStateException("Config " + newName + " already exists");
+ }
+
+ @Override
+ protected Boolean doInBackground(final Void... voids) {
+ Log.i(TAG, (oldConfig == null ? "Adding" : "Updating") + " config " + newName);
+ final File newFile = new File(getFilesDir(), newName + ".conf");
+ final File oldFile = new File(getFilesDir(), oldName + ".conf");
+ if (isRename() && 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 isRename() {
+ return oldConfig != null && !newConfig.getName().equals(oldConfig.getName());
+ }
+
+ @Override
+ protected void onPostExecute(final Boolean result) {
+ if (!result)
+ return;
+ if (oldConfig != null) {
+ configurations.remove(oldName);
+ oldConfig.copyFrom(newConfig);
+ newConfig = oldConfig;
+ }
+ newConfig.setEnabled(false);
+ configurations.put(newName, newConfig);
+ if (shouldConnect)
+ new ConfigEnabler(newConfig).execute();
+ }
+ }
+}