diff options
author | Eric Kuck <eric@bluelinelabs.com> | 2018-07-04 16:47:55 -0500 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-07-06 04:14:19 +0200 |
commit | 500a705531a3210aa98b17b77085cf1ea86c274d (patch) | |
tree | 8eee63dc202e5e1414354f5827873a1e9c642be9 /app | |
parent | 5729947d6ca3aa99d3811ca0e6624ad0ce0f969d (diff) |
AppListDialogFragment: add implementation for excluding applications
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
Diffstat (limited to 'app')
12 files changed, 550 insertions, 4 deletions
diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java index fbe9d8ea..466e580d 100644 --- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java +++ b/app/src/main/java/com/wireguard/android/backend/GoBackend.java @@ -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); } + } } diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java index ba6845ba..9a4a9a27 100644 --- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java +++ b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java @@ -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); } + } diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java new file mode 100644 index 00000000..8917441f --- /dev/null +++ b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java @@ -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; + } + } + +} diff --git a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java new file mode 100644 index 00000000..7b238dd5 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java @@ -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); + } + +} diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java index ea03077c..c2778c36 100644 --- a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java +++ b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java @@ -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()); + } + } diff --git a/app/src/main/java/com/wireguard/android/model/ApplicationData.java b/app/src/main/java/com/wireguard/android/model/ApplicationData.java new file mode 100644 index 00000000..77e8da33 --- /dev/null +++ b/app/src/main/java/com/wireguard/android/model/ApplicationData.java @@ -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; + } +} diff --git a/app/src/main/java/com/wireguard/config/Attribute.java b/app/src/main/java/com/wireguard/config/Attribute.java index 7fbffa7f..5ce6431d 100644 --- a/app/src/main/java/com/wireguard/config/Attribute.java +++ b/app/src/main/java/com/wireguard/config/Attribute.java @@ -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()); } diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/app/src/main/java/com/wireguard/config/Interface.java index 824bb122..c3068dbb 100644 --- a/app/src/main/java/com/wireguard/config/Interface.java +++ b/app/src/main/java/com/wireguard/config/Interface.java @@ -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); @@ -255,6 +287,11 @@ public class Interface { } @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); } } } diff --git a/app/src/main/res/layout/app_list_dialog_fragment.xml b/app/src/main/res/layout/app_list_dialog_fragment.xml new file mode 100644 index 00000000..25879b6b --- /dev/null +++ b/app/src/main/res/layout/app_list_dialog_fragment.xml @@ -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> diff --git a/app/src/main/res/layout/app_list_item.xml b/app/src/main/res/layout/app_list_item.xml new file mode 100644 index 00000000..d4a41c83 --- /dev/null +++ b/app/src/main/res/layout/app_list_item.xml @@ -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> diff --git a/app/src/main/res/layout/tunnel_editor_fragment.xml b/app/src/main/res/layout/tunnel_editor_fragment.xml index 060920d6..cbdc46cf 100644 --- a/app/src/main/res/layout/tunnel_editor_fragment.xml +++ b/app/src/main/res/layout/tunnel_editor_fragment.xml @@ -14,6 +14,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" /> </data> @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56966750..082a7477 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> |