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

View File

@ -12,14 +12,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.DarkActionBar">
<activity
android:name=".ProfileDetailActivity"
android:label=""
android:parentActivityName=".ProfileListActivity" />
<activity
android:name=".ProfileEditActivity"
android:label="@string/edit_activity_title" />
<activity android:name=".ProfileListActivity">
<activity android:name=".ConfigActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -37,7 +30,7 @@
</receiver>
<service
android:name=".ProfileService"
android:name=".VpnService"
android:exported="false" />
</application>

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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.
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}

View File

@ -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");

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -24,23 +24,23 @@ enum Attribute {
static {
map = new HashMap<>(Attribute.values().length);
for (Attribute key : Attribute.values())
for (final Attribute key : Attribute.values())
map.put(key.getToken(), key);
}
public static Attribute match(String line) {
public static Attribute match(final String line) {
return map.get(line.split("\\s|=")[0]);
}
private final String token;
private final Pattern pattern;
Attribute(String token) {
this.pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
Attribute(final String token) {
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
this.token = token;
}
public String composeWith(String value) {
public String composeWith(final String value) {
return token + " = " + value + "\n";
}
@ -48,7 +48,7 @@ enum Attribute {
return token;
}
public String parseFrom(String line) {
public String parseFrom(final String line) {
final Matcher matcher = pattern.matcher(line);
if (matcher.matches())
return matcher.group(1);

View File

@ -9,57 +9,49 @@ import android.databinding.ObservableList;
import com.wireguard.android.BR;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
/**
* Represents a wg-quick profile.
* Represents a wg-quick configuration file, its name, and its connection state.
*/
public class Profile extends BaseObservable implements Copyable<Profile>, Observable {
public static boolean isNameValid(String name) {
final int IFNAMSIZ = 16;
return !name.contains(" ") && name.getBytes(StandardCharsets.UTF_8).length <= IFNAMSIZ;
public class Config extends BaseObservable implements Copyable<Config>, Observable {
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,16}$");
private static boolean isNameValid(final String name) {
return PATTERN.matcher(name).matches();
}
private final Interface iface = new Interface();
private boolean isConnected;
private boolean isEnabled;
private String name;
private final ObservableList<Peer> peers = new ObservableArrayList<>();
public Profile(String name) {
super();
if (!isNameValid(name))
throw new IllegalArgumentException();
this.name = name;
@Override
public Config copy() {
final Config copy = new Config();
copy.copyFrom(this);
return copy;
}
private Profile(Profile original)
throws IOException {
this(original.getName());
parseFrom(original);
}
public Profile copy() {
try {
return new Profile(this);
} catch (IOException e) {
return null;
}
@Override
public void copyFrom(final Config source) {
iface.copyFrom(source.iface);
isEnabled = source.isEnabled;
name = source.name;
peers.clear();
for (final Peer peer : source.peers)
peers.add(peer.copy());
}
public Interface getInterface() {
return iface;
}
@Bindable
public boolean getIsConnected() {
return isConnected;
}
@Bindable
public String getName() {
return name;
@ -69,16 +61,24 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
return peers;
}
public void parseFrom(InputStream stream)
@Bindable
public boolean isEnabled() {
return isEnabled;
}
public void parseFrom(final InputStream stream)
throws IOException {
peers.clear();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
Peer currentPeer = null;
String line;
while ((line = reader.readLine()) != null) {
if (line.equals("[Interface]")) {
if (line.isEmpty())
continue;
if ("[Interface]".equals(line)) {
currentPeer = null;
} else if (line.equals("[Peer]")) {
} else if ("[Peer]".equals(line)) {
currentPeer = new Peer();
peers.add(currentPeer);
} else if (currentPeer == null) {
@ -90,28 +90,23 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
}
}
public void parseFrom(Profile profile)
throws IOException {
final byte configBytes[] = profile.toString().getBytes(StandardCharsets.UTF_8);
final ByteArrayInputStream configStream = new ByteArrayInputStream(configBytes);
parseFrom(configStream);
public void setEnabled(final boolean isEnabled) {
this.isEnabled = isEnabled;
notifyPropertyChanged(BR.enabled);
}
public void setIsConnected(boolean isConnected) {
this.isConnected = isConnected;
notifyPropertyChanged(BR.isConnected);
}
public void setName(String name) {
public void setName(final String name) {
if (name != null && !name.isEmpty() && !isNameValid(name))
throw new IllegalArgumentException();
this.name = name;
notifyPropertyChanged(BR.name);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder().append(iface.toString());
for (Peer peer : peers)
sb.append('\n').append(peer.toString());
final StringBuilder sb = new StringBuilder().append(iface);
for (final Peer peer : peers)
sb.append('\n').append(peer);
return sb.toString();
}
}

View File

@ -6,4 +6,5 @@ package com.wireguard.config;
public interface Copyable<T> {
T copy();
void copyFrom(T source);
}

View File

@ -12,13 +12,29 @@ import com.wireguard.crypto.KeyEncoding;
* Represents the configuration for a WireGuard interface (an [Interface] block).
*/
public class Interface extends BaseObservable implements Observable {
public class Interface extends BaseObservable implements Copyable<Interface>, Observable {
private String address;
private String dns;
private String listenPort;
private Keypair keypair;
private String mtu;
@Override
public Interface copy() {
final Interface copy = new Interface();
copy.copyFrom(this);
return copy;
}
@Override
public void copyFrom(final Interface source) {
address = source.address;
dns = source.dns;
listenPort = source.listenPort;
keypair = source.keypair;
mtu = source.mtu;
}
public void generateKeypair() {
keypair = new Keypair();
notifyPropertyChanged(BR.privateKey);
@ -55,7 +71,7 @@ public class Interface extends BaseObservable implements Observable {
return keypair != null ? keypair.getPublicKey() : null;
}
public void parseFrom(String line) {
public void parseFrom(final String line) {
final Attribute key = Attribute.match(line);
if (key == Attribute.ADDRESS)
address = key.parseFrom(line);
@ -67,29 +83,39 @@ public class Interface extends BaseObservable implements Observable {
mtu = key.parseFrom(line);
else if (key == Attribute.PRIVATE_KEY)
keypair = new Keypair(key.parseFrom(line));
else
throw new IllegalArgumentException(line);
}
public void setAddress(String address) {
if (address != null && address.isEmpty())
address = null;
this.address = address;
notifyPropertyChanged(BR.address);
}
public void setDns(String dns) {
if (dns != null && dns.isEmpty())
dns = null;
this.dns = dns;
notifyPropertyChanged(BR.dns);
}
public void setListenPort(String listenPort) {
if (listenPort != null && listenPort.isEmpty())
listenPort = null;
this.listenPort = listenPort;
notifyPropertyChanged(BR.listenPort);
}
public void setMtu(String mtu) {
if (mtu != null && mtu.isEmpty())
mtu = null;
this.mtu = mtu;
notifyPropertyChanged(BR.mtu);
}
public void setPrivateKey(String privateKey) {
public void setPrivateKey(final String privateKey) {
if (privateKey != null && !privateKey.isEmpty()) {
// Avoid exceptions from Keypair while the user is typing.
if (privateKey.length() != KeyEncoding.KEY_LENGTH_BASE64)

View File

@ -10,12 +10,27 @@ import com.android.databinding.library.baseAdapters.BR;
* Represents the configuration for a WireGuard peer (a [Peer] block).
*/
public class Peer extends BaseObservable implements Observable {
public class Peer extends BaseObservable implements Copyable<Peer>, Observable {
private String allowedIPs;
private String endpoint;
private String persistentKeepalive;
private String publicKey;
@Override
public Peer copy() {
final Peer copy = new Peer();
copy.copyFrom(this);
return copy;
}
@Override
public void copyFrom(final Peer source) {
allowedIPs = source.allowedIPs;
endpoint = source.endpoint;
persistentKeepalive = source.persistentKeepalive;
publicKey = source.publicKey;
}
@Bindable
public String getAllowedIPs() {
return allowedIPs;
@ -36,7 +51,7 @@ public class Peer extends BaseObservable implements Observable {
return publicKey;
}
public void parseFrom(String line) {
public void parseFrom(final String line) {
final Attribute key = Attribute.match(line);
if (key == Attribute.ALLOWED_IPS)
allowedIPs = key.parseFrom(line);
@ -46,24 +61,34 @@ public class Peer extends BaseObservable implements Observable {
persistentKeepalive = key.parseFrom(line);
else if (key == Attribute.PUBLIC_KEY)
publicKey = key.parseFrom(line);
else
throw new IllegalArgumentException(line);
}
public void setAllowedIPs(String allowedIPs) {
if (allowedIPs != null && allowedIPs.isEmpty())
allowedIPs = null;
this.allowedIPs = allowedIPs;
notifyPropertyChanged(BR.allowedIPs);
}
public void setEndpoint(String endpoint) {
if (endpoint != null && endpoint.isEmpty())
endpoint = null;
this.endpoint = endpoint;
notifyPropertyChanged(BR.endpoint);
}
public void setPersistentKeepalive(String persistentKeepalive) {
if (persistentKeepalive != null && persistentKeepalive.isEmpty())
persistentKeepalive = null;
this.persistentKeepalive = persistentKeepalive;
notifyPropertyChanged(BR.persistentKeepalive);
}
public void setPublicKey(String publicKey) {
if (publicKey != null && publicKey.isEmpty())
publicKey = null;
this.publicKey = publicKey;
notifyPropertyChanged(BR.publicKey);
}

View File

@ -38,6 +38,7 @@ import java.util.Arrays;
*
* References: http://cr.yp.to/ecdh.html, RFC 7748
*/
@SuppressWarnings("ALL")
public final class Curve25519 {
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.

View File

@ -28,10 +28,10 @@ public class KeyEncoding {
}
private static void encodeBase64(final byte[] src, final int src_offset,
char[] dest, final int dest_offset) {
final char[] dest, final int dest_offset) {
final byte[] input = {
(byte) ((src[0 + src_offset] >>> 2) & 63),
(byte) ((src[0 + src_offset] << 4 | ((src[1 + src_offset] & 0xff) >>> 4)) & 63),
(byte) ((src[src_offset] >>> 2) & 63),
(byte) ((src[src_offset] << 4 | ((src[1 + src_offset] & 0xff) >>> 4)) & 63),
(byte) ((src[1 + src_offset] << 2 | ((src[2 + src_offset] & 0xff) >>> 6)) & 63),
(byte) ((src[2 + src_offset]) & 63),
};
@ -54,12 +54,12 @@ public class KeyEncoding {
final int val = decodeBase64(input, i * 4);
if (val < 0)
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
key[i * 3 + 0] = (byte) ((val >>> 16) & 0xff);
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
key[i * 3 + 2] = (byte) (val & 0xff);
}
final char[] endSegment = {
input[i * 4 + 0],
input[i * 4],
input[i * 4 + 1],
input[i * 4 + 2],
'A',
@ -67,7 +67,7 @@ public class KeyEncoding {
final int val = decodeBase64(endSegment, 0);
if (val < 0 || (val & 0xff) != 0)
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
key[i * 3 + 0] = (byte) ((val >>> 16) & 0xff);
key[i * 3] = (byte) ((val >>> 16) & 0xff);
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
return key;
}
@ -80,7 +80,7 @@ public class KeyEncoding {
for (i = 0; i < KEY_LENGTH / 3; ++i)
encodeBase64(key, i * 3, output, i * 4);
final byte[] endSegment = {
key[i * 3 + 0],
key[i * 3],
key[i * 3 + 1],
0,
};

View File

@ -17,7 +17,7 @@ public class Keypair {
return privateKey;
}
private static byte[] generatePublicKey(byte[] privateKey) {
private static byte[] generatePublicKey(final byte[] privateKey) {
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
Curve25519.eval(publicKey, 0, privateKey, null);
return publicKey;
@ -30,12 +30,12 @@ public class Keypair {
this(generatePrivateKey());
}
private Keypair(byte[] privateKey) {
private Keypair(final byte[] privateKey) {
this.privateKey = privateKey;
publicKey = generatePublicKey(privateKey);
}
public Keypair(String privateKey) {
public Keypair(final String privateKey) {
this(KeyEncoding.keyFromBase64(privateKey));
}

View File

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/list_container"
android:id="@+id/master_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/fragment_container"
android:id="@+id/detail_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
android:layout_weight="2"
tools:ignore="InconsistentLayout" />
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_container"
android:id="@+id/master_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -1,44 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="profile"
type="com.wireguard.config.Profile" />
name="config"
type="com.wireguard.config.Config" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/profile_name_label"
android:id="@+id/config_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:labelFor="@+id/profile_name_text"
android:text="@string/profile_name" />
android:labelFor="@+id/config_name_text"
android:text="@string/config_name" />
<TextView
android:id="@+id/profile_name_text"
android:id="@+id/config_name_text"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/profile_name_label"
android:text="@{profile.name}" />
android:layout_below="@+id/config_name_label"
android:text="@{config.name}" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/profile_name_text"
android:layout_below="@+id/config_name_text"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
@ -50,7 +49,13 @@
android:layout_below="@+id/public_key_label"
android:ellipsize="end"
android:maxLines="1"
android:text="@{profile.interface.publicKey}" />
android:text="@{config.interface.publicKey}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/public_key_text"
android:text="@{config.toString()}" />
</RelativeLayout>
</ScrollView>
</layout>

View File

@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="config"
type="com.wireguard.config.Config" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/config_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:labelFor="@+id/config_name_text"
android:text="@string/config_name" />
<EditText
android:id="@+id/config_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/config_name_label"
android:inputType="textCapWords"
android:text="@={config.name}" />
<TextView
android:id="@+id/private_key_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/config_name_text"
android:labelFor="@+id/private_key_text"
android:text="@string/private_key" />
<EditText
android:id="@+id/private_key_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_label"
android:layout_toStartOf="@+id/generate_private_key_button"
android:inputType="textVisiblePassword"
android:text="@={config.interface.privateKey}" />
<Button
android:id="@+id/generate_private_key_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/private_key_text"
android:layout_alignParentEnd="true"
android:layout_below="@+id/private_key_label"
android:onClick="@{() -> config.interface.generateKeypair()}"
android:text="@string/generate" />
<TextView
android:id="@+id/public_key_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/private_key_text"
android:layout_toStartOf="@+id/listen_port_label"
android:labelFor="@+id/public_key_text"
android:text="@string/public_key" />
<TextView
android:id="@+id/public_key_text"
style="@android:style/Widget.EditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_label"
android:layout_toStartOf="@+id/listen_port_text"
android:ellipsize="end"
android:focusable="false"
android:hint="@string/hint_generated"
android:maxLines="1"
android:text="@{config.interface.publicKey}" />
<TextView
android:id="@+id/listen_port_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/public_key_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:layout_below="@+id/generate_private_key_button"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port" />
<EditText
android:id="@+id/listen_port_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/public_key_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:layout_below="@+id/listen_port_label"
android:hint="@string/hint_random"
android:inputType="number"
android:text="@={config.interface.listenPort}"
android:textAlignment="center" />
<TextView
android:id="@+id/dns_server_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/public_key_text"
android:layout_toStartOf="@+id/mtu_label"
android:labelFor="@+id/dns_server_text"
android:text="@string/dns_servers" />
<EditText
android:id="@+id/dns_server_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/dns_server_label"
android:layout_toStartOf="@+id/mtu_text"
android:hint="@string/hint_optional"
android:inputType="text"
android:text="@={config.interface.dns}" />
<TextView
android:id="@+id/mtu_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_server_label"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:layout_below="@+id/listen_port_text"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu" />
<EditText
android:id="@+id/mtu_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/dns_server_text"
android:layout_alignParentEnd="true"
android:layout_alignStart="@+id/generate_private_key_button"
android:layout_below="@+id/mtu_label"
android:hint="@string/hint_automatic"
android:inputType="number"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/dns_server_text"
android:text="@{config.toString()}" />
</RelativeLayout>
</ScrollView>
</layout>

View File

@ -6,14 +6,15 @@
<!--suppress AndroidDomInspection -->
<variable
name="profiles"
type="android.databinding.ObservableArrayMap&lt;String, com.wireguard.config.Profile&gt;" />
name="configs"
type="android.databinding.ObservableArrayMap&lt;String, com.wireguard.config.Config&gt;" />
</data>
<ListView
android:id="@+id/profile_list"
android:id="@+id/config_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{profiles}"
app:layout="@{@layout/profile_list_item}" />
android:choiceMode="singleChoice"
app:items="@{configs}"
app:layout="@{@layout/config_list_item}" />
</layout>

View File

@ -5,17 +5,16 @@
<variable
name="item"
type="com.wireguard.config.Profile" />
type="com.wireguard.config.Config" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:padding="16dp">
<TextView
android:id="@+id/profile_name"
android:id="@+id/config_name"
style="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -26,9 +25,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@+id/profile_name"
android:text="@{item.isConnected ? @string/connected : @string/disconnected}"
android:layout_toEndOf="@+id/config_name"
android:text="@{item.isEnabled ? @string/connected : @string/disconnected}"
android:textAlignment="textEnd"
android:textColor="@{item.isConnected ? @android:color/holo_green_dark : @android:color/holo_red_dark}" />
android:textColor="@{item.isEnabled ? @android:color/holo_green_dark : @android:color/holo_red_dark}" />
</RelativeLayout>
</layout>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.wireguard.android.ProfileDetailFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="detail" />

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.wireguard.android.ProfileEditFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="edit" />

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="profile"
type="com.wireguard.config.Profile" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/profile_name_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:labelFor="@+id/profile_name_text"
android:text="@string/profile_name" />
<EditText
android:id="@+id/profile_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/profile_name_label"
android:inputType="textCapWords"
android:text="@={profile.name}" />
</RelativeLayout>
</ScrollView>
</layout>

View File

@ -1,12 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WireGuard</string>
<string name="config_name">Configuration name</string>
<string name="connected">Connected</string>
<string name="disconnected">Disconnected</string>
<string name="dns_servers">DNS servers</string>
<string name="edit">Edit</string>
<string name="edit_activity_title">Edit WireGuard Profile</string>
<string name="placeholder_text">No profile selected</string>
<string name="profile_name">Profile name</string>
<string name="generate">Generate</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generated)</string>
<string name="hint_optional">(optional)</string>
<string name="hint_random">(random)</string>
<string name="listen_port">Listen port</string>
<string name="mtu">MTU</string>
<string name="placeholder_text">No configuration selected</string>
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="save">Save</string>
<string name="settings">Settings</string>