summaryrefslogtreecommitdiffhomepage
path: root/ui/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com')
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java142
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt117
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/BaseFragment.java129
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt108
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java121
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt105
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java165
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt138
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java264
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt236
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java450
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt415
12 files changed, 1119 insertions, 1271 deletions
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java
deleted file mode 100644
index ceb1725c..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.widget.Toast;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.AppListDialogFragmentBinding;
-import com.wireguard.android.model.ApplicationData;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.util.ObservableKeyedArrayList;
-import com.wireguard.android.util.ObservableKeyedList;
-import com.wireguard.util.NonNullForAll;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import java9.util.Comparators;
-import java9.util.stream.Collectors;
-import java9.util.stream.StreamSupport;
-
-@NonNullForAll
-public class AppListDialogFragment extends DialogFragment {
-
- private static final String KEY_EXCLUDED_APPS = "excludedApps";
- private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
- private List<String> currentlyExcludedApps = Collections.emptyList();
-
- public static <T extends Fragment & AppExclusionListener>
- AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
- final Bundle extras = new Bundle();
- extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
- final AppListDialogFragment fragment = new AppListDialogFragment();
- fragment.setTargetFragment(target, 0);
- fragment.setArguments(extras);
- return fragment;
- }
-
- private void loadData() {
- final Activity activity = getActivity();
- if (activity == null) {
- return;
- }
-
- final PackageManager pm = activity.getPackageManager();
- Application.getAsyncWorker().supplyAsync(() -> {
- final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
- launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
-
- final List<ApplicationData> applicationData = new ArrayList<>();
- for (ResolveInfo resolveInfo : resolveInfos) {
- String packageName = resolveInfo.activityInfo.packageName;
- applicationData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
- }
-
- Collections.sort(applicationData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
- return applicationData;
- }).whenComplete(((data, throwable) -> {
- if (data != null) {
- appData.clear();
- appData.addAll(data);
- } else {
- final String error = ErrorMessages.get(throwable);
- final String message = activity.getString(R.string.error_fetching_apps, error);
- Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
- dismissAllowingStateLoss();
- }
- }));
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- final List<String> excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS);
- currentlyExcludedApps = (excludedApps != null) ? excludedApps : Collections.emptyList();
- }
-
- @Override
- public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
- final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(requireActivity());
- alertDialogBuilder.setTitle(R.string.excluded_applications);
-
- final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(requireActivity().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());
- alertDialogBuilder.setNeutralButton(R.string.toggle_all, (dialog, which) -> {
- });
-
- binding.setFragment(this);
- binding.setAppData(appData);
-
- loadData();
-
- final AlertDialog dialog = alertDialogBuilder.create();
- dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
- final List<ApplicationData> selectedItems = StreamSupport.stream(appData)
- .filter(ApplicationData::isExcludedFromTunnel)
- .collect(Collectors.toList());
- final boolean excludeAll = selectedItems.isEmpty();
- for (final ApplicationData app : appData)
- app.setExcludedFromTunnel(excludeAll);
- }));
- return dialog;
- }
-
- private void setExclusionsAndDismiss() {
- final List<String> excludedApps = new ArrayList<>();
- for (final 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/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
new file mode 100644
index 00000000..01dadb08
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.databinding.AppListDialogFragmentBinding
+import com.wireguard.android.model.ApplicationData
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.ObservableKeyedArrayList
+import com.wireguard.android.util.ObservableKeyedList
+import java9.util.Comparators
+import java9.util.function.Function
+import java.util.Collections
+
+class AppListDialogFragment : DialogFragment() {
+ private val appData: ObservableKeyedList<String, ApplicationData> = ObservableKeyedArrayList()
+ private var currentlyExcludedApps = emptyList<String>()
+
+ private fun loadData() {
+ val activity = activity ?: return
+ val pm = activity.packageManager
+ Application.getAsyncWorker().supplyAsync<List<ApplicationData>> {
+ val launcherIntent = Intent(Intent.ACTION_MAIN, null)
+ launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
+ val resolveInfos = pm.queryIntentActivities(launcherIntent, 0)
+ val applicationData: MutableList<ApplicationData> = ArrayList()
+ resolveInfos.forEach {
+ val packageName = it.activityInfo.packageName
+ applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)))
+ }
+
+ Collections.sort(applicationData, Comparators.comparing(Function { obj: ApplicationData -> obj.name }, java.lang.String.CASE_INSENSITIVE_ORDER))
+ applicationData
+ }.whenComplete { data, throwable ->
+ if (data != null) {
+ appData.clear()
+ appData.addAll(data)
+ } else {
+ val error = ErrorMessages.get(throwable)
+ val message = activity.getString(R.string.error_fetching_apps, error)
+ Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
+ dismissAllowingStateLoss()
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS)
+ currentlyExcludedApps = (excludedApps ?: emptyList())
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val alertDialogBuilder = AlertDialog.Builder(requireActivity())
+ alertDialogBuilder.setTitle(R.string.excluded_applications)
+ val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
+ binding.executePendingBindings()
+ alertDialogBuilder.setView(binding.root)
+ alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() }
+ alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
+ alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
+ binding.fragment = this
+ binding.appData = appData
+ loadData()
+ val dialog = alertDialogBuilder.create()
+ dialog.setOnShowListener {
+ dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
+ val selectedItems = appData
+ .filter { obj: ApplicationData -> obj.isExcludedFromTunnel }
+
+ val excludeAll = selectedItems.isEmpty()
+ appData.forEach {
+ it.isExcludedFromTunnel = excludeAll
+ }
+ }
+ }
+ return dialog
+ }
+
+ private fun setExclusionsAndDismiss() {
+ val excludedApps: MutableList<String> = ArrayList()
+ for (data in appData) {
+ if (data.isExcludedFromTunnel) {
+ excludedApps.add(data.packageName)
+ }
+ }
+ (targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps)
+ dismiss()
+ }
+
+ interface AppExclusionListener {
+ fun onExcludedAppsSelected(excludedApps: List<String>)
+ }
+
+ companion object {
+ private const val KEY_EXCLUDED_APPS = "excludedApps"
+ fun <T> newInstance(excludedApps: ArrayList<String?>?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? {
+ val extras = Bundle()
+ extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps)
+ val fragment = AppListDialogFragment()
+ fragment.setTargetFragment(target, 0)
+ fragment.arguments = extras
+ return fragment
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.java b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.java
deleted file mode 100644
index 9e7b635a..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-import android.view.View;
-import android.widget.Toast;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.activity.BaseActivity;
-import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
-import com.wireguard.android.backend.GoBackend;
-import com.wireguard.android.backend.Tunnel.State;
-import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
-import com.wireguard.android.databinding.TunnelListItemBinding;
-import com.wireguard.android.model.ObservableTunnel;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.util.NonNullForAll;
-
-import androidx.annotation.Nullable;
-import androidx.databinding.DataBindingUtil;
-import androidx.databinding.ViewDataBinding;
-import androidx.fragment.app.Fragment;
-
-/**
- * Base class for fragments that need to know the currently-selected tunnel. Only does anything when
- * attached to a {@code BaseActivity}.
- */
-
-@NonNullForAll
-public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
- private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
- private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
- @Nullable private BaseActivity activity;
- @Nullable private ObservableTunnel pendingTunnel;
- @Nullable private Boolean pendingTunnelUp;
-
- @Nullable
- protected ObservableTunnel getSelectedTunnel() {
- return activity != null ? activity.getSelectedTunnel() : null;
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
- if (pendingTunnel != null && pendingTunnelUp != null)
- setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
- pendingTunnel = null;
- pendingTunnelUp = null;
- }
- }
-
- @Override
- public void onAttach(final Context context) {
- super.onAttach(context);
- if (context instanceof BaseActivity) {
- activity = (BaseActivity) context;
- activity.addOnSelectedTunnelChangedListener(this);
- } else {
- activity = null;
- }
- }
-
- @Override
- public void onDetach() {
- if (activity != null)
- activity.removeOnSelectedTunnelChangedListener(this);
- activity = null;
- super.onDetach();
- }
-
- protected void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
- if (activity != null)
- activity.setSelectedTunnel(tunnel);
- }
-
- public void setTunnelState(final View view, final boolean checked) {
- final ViewDataBinding binding = DataBindingUtil.findBinding(view);
- final ObservableTunnel tunnel;
- if (binding instanceof TunnelDetailFragmentBinding)
- tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
- else if (binding instanceof TunnelListItemBinding)
- tunnel = ((TunnelListItemBinding) binding).getItem();
- else
- return;
- if (tunnel == null)
- return;
-
- Application.getBackendAsync().thenAccept(backend -> {
- if (backend instanceof GoBackend) {
- final Intent intent = GoBackend.VpnService.prepare(view.getContext());
- if (intent != null) {
- pendingTunnel = tunnel;
- pendingTunnelUp = checked;
- startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
- return;
- }
- }
-
- setTunnelStateWithPermissionsResult(tunnel, checked);
- });
- }
-
- private void setTunnelStateWithPermissionsResult(final ObservableTunnel tunnel, final boolean checked) {
- tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
- if (throwable == null)
- return;
- final String error = ErrorMessages.get(throwable);
- final int messageResId = checked ? R.string.error_up : R.string.error_down;
- final String message = requireContext().getString(messageResId, error);
- final View view = getView();
- if (view != null)
- Snackbar.make(view, message, Snackbar.LENGTH_LONG).setAnchorView(view.findViewById(R.id.create_fab)).show();
- else
- Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
- Log.e(TAG, message, throwable);
- });
- }
-
-}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
new file mode 100644
index 00000000..12b99f25
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.activity.BaseActivity
+import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
+import com.wireguard.android.backend.Backend
+import com.wireguard.android.backend.GoBackend
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.TunnelDetailFragmentBinding
+import com.wireguard.android.databinding.TunnelListItemBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.ErrorMessages
+
+/**
+ * Base class for fragments that need to know the currently-selected tunnel. Only does anything when
+ * attached to a `BaseActivity`.
+ */
+abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
+ private var baseActivity: BaseActivity? = null
+ private var pendingTunnel: ObservableTunnel? = null
+ private var pendingTunnelUp: Boolean? = null
+ protected var selectedTunnel: ObservableTunnel?
+ get() = baseActivity?.selectedTunnel
+ protected set(tunnel) {
+ baseActivity?.selectedTunnel = tunnel
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
+ if (pendingTunnel != null && pendingTunnelUp != null) setTunnelStateWithPermissionsResult(pendingTunnel!!, pendingTunnelUp!!)
+ pendingTunnel = null
+ pendingTunnelUp = null
+ }
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ if (context is BaseActivity) {
+ baseActivity = context
+ baseActivity?.addOnSelectedTunnelChangedListener(this)
+ } else {
+ baseActivity = null
+ }
+ }
+
+ override fun onDetach() {
+ baseActivity?.removeOnSelectedTunnelChangedListener(this)
+ baseActivity = null
+ super.onDetach()
+ }
+
+ fun setTunnelState(view: View, checked: Boolean) {
+ val tunnel = when (val binding = DataBindingUtil.findBinding<ViewDataBinding>(view)) {
+ is TunnelDetailFragmentBinding -> binding.tunnel
+ is TunnelListItemBinding -> binding.item
+ else -> return
+ }
+ Application.getBackendAsync().thenAccept { backend: Backend? ->
+ if (backend is GoBackend) {
+ val intent = GoBackend.VpnService.prepare(view.context)
+ if (intent != null) {
+ pendingTunnel = tunnel
+ pendingTunnelUp = checked
+ startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION)
+ return@thenAccept
+ }
+ }
+ setTunnelStateWithPermissionsResult(tunnel!!, checked)
+ }
+ }
+
+ private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
+ tunnel.setState(Tunnel.State.of(checked)).whenComplete { _, throwable ->
+ if (throwable == null) return@whenComplete
+ val error = ErrorMessages.get(throwable)
+ val messageResId = if (checked) R.string.error_up else R.string.error_down
+ val message = requireContext().getString(messageResId, error)
+ val view = view
+ if (view != null)
+ Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+ .setAnchorView(view.findViewById<View>(R.id.create_fab))
+ .show()
+ else
+ Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
+ Log.e(TAG, message, throwable)
+ }
+ }
+
+ companion object {
+ private const val REQUEST_CODE_VPN_PERMISSION = 23491
+ private val TAG = "WireGuard/" + BaseFragment::class.java.simpleName
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java
deleted file mode 100644
index aa152172..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.view.inputmethod.InputMethodManager;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-import com.wireguard.util.NonNullForAll;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-@NonNullForAll
-public class ConfigNamingDialogFragment extends DialogFragment {
- private static final String KEY_CONFIG_TEXT = "config_text";
-
- @Nullable private ConfigNamingDialogFragmentBinding binding;
- @Nullable private Config config;
- @Nullable private InputMethodManager imm;
-
- public static ConfigNamingDialogFragment newInstance(final String configText) {
- final Bundle extras = new Bundle();
- extras.putString(KEY_CONFIG_TEXT, configText);
- final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
- fragment.setArguments(extras);
- return fragment;
- }
-
- private void createTunnelAndDismiss() {
- if (binding != null) {
- final String name = binding.tunnelNameText.getText().toString();
-
- Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
- if (tunnel != null) {
- dismiss();
- } else {
- binding.tunnelNameTextLayout.setError(throwable.getMessage());
- }
- });
- }
- }
-
- @Override
- public void dismiss() {
- setKeyboardVisible(false);
- super.dismiss();
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Bundle arguments = getArguments();
- final String configText = arguments.getString(KEY_CONFIG_TEXT);
- final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
- try {
- config = Config.parse(new ByteArrayInputStream(configBytes));
- } catch (final BadConfigException | IOException e) {
- throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
- }
- }
-
- @Override
- public Dialog onCreateDialog(final Bundle savedInstanceState) {
- final Activity activity = requireActivity();
-
- imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
-
- final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
- alertDialogBuilder.setTitle(R.string.import_from_qr_code);
-
- binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
- binding.executePendingBindings();
- alertDialogBuilder.setView(binding.getRoot());
-
- alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
- alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
-
- return alertDialogBuilder.create();
- }
-
- @Override public void onResume() {
- super.onResume();
-
- final AlertDialog dialog = (AlertDialog) getDialog();
- if (dialog != null) {
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
-
- setKeyboardVisible(true);
- }
- }
-
- private void setKeyboardVisible(final boolean visible) {
- Objects.requireNonNull(imm);
-
- if (visible) {
- imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
- } else if (binding != null) {
- imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
- }
- }
-
-}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
new file mode 100644
index 00000000..6a60ead6
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.app.Activity
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Config
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+
+class ConfigNamingDialogFragment : DialogFragment() {
+ private var binding: ConfigNamingDialogFragmentBinding? = null
+ private var config: Config? = null
+ private var imm: InputMethodManager? = null
+
+ private fun createTunnelAndDismiss() {
+ if (binding != null) {
+ val name = binding!!.tunnelNameText.text.toString()
+ Application.getTunnelManager().create(name, config).whenComplete { tunnel: ObservableTunnel?, throwable: Throwable ->
+ if (tunnel != null) {
+ dismiss()
+ } else {
+ binding!!.tunnelNameTextLayout.error = throwable.message
+ }
+ }
+ }
+ }
+
+ override fun dismiss() {
+ setKeyboardVisible(false)
+ super.dismiss()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val configText = requireArguments().getString(KEY_CONFIG_TEXT)
+ val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
+ config = try {
+ Config.parse(ByteArrayInputStream(configBytes))
+ } catch (e: BadConfigException) {
+ throw IllegalArgumentException("Invalid config passed to " + javaClass.simpleName, e)
+ } catch (e: IOException) {
+ throw IllegalArgumentException("Invalid config passed to " + javaClass.simpleName, e)
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val activity: Activity = requireActivity()
+ imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ val alertDialogBuilder = AlertDialog.Builder(activity)
+ alertDialogBuilder.setTitle(R.string.import_from_qr_code)
+ binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
+ binding?.apply {
+ executePendingBindings()
+ alertDialogBuilder.setView(root)
+ }
+ alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null)
+ alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
+ return alertDialogBuilder.create()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val dialog = dialog as AlertDialog?
+ if (dialog != null) {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { createTunnelAndDismiss() }
+ setKeyboardVisible(true)
+ }
+ }
+
+ private fun setKeyboardVisible(visible: Boolean) {
+ if (visible) {
+ imm!!.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
+ } else if (binding != null) {
+ imm!!.hideSoftInputFromWindow(binding!!.tunnelNameText.windowToken, 0)
+ }
+ }
+
+ companion object {
+ private const val KEY_CONFIG_TEXT = "config_text"
+
+ @JvmStatic
+ fun newInstance(configText: String?): ConfigNamingDialogFragment {
+ val extras = Bundle()
+ extras.putString(KEY_CONFIG_TEXT, configText)
+ val fragment = ConfigNamingDialogFragment()
+ fragment.arguments = extras
+ return fragment
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java
deleted file mode 100644
index eb76a220..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-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.R;
-import com.wireguard.android.backend.Tunnel.State;
-import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
-import com.wireguard.android.databinding.TunnelDetailPeerBinding;
-import com.wireguard.android.model.ObservableTunnel;
-import com.wireguard.android.ui.EdgeToEdge;
-import com.wireguard.crypto.Key;
-import com.wireguard.util.NonNullForAll;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-import androidx.annotation.Nullable;
-import androidx.databinding.DataBindingUtil;
-
-/**
- * Fragment that shows details about a specific tunnel.
- */
-
-@NonNullForAll
-public class TunnelDetailFragment extends BaseFragment {
- @Nullable private TunnelDetailFragmentBinding binding;
- @Nullable private State lastState = State.TOGGLE;
- @Nullable private Timer timer;
-
- @SuppressWarnings("MagicNumber")
- private String formatBytes(final long bytes) {
- if (bytes < 1024)
- return requireContext().getString(R.string.transfer_bytes, bytes);
- else if (bytes < 1024 * 1024)
- return requireContext().getString(R.string.transfer_kibibytes, bytes / 1024.0);
- else if (bytes < 1024 * 1024 * 1024)
- return requireContext().getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0));
- else if (bytes < 1024 * 1024 * 1024 * 1024L)
- return requireContext().getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0));
- return requireContext().getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0);
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.tunnel_detail, menu);
- }
-
- @Override
- public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
- binding.executePendingBindings();
- EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
- EdgeToEdge.setUpScrollingContent((ViewGroup) binding.getRoot(), null);
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- timer = new Timer();
- timer.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- updateStats();
- }
- }, 0, 1000);
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
- if (binding == null)
- return;
- binding.setTunnel(newTunnel);
- if (newTunnel == null)
- binding.setConfig(null);
- else
- newTunnel.getConfigAsync().thenAccept(binding::setConfig);
- lastState = State.TOGGLE;
- updateStats();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (timer != null) {
- timer.cancel();
- timer = null;
- }
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
- onSelectedTunnelChanged(null, getSelectedTunnel());
- super.onViewStateRestored(savedInstanceState);
- }
-
- private void updateStats() {
- if (binding == null || !isResumed())
- return;
- final ObservableTunnel tunnel = binding.getTunnel();
- if (tunnel == null)
- return;
- final State state = tunnel.getState();
- if (state != State.UP && lastState == state)
- return;
- lastState = state;
- tunnel.getStatisticsAsync().whenComplete((statistics, throwable) -> {
- if (throwable != null) {
- for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
- final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
- if (peer == null)
- continue;
- peer.transferLabel.setVisibility(View.GONE);
- peer.transferText.setVisibility(View.GONE);
- }
- return;
- }
- for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
- final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
- if (peer == null)
- continue;
- final Key publicKey = peer.getItem().getPublicKey();
- final long rx = statistics.peerRx(publicKey);
- final long tx = statistics.peerTx(publicKey);
- if (rx == 0 && tx == 0) {
- peer.transferLabel.setVisibility(View.GONE);
- peer.transferText.setVisibility(View.GONE);
- continue;
- }
- peer.transferText.setText(requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx)));
- peer.transferLabel.setVisibility(View.VISIBLE);
- peer.transferText.setVisibility(View.VISIBLE);
- }
- });
- }
-}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
new file mode 100644
index 00000000..22e5d94e
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+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 androidx.databinding.DataBindingUtil
+import com.wireguard.android.R
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.TunnelDetailFragmentBinding
+import com.wireguard.android.databinding.TunnelDetailPeerBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.ui.EdgeToEdge.setUpRoot
+import com.wireguard.android.ui.EdgeToEdge.setUpScrollingContent
+import com.wireguard.config.Config
+import java.util.Timer
+import java.util.TimerTask
+
+/**
+ * Fragment that shows details about a specific tunnel.
+ */
+class TunnelDetailFragment : BaseFragment() {
+ private var binding: TunnelDetailFragmentBinding? = null
+ private var lastState: Tunnel.State? = Tunnel.State.TOGGLE
+ private var timer: Timer? = null
+
+ private fun formatBytes(bytes: Long): String {
+ val context = requireContext()
+ return when {
+ bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
+ bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
+ bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
+ bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
+ else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.tunnel_detail, menu)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
+ binding?.apply {
+ executePendingBindings()
+ setUpRoot(root as ViewGroup)
+ setUpScrollingContent(root as ViewGroup, null)
+ }
+ return binding!!.root
+ }
+
+ override fun onDestroyView() {
+ binding = null
+ super.onDestroyView()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ timer = Timer()
+ timer!!.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ updateStats()
+ }
+ }, 0, 1000)
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ if (binding == null) return
+ binding!!.tunnel = newTunnel
+ if (newTunnel == null) binding!!.config = null else newTunnel.configAsync.thenAccept { config: Config? -> binding!!.config = config }
+ lastState = Tunnel.State.TOGGLE
+ updateStats()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (timer != null) {
+ timer!!.cancel()
+ timer = null
+ }
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ if (binding == null) {
+ return
+ }
+ binding!!.fragment = this
+ onSelectedTunnelChanged(null, selectedTunnel)
+ super.onViewStateRestored(savedInstanceState)
+ }
+
+ private fun updateStats() {
+ if (binding == null || !isResumed) return
+ val tunnel = binding!!.tunnel ?: return
+ val state = tunnel.state
+ if (state != Tunnel.State.UP && lastState == state) return
+ lastState = state
+ tunnel.statisticsAsync.whenComplete { statistics, throwable ->
+ if (throwable != null) {
+ for (i in 0 until binding!!.peersLayout.childCount) {
+ val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
+ ?: continue
+ peer.transferLabel.visibility = View.GONE
+ peer.transferText.visibility = View.GONE
+ }
+ return@whenComplete
+ }
+ for (i in 0 until binding!!.peersLayout.childCount) {
+ val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding!!.peersLayout.getChildAt(i))
+ ?: continue
+ val publicKey = peer.item!!.publicKey
+ val rx = statistics.peerRx(publicKey)
+ val tx = statistics.peerTx(publicKey)
+ if (rx == 0L && tx == 0L) {
+ peer.transferLabel.visibility = View.GONE
+ peer.transferText.visibility = View.GONE
+ continue
+ }
+ peer.transferText.text = requireContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx))
+ peer.transferLabel.visibility = View.VISIBLE
+ peer.transferText.visibility = View.VISIBLE
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java
deleted file mode 100644
index cf3ee34b..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Log;
-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 android.widget.Toast;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.backend.Tunnel;
-import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
-import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
-import com.wireguard.android.model.ObservableTunnel;
-import com.wireguard.android.model.TunnelManager;
-import com.wireguard.android.ui.EdgeToEdge;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.viewmodel.ConfigProxy;
-import com.wireguard.config.Config;
-import com.wireguard.util.NonNullForAll;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-import androidx.annotation.Nullable;
-import androidx.databinding.ObservableList;
-
-/**
- * Fragment for editing a WireGuard configuration.
- */
-
-@NonNullForAll
-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();
-
- @Nullable private TunnelEditorFragmentBinding binding;
- @Nullable private ObservableTunnel tunnel;
-
- private void onConfigLoaded(final Config config) {
- if (binding != null) {
- binding.setConfig(new ConfigProxy(config));
- }
- }
-
- private void onConfigSaved(final Tunnel savedTunnel, @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getString(R.string.config_save_success, savedTunnel.getName());
- Log.d(TAG, message);
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
- onFinished();
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.config_save_error, savedTunnel.getName(), error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.config_editor, menu);
- }
-
- @Override
- public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
- binding.executePendingBindings();
- EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
- EdgeToEdge.setUpScrollingContent(binding.mainContainer, null);
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onExcludedAppsSelected(final List<String> excludedApps) {
- Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
- final ObservableList<String> excludedApplications =
- binding.getConfig().getInterface().getExcludedApplications();
- excludedApplications.clear();
- excludedApplications.addAll(excludedApps);
- }
-
- private void onFinished() {
- // Hide the keyboard; it rarely goes away on its own.
- final Activity activity = getActivity();
- if (activity == null) return;
- final View focusedView = activity.getCurrentFocus();
- if (focusedView != null) {
- final InputMethodManager inputManager = (InputMethodManager)
- activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- if (inputManager != null)
- inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
- InputMethodManager.HIDE_NOT_ALWAYS);
- }
- // Tell the activity to finish itself or go back to the detail view.
- requireActivity().runOnUiThread(() -> {
- // TODO(smaeul): Remove this hack when fixing the Config ViewModel
- // The selected tunnel has to actually change, but we have to remember this one.
- final ObservableTunnel savedTunnel = tunnel;
- if (savedTunnel == getSelectedTunnel())
- setSelectedTunnel(null);
- setSelectedTunnel(savedTunnel);
- });
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- if (item.getItemId() == R.id.menu_action_save) {
- if (binding == null)
- return false;
- final Config newConfig;
- try {
- newConfig = binding.getConfig().resolve();
- } catch (final Exception e) {
- final String error = ErrorMessages.get(e);
- final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
- final String message = getString(R.string.config_save_error, tunnelName, error);
- Log.e(TAG, message, e);
- Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
- return false;
- }
- if (tunnel == null) {
- Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
- final TunnelManager manager = Application.getTunnelManager();
- manager.create(binding.getName(), newConfig)
- .whenComplete(this::onTunnelCreated);
- } else if (!tunnel.getName().equals(binding.getName())) {
- Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
- tunnel.setName(binding.getName())
- .whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
- } else {
- Log.d(TAG, "Attempting to save config of " + tunnel.getName());
- tunnel.setConfig(newConfig)
- .whenComplete((a, b) -> onConfigSaved(tunnel, b));
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
- if (binding != null) {
- final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
- final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
- fragment.show(getParentFragmentManager(), null);
- }
- }
-
- @Override
- public void onSaveInstanceState(final Bundle outState) {
- if (binding != null)
- outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
- outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
- @Nullable final ObservableTunnel newTunnel) {
- tunnel = newTunnel;
- if (binding == null)
- return;
- binding.setConfig(new ConfigProxy());
- if (tunnel != null) {
- binding.setName(tunnel.getName());
- tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
- } else {
- binding.setName("");
- }
- }
-
- private void onTunnelCreated(final ObservableTunnel newTunnel, @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- tunnel = newTunnel;
- message = getString(R.string.tunnel_create_success, tunnel.getName());
- Log.d(TAG, message);
- Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
- onFinished();
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.tunnel_create_error, error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- private void onTunnelRenamed(final ObservableTunnel renamedTunnel, final Config newConfig,
- @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
- Log.d(TAG, message);
- // Now save the rest of configuration changes.
- Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
- renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.tunnel_rename_error, error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
-
- if (savedInstanceState == null) {
- onSelectedTunnelChanged(null, getSelectedTunnel());
- } else {
- tunnel = getSelectedTunnel();
- final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
- final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
- if (tunnel != null && !tunnel.getName().equals(originalName))
- onSelectedTunnelChanged(null, tunnel);
- else
- binding.setConfig(config);
- }
-
- super.onViewStateRestored(savedInstanceState);
- }
-
-}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
new file mode 100644
index 00000000..b69f36bd
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+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 android.widget.Toast
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.TunnelEditorFragmentBinding
+import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.ui.EdgeToEdge.setUpRoot
+import com.wireguard.android.ui.EdgeToEdge.setUpScrollingContent
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.viewmodel.ConfigProxy
+import com.wireguard.config.Config
+
+/**
+ * Fragment for editing a WireGuard configuration.
+ */
+class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
+ private var binding: TunnelEditorFragmentBinding? = null
+ private var tunnel: ObservableTunnel? = null
+ private fun onConfigLoaded(config: Config) {
+ if (binding != null) {
+ binding!!.config = ConfigProxy(config)
+ }
+ }
+
+ private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
+ val message: String
+ if (throwable == null) {
+ message = getString(R.string.config_save_success, savedTunnel.name)
+ Log.d(TAG, message)
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
+ onFinished()
+ } else {
+ val error = ErrorMessages.get(throwable)
+ message = getString(R.string.config_save_error, savedTunnel.name, error)
+ Log.e(TAG, message, throwable)
+ if (binding != null) {
+ Snackbar.make(binding!!.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.config_editor, menu)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
+ binding?.apply {
+ executePendingBindings()
+ setUpRoot(root as ViewGroup)
+ setUpScrollingContent(mainContainer, null)
+ }
+ return binding?.root
+ }
+
+ override fun onDestroyView() {
+ binding = null
+ super.onDestroyView()
+ }
+
+ override fun onExcludedAppsSelected(excludedApps: List<String>) {
+ requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" }
+ binding!!.config!!.getInterface().excludedApplications.apply {
+ clear()
+ addAll(excludedApps)
+ }
+ }
+
+ private fun onFinished() {
+ // Hide the keyboard; it rarely goes away on its own.
+ val activity = activity ?: return
+ val focusedView = activity.currentFocus
+ if (focusedView != null) {
+ val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
+ inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS)
+ }
+ // Tell the activity to finish itself or go back to the detail view.
+ requireActivity().runOnUiThread {
+ // TODO(smaeul): Remove this hack when fixing the Config ViewModel
+ // The selected tunnel has to actually change, but we have to remember this one.
+ val savedTunnel = tunnel
+ if (savedTunnel === selectedTunnel) selectedTunnel = null
+ selectedTunnel = savedTunnel
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.menu_action_save) {
+ if (binding == null) return false
+ val newConfig: Config
+ newConfig = try {
+ binding!!.config!!.resolve()
+ } catch (e: Exception) {
+ val error = ErrorMessages.get(e)
+ val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
+ val message = getString(R.string.config_save_error, tunnelName, error)
+ Log.e(TAG, message, e)
+ Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
+ return false
+ }
+ when {
+ tunnel == null -> {
+ Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
+ val manager = Application.getTunnelManager()
+ manager.create(binding!!.name, newConfig)
+ .whenComplete { newTunnel, throwable -> onTunnelCreated(newTunnel, throwable) }
+ }
+ tunnel!!.name != binding!!.name -> {
+ Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
+ tunnel!!.setName(binding!!.name)
+ .whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
+ }
+ else -> {
+ Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
+ tunnel!!.setConfig(newConfig)
+ .whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
+ }
+ }
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun onRequestSetExcludedApplications(view: View?) {
+ if (binding != null) {
+ val excludedApps = ArrayList(binding!!.config!!.getInterface().excludedApplications)
+ val fragment = AppListDialogFragment.newInstance(excludedApps, this)
+ fragment.show(parentFragmentManager, null)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ if (binding != null) outState.putParcelable(KEY_LOCAL_CONFIG, binding!!.config)
+ outState.putString(KEY_ORIGINAL_NAME, if (tunnel == null) null else tunnel!!.name)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?) {
+ tunnel = newTunnel
+ if (binding == null) return
+ binding!!.config = ConfigProxy()
+ if (tunnel != null) {
+ binding!!.name = tunnel!!.name
+ tunnel!!.configAsync.thenAccept { config: Config -> onConfigLoaded(config) }
+ } else {
+ binding!!.name = ""
+ }
+ }
+
+ private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
+ val message: String
+ if (throwable == null) {
+ tunnel = newTunnel
+ message = getString(R.string.tunnel_create_success, tunnel!!.name)
+ Log.d(TAG, message)
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
+ onFinished()
+ } else {
+ val error = ErrorMessages.get(throwable)
+ message = getString(R.string.tunnel_create_error, error)
+ Log.e(TAG, message, throwable)
+ if (binding != null) {
+ Snackbar.make(binding!!.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
+ throwable: Throwable?) {
+ val message: String
+ if (throwable == null) {
+ message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
+ Log.d(TAG, message)
+ // Now save the rest of configuration changes.
+ Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
+ renamedTunnel.setConfig(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
+ } else {
+ val error = ErrorMessages.get(throwable)
+ message = getString(R.string.tunnel_rename_error, error)
+ Log.e(TAG, message, throwable)
+ if (binding != null) {
+ Snackbar.make(binding!!.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ if (binding == null) {
+ return
+ }
+ binding!!.fragment = this
+ if (savedInstanceState == null) {
+ onSelectedTunnelChanged(null, selectedTunnel)
+ } else {
+ tunnel = selectedTunnel
+ val config: ConfigProxy = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG)!!
+ val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME)
+ if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config
+ }
+ super.onViewStateRestored(savedInstanceState)
+ }
+
+ companion object {
+ private const val KEY_LOCAL_CONFIG = "local_config"
+ private const val KEY_ORIGINAL_NAME = "original_name"
+ private val TAG = "WireGuard/" + TunnelEditorFragment::class.java.simpleName
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
deleted file mode 100644
index 7ed82f1f..00000000
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
+++ /dev/null
@@ -1,450 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.OpenableColumns;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.google.zxing.integration.android.IntentIntegrator;
-import com.google.zxing.integration.android.IntentResult;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.activity.TunnelCreatorActivity;
-import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
-import com.wireguard.android.databinding.TunnelListFragmentBinding;
-import com.wireguard.android.databinding.TunnelListItemBinding;
-import com.wireguard.android.model.ObservableTunnel;
-import com.wireguard.android.ui.EdgeToEdge;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.widget.MultiselectableRelativeLayout;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-import com.wireguard.util.NonNullForAll;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.view.ActionMode;
-import androidx.recyclerview.widget.RecyclerView;
-import java9.util.concurrent.CompletableFuture;
-import java9.util.stream.StreamSupport;
-
-/**
- * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
- */
-
-@NonNullForAll
-public class TunnelListFragment extends BaseFragment {
- public static final int REQUEST_IMPORT = 1;
- private static final int REQUEST_TARGET_FRAGMENT = 2;
- private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
-
- private final ActionModeListener actionModeListener = new ActionModeListener();
- @Nullable private ActionMode actionMode;
- @Nullable private TunnelListFragmentBinding binding;
-
- private void importTunnel(@NonNull final String configText) {
- try {
- // Ensure the config text is parseable before proceeding…
- Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
-
- // Config text is valid, now create the tunnel…
- ConfigNamingDialogFragment.newInstance(configText).show(getParentFragmentManager(), null);
- } catch (final BadConfigException | IOException e) {
- onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
- }
- }
-
- private void importTunnel(@Nullable final Uri uri) {
- final Activity activity = getActivity();
- if (activity == null || uri == null)
- return;
- final ContentResolver contentResolver = activity.getContentResolver();
-
- final Collection<CompletableFuture<ObservableTunnel>> futureTunnels = new ArrayList<>();
- final List<Throwable> throwables = new ArrayList<>();
- Application.getAsyncWorker().supplyAsync(() -> {
- final String[] columns = {OpenableColumns.DISPLAY_NAME};
- String name = null;
- try (Cursor cursor = contentResolver.query(uri, columns,
- null, null, null)) {
- if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
- name = cursor.getString(0);
- }
- if (name == null)
- name = Uri.decode(uri.getLastPathSegment());
- int idx = name.lastIndexOf('/');
- if (idx >= 0) {
- if (idx >= name.length() - 1)
- throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
- name = name.substring(idx + 1);
- }
- boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
- if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
- name = name.substring(0, name.length() - ".conf".length());
- else if (!isZip)
- throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
-
- if (isZip) {
- try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
- BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
- ZipEntry entry;
- while ((entry = zip.getNextEntry()) != null) {
- if (entry.isDirectory())
- continue;
- name = entry.getName();
- idx = name.lastIndexOf('/');
- if (idx >= 0) {
- if (idx >= name.length() - 1)
- continue;
- name = name.substring(name.lastIndexOf('/') + 1);
- }
- if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
- name = name.substring(0, name.length() - ".conf".length());
- else
- continue;
- Config config = null;
- try {
- config = Config.parse(reader);
- } catch (Exception e) {
- throwables.add(e);
- }
- if (config != null)
- futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
- }
- }
- } else {
- futureTunnels.add(Application.getTunnelManager().create(name,
- Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
- }
-
- if (futureTunnels.isEmpty()) {
- if (throwables.size() == 1)
- throw throwables.get(0);
- else if (throwables.isEmpty())
- throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
- }
-
- return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
- }).whenComplete((future, exception) -> {
- if (exception != null) {
- onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
- } else {
- future.whenComplete((ignored1, ignored2) -> {
- final List<ObservableTunnel> tunnels = new ArrayList<>(futureTunnels.size());
- for (final CompletableFuture<ObservableTunnel> futureTunnel : futureTunnels) {
- ObservableTunnel tunnel = null;
- try {
- tunnel = futureTunnel.getNow(null);
- } catch (final Exception e) {
- throwables.add(e);
- }
- if (tunnel != null)
- tunnels.add(tunnel);
- }
- onTunnelImportFinished(tunnels, throwables);
- });
- }
- });
- }
-
- @Override
- public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (savedInstanceState != null) {
- final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
- if (checkedItems != null) {
- for (final Integer i : checkedItems)
- actionModeListener.setItemChecked(i, true);
- }
- }
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
- switch (requestCode) {
- case REQUEST_IMPORT:
- if (resultCode == Activity.RESULT_OK && data != null)
- importTunnel(data.getData());
- return;
- case IntentIntegrator.REQUEST_CODE:
- final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
- if (result != null && result.getContents() != null) {
- importTunnel(result.getContents());
- }
- return;
- default:
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelListFragmentBinding.inflate(inflater, container, false);
- binding.createFab.setOnClickListener(v -> {
- final AddTunnelsSheet bottomSheet = new AddTunnelsSheet();
- bottomSheet.setTargetFragment(this, REQUEST_TARGET_FRAGMENT);
- bottomSheet.show(getParentFragmentManager(), "BOTTOM_SHEET");
- });
- binding.executePendingBindings();
- EdgeToEdge.setUpRoot((ViewGroup) binding.getRoot());
- EdgeToEdge.setUpFAB(binding.createFab);
- EdgeToEdge.setUpScrollingContent(binding.tunnelList, binding.createFab);
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- }
-
- public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
- startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
- }
-
- @Override
- public void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel, @Nullable final ObservableTunnel newTunnel) {
- if (binding == null)
- return;
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- if (newTunnel != null)
- viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
- if (oldTunnel != null)
- viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
- });
- }
-
- private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getResources().getQuantityString(R.plurals.delete_success, count, count);
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
- Log.e(TAG, message, throwable);
- }
- showSnackbar(message);
- }
-
- private void onTunnelImportFinished(final List<ObservableTunnel> tunnels, final Collection<Throwable> throwables) {
- String message = null;
-
- for (final Throwable throwable : throwables) {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.import_error, error);
- Log.e(TAG, message, throwable);
- }
-
- if (tunnels.size() == 1 && throwables.isEmpty())
- message = getString(R.string.import_success, tunnels.get(0).getName());
- else if (tunnels.isEmpty() && throwables.size() == 1)
- /* Use the exception message from above. */ ;
- else if (throwables.isEmpty())
- message = getResources().getQuantityString(R.plurals.import_total_success,
- tunnels.size(), tunnels.size());
- else if (!throwables.isEmpty())
- message = getResources().getQuantityString(R.plurals.import_partial_success,
- tunnels.size() + throwables.size(),
- tunnels.size(), tunnels.size() + throwables.size());
-
- showSnackbar(message);
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- super.onViewStateRestored(savedInstanceState);
-
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
- Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
- binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel>) (binding, tunnel, position) -> {
- binding.setFragment(this);
- binding.getRoot().setOnClickListener(clicked -> {
- if (actionMode == null) {
- setSelectedTunnel(tunnel);
- } else {
- actionModeListener.toggleItemChecked(position);
- }
- });
- binding.getRoot().setOnLongClickListener(clicked -> {
- actionModeListener.toggleItemChecked(position);
- return true;
- });
-
- if (actionMode != null)
- ((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
- else
- ((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
- });
- }
-
- private void showSnackbar(final CharSequence message) {
- if (binding != null) {
- final Snackbar snackbar = Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG);
- snackbar.setAnchorView(binding.createFab);
- snackbar.show();
- }
- }
-
- private MultiselectableRelativeLayout viewForTunnel(final ObservableTunnel tunnel, final List tunnels) {
- return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
- }
-
- private final class ActionModeListener implements ActionMode.Callback {
- private final Collection<Integer> checkedItems = new HashSet<>();
-
- @Nullable private Resources resources;
-
- public ArrayList<Integer> getCheckedItems() {
- return new ArrayList<>(checkedItems);
- }
-
- @Override
- public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_delete:
- final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- final Collection<ObservableTunnel> tunnelsToDelete = new ArrayList<>();
- for (final Integer position : copyCheckedItems)
- tunnelsToDelete.add(tunnels.get(position));
-
- final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
- .map(ObservableTunnel::delete)
- .toArray(CompletableFuture[]::new);
- CompletableFuture.allOf(futures)
- .thenApply(x -> futures.length)
- .whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
-
- });
- checkedItems.clear();
- mode.finish();
- return true;
- case R.id.menu_action_select_all:
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- for (int i = 0; i < tunnels.size(); ++i) {
- setItemChecked(i, true);
- }
- });
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
- actionMode = mode;
- if (getActivity() != null) {
- resources = getActivity().getResources();
- }
- mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
- binding.tunnelList.getAdapter().notifyDataSetChanged();
- return true;
- }
-
- @Override
- public void onDestroyActionMode(final ActionMode mode) {
- actionMode = null;
- resources = null;
- checkedItems.clear();
- binding.tunnelList.getAdapter().notifyDataSetChanged();
- }
-
- @Override
- public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
- updateTitle(mode);
- return false;
- }
-
- void setItemChecked(final int position, final boolean checked) {
- if (checked) {
- checkedItems.add(position);
- } else {
- checkedItems.remove(position);
- }
-
- final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
-
- if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
- ((AppCompatActivity) getActivity()).startSupportActionMode(this);
- } else if (actionMode != null && checkedItems.isEmpty()) {
- actionMode.finish();
- }
-
- if (adapter != null)
- adapter.notifyItemChanged(position);
-
- updateTitle(actionMode);
- }
-
- void toggleItemChecked(final int position) {
- setItemChecked(position, !checkedItems.contains(position));
- }
-
- private void updateTitle(@Nullable final ActionMode mode) {
- if (mode == null) {
- return;
- }
-
- final int count = checkedItems.size();
- if (count == 0) {
- mode.setTitle("");
- } else {
- mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
- }
- }
- }
-
-}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
new file mode 100644
index 00000000..051babb2
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -0,0 +1,415 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Resources
+import android.net.Uri
+import android.os.Bundle
+import android.provider.OpenableColumns
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.view.ActionMode
+import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.integration.android.IntentIntegrator
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.activity.TunnelCreatorActivity
+import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
+import com.wireguard.android.databinding.TunnelListFragmentBinding
+import com.wireguard.android.databinding.TunnelListItemBinding
+import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.ui.EdgeToEdge.setUpFAB
+import com.wireguard.android.ui.EdgeToEdge.setUpRoot
+import com.wireguard.android.ui.EdgeToEdge.setUpScrollingContent
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.ObservableSortedKeyedList
+import com.wireguard.android.widget.MultiselectableRelativeLayout
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Config
+import java9.util.concurrent.CompletableFuture
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.util.ArrayList
+import java.util.HashSet
+import java.util.Locale
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+/**
+ * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
+ */
+class TunnelListFragment : BaseFragment() {
+ private val actionModeListener = ActionModeListener()
+ private var actionMode: ActionMode? = null
+ private var binding: TunnelListFragmentBinding? = null
+ private fun importTunnel(configText: String) {
+ try {
+ // Ensure the config text is parseable before proceeding…
+ Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
+
+ // Config text is valid, now create the tunnel…
+ newInstance(configText).show(parentFragmentManager, null)
+ } catch (e: BadConfigException) {
+ onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
+ } catch (e: IOException) {
+ onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
+ }
+ }
+
+ private fun importTunnel(uri: Uri?) {
+ val activity = activity
+ if (activity == null || uri == null) {
+ return
+ }
+ val contentResolver = activity.contentResolver
+
+ val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
+ val throwables = ArrayList<Throwable>()
+ Application.getAsyncWorker().supplyAsync {
+ val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
+ var name = ""
+ contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst() && !cursor.isNull(0)) {
+ name = cursor.getString(0)
+ }
+ cursor.close()
+ }
+ if (name.isEmpty()) {
+ name = Uri.decode(uri.lastPathSegment)
+ }
+ var idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ require(idx < name.length - 1) { "Illegal file name: $name" }
+ name = name.substring(idx + 1)
+ }
+ val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
+ if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ require(isZip) { "File must be .conf or .zip" }
+ }
+
+ if (isZip) {
+ ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
+ val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
+ var entry: ZipEntry
+ while (zip.nextEntry.also { entry = it } != null) {
+ name = entry.name
+ idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ if (idx >= name.length - 1) {
+ continue
+ }
+ name = name.substring(name.lastIndexOf('/') + 1)
+ }
+ if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ continue
+ }
+ val config: Config? = try {
+ Config.parse(reader)
+ } catch (e: Exception) {
+ throwables.add(e)
+ null
+ }
+
+ if (config != null) {
+ futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture())
+ }
+ }
+ }
+ } else {
+ futureTunnels.add(
+ Application.getTunnelManager().create(
+ name,
+ Config.parse(contentResolver.openInputStream(uri))
+ ).toCompletableFuture()
+ )
+ }
+
+ if (futureTunnels.isEmpty()) {
+ if (throwables.size == 1) {
+ throw throwables[0]
+ } else {
+ require(throwables.isNotEmpty()) { "No configurations found" }
+ }
+ }
+ CompletableFuture.allOf(*futureTunnels.toTypedArray())
+ }.whenComplete { future, exception ->
+ if (exception != null) {
+ onTunnelImportFinished(emptyList(), listOf(exception))
+ } else {
+ future.whenComplete { _, _ ->
+ val tunnels = mutableListOf<ObservableTunnel>()
+ for (futureTunnel in futureTunnels) {
+ val tunnel: ObservableTunnel? = try {
+ futureTunnel.getNow(null)
+ } catch (e: Exception) {
+ throwables.add(e)
+ null
+ }
+
+ if (tunnel != null) {
+ tunnels.add(tunnel)
+ }
+ }
+ onTunnelImportFinished(tunnels, throwables)
+ }
+ }
+ }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ if (savedInstanceState != null) {
+ val checkedItems: Collection<Int>? = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS")
+ if (checkedItems != null) {
+ for (i in checkedItems) actionModeListener.setItemChecked(i, true)
+ }
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ when (requestCode) {
+ REQUEST_IMPORT -> {
+ if (resultCode == Activity.RESULT_OK && data != null) importTunnel(data.data)
+ return
+ }
+ IntentIntegrator.REQUEST_CODE -> {
+ val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+ if (result != null && result.contents != null) {
+ importTunnel(result.contents)
+ }
+ return
+ }
+ else -> super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ binding = TunnelListFragmentBinding.inflate(inflater, container, false)
+ binding?.apply {
+ createFab.setOnClickListener {
+ val bottomSheet = AddTunnelsSheet()
+ bottomSheet.setTargetFragment(fragment, REQUEST_TARGET_FRAGMENT)
+ bottomSheet.show(parentFragmentManager, "BOTTOM_SHEET")
+ }
+ executePendingBindings()
+ setUpRoot(root as ViewGroup)
+ setUpFAB(createFab)
+ setUpScrollingContent(tunnelList, createFab)
+ }
+ return binding!!.root
+ }
+
+ override fun onDestroyView() {
+ binding = null
+ super.onDestroyView()
+ }
+
+ fun onRequestCreateConfig(view: View?) {
+ startActivity(Intent(activity, TunnelCreatorActivity::class.java))
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems())
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ if (binding == null) return
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?> ->
+ if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
+ if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
+ }
+ }
+
+ private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
+ val message: String
+ if (throwable == null) {
+ message = resources.getQuantityString(R.plurals.delete_success, count, count)
+ } else {
+ val error = ErrorMessages.get(throwable)
+ message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
+ Log.e(TAG, message, throwable)
+ }
+ showSnackbar(message)
+ }
+
+ private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
+ var message: String? = null
+ for (throwable in throwables) {
+ val error = ErrorMessages.get(throwable)
+ message = getString(R.string.import_error, error)
+ Log.e(TAG, message, throwable)
+ }
+ if (tunnels.size == 1 && throwables.isEmpty())
+ message = getString(R.string.import_success, tunnels[0].name)
+ else if (tunnels.isEmpty() && throwables.size == 1)
+ else if (throwables.isEmpty())
+ message = resources.getQuantityString(R.plurals.import_total_success,
+ tunnels.size, tunnels.size)
+ else if (!throwables.isEmpty())
+ message = resources.getQuantityString(R.plurals.import_partial_success,
+ tunnels.size + throwables.size,
+ tunnels.size, tunnels.size + throwables.size)
+ showSnackbar(message)
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ super.onViewStateRestored(savedInstanceState)
+ if (binding == null) {
+ return
+ }
+ binding!!.fragment = this
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?>? -> binding!!.tunnels = tunnels }
+ binding!!.rowConfigurationHandler = RowConfigurationHandler { binding: TunnelListItemBinding, tunnel: ObservableTunnel, position: Int ->
+ binding.fragment = this
+ binding.root.setOnClickListener {
+ if (actionMode == null) {
+ selectedTunnel = tunnel
+ } else {
+ actionModeListener.toggleItemChecked(position)
+ }
+ }
+ binding.root.setOnLongClickListener {
+ actionModeListener.toggleItemChecked(position)
+ true
+ }
+ if (actionMode != null)
+ (binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
+ else
+ (binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel === tunnel)
+ }
+ }
+
+ private fun showSnackbar(message: CharSequence?) {
+ if (binding != null) {
+ val snackbar = Snackbar.make(binding!!.mainContainer, message!!, Snackbar.LENGTH_LONG)
+ snackbar.anchorView = binding!!.createFab
+ snackbar.show()
+ }
+ }
+
+ private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout {
+ return binding!!.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))!!.itemView as MultiselectableRelativeLayout
+ }
+
+ private inner class ActionModeListener : ActionMode.Callback {
+ val checkedItems: MutableCollection<Int> = HashSet()
+ private var resources: Resources? = null
+ fun getCheckedItems(): ArrayList<Int> {
+ return ArrayList(checkedItems)
+ }
+
+ override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.menu_action_delete -> {
+ val copyCheckedItems: Iterable<Int> = HashSet(checkedItems)
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String, ObservableTunnel> ->
+ val tunnelsToDelete = ArrayList<ObservableTunnel>()
+ for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
+ val futures = tunnelsToDelete
+ .map { obj -> obj.delete() }
+ .toTypedArray()
+ CompletableFuture.allOf(*futures as Array<out CompletableFuture<*>>)
+ .thenApply { futures.size }
+ .whenComplete { count: Int, throwable: Throwable? -> onTunnelDeletionFinished(count, throwable) }
+ }
+ checkedItems.clear()
+ mode.finish()
+ true
+ }
+ R.id.menu_action_select_all -> {
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?> ->
+ var i = 0
+ while (i < tunnels.size) {
+ setItemChecked(i, true)
+ ++i
+ }
+ }
+ true
+ }
+ else -> false
+ }
+ }
+
+ override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+ actionMode = mode
+ if (activity != null) {
+ resources = activity!!.resources
+ }
+ mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
+ binding!!.tunnelList.adapter!!.notifyDataSetChanged()
+ return true
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode) {
+ actionMode = null
+ resources = null
+ checkedItems.clear()
+ binding!!.tunnelList.adapter!!.notifyDataSetChanged()
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+ updateTitle(mode)
+ return false
+ }
+
+ fun setItemChecked(position: Int, checked: Boolean) {
+ if (checked) {
+ checkedItems.add(position)
+ } else {
+ checkedItems.remove(position)
+ }
+ val adapter = if (binding == null) null else binding!!.tunnelList.adapter
+ if (actionMode == null && !checkedItems.isEmpty() && activity != null) {
+ (activity as AppCompatActivity?)!!.startSupportActionMode(this)
+ } else if (actionMode != null && checkedItems.isEmpty()) {
+ actionMode!!.finish()
+ }
+ adapter?.notifyItemChanged(position)
+ updateTitle(actionMode)
+ }
+
+ fun toggleItemChecked(position: Int) {
+ setItemChecked(position, !checkedItems.contains(position))
+ }
+
+ private fun updateTitle(mode: ActionMode?) {
+ if (mode == null) {
+ return
+ }
+ val count = checkedItems.size
+ if (count == 0) {
+ mode.title = ""
+ } else {
+ mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
+ }
+ }
+ }
+
+ companion object {
+ const val REQUEST_IMPORT = 1
+ private const val REQUEST_TARGET_FRAGMENT = 2
+ private val TAG = "WireGuard/" + TunnelListFragment::class.java.simpleName
+ }
+}