Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -68,6 +68,7 @@ ext { | |||
supportLibsVersion = '27.1.1' | |||
streamsupportVersion = '1.6.0' | |||
jsr305Version = '3.0.2' | |||
zxingEmbeddedVersion = '3.6.0' | |||
} | |||
dependencies { | |||
@@ -80,6 +81,7 @@ dependencies { | |||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion" | |||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion" | |||
implementation "com.google.code.findbugs:jsr305:$jsr305Version" | |||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion" | |||
} | |||
tasks.withType(JavaCompile) { | |||
@@ -4,6 +4,7 @@ | |||
package="com.wireguard.android" | |||
android:installLocation="internalOnly"> | |||
<uses-permission android:name="android.permission.CAMERA" /> | |||
<uses-permission android:name="android.permission.INTERNET" /> | |||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> | |||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | |||
@@ -50,6 +51,11 @@ | |||
android:label="@string/create_activity_title" | |||
android:parentActivityName=".activity.MainActivity" /> | |||
<activity | |||
android:name="com.journeyapps.barcodescanner.CaptureActivity" | |||
android:screenOrientation="fullSensor" | |||
tools:replace="screenOrientation" /> | |||
<receiver android:name=".BootShutdownReceiver"> | |||
<intent-filter> | |||
<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.Nullable; | |||
import android.support.design.widget.Snackbar; | |||
import android.support.v4.app.FragmentManager; | |||
import android.support.v7.app.AppCompatActivity; | |||
import android.support.v7.view.ActionMode; | |||
import android.util.Log; | |||
@@ -27,6 +28,8 @@ 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; | |||
@@ -39,6 +42,7 @@ import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollLis | |||
import com.wireguard.config.Config; | |||
import java.io.BufferedReader; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
@@ -72,6 +76,22 @@ public class TunnelListFragment extends BaseFragment { | |||
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) { | |||
final Activity activity = getActivity(); | |||
if (activity == null || uri == null) | |||
@@ -172,6 +192,12 @@ public class TunnelListFragment extends BaseFragment { | |||
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); | |||
} | |||
@@ -217,6 +243,15 @@ public class TunnelListFragment extends BaseFragment { | |||
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 | |||
public void onPause() { | |||
if (binding != null) { | |||
@@ -20,6 +20,7 @@ import java.io.BufferedReader; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.InputStreamReader; | |||
import java.io.StringReader; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
@@ -32,6 +33,10 @@ public class Config { | |||
private final Interface interfaceSection = new Interface(); | |||
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 { | |||
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> |
@@ -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: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 | |||
android:id="@+id/create_menu" | |||
android:clipChildren="false" | |||
@@ -66,29 +89,16 @@ | |||
app:srcCompat="@drawable/ic_action_open_white" | |||
app:fabSize="mini" | |||
app:fab_title="@string/create_from_file" /> | |||
</com.wireguard.android.widget.fab.FloatingActionsMenu> | |||
<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 | |||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton | |||
android:id="@+id/scan_qr_code" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_gravity="center" | |||
android:textSize="20sp" | |||
android:text="@string/tunnel_list_placeholder" /> | |||
</LinearLayout> | |||
android:onClick="@{fragment::onRequestScanQRCode}" | |||
app:srcCompat="@drawable/ic_action_scan_qr_code_white" | |||
app:fabSize="mini" | |||
app:fab_title="@string/scan_qr_code" /> | |||
</com.wireguard.android.widget.fab.FloatingActionsMenu> | |||
</android.support.design.widget.CoordinatorLayout> | |||
</layout> |
@@ -30,6 +30,7 @@ | |||
<string name="create_activity_title">Create WireGuard Tunnel</string> | |||
<string name="create_empty">Create from scratch</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_summary_on">Currently using dark night 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_title">Restore on boot</string> | |||
<string name="save">Save</string> | |||
<string name="scan_qr_code">Scan QR Code</string> | |||
<plurals name="set_excluded_applications"> | |||
<item quantity="one">%d Excluded Application</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_success">Successfully created tunnel “%s”</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_success">Successfully renamed tunnel to “%s”</string> | |||
<string name="version_title">WireGuard for Android v%s"</string> | |||