Signed-off-by: Eric Kuck <eric@bluelinelabs.com>master
@@ -171,6 +171,9 @@ public final class GoBackend implements Backend { | |||
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |||
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0)); | |||
for (final String excludedApplication : config.getInterface().getExcludedApplications()) | |||
builder.addDisallowedApplication(excludedApplication); | |||
for (final InetNetwork addr : config.getInterface().getAddresses()) | |||
builder.addAddress(addr.getAddress(), addr.getMask()); | |||
@@ -250,5 +253,6 @@ public final class GoBackend implements Backend { | |||
} | |||
return super.onStartCommand(intent, flags, startId); | |||
} | |||
} | |||
} |
@@ -9,16 +9,18 @@ package com.wireguard.android.databinding; | |||
import android.databinding.BindingAdapter; | |||
import android.databinding.ObservableList; | |||
import android.databinding.adapters.ListenerUtil; | |||
import android.support.v7.widget.LinearLayoutManager; | |||
import android.support.v7.widget.RecyclerView; | |||
import android.text.InputFilter; | |||
import android.widget.LinearLayout; | |||
import android.widget.ListView; | |||
import android.widget.TextView; | |||
import com.wireguard.android.R; | |||
import com.wireguard.util.Keyed; | |||
import com.wireguard.android.util.ObservableKeyedList; | |||
import com.wireguard.android.widget.ToggleSwitch; | |||
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener; | |||
import com.wireguard.util.Keyed; | |||
/** | |||
* Static methods for use by generated code in the Android data binding library. | |||
@@ -91,9 +93,39 @@ public final class BindingAdapters { | |||
adapter.setList(newList); | |||
} | |||
@BindingAdapter({"items", "layout"}) | |||
public static <K, E extends Keyed<? extends K>> | |||
void setItems(final RecyclerView view, | |||
final ObservableKeyedList<K, E> oldList, final int oldLayoutId, | |||
final ObservableKeyedList<K, E> newList, final int newLayoutId) { | |||
if (view.getLayoutManager() == null) | |||
view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false)); | |||
if (oldList == newList && oldLayoutId == newLayoutId) | |||
return; | |||
// The ListAdapter interface is not generic, so this cannot be checked. | |||
@SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter = | |||
(ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter(); | |||
// If the layout changes, any existing adapter must be replaced. | |||
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) { | |||
adapter.setList(null); | |||
adapter = null; | |||
} | |||
// Avoid setting an adapter when there is no new list or layout. | |||
if (newList == null || newLayoutId == 0) | |||
return; | |||
if (adapter == null) { | |||
adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList); | |||
view.setAdapter(adapter); | |||
} | |||
// Either the list changed, or this is an entirely new listener because the layout changed. | |||
adapter.setList(newList); | |||
} | |||
@BindingAdapter("onBeforeCheckedChanged") | |||
public static void setOnBeforeCheckedChanged(final ToggleSwitch view, | |||
final OnBeforeCheckedChangeListener listener) { | |||
view.setOnBeforeCheckedChangeListener(listener); | |||
} | |||
} |
@@ -0,0 +1,135 @@ | |||
package com.wireguard.android.databinding; | |||
import android.content.Context; | |||
import android.databinding.DataBindingUtil; | |||
import android.databinding.ObservableList; | |||
import android.databinding.ViewDataBinding; | |||
import android.support.annotation.NonNull; | |||
import android.support.v7.widget.RecyclerView; | |||
import android.support.v7.widget.RecyclerView.Adapter; | |||
import android.view.LayoutInflater; | |||
import android.view.ViewGroup; | |||
import com.wireguard.android.BR; | |||
import com.wireguard.android.util.ObservableKeyedList; | |||
import com.wireguard.util.Keyed; | |||
import java.lang.ref.WeakReference; | |||
/** | |||
* A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}. | |||
*/ | |||
class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> { | |||
private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this); | |||
private final int layoutId; | |||
private final LayoutInflater layoutInflater; | |||
private ObservableKeyedList<K, E> list; | |||
ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId, | |||
final ObservableKeyedList<K, E> list) { | |||
this.layoutId = layoutId; | |||
layoutInflater = LayoutInflater.from(context); | |||
setList(list); | |||
} | |||
@Override | |||
public int getItemCount() { | |||
return list != null ? list.size() : 0; | |||
} | |||
private E getItem(final int position) { | |||
if (list == null || position < 0 || position >= list.size()) | |||
return null; | |||
return list.get(position); | |||
} | |||
@Override | |||
public long getItemId(final int position) { | |||
final K key = getKey(position); | |||
return key != null ? key.hashCode() : -1; | |||
} | |||
private K getKey(final int position) { | |||
final E item = getItem(position); | |||
return item != null ? item.getKey() : null; | |||
} | |||
@NonNull @Override | |||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | |||
return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false)); | |||
} | |||
@Override | |||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) { | |||
holder.binding.setVariable(BR.collection, list); | |||
holder.binding.setVariable(BR.key, getKey(position)); | |||
holder.binding.setVariable(BR.item, getItem(position)); | |||
holder.binding.executePendingBindings(); | |||
} | |||
void setList(final ObservableKeyedList<K, E> newList) { | |||
if (list != null) | |||
list.removeOnListChangedCallback(callback); | |||
list = newList; | |||
if (list != null) { | |||
list.addOnListChangedCallback(callback); | |||
} | |||
notifyDataSetChanged(); | |||
} | |||
private static final class OnListChangedCallback<E extends Keyed<?>> | |||
extends ObservableList.OnListChangedCallback<ObservableList<E>> { | |||
private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter; | |||
private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) { | |||
weakAdapter = new WeakReference<>(adapter); | |||
} | |||
@Override | |||
public void onChanged(final ObservableList<E> sender) { | |||
final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get(); | |||
if (adapter != null) | |||
adapter.notifyDataSetChanged(); | |||
else | |||
sender.removeOnListChangedCallback(this); | |||
} | |||
@Override | |||
public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart, | |||
final int itemCount) { | |||
onChanged(sender); | |||
} | |||
@Override | |||
public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart, | |||
final int itemCount) { | |||
onChanged(sender); | |||
} | |||
@Override | |||
public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition, | |||
final int toPosition, final int itemCount) { | |||
onChanged(sender); | |||
} | |||
@Override | |||
public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart, | |||
final int itemCount) { | |||
onChanged(sender); | |||
} | |||
} | |||
public static class ViewHolder extends RecyclerView.ViewHolder { | |||
final ViewDataBinding binding; | |||
public ViewHolder(ViewDataBinding binding) { | |||
super(binding.getRoot()); | |||
this.binding = binding; | |||
} | |||
} | |||
} |
@@ -0,0 +1,132 @@ | |||
package com.wireguard.android.fragment; | |||
import android.app.Activity; | |||
import android.app.Dialog; | |||
import android.content.Context; | |||
import android.content.Intent; | |||
import android.content.pm.PackageManager; | |||
import android.content.pm.ResolveInfo; | |||
import android.os.Bundle; | |||
import android.support.annotation.Nullable; | |||
import android.support.v4.app.DialogFragment; | |||
import android.support.v4.app.Fragment; | |||
import android.support.v7.app.AlertDialog; | |||
import android.widget.Toast; | |||
import com.wireguard.android.Application; | |||
import com.wireguard.android.R; | |||
import com.wireguard.android.activity.BaseActivity; | |||
import com.wireguard.android.databinding.AppListDialogFragmentBinding; | |||
import com.wireguard.android.model.ApplicationData; | |||
import com.wireguard.android.model.Tunnel; | |||
import com.wireguard.android.util.ExceptionLoggers; | |||
import com.wireguard.android.util.ObservableKeyedArrayList; | |||
import com.wireguard.android.util.ObservableKeyedList; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
public class AppListDialogFragment extends DialogFragment { | |||
private static final String KEY_EXCLUDED_APPS = "excludedApps"; | |||
private List<String> currentlyExcludedApps; | |||
private Tunnel tunnel; | |||
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>(); | |||
public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(String[] excludedApps, T target) { | |||
Bundle extras = new Bundle(); | |||
extras.putStringArray(KEY_EXCLUDED_APPS, excludedApps); | |||
AppListDialogFragment fragment = new AppListDialogFragment(); | |||
fragment.setTargetFragment(target, 0); | |||
fragment.setArguments(extras); | |||
return fragment; | |||
} | |||
@Override | |||
public void onCreate(@Nullable Bundle savedInstanceState) { | |||
super.onCreate(savedInstanceState); | |||
currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS)); | |||
} | |||
@Override | |||
public void onAttach(final Context context) { | |||
super.onAttach(context); | |||
if (context instanceof BaseActivity) { | |||
tunnel = ((BaseActivity) context).getSelectedTunnel(); | |||
} | |||
} | |||
@Override | |||
public Dialog onCreateDialog(Bundle savedInstanceState) { | |||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); | |||
alertDialogBuilder.setTitle(R.string.excluded_applications); | |||
AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false); | |||
binding.executePendingBindings(); | |||
alertDialogBuilder.setView(binding.getRoot()); | |||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss()); | |||
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); | |||
binding.setFragment(this); | |||
binding.setAppData(appData); | |||
loadData(); | |||
return alertDialogBuilder.create(); | |||
} | |||
private void loadData() { | |||
final Activity activity = getActivity(); | |||
if (activity == null) { | |||
return; | |||
} | |||
final PackageManager pm = activity.getPackageManager(); | |||
Application.getAsyncWorker().supplyAsync(() -> { | |||
Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null); | |||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER); | |||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0); | |||
List<ApplicationData> appData = new ArrayList<>(); | |||
for (ResolveInfo resolveInfo : resolveInfos) { | |||
String packageName = resolveInfo.activityInfo.packageName; | |||
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName))); | |||
} | |||
Collections.sort(appData, (lhs, rhs) -> lhs.getName().toLowerCase().compareTo(rhs.getName().toLowerCase())); | |||
return appData; | |||
}).whenComplete(((data, throwable) -> { | |||
if (data != null) { | |||
appData.clear(); | |||
appData.addAll(data); | |||
} else { | |||
final String error = throwable != null ? ExceptionLoggers.unwrapMessage(throwable) : "Unknown"; | |||
final String message = activity.getString(R.string.error_fetching_apps, error); | |||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); | |||
dismissAllowingStateLoss(); | |||
} | |||
})); | |||
} | |||
void setExclusionsAndDismiss() { | |||
final List<String> excludedApps = new ArrayList<>(); | |||
for (ApplicationData data : appData) { | |||
if (data.isExcludedFromTunnel()) { | |||
excludedApps.add(data.getPackageName()); | |||
} | |||
} | |||
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps); | |||
dismiss(); | |||
} | |||
public interface AppExclusionListener { | |||
void onExcludedAppsSelected(List<String> excludedApps); | |||
} | |||
} |
@@ -11,6 +11,7 @@ import android.content.Context; | |||
import android.os.Bundle; | |||
import android.support.annotation.NonNull; | |||
import android.support.design.widget.Snackbar; | |||
import android.support.v4.app.FragmentManager; | |||
import android.util.Log; | |||
import android.view.LayoutInflater; | |||
import android.view.Menu; | |||
@@ -24,16 +25,20 @@ import android.widget.Toast; | |||
import com.wireguard.android.Application; | |||
import com.wireguard.android.R; | |||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding; | |||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener; | |||
import com.wireguard.android.model.Tunnel; | |||
import com.wireguard.android.model.TunnelManager; | |||
import com.wireguard.android.util.ExceptionLoggers; | |||
import com.wireguard.config.Attribute; | |||
import com.wireguard.config.Config; | |||
import java.util.List; | |||
/** | |||
* Fragment for editing a WireGuard configuration. | |||
*/ | |||
public class TunnelEditorFragment extends BaseFragment { | |||
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener { | |||
private static final String KEY_LOCAL_CONFIG = "local_config"; | |||
private static final String KEY_ORIGINAL_NAME = "original_name"; | |||
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName(); | |||
@@ -202,6 +207,8 @@ public class TunnelEditorFragment extends BaseFragment { | |||
@Override | |||
public void onViewStateRestored(final Bundle savedInstanceState) { | |||
binding.setFragment(this); | |||
if (savedInstanceState == null) { | |||
onSelectedTunnelChanged(null, getSelectedTunnel()); | |||
} else { | |||
@@ -216,4 +223,23 @@ public class TunnelEditorFragment extends BaseFragment { | |||
super.onViewStateRestored(savedInstanceState); | |||
} | |||
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) { | |||
FragmentManager fragmentManager = getFragmentManager(); | |||
if (fragmentManager != null) { | |||
String[] excludedApps = excludedApplications(); | |||
AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this); | |||
fragment.show(getFragmentManager(), null); | |||
} | |||
} | |||
@Override | |||
public void onExcludedAppsSelected(List<String> excludedApps) { | |||
binding.getConfig().getInterfaceSection().setExcludedApplications(Attribute.iterableToString(excludedApps)); | |||
} | |||
public String[] excludedApplications() { | |||
return Attribute.stringToList(binding.getConfig().getInterfaceSection().getExcludedApplications()); | |||
} | |||
} |
@@ -0,0 +1,54 @@ | |||
package com.wireguard.android.model; | |||
import android.databinding.BaseObservable; | |||
import android.databinding.Bindable; | |||
import android.graphics.drawable.Drawable; | |||
import android.support.annotation.NonNull; | |||
import com.wireguard.android.BR; | |||
import com.wireguard.util.Keyed; | |||
public class ApplicationData extends BaseObservable implements Keyed<String> { | |||
@NonNull private final Drawable icon; | |||
@NonNull private final String name; | |||
@NonNull private final String packageName; | |||
private boolean excludedFromTunnel; | |||
public ApplicationData(@NonNull Drawable icon, @NonNull String name, @NonNull String packageName, boolean excludedFromTunnel) { | |||
this.icon = icon; | |||
this.name = name; | |||
this.packageName = packageName; | |||
this.excludedFromTunnel = excludedFromTunnel; | |||
} | |||
@NonNull | |||
public Drawable getIcon() { | |||
return icon; | |||
} | |||
@NonNull | |||
public String getName() { | |||
return name; | |||
} | |||
@NonNull | |||
public String getPackageName() { | |||
return packageName; | |||
} | |||
@Bindable | |||
public boolean isExcludedFromTunnel() { | |||
return excludedFromTunnel; | |||
} | |||
public void setExcludedFromTunnel(boolean excludedFromTunnel) { | |||
this.excludedFromTunnel = excludedFromTunnel; | |||
notifyPropertyChanged(BR.excludedFromTunnel); | |||
} | |||
@Override | |||
public String getKey() { | |||
return name; | |||
} | |||
} |
@@ -18,10 +18,11 @@ import java.util.regex.Pattern; | |||
* The set of valid attributes for an interface or peer in a WireGuard configuration file. | |||
*/ | |||
enum Attribute { | |||
public enum Attribute { | |||
ADDRESS("Address"), | |||
ALLOWED_IPS("AllowedIPs"), | |||
DNS("DNS"), | |||
EXCLUDED_APPLICATIONS("ExcludedApplications"), | |||
ENDPOINT("Endpoint"), | |||
LISTEN_PORT("ListenPort"), | |||
MTU("MTU"), | |||
@@ -59,7 +60,7 @@ enum Attribute { | |||
} | |||
public static String[] stringToList(final String string) { | |||
if (string == null) | |||
if (TextUtils.isEmpty(string)) | |||
return EMPTY_LIST; | |||
return LIST_SEPARATOR_PATTERN.split(string.trim()); | |||
} | |||
@@ -16,6 +16,7 @@ import com.wireguard.crypto.Keypair; | |||
import java.net.InetAddress; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
/** | |||
@@ -25,6 +26,7 @@ import java.util.List; | |||
public class Interface { | |||
private final List<InetNetwork> addressList; | |||
private final List<InetAddress> dnsList; | |||
private final List<String> excludedApplications; | |||
private Keypair keypair; | |||
private int listenPort; | |||
private int mtu; | |||
@@ -32,6 +34,7 @@ public class Interface { | |||
public Interface() { | |||
addressList = new ArrayList<>(); | |||
dnsList = new ArrayList<>(); | |||
excludedApplications = new ArrayList<>(); | |||
} | |||
private void addAddresses(final String[] addresses) { | |||
@@ -52,6 +55,12 @@ public class Interface { | |||
} | |||
} | |||
private void addExcludedApplications(final String[] applications) { | |||
if (applications != null && applications.length > 0) { | |||
excludedApplications.addAll(Arrays.asList(applications)); | |||
} | |||
} | |||
private String getAddressString() { | |||
if (addressList.isEmpty()) | |||
return null; | |||
@@ -79,6 +88,16 @@ public class Interface { | |||
return dnsList.toArray(new InetAddress[dnsList.size()]); | |||
} | |||
private String getExcludedApplicationsString() { | |||
if (excludedApplications.isEmpty()) | |||
return null; | |||
return Attribute.iterableToString(excludedApplications); | |||
} | |||
public String[] getExcludedApplications() { | |||
return excludedApplications.toArray(new String[excludedApplications.size()]); | |||
} | |||
public int getListenPort() { | |||
return listenPort; | |||
} | |||
@@ -120,6 +139,9 @@ public class Interface { | |||
case DNS: | |||
addDnses(key.parseList(line)); | |||
break; | |||
case EXCLUDED_APPLICATIONS: | |||
addExcludedApplications(key.parseList(line)); | |||
break; | |||
case LISTEN_PORT: | |||
setListenPortString(key.parse(line)); | |||
break; | |||
@@ -144,6 +166,11 @@ public class Interface { | |||
addDnses(Attribute.stringToList(dnsString)); | |||
} | |||
private void setExcludedApplicationsString(final String applicationsString) { | |||
excludedApplications.clear(); | |||
addExcludedApplications(Attribute.stringToList(applicationsString)); | |||
} | |||
private void setListenPort(final int listenPort) { | |||
this.listenPort = listenPort; | |||
} | |||
@@ -179,6 +206,8 @@ public class Interface { | |||
sb.append(Attribute.ADDRESS.composeWith(addressList)); | |||
if (!dnsList.isEmpty()) | |||
sb.append(Attribute.DNS.composeWith(getDnsStrings())); | |||
if (!excludedApplications.isEmpty()) | |||
sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications)); | |||
if (listenPort != 0) | |||
sb.append(Attribute.LISTEN_PORT.composeWith(listenPort)); | |||
if (mtu != 0) | |||
@@ -202,6 +231,7 @@ public class Interface { | |||
}; | |||
private String addresses; | |||
private String dnses; | |||
private String excludedApplications; | |||
private String listenPort; | |||
private String mtu; | |||
private String privateKey; | |||
@@ -219,11 +249,13 @@ public class Interface { | |||
privateKey = in.readString(); | |||
listenPort = in.readString(); | |||
mtu = in.readString(); | |||
excludedApplications = in.readString(); | |||
} | |||
public void commitData(final Interface parent) { | |||
parent.setAddressString(addresses); | |||
parent.setDnsString(dnses); | |||
parent.setExcludedApplicationsString(excludedApplications); | |||
parent.setPrivateKey(privateKey); | |||
parent.setListenPortString(listenPort); | |||
parent.setMtuString(mtu); | |||
@@ -254,6 +286,11 @@ public class Interface { | |||
return dnses; | |||
} | |||
@Bindable | |||
public String getExcludedApplications() { | |||
return excludedApplications; | |||
} | |||
@Bindable | |||
public String getListenPort() { | |||
return listenPort; | |||
@@ -277,6 +314,7 @@ public class Interface { | |||
protected void loadData(final Interface parent) { | |||
addresses = parent.getAddressString(); | |||
dnses = parent.getDnsString(); | |||
excludedApplications = parent.getExcludedApplicationsString(); | |||
publicKey = parent.getPublicKey(); | |||
privateKey = parent.getPrivateKey(); | |||
listenPort = parent.getListenPortString(); | |||
@@ -293,6 +331,11 @@ public class Interface { | |||
notifyPropertyChanged(BR.dnses); | |||
} | |||
public void setExcludedApplications(final String excludedApplications) { | |||
this.excludedApplications = excludedApplications; | |||
notifyPropertyChanged(BR.excludedApplications); | |||
} | |||
public void setListenPort(final String listenPort) { | |||
this.listenPort = listenPort; | |||
notifyPropertyChanged(BR.listenPort); | |||
@@ -324,6 +367,7 @@ public class Interface { | |||
dest.writeString(privateKey); | |||
dest.writeString(listenPort); | |||
dest.writeString(mtu); | |||
dest.writeString(excludedApplications); | |||
} | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<layout xmlns:android="http://schemas.android.com/apk/res/android" | |||
xmlns:app="http://schemas.android.com/apk/res-auto"> | |||
<data> | |||
<import type="android.view.View" /> | |||
<import type="com.wireguard.android.model.ApplicationData" /> | |||
<variable | |||
name="fragment" | |||
type="com.wireguard.android.fragment.AppListDialogFragment" /> | |||
<variable | |||
name="appData" | |||
type="com.wireguard.android.util.ObservableKeyedList<String, ApplicationData>" /> | |||
</data> | |||
<FrameLayout | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:minHeight="200dp" > | |||
<ProgressBar | |||
android:id="@+id/progress_bar" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_gravity="center" | |||
android:indeterminate="true" | |||
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"/> | |||
<android.support.v7.widget.RecyclerView | |||
android:id="@+id/app_list" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
app:items="@{appData}" | |||
app:layout="@{@layout/app_list_item}" /> | |||
</FrameLayout> | |||
</layout> |
@@ -0,0 +1,56 @@ | |||
<?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> | |||
<import type="com.wireguard.android.model.ApplicationData" /> | |||
<variable | |||
name="collection" | |||
type="com.wireguard.android.util.ObservableKeyedList<String, com.wireguard.android.model.ApplicationData>" /> | |||
<variable | |||
name="key" | |||
type="String" /> | |||
<variable | |||
name="item" | |||
type="com.wireguard.android.model.ApplicationData" /> | |||
</data> | |||
<LinearLayout | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:background="@drawable/list_item_background_anim" | |||
android:padding="16dp" | |||
android:orientation="horizontal" | |||
android:gravity="center_vertical" | |||
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"> | |||
<ImageView | |||
android:id="@+id/app_icon" | |||
android:layout_width="32dp" | |||
android:layout_height="32dp" | |||
android:src="@{item.icon}" /> | |||
<TextView | |||
android:id="@+id/app_name" | |||
style="?android:attr/textAppearanceMedium" | |||
android:layout_width="0dp" | |||
android:layout_height="wrap_content" | |||
android:layout_weight="1" | |||
android:ellipsize="end" | |||
android:maxLines="1" | |||
android:paddingEnd="8dp" | |||
android:paddingStart="8dp" | |||
android:text="@{key}" /> | |||
<CheckBox | |||
android:id="@+id/excluded_checkbox" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:checked="@={item.excludedFromTunnel}" /> | |||
</LinearLayout> | |||
</layout> |
@@ -13,6 +13,10 @@ | |||
<import type="com.wireguard.config.Peer" /> | |||
<variable | |||
name="fragment" | |||
type="com.wireguard.android.fragment.TunnelEditorFragment" /> | |||
<variable | |||
name="config" | |||
type="com.wireguard.config.Config.Observable" /> | |||
@@ -211,6 +215,15 @@ | |||
android:inputType="number" | |||
android:text="@={config.interfaceSection.mtu}" | |||
android:textAlignment="center" /> | |||
<Button | |||
style="@style/Widget.AppCompat.Button.Borderless.Colored" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_marginLeft="-8dp" | |||
android:layout_below="@+id/dns_servers_text" | |||
android:onClick="@{fragment::onRequestSetExcludedApplications}" | |||
android:text="@{fragment.excludedApplications().length == 0 ? @string/set_excluded_applications : String.format(@string/x_excluded_applications, fragment.excludedApplications().length)}" /> | |||
</RelativeLayout> | |||
</android.support.v7.widget.CardView> | |||
@@ -24,6 +24,7 @@ | |||
<string name="addresses">Addresses</string> | |||
<string name="allowed_ips">Allowed IPs</string> | |||
<string name="app_name">WireGuard</string> | |||
<string name="cancel">Cancel</string> | |||
<string name="config_save_error">Unable to save configuration for “%s”: %s</string> | |||
<string name="config_save_success">Successfully saved configuration for “%s”</string> | |||
<string name="create_activity_title">Create WireGuard Tunnel</string> | |||
@@ -37,9 +38,11 @@ | |||
<string name="dns_servers">DNS servers</string> | |||
<string name="edit">Edit</string> | |||
<string name="endpoint">Endpoint</string> | |||
<string name="error_fetching_apps">Error fetching apps list: %s</string> | |||
<string name="error_down">Error bringing down tunnel: %s</string> | |||
<string name="error_root">Please obtain root access and try again</string> | |||
<string name="error_up">Error bringing up tunnel: %s</string> | |||
<string name="excluded_applications">Excluded Applications</string> | |||
<string name="generate">Generate</string> | |||
<string name="hint_automatic">(auto)</string> | |||
<string name="hint_generated">(generated)</string> | |||
@@ -66,6 +69,8 @@ | |||
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string> | |||
<string name="restore_on_boot_title">Restore on boot</string> | |||
<string name="save">Save</string> | |||
<string name="set_excluded_applications">Set Excluded Applications</string> | |||
<string name="set_exclusions">Set Exclusions</string> | |||
<string name="settings">Settings</string> | |||
<string name="toggle_error">Error toggling WireGuard tunnel: %s</string> | |||
<string name="tools_installer_already">wg and wg-quick are already installed</string> | |||
@@ -85,6 +90,7 @@ | |||
<string name="version_summary">%s backend v%s</string> | |||
<string name="version_summary_checking">Checking %s backend version</string> | |||
<string name="version_summary_unknown">Unknown %s version</string> | |||
<string name="x_excluded_applications">%d Excluded Applications</string> | |||
<string name="zip_exporter_title">Export tunnels to zip file</string> | |||
<string name="zip_export_error">Unable to export tunnels: %s</string> | |||
<string name="zip_export_success">Saved to %s</string> | |||