diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93973b7..6985965 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 0000000..8359c34 --- /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 0000000..4a754b6 --- /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 77c6f65..6cd4a70 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 6488736..68cb5f1 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 0000000..f672c59 --- /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 0000000..f7f6e6e --- /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 0000000..395358f --- /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 0000000..870453d --- /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 d2a5a4c..dd3a380 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 475bafb..66cb957 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 e17aac0..db5d7b3 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 29d249d..0000000 --- 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 3e70de9..0000000 --- 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 0fed770..0000000 --- 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 3462027..0000000 --- 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 2249a8a..0000000 --- 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 0e9092e..0000000 --- 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 4965120..0000000 --- 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 be1358a..0000000 --- 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 984cf4b..0000000 --- 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 65dc27a..0000000 --- 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 f5879f0..973a5d0 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 263e012..0000000 --- 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 7377151..0000000 --- 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 c77fe93..0000000 --- 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 79a0efd..0000000 --- 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 c91a1bd..44e9b9b 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 0000000..2f3d97c --- /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 7629456..e00ebb4 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 a038842..3fe069a 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; - } - - private Profile(Profile original) - throws IOException { - this(original.getName()); - parseFrom(original); + @Override + public Config copy() { + final Config copy = new Config(); + copy.copyFrom(this); + return copy; } - 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 setIsConnected(boolean isConnected) { - this.isConnected = isConnected; - notifyPropertyChanged(BR.isConnected); + public void setEnabled(final boolean isEnabled) { + this.isEnabled = isEnabled; + notifyPropertyChanged(BR.enabled); } - 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 bde6dd9..826cfbb 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 6116a4a..deb8587 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 64977a1..5a8c678 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 17f1b3d..fdc8963 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 070a1a9..f83fd0b 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 bd6fd90..e0d35d6 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 3f02dcc..b9c729f 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 41d772a..d67e64b 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 c08236c..1bae045 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 0000000..3351c00 --- /dev/null +++ b/app/src/main/res/layout/config_edit_fragment.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + +