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