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:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar">
|
android:theme="@android:style/Theme.Material.Light.DarkActionBar">
|
||||||
<activity
|
<activity android:name=".ConfigActivity">
|
||||||
android:name=".ProfileDetailActivity"
|
|
||||||
android:label=""
|
|
||||||
android:parentActivityName=".ProfileListActivity" />
|
|
||||||
<activity
|
|
||||||
android:name=".ProfileEditActivity"
|
|
||||||
android:label="@string/edit_activity_title" />
|
|
||||||
<activity android:name=".ProfileListActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -37,7 +30,7 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".ProfileService"
|
android:name=".VpnService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
</application>
|
</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.
|
* Static methods for use by generated code in the Android data binding library.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public final class BindingAdapters {
|
public final class BindingAdapters {
|
||||||
@BindingAdapter({"items", "layout"})
|
@BindingAdapter({"items", "layout"})
|
||||||
public static <K, V> void arrayMapBinding(ListView view, ObservableArrayMap<K, V> oldMap,
|
public static <K, V> void arrayMapBinding(final ListView view,
|
||||||
int oldLayoutId, ObservableArrayMap<K, V> newMap,
|
final ObservableArrayMap<K, V> oldMap,
|
||||||
int newLayoutId) {
|
final int oldLayoutId,
|
||||||
|
final ObservableArrayMap<K, V> newMap,
|
||||||
|
final int newLayoutId) {
|
||||||
// Remove any existing binding when there is no new map.
|
// Remove any existing binding when there is no new map.
|
||||||
if (newMap == null) {
|
if (newMap == null) {
|
||||||
view.setAdapter(null);
|
view.setAdapter(null);
|
||||||
@ -37,8 +40,9 @@ public final class BindingAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter({"items", "layout"})
|
@BindingAdapter({"items", "layout"})
|
||||||
public static <T> void listBinding(ListView view, ObservableList<T> oldList, int oldLayoutId,
|
public static <T> void listBinding(final ListView view,
|
||||||
ObservableList<T> newList, int newLayoutId) {
|
final ObservableList<T> oldList, final int oldLayoutId,
|
||||||
|
final ObservableList<T> newList, final int newLayoutId) {
|
||||||
// Remove any existing binding when there is no new list.
|
// Remove any existing binding when there is no new list.
|
||||||
if (newList == null) {
|
if (newList == null) {
|
||||||
view.setAdapter(null);
|
view.setAdapter(null);
|
||||||
@ -61,5 +65,6 @@ public final class BindingAdapters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private BindingAdapters() {
|
private BindingAdapters() {
|
||||||
|
// Prevent instantiation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,9 @@ import android.content.Intent;
|
|||||||
public class BootCompletedReceiver extends BroadcastReceiver {
|
public class BootCompletedReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
@Override
|
@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))
|
if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED))
|
||||||
return;
|
return;
|
||||||
Intent startServiceIntent = new Intent(context, ProfileService.class);
|
context.startService(new Intent(context, VpnService.class));
|
||||||
context.startService(startServiceIntent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
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 {
|
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 ObservableArrayMap<K, V> map;
|
||||||
private final OnMapChangedCallback<K, V> callback = new OnMapChangedCallback<>(this);
|
private final OnMapChangedCallback<K, V> callback = new OnMapChangedCallback<>(this);
|
||||||
|
|
||||||
ObservableArrayMapAdapter(Context context, int layoutId, ObservableArrayMap<K, V> map) {
|
ObservableArrayMapAdapter(final Context context, final int layoutId,
|
||||||
this.layoutInflater = LayoutInflater.from(context);
|
final ObservableArrayMap<K, V> map) {
|
||||||
|
super();
|
||||||
|
layoutInflater = LayoutInflater.from(context);
|
||||||
this.layoutId = layoutId;
|
this.layoutId = layoutId;
|
||||||
setMap(map);
|
setMap(map);
|
||||||
}
|
}
|
||||||
@ -35,17 +37,17 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public V getItem(int position) {
|
public V getItem(final int position) {
|
||||||
return map != null ? map.get(map.keyAt(position)) : null;
|
return map != null ? map.valueAt(position) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getItemId(int position) {
|
public long getItemId(final int position) {
|
||||||
return position;
|
return getItem(position) != null ? getItem(position).hashCode() : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
|
||||||
if (binding == null)
|
if (binding == null)
|
||||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
||||||
@ -54,7 +56,12 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
|
|||||||
return binding.getRoot();
|
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)
|
if (map != null)
|
||||||
map.removeOnMapChangedCallback(callback);
|
map.removeOnMapChangedCallback(callback);
|
||||||
map = newMap;
|
map = newMap;
|
||||||
@ -68,12 +75,13 @@ class ObservableArrayMapAdapter<K, V> extends BaseAdapter implements ListAdapter
|
|||||||
|
|
||||||
private final WeakReference<ObservableArrayMapAdapter<K, V>> weakAdapter;
|
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);
|
weakAdapter = new WeakReference<>(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
final ObservableArrayMapAdapter<K, V> adapter = weakAdapter.get();
|
||||||
if (adapter != null)
|
if (adapter != null)
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
|
@ -22,8 +22,9 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
|||||||
private ObservableList<T> list;
|
private ObservableList<T> list;
|
||||||
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
|
||||||
|
|
||||||
ObservableListAdapter(Context context, int layoutId, ObservableList<T> list) {
|
ObservableListAdapter(final Context context, final int layoutId, final ObservableList<T> list) {
|
||||||
this.layoutInflater = LayoutInflater.from(context);
|
super();
|
||||||
|
layoutInflater = LayoutInflater.from(context);
|
||||||
this.layoutId = layoutId;
|
this.layoutId = layoutId;
|
||||||
setList(list);
|
setList(list);
|
||||||
}
|
}
|
||||||
@ -34,17 +35,17 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T getItem(int position) {
|
public T getItem(final int position) {
|
||||||
return list != null ? list.get(position) : null;
|
return list != null ? list.get(position) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getItemId(int position) {
|
public long getItemId(final int position) {
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
ViewDataBinding binding = DataBindingUtil.getBinding(convertView);
|
||||||
if (binding == null)
|
if (binding == null)
|
||||||
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
binding = DataBindingUtil.inflate(layoutInflater, layoutId, parent, false);
|
||||||
@ -53,7 +54,7 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
|||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setList(ObservableList<T> newList) {
|
public void setList(final ObservableList<T> newList) {
|
||||||
if (list != null)
|
if (list != null)
|
||||||
list.removeOnListChangedCallback(callback);
|
list.removeOnListChangedCallback(callback);
|
||||||
list = newList;
|
list = newList;
|
||||||
@ -67,12 +68,13 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
|||||||
|
|
||||||
private final WeakReference<ObservableListAdapter<U>> weakAdapter;
|
private final WeakReference<ObservableListAdapter<U>> weakAdapter;
|
||||||
|
|
||||||
private OnListChangedCallback(ObservableListAdapter<U> adapter) {
|
private OnListChangedCallback(final ObservableListAdapter<U> adapter) {
|
||||||
|
super();
|
||||||
weakAdapter = new WeakReference<>(adapter);
|
weakAdapter = new WeakReference<>(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(ObservableList<U> sender) {
|
public void onChanged(final ObservableList<U> sender) {
|
||||||
final ObservableListAdapter<U> adapter = weakAdapter.get();
|
final ObservableListAdapter<U> adapter = weakAdapter.get();
|
||||||
if (adapter != null)
|
if (adapter != null)
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
@ -81,24 +83,26 @@ class ObservableListAdapter<T> extends BaseAdapter implements ListAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
onChanged(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemRangeInserted(ObservableList<U> sender, int positionStart,
|
public void onItemRangeInserted(final ObservableList<U> sender, final int positionStart,
|
||||||
int itemCount) {
|
final int itemCount) {
|
||||||
onChanged(sender);
|
onChanged(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemRangeMoved(ObservableList<U> sender, int fromPosition, int toPosition,
|
public void onItemRangeMoved(final ObservableList<U> sender, final int fromPosition,
|
||||||
int itemCount) {
|
final int toPosition, final int itemCount) {
|
||||||
onChanged(sender);
|
onChanged(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
onChanged(sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
public class PlaceholderFragment extends Fragment {
|
public class PlaceholderFragment extends Fragment {
|
||||||
@Override
|
@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);
|
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 SETUP_TEMPLATE = "export TMPDIR=%s\ntrap 'echo $?' EXIT\n";
|
||||||
private static final String TAG = "RootShell";
|
private static final String TAG = "RootShell";
|
||||||
|
|
||||||
private final byte setupCommands[];
|
private final byte[] setupCommands;
|
||||||
private final String shell;
|
private final String shell;
|
||||||
|
|
||||||
RootShell(Context context) {
|
RootShell(final Context context) {
|
||||||
this(context, "su");
|
this(context, "su");
|
||||||
}
|
}
|
||||||
|
|
||||||
RootShell(Context context, String shell) {
|
RootShell(final Context context, final String shell) {
|
||||||
final String tmpdir = context.getCacheDir().getPath();
|
final String tmpdir = context.getCacheDir().getPath();
|
||||||
setupCommands = String.format(SETUP_TEMPLATE, tmpdir).getBytes(StandardCharsets.UTF_8);
|
setupCommands = String.format(SETUP_TEMPLATE, tmpdir).getBytes(StandardCharsets.UTF_8);
|
||||||
this.shell = shell;
|
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).
|
* @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.
|
* @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)
|
if (commands.length < 1)
|
||||||
throw new IndexOutOfBoundsException("At least one command must be supplied");
|
throw new IndexOutOfBoundsException("At least one command must be supplied");
|
||||||
int exitValue = -1;
|
int exitValue = -1;
|
||||||
@ -54,7 +54,7 @@ class RootShell {
|
|||||||
final Process process = builder.command(shell).start();
|
final Process process = builder.command(shell).start();
|
||||||
final OutputStream stdin = process.getOutputStream();
|
final OutputStream stdin = process.getOutputStream();
|
||||||
stdin.write(setupCommands);
|
stdin.write(setupCommands);
|
||||||
for (String command : commands)
|
for (final String command : commands)
|
||||||
stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8));
|
stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8));
|
||||||
stdin.close();
|
stdin.close();
|
||||||
Log.d(TAG, "Sent " + commands.length + " command(s), now reading output");
|
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;
|
package com.wireguard.android;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
public class SettingsActivity extends Activity {
|
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 {
|
static {
|
||||||
map = new HashMap<>(Attribute.values().length);
|
map = new HashMap<>(Attribute.values().length);
|
||||||
for (Attribute key : Attribute.values())
|
for (final Attribute key : Attribute.values())
|
||||||
map.put(key.getToken(), key);
|
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]);
|
return map.get(line.split("\\s|=")[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
private final Pattern pattern;
|
private final Pattern pattern;
|
||||||
|
|
||||||
Attribute(String token) {
|
Attribute(final String token) {
|
||||||
this.pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String composeWith(String value) {
|
public String composeWith(final String value) {
|
||||||
return token + " = " + value + "\n";
|
return token + " = " + value + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ enum Attribute {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String parseFrom(String line) {
|
public String parseFrom(final String line) {
|
||||||
final Matcher matcher = pattern.matcher(line);
|
final Matcher matcher = pattern.matcher(line);
|
||||||
if (matcher.matches())
|
if (matcher.matches())
|
||||||
return matcher.group(1);
|
return matcher.group(1);
|
||||||
|
@ -9,57 +9,49 @@ import android.databinding.ObservableList;
|
|||||||
import com.wireguard.android.BR;
|
import com.wireguard.android.BR;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
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 class Config extends BaseObservable implements Copyable<Config>, Observable {
|
||||||
public static boolean isNameValid(String name) {
|
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_=+.-]{1,16}$");
|
||||||
final int IFNAMSIZ = 16;
|
|
||||||
return !name.contains(" ") && name.getBytes(StandardCharsets.UTF_8).length <= IFNAMSIZ;
|
private static boolean isNameValid(final String name) {
|
||||||
|
return PATTERN.matcher(name).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Interface iface = new Interface();
|
private final Interface iface = new Interface();
|
||||||
private boolean isConnected;
|
private boolean isEnabled;
|
||||||
private String name;
|
private String name;
|
||||||
private final ObservableList<Peer> peers = new ObservableArrayList<>();
|
private final ObservableList<Peer> peers = new ObservableArrayList<>();
|
||||||
|
|
||||||
public Profile(String name) {
|
@Override
|
||||||
super();
|
public Config copy() {
|
||||||
if (!isNameValid(name))
|
final Config copy = new Config();
|
||||||
throw new IllegalArgumentException();
|
copy.copyFrom(this);
|
||||||
this.name = name;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Profile(Profile original)
|
@Override
|
||||||
throws IOException {
|
public void copyFrom(final Config source) {
|
||||||
this(original.getName());
|
iface.copyFrom(source.iface);
|
||||||
parseFrom(original);
|
isEnabled = source.isEnabled;
|
||||||
}
|
name = source.name;
|
||||||
|
peers.clear();
|
||||||
public Profile copy() {
|
for (final Peer peer : source.peers)
|
||||||
try {
|
peers.add(peer.copy());
|
||||||
return new Profile(this);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Interface getInterface() {
|
public Interface getInterface() {
|
||||||
return iface;
|
return iface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bindable
|
|
||||||
public boolean getIsConnected() {
|
|
||||||
return isConnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bindable
|
@Bindable
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
@ -69,16 +61,24 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
|
|||||||
return peers;
|
return peers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseFrom(InputStream stream)
|
@Bindable
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void parseFrom(final InputStream stream)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
peers.clear();
|
||||||
try (BufferedReader reader = new BufferedReader(
|
try (BufferedReader reader = new BufferedReader(
|
||||||
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||||
Peer currentPeer = null;
|
Peer currentPeer = null;
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
if (line.equals("[Interface]")) {
|
if (line.isEmpty())
|
||||||
|
continue;
|
||||||
|
if ("[Interface]".equals(line)) {
|
||||||
currentPeer = null;
|
currentPeer = null;
|
||||||
} else if (line.equals("[Peer]")) {
|
} else if ("[Peer]".equals(line)) {
|
||||||
currentPeer = new Peer();
|
currentPeer = new Peer();
|
||||||
peers.add(currentPeer);
|
peers.add(currentPeer);
|
||||||
} else if (currentPeer == null) {
|
} else if (currentPeer == null) {
|
||||||
@ -90,28 +90,23 @@ public class Profile extends BaseObservable implements Copyable<Profile>, Observ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseFrom(Profile profile)
|
public void setEnabled(final boolean isEnabled) {
|
||||||
throws IOException {
|
this.isEnabled = isEnabled;
|
||||||
final byte configBytes[] = profile.toString().getBytes(StandardCharsets.UTF_8);
|
notifyPropertyChanged(BR.enabled);
|
||||||
final ByteArrayInputStream configStream = new ByteArrayInputStream(configBytes);
|
|
||||||
parseFrom(configStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsConnected(boolean isConnected) {
|
public void setName(final String name) {
|
||||||
this.isConnected = isConnected;
|
if (name != null && !name.isEmpty() && !isNameValid(name))
|
||||||
notifyPropertyChanged(BR.isConnected);
|
throw new IllegalArgumentException();
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
notifyPropertyChanged(BR.name);
|
notifyPropertyChanged(BR.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder().append(iface.toString());
|
final StringBuilder sb = new StringBuilder().append(iface);
|
||||||
for (Peer peer : peers)
|
for (final Peer peer : peers)
|
||||||
sb.append('\n').append(peer.toString());
|
sb.append('\n').append(peer);
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,4 +6,5 @@ package com.wireguard.config;
|
|||||||
|
|
||||||
public interface Copyable<T> {
|
public interface Copyable<T> {
|
||||||
T copy();
|
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).
|
* 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 address;
|
||||||
private String dns;
|
private String dns;
|
||||||
private String listenPort;
|
private String listenPort;
|
||||||
private Keypair keypair;
|
private Keypair keypair;
|
||||||
private String mtu;
|
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() {
|
public void generateKeypair() {
|
||||||
keypair = new Keypair();
|
keypair = new Keypair();
|
||||||
notifyPropertyChanged(BR.privateKey);
|
notifyPropertyChanged(BR.privateKey);
|
||||||
@ -55,7 +71,7 @@ public class Interface extends BaseObservable implements Observable {
|
|||||||
return keypair != null ? keypair.getPublicKey() : null;
|
return keypair != null ? keypair.getPublicKey() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseFrom(String line) {
|
public void parseFrom(final String line) {
|
||||||
final Attribute key = Attribute.match(line);
|
final Attribute key = Attribute.match(line);
|
||||||
if (key == Attribute.ADDRESS)
|
if (key == Attribute.ADDRESS)
|
||||||
address = key.parseFrom(line);
|
address = key.parseFrom(line);
|
||||||
@ -67,29 +83,39 @@ public class Interface extends BaseObservable implements Observable {
|
|||||||
mtu = key.parseFrom(line);
|
mtu = key.parseFrom(line);
|
||||||
else if (key == Attribute.PRIVATE_KEY)
|
else if (key == Attribute.PRIVATE_KEY)
|
||||||
keypair = new Keypair(key.parseFrom(line));
|
keypair = new Keypair(key.parseFrom(line));
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAddress(String address) {
|
public void setAddress(String address) {
|
||||||
|
if (address != null && address.isEmpty())
|
||||||
|
address = null;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
notifyPropertyChanged(BR.address);
|
notifyPropertyChanged(BR.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDns(String dns) {
|
public void setDns(String dns) {
|
||||||
|
if (dns != null && dns.isEmpty())
|
||||||
|
dns = null;
|
||||||
this.dns = dns;
|
this.dns = dns;
|
||||||
notifyPropertyChanged(BR.dns);
|
notifyPropertyChanged(BR.dns);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setListenPort(String listenPort) {
|
public void setListenPort(String listenPort) {
|
||||||
|
if (listenPort != null && listenPort.isEmpty())
|
||||||
|
listenPort = null;
|
||||||
this.listenPort = listenPort;
|
this.listenPort = listenPort;
|
||||||
notifyPropertyChanged(BR.listenPort);
|
notifyPropertyChanged(BR.listenPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMtu(String mtu) {
|
public void setMtu(String mtu) {
|
||||||
|
if (mtu != null && mtu.isEmpty())
|
||||||
|
mtu = null;
|
||||||
this.mtu = mtu;
|
this.mtu = mtu;
|
||||||
notifyPropertyChanged(BR.mtu);
|
notifyPropertyChanged(BR.mtu);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPrivateKey(String privateKey) {
|
public void setPrivateKey(final String privateKey) {
|
||||||
if (privateKey != null && !privateKey.isEmpty()) {
|
if (privateKey != null && !privateKey.isEmpty()) {
|
||||||
// Avoid exceptions from Keypair while the user is typing.
|
// Avoid exceptions from Keypair while the user is typing.
|
||||||
if (privateKey.length() != KeyEncoding.KEY_LENGTH_BASE64)
|
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).
|
* 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 allowedIPs;
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
private String persistentKeepalive;
|
private String persistentKeepalive;
|
||||||
private String publicKey;
|
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
|
@Bindable
|
||||||
public String getAllowedIPs() {
|
public String getAllowedIPs() {
|
||||||
return allowedIPs;
|
return allowedIPs;
|
||||||
@ -36,7 +51,7 @@ public class Peer extends BaseObservable implements Observable {
|
|||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseFrom(String line) {
|
public void parseFrom(final String line) {
|
||||||
final Attribute key = Attribute.match(line);
|
final Attribute key = Attribute.match(line);
|
||||||
if (key == Attribute.ALLOWED_IPS)
|
if (key == Attribute.ALLOWED_IPS)
|
||||||
allowedIPs = key.parseFrom(line);
|
allowedIPs = key.parseFrom(line);
|
||||||
@ -46,24 +61,34 @@ public class Peer extends BaseObservable implements Observable {
|
|||||||
persistentKeepalive = key.parseFrom(line);
|
persistentKeepalive = key.parseFrom(line);
|
||||||
else if (key == Attribute.PUBLIC_KEY)
|
else if (key == Attribute.PUBLIC_KEY)
|
||||||
publicKey = key.parseFrom(line);
|
publicKey = key.parseFrom(line);
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAllowedIPs(String allowedIPs) {
|
public void setAllowedIPs(String allowedIPs) {
|
||||||
|
if (allowedIPs != null && allowedIPs.isEmpty())
|
||||||
|
allowedIPs = null;
|
||||||
this.allowedIPs = allowedIPs;
|
this.allowedIPs = allowedIPs;
|
||||||
notifyPropertyChanged(BR.allowedIPs);
|
notifyPropertyChanged(BR.allowedIPs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEndpoint(String endpoint) {
|
public void setEndpoint(String endpoint) {
|
||||||
|
if (endpoint != null && endpoint.isEmpty())
|
||||||
|
endpoint = null;
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
notifyPropertyChanged(BR.endpoint);
|
notifyPropertyChanged(BR.endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPersistentKeepalive(String persistentKeepalive) {
|
public void setPersistentKeepalive(String persistentKeepalive) {
|
||||||
|
if (persistentKeepalive != null && persistentKeepalive.isEmpty())
|
||||||
|
persistentKeepalive = null;
|
||||||
this.persistentKeepalive = persistentKeepalive;
|
this.persistentKeepalive = persistentKeepalive;
|
||||||
notifyPropertyChanged(BR.persistentKeepalive);
|
notifyPropertyChanged(BR.persistentKeepalive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPublicKey(String publicKey) {
|
public void setPublicKey(String publicKey) {
|
||||||
|
if (publicKey != null && publicKey.isEmpty())
|
||||||
|
publicKey = null;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
notifyPropertyChanged(BR.publicKey);
|
notifyPropertyChanged(BR.publicKey);
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import java.util.Arrays;
|
|||||||
*
|
*
|
||||||
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
public final class Curve25519 {
|
public final class Curve25519 {
|
||||||
|
|
||||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
// 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,
|
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 = {
|
final byte[] input = {
|
||||||
(byte) ((src[0 + src_offset] >>> 2) & 63),
|
(byte) ((src[src_offset] >>> 2) & 63),
|
||||||
(byte) ((src[0 + src_offset] << 4 | ((src[1 + src_offset] & 0xff) >>> 4)) & 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[1 + src_offset] << 2 | ((src[2 + src_offset] & 0xff) >>> 6)) & 63),
|
||||||
(byte) ((src[2 + src_offset]) & 63),
|
(byte) ((src[2 + src_offset]) & 63),
|
||||||
};
|
};
|
||||||
@ -54,12 +54,12 @@ public class KeyEncoding {
|
|||||||
final int val = decodeBase64(input, i * 4);
|
final int val = decodeBase64(input, i * 4);
|
||||||
if (val < 0)
|
if (val < 0)
|
||||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
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 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||||
key[i * 3 + 2] = (byte) (val & 0xff);
|
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||||
}
|
}
|
||||||
final char[] endSegment = {
|
final char[] endSegment = {
|
||||||
input[i * 4 + 0],
|
input[i * 4],
|
||||||
input[i * 4 + 1],
|
input[i * 4 + 1],
|
||||||
input[i * 4 + 2],
|
input[i * 4 + 2],
|
||||||
'A',
|
'A',
|
||||||
@ -67,7 +67,7 @@ public class KeyEncoding {
|
|||||||
final int val = decodeBase64(endSegment, 0);
|
final int val = decodeBase64(endSegment, 0);
|
||||||
if (val < 0 || (val & 0xff) != 0)
|
if (val < 0 || (val & 0xff) != 0)
|
||||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
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 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ public class KeyEncoding {
|
|||||||
for (i = 0; i < KEY_LENGTH / 3; ++i)
|
for (i = 0; i < KEY_LENGTH / 3; ++i)
|
||||||
encodeBase64(key, i * 3, output, i * 4);
|
encodeBase64(key, i * 3, output, i * 4);
|
||||||
final byte[] endSegment = {
|
final byte[] endSegment = {
|
||||||
key[i * 3 + 0],
|
key[i * 3],
|
||||||
key[i * 3 + 1],
|
key[i * 3 + 1],
|
||||||
0,
|
0,
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ public class Keypair {
|
|||||||
return privateKey;
|
return privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] generatePublicKey(byte[] privateKey) {
|
private static byte[] generatePublicKey(final byte[] privateKey) {
|
||||||
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
|
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
|
||||||
Curve25519.eval(publicKey, 0, privateKey, null);
|
Curve25519.eval(publicKey, 0, privateKey, null);
|
||||||
return publicKey;
|
return publicKey;
|
||||||
@ -30,12 +30,12 @@ public class Keypair {
|
|||||||
this(generatePrivateKey());
|
this(generatePrivateKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Keypair(byte[] privateKey) {
|
private Keypair(final byte[] privateKey) {
|
||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
publicKey = generatePublicKey(privateKey);
|
publicKey = generatePublicKey(privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Keypair(String privateKey) {
|
public Keypair(final String privateKey) {
|
||||||
this(KeyEncoding.keyFromBase64(privateKey));
|
this(KeyEncoding.keyFromBase64(privateKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/list_container"
|
android:id="@+id/master_fragment"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/fragment_container"
|
android:id="@+id/detail_fragment"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="2" />
|
android:layout_weight="2"
|
||||||
|
tools:ignore="InconsistentLayout" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
@ -1,44 +1,43 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="profile"
|
name="config"
|
||||||
type="com.wireguard.config.Profile" />
|
type="com.wireguard.config.Config" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/profile_name_label"
|
android:id="@+id/config_name_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:labelFor="@+id/profile_name_text"
|
android:labelFor="@+id/config_name_text"
|
||||||
android:text="@string/profile_name" />
|
android:text="@string/config_name" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/profile_name_text"
|
android:id="@+id/config_name_text"
|
||||||
style="?android:attr/textAppearanceMedium"
|
style="?android:attr/textAppearanceMedium"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/profile_name_label"
|
android:layout_below="@+id/config_name_label"
|
||||||
android:text="@{profile.name}" />
|
android:text="@{config.name}" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/public_key_label"
|
android:id="@+id/public_key_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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:labelFor="@+id/public_key_text"
|
||||||
android:text="@string/public_key" />
|
android:text="@string/public_key" />
|
||||||
|
|
||||||
@ -50,7 +49,13 @@
|
|||||||
android:layout_below="@+id/public_key_label"
|
android:layout_below="@+id/public_key_label"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
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>
|
</RelativeLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</layout>
|
</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 -->
|
<!--suppress AndroidDomInspection -->
|
||||||
<variable
|
<variable
|
||||||
name="profiles"
|
name="configs"
|
||||||
type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Profile>" />
|
type="android.databinding.ObservableArrayMap<String, com.wireguard.config.Config>" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/profile_list"
|
android:id="@+id/config_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:items="@{profiles}"
|
android:choiceMode="singleChoice"
|
||||||
app:layout="@{@layout/profile_list_item}" />
|
app:items="@{configs}"
|
||||||
|
app:layout="@{@layout/config_list_item}" />
|
||||||
</layout>
|
</layout>
|
@ -5,17 +5,16 @@
|
|||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="item"
|
name="item"
|
||||||
type="com.wireguard.config.Profile" />
|
type="com.wireguard.config.Config" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:attr/activatedBackgroundIndicator"
|
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/profile_name"
|
android:id="@+id/config_name"
|
||||||
style="?android:attr/textAppearanceMedium"
|
style="?android:attr/textAppearanceMedium"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -26,9 +25,9 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_toEndOf="@+id/profile_name"
|
android:layout_toEndOf="@+id/config_name"
|
||||||
android:text="@{item.isConnected ? @string/connected : @string/disconnected}"
|
android:text="@{item.isEnabled ? @string/connected : @string/disconnected}"
|
||||||
android:textAlignment="textEnd"
|
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>
|
</RelativeLayout>
|
||||||
</layout>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WireGuard</string>
|
<string name="app_name">WireGuard</string>
|
||||||
|
<string name="config_name">Configuration name</string>
|
||||||
<string name="connected">Connected</string>
|
<string name="connected">Connected</string>
|
||||||
<string name="disconnected">Disconnected</string>
|
<string name="disconnected">Disconnected</string>
|
||||||
|
<string name="dns_servers">DNS servers</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="edit_activity_title">Edit WireGuard Profile</string>
|
<string name="generate">Generate</string>
|
||||||
<string name="placeholder_text">No profile selected</string>
|
<string name="hint_automatic">(auto)</string>
|
||||||
<string name="profile_name">Profile name</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="public_key">Public key</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user