Rename app module to ui
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.util.resolveAttribute
|
||||
|
||||
class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTheme(): Int {
|
||||
return R.style.BottomSheetDialogTheme
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (savedInstanceState != null) dismiss()
|
||||
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
val dialog = dialog as BottomSheetDialog? ?: return
|
||||
behavior = dialog.behavior
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.peekHeight = 0
|
||||
behavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
dialog.findViewById<View>(R.id.create_empty)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestCreateConfig()
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_from_file)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestImportConfig()
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestScanQRCode()
|
||||
}
|
||||
}
|
||||
})
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(requireContext().resolveAttribute(R.attr.colorBackground))
|
||||
}
|
||||
view.background = gradientDrawable
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
behavior.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun requireTargetFragment(): Fragment {
|
||||
return requireNotNull(targetFragment) { "A target fragment should always be set" }
|
||||
}
|
||||
|
||||
private fun onRequestCreateConfig() {
|
||||
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
|
||||
}
|
||||
|
||||
private fun onRequestImportConfig() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
|
||||
}
|
||||
|
||||
private fun onRequestScanQRCode() {
|
||||
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
|
||||
setOrientationLocked(false)
|
||||
setBeepEnabled(false)
|
||||
setPrompt(getString(R.string.qr_code_hint))
|
||||
}
|
||||
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import java9.util.Comparators;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
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 {@code BaseActivity}.
|
||||
*/
|
||||
|
||||
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).show();
|
||||
else
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, message, throwable);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
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 java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
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.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelDetailPeerBinding;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.ui.EdgeToEdge;
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
*/
|
||||
|
||||
public class TunnelDetailFragment extends BaseFragment {
|
||||
@Nullable private TunnelDetailFragmentBinding binding;
|
||||
@Nullable private Timer timer;
|
||||
@Nullable private State lastState = State.TOGGLE;
|
||||
|
||||
@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 void onStop() {
|
||||
super.onStop();
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateStats();
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
@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 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 onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
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*1024)
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* 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 androidx.databinding.ObservableList;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
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.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
|
||||
import com.wireguard.android.model.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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
|
||||
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 ObservableTunnel 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 Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
final InputMethodManager inputManager = (InputMethodManager) 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.
|
||||
getActivity().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) {
|
||||
switch (item.getItemId()) {
|
||||
case 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;
|
||||
default:
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
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.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 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 java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
*/
|
||||
|
||||
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 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 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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user