Added QR code scanner as tunnel import method
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
bb20c89cd5
commit
8e0835e570
@ -68,6 +68,7 @@ ext {
|
|||||||
supportLibsVersion = '27.1.1'
|
supportLibsVersion = '27.1.1'
|
||||||
streamsupportVersion = '1.6.0'
|
streamsupportVersion = '1.6.0'
|
||||||
jsr305Version = '3.0.2'
|
jsr305Version = '3.0.2'
|
||||||
|
zxingEmbeddedVersion = '3.6.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -80,6 +81,7 @@ dependencies {
|
|||||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||||
|
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package="com.wireguard.android"
|
package="com.wireguard.android"
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
@ -50,6 +51,11 @@
|
|||||||
android:label="@string/create_activity_title"
|
android:label="@string/create_activity_title"
|
||||||
android:parentActivityName=".activity.MainActivity" />
|
android:parentActivityName=".activity.MainActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
tools:replace="screenOrientation" />
|
||||||
|
|
||||||
<receiver android:name=".BootShutdownReceiver">
|
<receiver android:name=".BootShutdownReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2018 Eric Kuck <eric@bluelinelabs.com>.
|
||||||
|
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. 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.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
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.Config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ConfigNamingDialogFragment extends DialogFragment {
|
||||||
|
|
||||||
|
private static final String KEY_CONFIG_TEXT = "config_text";
|
||||||
|
|
||||||
|
@Nullable private Config config;
|
||||||
|
@Nullable private ConfigNamingDialogFragmentBinding binding;
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = Config.from(getArguments().getString(KEY_CONFIG_TEXT));
|
||||||
|
} catch (final IOException exception) {
|
||||||
|
throw new RuntimeException("Invalid config passed to " + getClass().getSimpleName(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
final AlertDialog dialog = (AlertDialog) getDialog();
|
||||||
|
if (dialog != null) {
|
||||||
|
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
|
||||||
|
|
||||||
|
setKeyboardVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
|
||||||
|
imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
|
||||||
|
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
|
||||||
|
alertDialogBuilder.setTitle(R.string.create_tunnel);
|
||||||
|
|
||||||
|
binding = ConfigNamingDialogFragmentBinding.inflate(getActivity().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 dismiss() {
|
||||||
|
setKeyboardVisible(false);
|
||||||
|
super.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,7 @@ import android.provider.OpenableColumns;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.view.ActionMode;
|
import android.support.v7.view.ActionMode;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -27,6 +28,8 @@ import android.view.MenuItem;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.Application;
|
||||||
import com.wireguard.android.R;
|
import com.wireguard.android.R;
|
||||||
import com.wireguard.android.activity.TunnelCreatorActivity;
|
import com.wireguard.android.activity.TunnelCreatorActivity;
|
||||||
@ -39,6 +42,7 @@ import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollLis
|
|||||||
import com.wireguard.config.Config;
|
import com.wireguard.config.Config;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -72,6 +76,22 @@ public class TunnelListFragment extends BaseFragment {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void importTunnel(@NonNull final String configText) {
|
||||||
|
try {
|
||||||
|
// Ensure the config text is parseable before proceeding…
|
||||||
|
Config.from(configText);
|
||||||
|
|
||||||
|
// Config text is valid, now create the tunnel…
|
||||||
|
final FragmentManager fragmentManager = getFragmentManager();
|
||||||
|
if (fragmentManager != null) {
|
||||||
|
final ConfigNamingDialogFragment fragment = ConfigNamingDialogFragment.newInstance(configText);
|
||||||
|
fragment.show(fragmentManager, null);
|
||||||
|
}
|
||||||
|
} catch (final IllegalArgumentException|IOException exception) {
|
||||||
|
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void importTunnel(@Nullable final Uri uri) {
|
private void importTunnel(@Nullable final Uri uri) {
|
||||||
final Activity activity = getActivity();
|
final Activity activity = getActivity();
|
||||||
if (activity == null || uri == null)
|
if (activity == null || uri == null)
|
||||||
@ -172,6 +192,12 @@ public class TunnelListFragment extends BaseFragment {
|
|||||||
if (resultCode == Activity.RESULT_OK && data != null)
|
if (resultCode == Activity.RESULT_OK && data != null)
|
||||||
importTunnel(data.getData());
|
importTunnel(data.getData());
|
||||||
return;
|
return;
|
||||||
|
case IntentIntegrator.REQUEST_CODE:
|
||||||
|
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
|
||||||
|
if (result != null && result.getContents() != null) {
|
||||||
|
importTunnel(result.getContents());
|
||||||
|
}
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
}
|
}
|
||||||
@ -217,6 +243,15 @@ public class TunnelListFragment extends BaseFragment {
|
|||||||
binding.createMenu.collapse();
|
binding.createMenu.collapse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
|
||||||
|
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
|
||||||
|
intentIntegrator.setOrientationLocked(false);
|
||||||
|
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
|
||||||
|
|
||||||
|
if (binding != null)
|
||||||
|
binding.createMenu.collapse();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
|
@ -20,6 +20,7 @@ import java.io.BufferedReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -32,6 +33,10 @@ public class Config {
|
|||||||
private final Interface interfaceSection = new Interface();
|
private final Interface interfaceSection = new Interface();
|
||||||
private List<Peer> peers = new ArrayList<>();
|
private List<Peer> peers = new ArrayList<>();
|
||||||
|
|
||||||
|
public static Config from(final String string) throws IOException {
|
||||||
|
return from(new BufferedReader(new StringReader(string)));
|
||||||
|
}
|
||||||
|
|
||||||
public static Config from(final InputStream stream) throws IOException {
|
public static Config from(final InputStream stream) throws IOException {
|
||||||
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
|
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
|
||||||
|
</vector>
|
25
app/src/main/res/layout/config_naming_dialog_fragment.xml
Normal file
25
app/src/main/res/layout/config_naming_dialog_fragment.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:id="@+id/tunnel_name_text_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/tunnel_name_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/tunnel_name" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</layout>
|
@ -38,6 +38,29 @@
|
|||||||
app:layout="@{@layout/tunnel_list_item}"
|
app:layout="@{@layout/tunnel_list_item}"
|
||||||
app:configurationHandler="@{rowConfigurationHandler}" />
|
app:configurationHandler="@{rowConfigurationHandler}" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<android.support.v7.widget.AppCompatImageView
|
||||||
|
android:id="@+id/logo_placeholder"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:tint="@color/transparent_background_placeholder"
|
||||||
|
android:layout_marginTop="-70dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:src="@mipmap/ic_launcher" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:text="@string/tunnel_list_placeholder" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
||||||
android:id="@+id/create_menu"
|
android:id="@+id/create_menu"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
@ -66,29 +89,16 @@
|
|||||||
app:srcCompat="@drawable/ic_action_open_white"
|
app:srcCompat="@drawable/ic_action_open_white"
|
||||||
app:fabSize="mini"
|
app:fabSize="mini"
|
||||||
app:fab_title="@string/create_from_file" />
|
app:fab_title="@string/create_from_file" />
|
||||||
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
|
||||||
|
|
||||||
<LinearLayout
|
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/scan_qr_code"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
|
||||||
android:layout_gravity="center">
|
|
||||||
<android.support.v7.widget.AppCompatImageView
|
|
||||||
android:id="@+id/logo_placeholder"
|
|
||||||
android:layout_width="140dp"
|
|
||||||
android:layout_height="140dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:tint="@color/transparent_background_placeholder"
|
|
||||||
android:layout_marginTop="-70dp"
|
|
||||||
android:layout_marginBottom="20dp"
|
|
||||||
android:src="@mipmap/ic_launcher" />
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:onClick="@{fragment::onRequestScanQRCode}"
|
||||||
android:textSize="20sp"
|
app:srcCompat="@drawable/ic_action_scan_qr_code_white"
|
||||||
android:text="@string/tunnel_list_placeholder" />
|
app:fabSize="mini"
|
||||||
</LinearLayout>
|
app:fab_title="@string/scan_qr_code" />
|
||||||
|
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<string name="create_activity_title">Create WireGuard Tunnel</string>
|
<string name="create_activity_title">Create WireGuard Tunnel</string>
|
||||||
<string name="create_empty">Create from scratch</string>
|
<string name="create_empty">Create from scratch</string>
|
||||||
<string name="create_from_file">Create from file or archive</string>
|
<string name="create_from_file">Create from file or archive</string>
|
||||||
|
<string name="create_tunnel">Create Tunnel</string>
|
||||||
<string name="dark_theme_title">Use dark theme</string>
|
<string name="dark_theme_title">Use dark theme</string>
|
||||||
<string name="dark_theme_summary_on">Currently using dark night theme</string>
|
<string name="dark_theme_summary_on">Currently using dark night theme</string>
|
||||||
<string name="dark_theme_summary_off">Currently using light day theme</string>
|
<string name="dark_theme_summary_off">Currently using light day theme</string>
|
||||||
@ -71,6 +72,7 @@
|
|||||||
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string>
|
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string>
|
||||||
<string name="restore_on_boot_title">Restore on boot</string>
|
<string name="restore_on_boot_title">Restore on boot</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
|
<string name="scan_qr_code">Scan QR Code</string>
|
||||||
<plurals name="set_excluded_applications">
|
<plurals name="set_excluded_applications">
|
||||||
<item quantity="one">%d Excluded Application</item>
|
<item quantity="one">%d Excluded Application</item>
|
||||||
<item quantity="other">%d Excluded Applications</item>
|
<item quantity="other">%d Excluded Applications</item>
|
||||||
@ -90,6 +92,7 @@
|
|||||||
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
|
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
|
||||||
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
|
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
|
||||||
<string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
|
<string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
|
||||||
|
<string name="tunnel_name">Tunnel Name</string>
|
||||||
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
|
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
|
||||||
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
|
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
|
||||||
<string name="version_title">WireGuard for Android v%s"</string>
|
<string name="version_title">WireGuard for Android v%s"</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user