diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 93973b78..69859655 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,14 +12,7 @@
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@android:style/Theme.Material.Light.DarkActionBar">
-        
-        
-        
+        
             
                 
 
@@ -37,7 +30,7 @@
         
 
         
     
 
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  void arrayMapBinding(ListView view, ObservableArrayMap oldMap,
-                                              int oldLayoutId, ObservableArrayMap newMap,
-                                              int newLayoutId) {
+    public static  void arrayMapBinding(final ListView view,
+                                              final ObservableArrayMap oldMap,
+                                              final int oldLayoutId,
+                                              final ObservableArrayMap 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  void listBinding(ListView view, ObservableList oldList, int oldLayoutId,
-                                       ObservableList newList, int newLayoutId) {
+    public static  void listBinding(final ListView view,
+                                       final ObservableList oldList, final int oldLayoutId,
+                                       final ObservableList 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 extends BaseAdapter implements ListAdapter {
@@ -23,8 +23,10 @@ class ObservableArrayMapAdapter extends BaseAdapter implements ListAdapter
     private ObservableArrayMap map;
     private final OnMapChangedCallback callback = new OnMapChangedCallback<>(this);
 
-    ObservableArrayMapAdapter(Context context, int layoutId, ObservableArrayMap map) {
-        this.layoutInflater = LayoutInflater.from(context);
+    ObservableArrayMapAdapter(final Context context, final int layoutId,
+                              final ObservableArrayMap map) {
+        super();
+        layoutInflater = LayoutInflater.from(context);
         this.layoutId = layoutId;
         setMap(map);
     }
@@ -35,17 +37,17 @@ class ObservableArrayMapAdapter 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 extends BaseAdapter implements ListAdapter
         return binding.getRoot();
     }
 
-    public void setMap(ObservableArrayMap newMap) {
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    public void setMap(final ObservableArrayMap newMap) {
         if (map != null)
             map.removeOnMapChangedCallback(callback);
         map = newMap;
@@ -68,12 +75,13 @@ class ObservableArrayMapAdapter extends BaseAdapter implements ListAdapter
 
         private final WeakReference> weakAdapter;
 
-        private OnMapChangedCallback(ObservableArrayMapAdapter adapter) {
+        private OnMapChangedCallback(final ObservableArrayMapAdapter adapter) {
+            super();
             weakAdapter = new WeakReference<>(adapter);
         }
 
         @Override
-        public void onMapChanged(ObservableMap sender, K key) {
+        public void onMapChanged(final ObservableMap sender, final K key) {
             final ObservableArrayMapAdapter 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 extends BaseAdapter implements ListAdapter {
     private ObservableList list;
     private final OnListChangedCallback callback = new OnListChangedCallback<>(this);
 
-    ObservableListAdapter(Context context, int layoutId, ObservableList list) {
-        this.layoutInflater = LayoutInflater.from(context);
+    ObservableListAdapter(final Context context, final int layoutId, final ObservableList list) {
+        super();
+        layoutInflater = LayoutInflater.from(context);
         this.layoutId = layoutId;
         setList(list);
     }
@@ -34,17 +35,17 @@ class ObservableListAdapter 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 extends BaseAdapter implements ListAdapter {
         return binding.getRoot();
     }
 
-    public void setList(ObservableList newList) {
+    public void setList(final ObservableList newList) {
         if (list != null)
             list.removeOnListChangedCallback(callback);
         list = newList;
@@ -67,12 +68,13 @@ class ObservableListAdapter extends BaseAdapter implements ListAdapter {
 
         private final WeakReference> weakAdapter;
 
-        private OnListChangedCallback(ObservableListAdapter adapter) {
+        private OnListChangedCallback(final ObservableListAdapter adapter) {
+            super();
             weakAdapter = new WeakReference<>(adapter);
         }
 
         @Override
-        public void onChanged(ObservableList sender) {
+        public void onChanged(final ObservableList sender) {
             final ObservableListAdapter adapter = weakAdapter.get();
             if (adapter != null)
                 adapter.notifyDataSetChanged();
@@ -81,24 +83,26 @@ class ObservableListAdapter extends BaseAdapter implements ListAdapter {
         }
 
         @Override
-        public void onItemRangeChanged(ObservableList sender, int positionStart, int itemCount) {
+        public void onItemRangeChanged(final ObservableList sender, final int positionStart,
+                                       final int itemCount) {
             onChanged(sender);
         }
 
         @Override
-        public void onItemRangeInserted(ObservableList sender, int positionStart,
-                                        int itemCount) {
+        public void onItemRangeInserted(final ObservableList sender, final int positionStart,
+                                        final int itemCount) {
             onChanged(sender);
         }
 
         @Override
-        public void onItemRangeMoved(ObservableList sender, int fromPosition, int toPosition,
-                                     int itemCount) {
+        public void onItemRangeMoved(final ObservableList sender, final int fromPosition,
+                                     final int toPosition, final int itemCount) {
             onChanged(sender);
         }
 
         @Override
-        public void onItemRangeRemoved(ObservableList sender, int positionStart, int itemCount) {
+        public void onItemRangeRemoved(final ObservableList 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 {
-    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 {
-    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 {
-    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 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 {
-        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 {
-        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> {
-        @Override
-        protected List doInBackground(File... files) {
-            final List interfaceNames = new LinkedList<>();
-            final List 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 loadedProfiles) {
-            if (loadedProfiles == null)
-                return;
-            for (Profile profile : loadedProfiles)
-                profiles.put(profile.getName(), profile);
-        }
-    }
-
-    private class ProfileRemover extends AsyncTask {
-        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 {
-        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 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 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 output, String... commands) {
+    int run(final List 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 extends Activity implements ServiceConnectionProvider {
-    private final ServiceConnectionCallbacks callbacks = new ServiceConnectionCallbacks();
-    private final List> listeners = new ArrayList<>();
-    private T service;
-    private final Class> serviceClass;
-
-    protected ServiceClientActivity(Class> serviceClass) {
-        this.serviceClass = serviceClass;
-    }
-
-    @Override
-    public void addServiceConnectionListener(ServiceConnectionListener 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 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 listener : listeners)
-                listener.onServiceConnected(service);
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName component) {
-            service = null;
-            for (ServiceConnectionListener 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 extends Fragment implements ServiceConnectionListener {
-    private ServiceConnectionProvider provider;
-    private T service;
-
-    protected T getService() {
-        return service;
-    }
-
-    @Override
-    public void onAttach(Context context) {
-        super.onAttach(context);
-        @SuppressWarnings("unchecked")
-        final ServiceConnectionProvider localContext = (ServiceConnectionProvider) 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 {
-    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 {
-    void addServiceConnectionListener(ServiceConnectionListener listener);
-
-    T getService();
-
-    void removeServiceConnectionListener(ServiceConnectionListener 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 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 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 {
+        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 {
+        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> {
+        @Override
+        protected List doInBackground(final File... files) {
+            final List configs = new LinkedList<>();
+            final List 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 configs) {
+            if (configs == null)
+                return;
+            for (final Config config : configs)
+                configurations.put(config.getName(), config);
+        }
+    }
+
+    private class ConfigRemover extends AsyncTask {
+        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 {
+        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();
+        }
+    }
+}
diff --git a/app/src/main/java/com/wireguard/config/Attribute.java b/app/src/main/java/com/wireguard/config/Attribute.java
index 7629456a..e00ebb4c 100644
--- a/app/src/main/java/com/wireguard/config/Attribute.java
+++ b/app/src/main/java/com/wireguard/config/Attribute.java
@@ -24,23 +24,23 @@ enum Attribute {
 
     static {
         map = new HashMap<>(Attribute.values().length);
-        for (Attribute key : Attribute.values())
+        for (final Attribute key : Attribute.values())
             map.put(key.getToken(), key);
     }
 
-    public static Attribute match(String line) {
+    public static Attribute match(final String line) {
         return map.get(line.split("\\s|=")[0]);
     }
 
     private final String token;
     private final Pattern pattern;
 
-    Attribute(String token) {
-        this.pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
+    Attribute(final String token) {
+        pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
         this.token = token;
     }
 
-    public String composeWith(String value) {
+    public String composeWith(final String value) {
         return token + " = " + value + "\n";
     }
 
@@ -48,7 +48,7 @@ enum Attribute {
         return token;
     }
 
-    public String parseFrom(String line) {
+    public String parseFrom(final String line) {
         final Matcher matcher = pattern.matcher(line);
         if (matcher.matches())
             return matcher.group(1);
diff --git a/app/src/main/java/com/wireguard/config/Profile.java b/app/src/main/java/com/wireguard/config/Config.java
similarity index 51%
rename from app/src/main/java/com/wireguard/config/Profile.java
rename to app/src/main/java/com/wireguard/config/Config.java
index a0388427..3fe069a1 100644
--- a/app/src/main/java/com/wireguard/config/Profile.java
+++ b/app/src/main/java/com/wireguard/config/Config.java
@@ -9,57 +9,49 @@ import android.databinding.ObservableList;
 import com.wireguard.android.BR;
 
 import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
 
 /**
- * Represents a wg-quick profile.
+ * Represents a wg-quick configuration file, its name, and its connection state.
  */
 
-public class Profile extends BaseObservable implements Copyable, Observable {
-    public static boolean isNameValid(String name) {
-        final int IFNAMSIZ = 16;
-        return !name.contains(" ") && name.getBytes(StandardCharsets.UTF_8).length <= IFNAMSIZ;
+public class Config extends BaseObservable implements Copyable, Observable {
+    private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,16}$");
+
+    private static boolean isNameValid(final String name) {
+        return PATTERN.matcher(name).matches();
     }
 
     private final Interface iface = new Interface();
-    private boolean isConnected;
+    private boolean isEnabled;
     private String name;
     private final ObservableList peers = new ObservableArrayList<>();
 
-    public Profile(String name) {
-        super();
-        if (!isNameValid(name))
-            throw new IllegalArgumentException();
-        this.name = name;
+    @Override
+    public Config copy() {
+        final Config copy = new Config();
+        copy.copyFrom(this);
+        return copy;
     }
 
-    private Profile(Profile original)
-            throws IOException {
-        this(original.getName());
-        parseFrom(original);
-    }
-
-    public Profile copy() {
-        try {
-            return new Profile(this);
-        } catch (IOException e) {
-            return null;
-        }
+    @Override
+    public void copyFrom(final Config source) {
+        iface.copyFrom(source.iface);
+        isEnabled = source.isEnabled;
+        name = source.name;
+        peers.clear();
+        for (final Peer peer : source.peers)
+            peers.add(peer.copy());
     }
 
     public Interface getInterface() {
         return iface;
     }
 
-    @Bindable
-    public boolean getIsConnected() {
-        return isConnected;
-    }
-
     @Bindable
     public String getName() {
         return name;
@@ -69,16 +61,24 @@ public class Profile extends BaseObservable implements Copyable, Observ
         return peers;
     }
 
-    public void parseFrom(InputStream stream)
+    @Bindable
+    public boolean isEnabled() {
+        return isEnabled;
+    }
+
+    public void parseFrom(final InputStream stream)
             throws IOException {
+        peers.clear();
         try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(stream, StandardCharsets.UTF_8))) {
             Peer currentPeer = null;
             String line;
             while ((line = reader.readLine()) != null) {
-                if (line.equals("[Interface]")) {
+                if (line.isEmpty())
+                    continue;
+                if ("[Interface]".equals(line)) {
                     currentPeer = null;
-                } else if (line.equals("[Peer]")) {
+                } else if ("[Peer]".equals(line)) {
                     currentPeer = new Peer();
                     peers.add(currentPeer);
                 } else if (currentPeer == null) {
@@ -90,28 +90,23 @@ public class Profile extends BaseObservable implements Copyable, Observ
         }
     }
 
-    public void parseFrom(Profile profile)
-            throws IOException {
-        final byte configBytes[] = profile.toString().getBytes(StandardCharsets.UTF_8);
-        final ByteArrayInputStream configStream = new ByteArrayInputStream(configBytes);
-        parseFrom(configStream);
+    public void setEnabled(final boolean isEnabled) {
+        this.isEnabled = isEnabled;
+        notifyPropertyChanged(BR.enabled);
     }
 
-    public void setIsConnected(boolean isConnected) {
-        this.isConnected = isConnected;
-        notifyPropertyChanged(BR.isConnected);
-    }
-
-    public void setName(String name) {
+    public void setName(final String name) {
+        if (name != null && !name.isEmpty() && !isNameValid(name))
+            throw new IllegalArgumentException();
         this.name = name;
         notifyPropertyChanged(BR.name);
     }
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder().append(iface.toString());
-        for (Peer peer : peers)
-            sb.append('\n').append(peer.toString());
+        final StringBuilder sb = new StringBuilder().append(iface);
+        for (final Peer peer : peers)
+            sb.append('\n').append(peer);
         return sb.toString();
     }
 }
diff --git a/app/src/main/java/com/wireguard/config/Copyable.java b/app/src/main/java/com/wireguard/config/Copyable.java
index bde6dd9e..826cfbb5 100644
--- a/app/src/main/java/com/wireguard/config/Copyable.java
+++ b/app/src/main/java/com/wireguard/config/Copyable.java
@@ -6,4 +6,5 @@ package com.wireguard.config;
 
 public interface Copyable {
     T copy();
+    void copyFrom(T source);
 }
diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java
index 6116a4a7..deb85876 100644
--- a/app/src/main/java/com/wireguard/config/Interface.java
+++ b/app/src/main/java/com/wireguard/config/Interface.java
@@ -12,13 +12,29 @@ import com.wireguard.crypto.KeyEncoding;
  * Represents the configuration for a WireGuard interface (an [Interface] block).
  */
 
-public class Interface extends BaseObservable implements Observable {
+public class Interface extends BaseObservable implements Copyable, Observable {
     private String address;
     private String dns;
     private String listenPort;
     private Keypair keypair;
     private String mtu;
 
+    @Override
+    public Interface copy() {
+        final Interface copy = new Interface();
+        copy.copyFrom(this);
+        return copy;
+    }
+
+    @Override
+    public void copyFrom(final Interface source) {
+        address = source.address;
+        dns = source.dns;
+        listenPort = source.listenPort;
+        keypair = source.keypair;
+        mtu = source.mtu;
+    }
+
     public void generateKeypair() {
         keypair = new Keypair();
         notifyPropertyChanged(BR.privateKey);
@@ -55,7 +71,7 @@ public class Interface extends BaseObservable implements Observable {
         return keypair != null ? keypair.getPublicKey() : null;
     }
 
-    public void parseFrom(String line) {
+    public void parseFrom(final String line) {
         final Attribute key = Attribute.match(line);
         if (key == Attribute.ADDRESS)
             address = key.parseFrom(line);
@@ -67,29 +83,39 @@ public class Interface extends BaseObservable implements Observable {
             mtu = key.parseFrom(line);
         else if (key == Attribute.PRIVATE_KEY)
             keypair = new Keypair(key.parseFrom(line));
+        else
+            throw new IllegalArgumentException(line);
     }
 
     public void setAddress(String address) {
+        if (address != null && address.isEmpty())
+            address = null;
         this.address = address;
         notifyPropertyChanged(BR.address);
     }
 
     public void setDns(String dns) {
+        if (dns != null && dns.isEmpty())
+            dns = null;
         this.dns = dns;
         notifyPropertyChanged(BR.dns);
     }
 
     public void setListenPort(String listenPort) {
+        if (listenPort != null && listenPort.isEmpty())
+            listenPort = null;
         this.listenPort = listenPort;
         notifyPropertyChanged(BR.listenPort);
     }
 
     public void setMtu(String mtu) {
+        if (mtu != null && mtu.isEmpty())
+            mtu = null;
         this.mtu = mtu;
         notifyPropertyChanged(BR.mtu);
     }
 
-    public void setPrivateKey(String privateKey) {
+    public void setPrivateKey(final String privateKey) {
         if (privateKey != null && !privateKey.isEmpty()) {
             // Avoid exceptions from Keypair while the user is typing.
             if (privateKey.length() != KeyEncoding.KEY_LENGTH_BASE64)
diff --git a/app/src/main/java/com/wireguard/config/Peer.java b/app/src/main/java/com/wireguard/config/Peer.java
index 64977a16..5a8c6789 100644
--- a/app/src/main/java/com/wireguard/config/Peer.java
+++ b/app/src/main/java/com/wireguard/config/Peer.java
@@ -10,12 +10,27 @@ import com.android.databinding.library.baseAdapters.BR;
  * Represents the configuration for a WireGuard peer (a [Peer] block).
  */
 
-public class Peer extends BaseObservable implements Observable {
+public class Peer extends BaseObservable implements Copyable, Observable {
     private String allowedIPs;
     private String endpoint;
     private String persistentKeepalive;
     private String publicKey;
 
+    @Override
+    public Peer copy() {
+        final Peer copy = new Peer();
+        copy.copyFrom(this);
+        return copy;
+    }
+
+    @Override
+    public void copyFrom(final Peer source) {
+        allowedIPs = source.allowedIPs;
+        endpoint = source.endpoint;
+        persistentKeepalive = source.persistentKeepalive;
+        publicKey = source.publicKey;
+    }
+
     @Bindable
     public String getAllowedIPs() {
         return allowedIPs;
@@ -36,7 +51,7 @@ public class Peer extends BaseObservable implements Observable {
         return publicKey;
     }
 
-    public void parseFrom(String line) {
+    public void parseFrom(final String line) {
         final Attribute key = Attribute.match(line);
         if (key == Attribute.ALLOWED_IPS)
             allowedIPs = key.parseFrom(line);
@@ -46,24 +61,34 @@ public class Peer extends BaseObservable implements Observable {
             persistentKeepalive = key.parseFrom(line);
         else if (key == Attribute.PUBLIC_KEY)
             publicKey = key.parseFrom(line);
+        else
+            throw new IllegalArgumentException(line);
     }
 
     public void setAllowedIPs(String allowedIPs) {
+        if (allowedIPs != null && allowedIPs.isEmpty())
+            allowedIPs = null;
         this.allowedIPs = allowedIPs;
         notifyPropertyChanged(BR.allowedIPs);
     }
 
     public void setEndpoint(String endpoint) {
+        if (endpoint != null && endpoint.isEmpty())
+            endpoint = null;
         this.endpoint = endpoint;
         notifyPropertyChanged(BR.endpoint);
     }
 
     public void setPersistentKeepalive(String persistentKeepalive) {
+        if (persistentKeepalive != null && persistentKeepalive.isEmpty())
+            persistentKeepalive = null;
         this.persistentKeepalive = persistentKeepalive;
         notifyPropertyChanged(BR.persistentKeepalive);
     }
 
     public void setPublicKey(String publicKey) {
+        if (publicKey != null && publicKey.isEmpty())
+            publicKey = null;
         this.publicKey = publicKey;
         notifyPropertyChanged(BR.publicKey);
     }
diff --git a/app/src/main/java/com/wireguard/crypto/Curve25519.java b/app/src/main/java/com/wireguard/crypto/Curve25519.java
index 17f1b3d7..fdc89635 100644
--- a/app/src/main/java/com/wireguard/crypto/Curve25519.java
+++ b/app/src/main/java/com/wireguard/crypto/Curve25519.java
@@ -38,6 +38,7 @@ import java.util.Arrays;
  *
  * References: http://cr.yp.to/ecdh.html, RFC 7748
  */
+@SuppressWarnings("ALL")
 public final class Curve25519 {
 
 	// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
diff --git a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
index 070a1a99..f83fd0b1 100644
--- a/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
+++ b/app/src/main/java/com/wireguard/crypto/KeyEncoding.java
@@ -28,10 +28,10 @@ public class KeyEncoding {
     }
 
     private static void encodeBase64(final byte[] src, final int src_offset,
-                                     char[] dest, final int dest_offset) {
+                                     final char[] dest, final int dest_offset) {
         final byte[] input = {
-                (byte) ((src[0 + src_offset] >>> 2) & 63),
-                (byte) ((src[0 + src_offset] << 4 | ((src[1 + src_offset] & 0xff) >>> 4)) & 63),
+                (byte) ((src[src_offset] >>> 2) & 63),
+                (byte) ((src[src_offset] << 4 | ((src[1 + src_offset] & 0xff) >>> 4)) & 63),
                 (byte) ((src[1 + src_offset] << 2 | ((src[2 + src_offset] & 0xff) >>> 6)) & 63),
                 (byte) ((src[2 + src_offset]) & 63),
         };
@@ -54,12 +54,12 @@ public class KeyEncoding {
             final int val = decodeBase64(input, i * 4);
             if (val < 0)
                 throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
-            key[i * 3 + 0] = (byte) ((val >>> 16) & 0xff);
+            key[i * 3] = (byte) ((val >>> 16) & 0xff);
             key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
             key[i * 3 + 2] = (byte) (val & 0xff);
         }
         final char[] endSegment = {
-                input[i * 4 + 0],
+                input[i * 4],
                 input[i * 4 + 1],
                 input[i * 4 + 2],
                 'A',
@@ -67,7 +67,7 @@ public class KeyEncoding {
         final int val = decodeBase64(endSegment, 0);
         if (val < 0 || (val & 0xff) != 0)
             throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
-        key[i * 3 + 0] = (byte) ((val >>> 16) & 0xff);
+        key[i * 3] = (byte) ((val >>> 16) & 0xff);
         key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
         return key;
     }
@@ -80,7 +80,7 @@ public class KeyEncoding {
         for (i = 0; i < KEY_LENGTH / 3; ++i)
             encodeBase64(key, i * 3, output, i * 4);
         final byte[] endSegment = {
-                key[i * 3 + 0],
+                key[i * 3],
                 key[i * 3 + 1],
                 0,
         };
diff --git a/app/src/main/java/com/wireguard/crypto/Keypair.java b/app/src/main/java/com/wireguard/crypto/Keypair.java
index bd6fd90f..e0d35d64 100644
--- a/app/src/main/java/com/wireguard/crypto/Keypair.java
+++ b/app/src/main/java/com/wireguard/crypto/Keypair.java
@@ -17,7 +17,7 @@ public class Keypair {
         return privateKey;
     }
 
-    private static byte[] generatePublicKey(byte[] privateKey) {
+    private static byte[] generatePublicKey(final byte[] privateKey) {
         final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
         Curve25519.eval(publicKey, 0, privateKey, null);
         return publicKey;
@@ -30,12 +30,12 @@ public class Keypair {
         this(generatePrivateKey());
     }
 
-    private Keypair(byte[] privateKey) {
+    private Keypair(final byte[] privateKey) {
         this.privateKey = privateKey;
         publicKey = generatePublicKey(privateKey);
     }
 
-    public Keypair(String privateKey) {
+    public Keypair(final String privateKey) {
         this(KeyEncoding.keyFromBase64(privateKey));
     }
 
diff --git a/app/src/main/res/layout-land/profile_list_activity.xml b/app/src/main/res/layout-land/config_activity.xml
similarity index 70%
rename from app/src/main/res/layout-land/profile_list_activity.xml
rename to app/src/main/res/layout-land/config_activity.xml
index 3f02dcca..b9c729fa 100644
--- a/app/src/main/res/layout-land/profile_list_activity.xml
+++ b/app/src/main/res/layout-land/config_activity.xml
@@ -1,19 +1,21 @@
 
 
 
     
 
     
+        android:layout_weight="2"
+        tools:ignore="InconsistentLayout" />
 
diff --git a/app/src/main/res/layout/profile_list_activity.xml b/app/src/main/res/layout/config_activity.xml
similarity index 83%
rename from app/src/main/res/layout/profile_list_activity.xml
rename to app/src/main/res/layout/config_activity.xml
index 41d772a7..d67e64bc 100644
--- a/app/src/main/res/layout/profile_list_activity.xml
+++ b/app/src/main/res/layout/config_activity.xml
@@ -1,5 +1,5 @@
 
 
diff --git a/app/src/main/res/layout/profile_detail_fragment.xml b/app/src/main/res/layout/config_detail_fragment.xml
similarity index 60%
rename from app/src/main/res/layout/profile_detail_fragment.xml
rename to app/src/main/res/layout/config_detail_fragment.xml
index c08236ce..1bae0450 100644
--- a/app/src/main/res/layout/profile_detail_fragment.xml
+++ b/app/src/main/res/layout/config_detail_fragment.xml
@@ -1,44 +1,43 @@
 
-
+
 
     
 
         
+            name="config"
+            type="com.wireguard.config.Config" />
     
 
     
+        android:layout_height="match_parent">
 
         
+            android:layout_height="wrap_content"
+            android:padding="16dp">
 
             
+                android:labelFor="@+id/config_name_text"
+                android:text="@string/config_name" />
 
             
+                android:layout_below="@+id/config_name_label"
+                android:text="@{config.name}" />
 
             
 
@@ -50,7 +49,13 @@
                 android:layout_below="@+id/public_key_label"
                 android:ellipsize="end"
                 android:maxLines="1"
-                android:text="@{profile.interface.publicKey}" />
+                android:text="@{config.interface.publicKey}" />
+
+            
         
     
 
diff --git a/app/src/main/res/layout/config_edit_fragment.xml b/app/src/main/res/layout/config_edit_fragment.xml
new file mode 100644
index 00000000..3351c008
--- /dev/null
+++ b/app/src/main/res/layout/config_edit_fragment.xml
@@ -0,0 +1,164 @@
+
+
+
+    
+
+        
+    
+
+    
+
+        
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+        
+    
+
diff --git a/app/src/main/res/layout/profile_list_fragment.xml b/app/src/main/res/layout/config_list_fragment.xml
similarity index 65%
rename from app/src/main/res/layout/profile_list_fragment.xml
rename to app/src/main/res/layout/config_list_fragment.xml
index f5954092..155e708a 100644
--- a/app/src/main/res/layout/profile_list_fragment.xml
+++ b/app/src/main/res/layout/config_list_fragment.xml
@@ -6,14 +6,15 @@
 
         
         
+            name="configs"
+            type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Config>" />
     
 
     
+        android:choiceMode="singleChoice"
+        app:items="@{configs}"
+        app:layout="@{@layout/config_list_item}" />
 
diff --git a/app/src/main/res/layout/profile_list_item.xml b/app/src/main/res/layout/config_list_item.xml
similarity index 65%
rename from app/src/main/res/layout/profile_list_item.xml
rename to app/src/main/res/layout/config_list_item.xml
index 22e93f5c..33f447c8 100644
--- a/app/src/main/res/layout/profile_list_item.xml
+++ b/app/src/main/res/layout/config_list_item.xml
@@ -5,17 +5,16 @@
 
         
+            type="com.wireguard.config.Config" />
     
 
     
 
         
+            android:textColor="@{item.isEnabled ? @android:color/holo_green_dark : @android:color/holo_red_dark}" />
     
 
diff --git a/app/src/main/res/layout/profile_detail_activity.xml b/app/src/main/res/layout/profile_detail_activity.xml
deleted file mode 100644
index 0be8d4bd..00000000
--- a/app/src/main/res/layout/profile_detail_activity.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/profile_edit_activity.xml b/app/src/main/res/layout/profile_edit_activity.xml
deleted file mode 100644
index aee54638..00000000
--- a/app/src/main/res/layout/profile_edit_activity.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/profile_edit_fragment.xml b/app/src/main/res/layout/profile_edit_fragment.xml
deleted file mode 100644
index 2c25257b..00000000
--- a/app/src/main/res/layout/profile_edit_fragment.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-    
-
-        
-    
-
-    
-
-        
-
-            
-
-            
-        
-    
-
diff --git a/app/src/main/res/menu/profile_detail.xml b/app/src/main/res/menu/config_detail.xml
similarity index 100%
rename from app/src/main/res/menu/profile_detail.xml
rename to app/src/main/res/menu/config_detail.xml
diff --git a/app/src/main/res/menu/profile_edit.xml b/app/src/main/res/menu/config_edit.xml
similarity index 100%
rename from app/src/main/res/menu/profile_edit.xml
rename to app/src/main/res/menu/config_edit.xml
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 32543006..9b3d1448 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,12 +1,20 @@
 
 
     WireGuard
+    Configuration name
     Connected
     Disconnected
+    DNS servers
     Edit
-    Edit WireGuard Profile
-    No profile selected
-    Profile name
+    Generate
+    (auto)
+    (generated)
+    (optional)
+    (random)
+    Listen port
+    MTU
+    No configuration selected
+    Private key
     Public key
     Save
     Settings