Apparently "configuration" is the proper term, not "profile". Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -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> | |||
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
@@ -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; | |||
} | |||
private Profile(Profile original) | |||
throws IOException { | |||
this(original.getName()); | |||
parseFrom(original); | |||
@Override | |||
public Config copy() { | |||
final Config copy = new Config(); | |||
copy.copyFrom(this); | |||
return copy; | |||
} | |||
public Profile copy() { | |||
try { | |||
return new Profile(this); | |||
} catch (IOException e) { | |||
return null; | |||
} | |||
@Override | |||
public void copyFrom(final Config source) { | |||
iface.copyFrom(source.iface); | |||
isEnabled = source.isEnabled; | |||
name = source.name; | |||
peers.clear(); | |||
for (final Peer peer : source.peers) | |||
peers.add(peer.copy()); | |||
} | |||
public Interface getInterface() { | |||
return iface; | |||
} | |||
@Bindable | |||
public boolean getIsConnected() { | |||
return isConnected; | |||
} | |||
@Bindable | |||
public String getName() { | |||
return name; | |||
@@ -69,16 +61,24 @@ public class Profile extends BaseObservable implements Copyable<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 setIsConnected(boolean isConnected) { | |||
this.isConnected = isConnected; | |||
notifyPropertyChanged(BR.isConnected); | |||
public void setEnabled(final boolean isEnabled) { | |||
this.isEnabled = isEnabled; | |||
notifyPropertyChanged(BR.enabled); | |||
} | |||
public void setName(String name) { | |||
public void setName(final String name) { | |||
if (name != null && !name.isEmpty() && !isNameValid(name)) | |||
throw new IllegalArgumentException(); | |||
this.name = name; | |||
notifyPropertyChanged(BR.name); | |||
} | |||
@Override | |||
public String toString() { | |||
StringBuilder sb = new StringBuilder().append(iface.toString()); | |||
for (Peer peer : peers) | |||
sb.append('\n').append(peer.toString()); | |||
final StringBuilder sb = new StringBuilder().append(iface); | |||
for (final Peer peer : peers) | |||
sb.append('\n').append(peer); | |||
return sb.toString(); | |||
} | |||
} |
@@ -6,4 +6,5 @@ package com.wireguard.config; | |||
public interface Copyable<T> { | |||
T copy(); | |||
void copyFrom(T source); | |||
} |
@@ -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) | |||
@@ -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); | |||
} | |||
@@ -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. | |||
@@ -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, | |||
}; | |||
@@ -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)); | |||
} | |||
@@ -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> |
@@ -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" /> |
@@ -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> |
@@ -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> |
@@ -6,14 +6,15 @@ | |||
<!--suppress AndroidDomInspection --> | |||
<variable | |||
name="profiles" | |||
type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Profile>" /> | |||
name="configs" | |||
type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Config>" /> | |||
</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> |
@@ -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> |
@@ -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" /> |
@@ -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" /> |
@@ -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> |
@@ -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> | |||