Major renaming and refactoring in activity and service
Apparently "configuration" is the proper term, not "profile". Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
c72d30a1af
commit
5e55d196be
@ -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));
|
||||
}
|
||||
}
|
||||
|
158
app/src/main/java/com/wireguard/android/ConfigActivity.java
Normal file
158
app/src/main/java/com/wireguard/android/ConfigActivity.java
Normal file
@ -0,0 +1,158 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Activity that allows creating/viewing/editing/deleting WireGuard configurations.
|
||||
*/
|
||||
|
||||
public class ConfigActivity extends BaseConfigActivity {
|
||||
private boolean canAddFragments;
|
||||
private int containerId;
|
||||
private final FragmentManager fm = getFragmentManager();
|
||||
private boolean isEditing;
|
||||
private boolean isSplitLayout;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
// Make sure the current config is cleared when going back to the list.
|
||||
if (isEditing)
|
||||
isEditing = false;
|
||||
else
|
||||
setCurrentConfig(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.config_activity);
|
||||
isSplitLayout = findViewById(R.id.detail_fragment) != null;
|
||||
if (isSplitLayout)
|
||||
containerId = R.id.detail_fragment;
|
||||
else
|
||||
containerId = R.id.master_fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCurrentConfigChanged(final Config config) {
|
||||
if (!canAddFragments)
|
||||
return;
|
||||
final Fragment currentFragment = fm.findFragmentById(containerId);
|
||||
Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" +
|
||||
(config != null ? config.getName() : null) + " fragment=" + currentFragment);
|
||||
if (currentFragment instanceof ConfigDetailFragment) {
|
||||
// Handle the case when the split layout is switching from one config to another.
|
||||
final ConfigDetailFragment detailFragment = (ConfigDetailFragment) currentFragment;
|
||||
if (detailFragment.getCurrentConfig() != config)
|
||||
detailFragment.setCurrentConfig(config);
|
||||
} else if (currentFragment instanceof ConfigEditFragment) {
|
||||
// Handle the case when ConfigEditFragment is finished updating a config.
|
||||
fm.popBackStack();
|
||||
isEditing = false;
|
||||
final ConfigDetailFragment detailFragment =
|
||||
(ConfigDetailFragment) fm.findFragmentByTag(TAG_DETAIL);
|
||||
if (detailFragment.getCurrentConfig() != config)
|
||||
detailFragment.setCurrentConfig(config);
|
||||
} else if (config != null) {
|
||||
// Handle the single-fragment-layout case and the case when a placeholder is replaced.
|
||||
ConfigDetailFragment detailFragment =
|
||||
(ConfigDetailFragment) fm.findFragmentByTag(TAG_DETAIL);
|
||||
if (detailFragment != null) {
|
||||
detailFragment.setCurrentConfig(config);
|
||||
} else {
|
||||
detailFragment = new ConfigDetailFragment();
|
||||
final Bundle arguments = new Bundle();
|
||||
arguments.putString(KEY_CURRENT_CONFIG, config.getName());
|
||||
detailFragment.setArguments(arguments);
|
||||
}
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
if (!isSplitLayout)
|
||||
transaction.addToBackStack(TAG_DETAIL);
|
||||
transaction.replace(containerId, detailFragment, TAG_DETAIL);
|
||||
transaction.commit();
|
||||
} else {
|
||||
if (isSplitLayout) {
|
||||
// Handle the split layout case when there is no config, so a placeholder is shown.
|
||||
PlaceholderFragment placeholderFragment =
|
||||
(PlaceholderFragment) fm.findFragmentByTag(TAG_PLACEHOLDER);
|
||||
if (placeholderFragment == null)
|
||||
placeholderFragment = new PlaceholderFragment();
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
transaction.replace(containerId, placeholderFragment, TAG_PLACEHOLDER);
|
||||
transaction.commit();
|
||||
}
|
||||
}
|
||||
// If the config change came from the intent or ConfigEditFragment, forward it to the list.
|
||||
ConfigListFragment listFragment = (ConfigListFragment) fm.findFragmentByTag(TAG_LIST);
|
||||
if (listFragment == null) {
|
||||
listFragment = new ConfigListFragment();
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
transaction.replace(R.id.master_fragment, listFragment, TAG_LIST);
|
||||
transaction.commit();
|
||||
}
|
||||
if (listFragment.getCurrentConfig() != config)
|
||||
listFragment.setCurrentConfig(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_edit:
|
||||
ConfigEditFragment editFragment =
|
||||
(ConfigEditFragment) fm.findFragmentByTag(TAG_EDIT);
|
||||
if (editFragment != null) {
|
||||
editFragment.setCurrentConfig(getCurrentConfig());
|
||||
} else {
|
||||
editFragment = new ConfigEditFragment();
|
||||
final Bundle arguments = new Bundle();
|
||||
arguments.putString(KEY_CURRENT_CONFIG, getCurrentConfig().getName());
|
||||
editFragment.setArguments(arguments);
|
||||
}
|
||||
final FragmentTransaction transaction = fm.beginTransaction();
|
||||
transaction.addToBackStack(TAG_EDIT);
|
||||
transaction.replace(containerId, editFragment, TAG_EDIT);
|
||||
transaction.commit();
|
||||
isEditing = true;
|
||||
return true;
|
||||
case R.id.menu_action_save:
|
||||
// This menu item is handled by the current fragment.
|
||||
return false;
|
||||
case R.id.menu_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
// We cannot save fragments that might switch between containers if the layout changes.
|
||||
if (fm.getBackStackEntryCount() > 0) {
|
||||
final int bottomEntryId = fm.getBackStackEntryAt(0).getId();
|
||||
fm.popBackStackImmediate(bottomEntryId, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
}
|
||||
if (isSplitLayout) {
|
||||
final Fragment oldFragment = fm.findFragmentById(containerId);
|
||||
if (oldFragment != null)
|
||||
fm.beginTransaction().remove(oldFragment).commit();
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceAvailable() {
|
||||
// Create the initial fragment set.
|
||||
canAddFragments = true;
|
||||
onCurrentConfigChanged(getCurrentConfig());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
339
app/src/main/java/com/wireguard/android/VpnService.java
Normal file
339
app/src/main/java/com/wireguard/android/VpnService.java
Normal file
@ -0,0 +1,339 @@
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.databinding.ObservableArrayMap;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service that handles config state coordination and all background processing for the application.
|
||||
*/
|
||||
|
||||
public class VpnService extends Service {
|
||||
private static final String TAG = "VpnService";
|
||||
|
||||
private static VpnService instance;
|
||||
|
||||
public static VpnService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final IBinder binder = new Binder();
|
||||
private final ObservableArrayMap<String, Config> configurations = new ObservableArrayMap<>();
|
||||
private RootShell rootShell;
|
||||
|
||||
/**
|
||||
* Add a new configuration to the set of known configurations. The configuration will initially
|
||||
* be disabled. The configuration's name must be unique within the set of known configurations.
|
||||
*
|
||||
* @param config The configuration to add.
|
||||
*/
|
||||
public void add(final Config config) {
|
||||
new ConfigUpdater(null, config, false).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to disable and tear down an interface for this configuration. The configuration's
|
||||
* enabled state will be updated the operation is successful. If this configuration is already
|
||||
* disconnected, or it is not a known configuration, no changes will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to disable.
|
||||
*/
|
||||
public void disable(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null || !config.isEnabled())
|
||||
return;
|
||||
new ConfigDisabler(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to set up and enable an interface for this configuration. The configuration's enabled
|
||||
* state will be updated if the operation is successful. If this configuration is already
|
||||
* enabled, or it is not a known configuration, no changes will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to enable.
|
||||
*/
|
||||
public void enable(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null || config.isEnabled())
|
||||
return;
|
||||
new ConfigEnabler(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a configuration known and managed by this service. The returned object must not be
|
||||
* modified directly.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to retrieve.
|
||||
* @return An object representing the configuration. This object must not be modified.
|
||||
*/
|
||||
public Config get(final String name) {
|
||||
return configurations.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the set of configurations known and managed by the service. Configurations in this
|
||||
* set must not be modified directly. If a configuration is to be updated, first create a copy
|
||||
* of it by calling getCopy().
|
||||
*
|
||||
* @return The set of known configurations.
|
||||
*/
|
||||
public ObservableArrayMap<String, Config> getConfigs() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
instance = this;
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Ensure the service sticks around after being unbound. This only needs to happen once.
|
||||
startService(new Intent(this, getClass()));
|
||||
rootShell = new RootShell(this);
|
||||
new ConfigLoader().execute(getFilesDir().listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(final File dir, final String name) {
|
||||
return name.endsWith(".conf");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
instance = this;
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a configuration from being managed by the service. If it is currently enabled, the
|
||||
* the configuration will be disabled before removal. If successful, the configuration will be
|
||||
* removed from persistent storage. If the configuration is not known to the service, no changes
|
||||
* will be made.
|
||||
*
|
||||
* @param name The name of the configuration (in the set of known configurations) to remove.
|
||||
*/
|
||||
public void remove(final String name) {
|
||||
final Config config = configurations.get(name);
|
||||
if (config == null)
|
||||
return;
|
||||
if (config.isEnabled())
|
||||
new ConfigDisabler(config).execute();
|
||||
new ConfigRemover(config).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the attributes of the named configuration. If the configuration is currently enabled,
|
||||
* it will be disabled before the update, and the service will attempt to re-enable it
|
||||
* afterward. If successful, the updated configuration will be saved to persistent storage.
|
||||
*
|
||||
* @param name The name of an existing configuration to update.
|
||||
* @param config A copy of the configuration, with updated attributes.
|
||||
*/
|
||||
public void update(final String name, final Config config) {
|
||||
if (name == null)
|
||||
return;
|
||||
if (configurations.containsValue(config))
|
||||
throw new IllegalArgumentException("Config " + config.getName() + " modified directly");
|
||||
final Config oldConfig = configurations.get(name);
|
||||
if (oldConfig == null)
|
||||
return;
|
||||
final boolean wasEnabled = oldConfig.isEnabled();
|
||||
if (wasEnabled)
|
||||
new ConfigDisabler(oldConfig).execute();
|
||||
new ConfigUpdater(oldConfig, config, wasEnabled).execute();
|
||||
}
|
||||
|
||||
private class ConfigDisabler extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigDisabler(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, "Running wg-quick down for " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
return rootShell.run(null, "wg-quick down '" + configFile.getPath() + "'") == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
config.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigEnabler extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigEnabler(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, "Running wg-quick up for " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
return rootShell.run(null, "wg-quick up '" + configFile.getPath() + "'") == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
config.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigLoader extends AsyncTask<File, Void, List<Config>> {
|
||||
@Override
|
||||
protected List<Config> doInBackground(final File... files) {
|
||||
final List<Config> configs = new LinkedList<>();
|
||||
final List<String> interfaces = new LinkedList<>();
|
||||
final String command = "wg show interfaces";
|
||||
if (rootShell.run(interfaces, command) == 0 && interfaces.size() == 1) {
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
final String nameList = interfaces.get(0);
|
||||
Collections.addAll(interfaces, nameList.split(" "));
|
||||
interfaces.remove(0);
|
||||
} else {
|
||||
interfaces.clear();
|
||||
Log.w(TAG, "No existing WireGuard interfaces found. Maybe they are all disabled?");
|
||||
}
|
||||
for (final File file : files) {
|
||||
if (isCancelled())
|
||||
return null;
|
||||
final String fileName = file.getName();
|
||||
final String configName = fileName.substring(0, fileName.length() - 5);
|
||||
Log.v(TAG, "Attempting to load config " + configName);
|
||||
try {
|
||||
final Config config = new Config();
|
||||
config.parseFrom(openFileInput(fileName));
|
||||
config.setEnabled(interfaces.contains(configName));
|
||||
config.setName(configName);
|
||||
configs.add(config);
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
Log.w(TAG, "Failed to load config from " + fileName, e);
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final List<Config> configs) {
|
||||
if (configs == null)
|
||||
return;
|
||||
for (final Config config : configs)
|
||||
configurations.put(config.getName(), config);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigRemover extends AsyncTask<Void, Void, Boolean> {
|
||||
private final Config config;
|
||||
|
||||
private ConfigRemover(final Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, "Removing config " + config.getName());
|
||||
final File configFile = new File(getFilesDir(), config.getName() + ".conf");
|
||||
if (configFile.delete()) {
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "Could not delete configuration for config " + config.getName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
configurations.remove(config.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigUpdater extends AsyncTask<Void, Void, Boolean> {
|
||||
private Config newConfig;
|
||||
private final String newName;
|
||||
private final Config oldConfig;
|
||||
private final String oldName;
|
||||
private final Boolean shouldConnect;
|
||||
|
||||
private ConfigUpdater(final Config oldConfig, final Config newConfig,
|
||||
final Boolean shouldConnect) {
|
||||
super();
|
||||
this.newConfig = newConfig;
|
||||
this.oldConfig = oldConfig;
|
||||
this.shouldConnect = shouldConnect;
|
||||
newName = newConfig.getName();
|
||||
oldName = oldConfig.getName();
|
||||
if (isRename() && configurations.containsKey(newName))
|
||||
throw new IllegalStateException("Config " + newName + " already exists");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(final Void... voids) {
|
||||
Log.i(TAG, (oldConfig == null ? "Adding" : "Updating") + " config " + newName);
|
||||
final File newFile = new File(getFilesDir(), newName + ".conf");
|
||||
final File oldFile = new File(getFilesDir(), oldName + ".conf");
|
||||
if (isRename() && newFile.exists()) {
|
||||
Log.w(TAG, "Refusing to overwrite existing config configuration");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final FileOutputStream stream = openFileOutput(oldFile.getName(), MODE_PRIVATE);
|
||||
stream.write(newConfig.toString().getBytes(StandardCharsets.UTF_8));
|
||||
stream.close();
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Could not save configuration for config " + oldName, e);
|
||||
return false;
|
||||
}
|
||||
if (isRename() && !oldFile.renameTo(newFile)) {
|
||||
Log.e(TAG, "Could not rename " + oldFile.getName() + " to " + newFile.getName());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isRename() {
|
||||
return oldConfig != null && !newConfig.getName().equals(oldConfig.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final Boolean result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (oldConfig != null) {
|
||||
configurations.remove(oldName);
|
||||
oldConfig.copyFrom(newConfig);
|
||||
newConfig = oldConfig;
|
||||
}
|
||||
newConfig.setEnabled(false);
|
||||
configurations.put(newName, newConfig);
|
||||
if (shouldConnect)
|
||||
new ConfigEnabler(newConfig).execute();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
@Override
|
||||
public Config copy() {
|
||||
final Config copy = new Config();
|
||||
copy.copyFrom(this);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private Profile(Profile original)
|
||||
throws IOException {
|
||||
this(original.getName());
|
||||
parseFrom(original);
|
||||
}
|
||||
|
||||
public Profile copy() {
|
||||
try {
|
||||
return new Profile(this);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public void copyFrom(final Config source) {
|
||||
iface.copyFrom(source.iface);
|
||||
isEnabled = source.isEnabled;
|
||||
name = source.name;
|
||||
peers.clear();
|
||||
for (final Peer peer : source.peers)
|
||||
peers.add(peer.copy());
|
||||
}
|
||||
|
||||
public Interface getInterface() {
|
||||
return iface;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getIsConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name;
|
||||
@ -69,16 +61,24 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
|
||||
return peers;
|
||||
}
|
||||
|
||||
public void parseFrom(InputStream stream)
|
||||
@Bindable
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
public void parseFrom(final InputStream stream)
|
||||
throws IOException {
|
||||
peers.clear();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||
Peer currentPeer = null;
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.equals("[Interface]")) {
|
||||
if (line.isEmpty())
|
||||
continue;
|
||||
if ("[Interface]".equals(line)) {
|
||||
currentPeer = null;
|
||||
} else if (line.equals("[Peer]")) {
|
||||
} else if ("[Peer]".equals(line)) {
|
||||
currentPeer = new Peer();
|
||||
peers.add(currentPeer);
|
||||
} else if (currentPeer == null) {
|
||||
@ -90,28 +90,23 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
|
||||
}
|
||||
}
|
||||
|
||||
public void parseFrom(Profile profile)
|
||||
throws IOException {
|
||||
final byte configBytes[] = profile.toString().getBytes(StandardCharsets.UTF_8);
|
||||
final ByteArrayInputStream configStream = new ByteArrayInputStream(configBytes);
|
||||
parseFrom(configStream);
|
||||
public void setEnabled(final boolean isEnabled) {
|
||||
this.isEnabled = isEnabled;
|
||||
notifyPropertyChanged(BR.enabled);
|
||||
}
|
||||
|
||||
public void setIsConnected(boolean isConnected) {
|
||||
this.isConnected = isConnected;
|
||||
notifyPropertyChanged(BR.isConnected);
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setName(final String name) {
|
||||
if (name != null && !name.isEmpty() && !isNameValid(name))
|
||||
throw new IllegalArgumentException();
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder().append(iface.toString());
|
||||
for (Peer peer : peers)
|
||||
sb.append('\n').append(peer.toString());
|
||||
final StringBuilder sb = new StringBuilder().append(iface);
|
||||
for (final Peer peer : peers)
|
||||
sb.append('\n').append(peer);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -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>
|
164
app/src/main/res/layout/config_edit_fragment.xml
Normal file
164
app/src/main/res/layout/config_edit_fragment.xml
Normal file
@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="config"
|
||||
type="com.wireguard.config.Config" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/config_name_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:labelFor="@+id/config_name_text"
|
||||
android:text="@string/config_name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/config_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/config_name_label"
|
||||
android:inputType="textCapWords"
|
||||
android:text="@={config.name}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/private_key_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/config_name_text"
|
||||
android:labelFor="@+id/private_key_text"
|
||||
android:text="@string/private_key" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/private_key_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:layout_toStartOf="@+id/generate_private_key_button"
|
||||
android:inputType="textVisiblePassword"
|
||||
android:text="@={config.interface.privateKey}" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/generate_private_key_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/private_key_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/private_key_label"
|
||||
android:onClick="@{() -> config.interface.generateKeypair()}"
|
||||
android:text="@string/generate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/private_key_text"
|
||||
android:layout_toStartOf="@+id/listen_port_label"
|
||||
android:labelFor="@+id/public_key_text"
|
||||
android:text="@string/public_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/public_key_text"
|
||||
style="@android:style/Widget.EditText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/public_key_label"
|
||||
android:layout_toStartOf="@+id/listen_port_text"
|
||||
android:ellipsize="end"
|
||||
android:focusable="false"
|
||||
android:hint="@string/hint_generated"
|
||||
android:maxLines="1"
|
||||
android:text="@{config.interface.publicKey}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/listen_port_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/public_key_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:layout_below="@+id/generate_private_key_button"
|
||||
android:labelFor="@+id/listen_port_text"
|
||||
android:text="@string/listen_port" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/listen_port_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/public_key_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:layout_below="@+id/listen_port_label"
|
||||
android:hint="@string/hint_random"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.listenPort}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dns_server_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/public_key_text"
|
||||
android:layout_toStartOf="@+id/mtu_label"
|
||||
android:labelFor="@+id/dns_server_text"
|
||||
android:text="@string/dns_servers" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dns_server_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_below="@+id/dns_server_label"
|
||||
android:layout_toStartOf="@+id/mtu_text"
|
||||
android:hint="@string/hint_optional"
|
||||
android:inputType="text"
|
||||
android:text="@={config.interface.dns}" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mtu_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_server_label"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:layout_below="@+id/listen_port_text"
|
||||
android:labelFor="@+id/mtu_text"
|
||||
android:text="@string/mtu" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/mtu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@+id/dns_server_text"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignStart="@+id/generate_private_key_button"
|
||||
android:layout_below="@+id/mtu_label"
|
||||
android:hint="@string/hint_automatic"
|
||||
android:inputType="number"
|
||||
android:text="@={config.interface.mtu}"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/dns_server_text"
|
||||
android:text="@{config.toString()}" />
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user