ui: Convert fragment package to Kotlin

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2020-03-16 14:46:36 +05:30
parent b2ed5dbbc8
commit fc0660ca8d
12 changed files with 1119 additions and 1271 deletions

View File

@ -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);
}
}

View File

@ -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
}
}
}

View File

@ -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);
});
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
});
}
}

View File

@ -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
}
}
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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));
}
}
}
}

View File

@ -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
}
}