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>
This commit is contained in:
Samuel Holland
2017-08-13 07:24:03 -05:00
parent c72d30a1af
commit 5e55d196be
49 changed files with 1193 additions and 1218 deletions
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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.
}
}
@@ -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));
}
}
@@ -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());
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
}
@@ -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;
}
}
@@ -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();
}
}
}
}
@@ -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);
}
@@ -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");
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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);
}
@@ -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);
}
}
@@ -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();
}
}
}