Compare commits

...

73 Commits

Author SHA1 Message Date
Jason A. Donenfeld d580200989 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:15:23 -06:00
Jason A. Donenfeld c2cdde73d1 tools: bump submodules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:09:36 -06:00
Jason A. Donenfeld bc74d4d7f8 libwg-go: update golang version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 11:08:45 -06:00
Harsh Shandilya 535c611f2d QuickTileService: Don't use deprecated getDrawable method
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 6efbf65405 Target SDK 28
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya abb121224d FragmentUtils: Make final and prevent instantiation
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 8ad4657d6f Sort dependencies
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya 6d3f1e00a5 Supress false-positive DefaultLocale warnings
We decided in 402472237e8f that it's a bad idea for our use-case

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Harsh Shandilya c38f6c471d Extract error messages to string resources
Useful for validation errors and localisation later on

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-08-26 11:04:01 -06:00
Jason A. Donenfeld 85462de254 MonkeyedSnackbar: remove
This didn't actually help with much and caused problems.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-26 10:51:21 -06:00
Zhao Gang 61d4f17f5d config: fix wrong Peer endpoint string format
When a tunnel is running, saving the tunnel's config with an IPv6
address endpoint like [::1]:42 would result in the wrong format ::1:42.
This patch fixes it.

For endpoints with an IPv6 address(e.g. [::1]:42). Since the default
endpoint InetSocketAddress is created unresolved, getEndpointString()
returns "[::1]:42" (InetSocketAddress.getHostString() returns the
literal hostname). After the endpoint is resolved, getEndpointString()
returns "::1:42" (InetSocketAddress.getHostString() returns the IPv6
address without the square brackets). This inconsistent return values
caused the above mentioned bug.

With this patch, function getEndpointString would return the right
format string whether the endpoint is resolved or not.

Signed-off-by: Zhao Gang <gang.zhao.42@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-08-16 12:08:09 -07:00
Jason A. Donenfeld 09cf73cd3c GoBackend: make socket IPC optional
This fixes the multi-user case, which cannot be determined at compile
time and probably isn't reasonable to consider anyway in a global
manner.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-31 14:27:29 +02:00
Jason A. Donenfeld ffa3cefa67 Style: use attrs from appcompat rather than from frameworks
This isn't possible for colorForeground, colorBackground,
textAppearanceMedium, but at least it's useful for some things here.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 22:41:55 +02:00
Jason A. Donenfeld 8ec2cc8582 FloatingActionsMenu: don't wrap context on asus api 21
Works around frameworks bug.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 17:35:08 +02:00
Jason A. Donenfeld 75dfa0643b Show different color for multiselection
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 16:28:03 +02:00
Jason A. Donenfeld d5cde43158 Replace hard-coded colors with theme colors
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-30 14:43:56 +02:00
Harsh Shandilya 6493a9a1f2 Remove placeholder color and directly use alpha
Why wasn't this done like this in the first place?

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-30 14:28:27 +02:00
Jason A. Donenfeld f35e059194 MonkeyedSnackbar: fix typos
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 19:17:10 +02:00
Jason A. Donenfeld 3cb3e9d8b7 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:52:54 +02:00
Jason A. Donenfeld 5dfc5659ad ConfigNamingDialog: use name input filter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:44:30 +02:00
Jason A. Donenfeld c4102992ae MonkeyedSnackbar: work around Harsh's broken phone
I think I'd probably like to revert this, since presumably there's a
good reason in the first place why the support lib disables animations
when accessibility services are turned on?

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 18:35:26 +02:00
Jason A. Donenfeld 219f4e8016 MSF: make it more obvious what needs to be done
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld 6558140a7c FloatingActionButtonBehavior: animate transitions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 17:21:30 +02:00
Jason A. Donenfeld db7b61ab80 Roll back to API 27 for now
There's no source available for API 28, which is a pain. But this commit
should be reverted whenever source is released.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 16:01:44 +02:00
Jason A. Donenfeld bce5d852e1 Target API 28
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 15:46:01 +02:00
Jason A. Donenfeld b960b4a6cd TunnelListFragment: show selected tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 05:57:53 +02:00
Jason A. Donenfeld 1b10e75168 TunnelListFragment: fix multiselection on rotation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:50:16 +02:00
Jason A. Donenfeld 07359e392c FloatingActionButonBehavior: set translation back to 0 when snackbar dies
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 04:23:30 +02:00
Jason A. Donenfeld e5a5bad240 MainActivity: only show two column on tablets
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-29 03:46:45 +02:00
Samuel Holland ca92ac60b7 MainActivity: Fix fragment selection logic
Signed-off-by: Samuel Holland <samuel@sholland.org>
2018-07-28 17:07:37 -05:00
Jason A. Donenfeld e29c21f8df Application: refuse to run on old android
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 18:27:45 +02:00
Jason A. Donenfeld 6ceeac93bf QuickTileService: fix bug the wrong way
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-28 17:55:24 +02:00
Jason A. Donenfeld 9f861096ac config: show more informative error message on wrong key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-27 15:20:23 +02:00
Jason A. Donenfeld 520df16885 Set ACRA install source
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 13:46:18 +02:00
Jason A. Donenfeld c905ef6083 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:29:49 +02:00
Eric Kuck 9652fe99df TunnelDetailFragment now restores state correctly after process death
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 05:17:43 +02:00
Jason A. Donenfeld 62d8beff96 Application: use proper completablefuture for backend
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 04:54:14 +02:00
Jason A. Donenfeld 7d438e9dbc Wire up ACRA
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-26 01:41:29 +02:00
Harsh Shandilya b364221c93 FloatingActionBehaviour: Adjust constructors
Get these in line with the parent class

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-26 01:36:53 +02:00
Jason A. Donenfeld b8052bd8fb ThemeChangeAwareActivity: reintroduce cache buster
This is still needed by certain icons, like the trash icon in the peer
editor.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 04:05:41 +02:00
Jason A. Donenfeld e437ac389e Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 03:11:33 +02:00
Jason A. Donenfeld 2db2a0921d MainActivity: attempt to fix commit state exceptions
This is an attempt to fix:

java.lang.IllegalStateException:
at android.support.v4.app.FragmentManagerImpl.checkStateLoss (FragmentManager.java:2053)
at android.support.v4.app.FragmentManagerImpl.enqueueAction (FragmentManager.java:2079)
at android.support.v4.app.BackStackRecord.commitInternal (BackStackRecord.java:678)
at android.support.v4.app.BackStackRecord.commit (BackStackRecord.java:632)
at com.wireguard.android.activity.MainActivity.moveToState (MainActivity.java:58)
at com.wireguard.android.activity.MainActivity.onSelectedTunnelChanged (MainActivity.java:157)
at com.wireguard.android.activity.BaseActivity.setSelectedTunnel (BaseActivity.java:75)
at com.wireguard.android.fragment.BaseFragment.setSelectedTunnel (BaseFragment.java:82)
at com.wireguard.android.fragment.TunnelListFragment.lambda$null$4$TunnelListFragment (TunnelListFragment.java:307)
at com.wireguard.android.fragment.TunnelListFragment$$Lambda$4.onClick (Unknown Source:6)
at android.view.View.performClick (View.java:6274)
at android.view.View$PerformClick.run (View.java:24729)
at android.os.Handler.handleCallback (Handler.java:789)
at android.os.Handler.dispatchMessage (Handler.java:98)
at android.os.Looper.loop (Looper.java:169)
at android.app.ActivityThread.main (ActivityThread.java:6595)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:240)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:767)

But this is probably the wrong way to fix it and instead moveToState
needs to be reimagined.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 03:07:16 +02:00
Jason A. Donenfeld d7889c4e88 style: coloring the navbar looks a bit strange when rotated
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Jason A. Donenfeld 559add3f71 TunnelEditorFragment: plug memory leak on listeners
Apparently these don't get GC'd unless they're removed explicitly,
because there's a global singleton registry of them. So, introduce a
little registry of our own.

Reported-by: Samuel Holland <samuel@sholland.org>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Jason A. Donenfeld d615304e83 qrcode: minor adjustments
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:59:26 +02:00
Eric Kuck 8e0835e570 Added QR code scanner as tunnel import method
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-25 02:05:11 +02:00
Harsh Shandilya bb20c89cd5 tools: Bump wireguard submodule
This force pushing is going to be the death of me

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-24 18:26:53 +02:00
Jason A. Donenfeld 996cbb5f2b tools: support ancient NDKs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 18:09:18 +02:00
Jason A. Donenfeld 8028d708cb tools: let wg(8) play with userspace implementation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 17:57:56 +02:00
Harsh Shandilya 7689905c78 config: Remove Locale based string format
The configurations are supposed to be in a very specific
format which is not user-facing and hence doesn't have to
be adjusted for locale avoiding both the redundancy as well
as potential breakages in the configuration file format from
different locales.

Fixes: 71c67aa24ae2 ("config: Minor cleanup")
Reported-by: Samuel Holland <samuel@sholland.org>
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-24 17:57:56 +02:00
Jason A. Donenfeld 284e42647c tools: pass in debug package name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-24 17:57:56 +02:00
Jason A. Donenfeld 3a16c08821 Make placeholder pretty
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-23 00:05:12 +02:00
Harsh Shandilya 76fb6a318e Show help text when no tunnels are imported
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 23:21:17 +02:00
Jason A. Donenfeld c633f96374 FloatingActionsMenuRecyclerViewScrollListener: add final modifiers
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-22 23:21:17 +02:00
Harsh Shandilya 3357be9557 RTL layout fixes
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 18:21:39 +02:00
Harsh Shandilya 6696e838da treewide: Optimize imports
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-22 18:20:23 +02:00
Jason A. Donenfeld d0d56f3a1b fab: move in direct ratio to scroll
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 16:41:53 +02:00
Jason A. Donenfeld fbf32a6c29 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 15:54:46 +02:00
Harsh Shandilya 3e37289b68 UI: use background color for navbar in dark mode
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-20 15:53:41 +02:00
Jason A. Donenfeld 2d6a45f824 fab: make icons always white
With the new shade of blue, perhaps this simply looks better.

I don't like hard coding the color away from the theme, however.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 15:45:21 +02:00
Jason A. Donenfeld ed802336e6 theme: use less disgusting color
Harsh's "users" were offended by the dark theme. So, we change the
accent to that used by gboard's dark theme, which should be pretty
uncontroversial.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-20 03:23:17 +02:00
Harsh Shandilya b1d1e3b436 TunnelListFragment: Allocate interpolators outside scroll listener
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-20 03:17:49 +02:00
Jason A. Donenfeld d33e322b67 TunnelEditorFragment: fix null pointer dereference
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-19 20:57:00 +02:00
Jason A. Donenfeld 60a6e29350 QuickTileService: fix null pointer dereference
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-19 20:57:00 +02:00
Jason A. Donenfeld 3b0e0c2f16 tools: bump version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 21:54:14 +02:00
Jason A. Donenfeld ffb8bccbc5 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:40:16 +02:00
Jason A. Donenfeld 08170f7e55 TunnelListFragment: setOnScrollListener is old, but we support API 21, so we have to use it
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:32:42 +02:00
Jason A. Donenfeld 28d47b3470 AppListDialogFragment: getArguments is null before onCreate
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 18:08:08 +02:00
Jason A. Donenfeld 8e3586328c fab: use auto calculated fling threshold
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 02:32:05 +02:00
Jason A. Donenfeld f315654d40 fab: add fab sized padding at bottom of recycler view
This way we can keep scrolling when there are exactly the right number
of items, so that the toggle switch becomes visible.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-07-18 02:15:14 +02:00
Harsh Shandilya fdfab18d45 fab: make fab respond to recyclerview scroll events
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-18 02:15:06 +02:00
Harsh Shandilya d43e77867c fab: slide fab up when a snackbar shows
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-18 02:15:06 +02:00
Harsh Shandilya df03bdd7f9 android: QuickTileService: Do not use slashed icon on Android P
Android P features circle masked QS tiles which make use of colors
to denote STATE_ACTIVE/STATE_INACTIVE rather than a slash across
the drawable as seen on Android Oreo.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2018-07-16 17:31:14 +02:00
61 changed files with 1166 additions and 339 deletions
+20 -5
View File
@@ -11,14 +11,15 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 27
compileSdkVersion 28
dataBinding.enabled true
defaultConfig {
applicationId 'com.wireguard.android'
minSdkVersion 21
targetSdkVersion 27
versionCode 433
versionName '0.0.20180716'
targetSdkVersion 28
versionCode 439
versionName '0.0.20180826'
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
}
// If the keystore file exists
if (keystorePropertiesFile.exists()) {
@@ -40,10 +41,20 @@ android {
buildTypes {
release {
if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
}
}
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
externalNativeBuild {
cmake {
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
}
}
}
}
externalNativeBuild {
@@ -58,6 +69,8 @@ ext {
supportLibsVersion = '27.1.1'
streamsupportVersion = '1.6.0'
jsr305Version = '3.0.2'
zxingEmbeddedVersion = '3.6.0'
acraVersion = '5.2.0-rc2'
}
dependencies {
@@ -67,9 +80,11 @@ dependencies {
implementation "com.android.support:design:$supportLibsVersion"
implementation "com.android.support:preference-v14:$supportLibsVersion"
implementation "com.android.support:support-annotations:$supportLibsVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
implementation "ch.acra:acra-http:$acraVersion"
}
tasks.withType(JavaCompile) {
+6
View File
@@ -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" />
@@ -5,8 +5,13 @@
package com.wireguard.android;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
@@ -16,20 +21,36 @@ import android.support.v7.app.AppCompatDelegate;
import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.configStore.FileConfigStore;
import com.wireguard.android.model.TunnelManager;
import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraHttpSender;
import org.acra.data.StringFormat;
import org.acra.sender.HttpSender;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java9.util.concurrent.CompletableFuture;
@AcraCore(reportFormat = StringFormat.JSON,
buildConfigClass = BuildConfig.class,
logcatArguments = {"-b", "all", "-d", "-v", "threadtime", "*:V"},
excludeMatchingSharedPreferencesKeys = {"last_used_tunnel", "enabled_configs"})
@AcraHttpSender(uri = "https://crashreport.zx2c4.com/android/report",
basicAuthLogin = "6RCovLxEVCTXGiW5",
basicAuthPassword = "O7I3sVa5ULVdiC51",
httpMethod = HttpSender.Method.POST,
compress = true)
public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
@@ -37,15 +58,65 @@ public class Application extends android.app.Application {
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
@SuppressWarnings("NullableProblems") private Handler handler;
@Nullable private Backend backend;
@Nullable private Collection<BackendCallback> haveBackendCallbacks = new ArrayList<>();
private final Object haveBackendCallbacksLock = new Object();
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
public Application() {
weakSelf = new WeakReference<>(this);
}
/* The ACRA password can be trivially reverse engineered and is open source anyway,
* so there's no point in trying to protect it. However, we do want to at least
* prevent innocent self-builders from uploading stuff to our crash reporter. So, we
* check the DN of the certs that signed the apk, without even bothering to try
* validating that they're authentic. It's a good enough heuristic.
*/
@SuppressWarnings("deprecation")
@Nullable
private static String getInstallSource(final Context context) {
if (BuildConfig.DEBUG)
return null;
try {
final CertificateFactory cf = CertificateFactory.getInstance("X509");
for (final Signature sig : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures) {
try {
for (final String category : ((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(sig.toByteArray()))).getSubjectDN().getName().split(", *")) {
final String[] parts = category.split("=", 2);
if (!"O".equals(parts[0]))
continue;
switch (parts[1]) {
case "Google Inc.":
return "Play Store";
case "fdroid.org":
return "F-Droid";
}
}
} catch (final Exception ignored) { }
}
} catch (final Exception ignored) { }
return null;
}
@Override
protected void attachBaseContext(final Context context) {
super.attachBaseContext(context);
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
System.exit(0);
}
final String installSource = getInstallSource(context);
if (installSource != null) {
ACRA.init(this);
ACRA.getErrorReporter().putCustomData("installSource", installSource);
}
}
public static Application get() {
return weakSelf.get();
}
@@ -56,43 +127,26 @@ public class Application extends android.app.Application {
public static Backend getBackend() {
final Application app = get();
synchronized (app) {
synchronized (app.futureBackend) {
if (app.backend == null) {
Backend backend = null;
if (new File("/sys/module/wireguard").exists()) {
try {
app.rootShell.start();
app.backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) { }
}
if (app.backend == null)
app.backend = new GoBackend(app.getApplicationContext());
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks != null) {
for (final BackendCallback callback : app.haveBackendCallbacks)
app.handler.post(() -> callback.callback(app.backend));
app.haveBackendCallbacks = null;
backend = new WgQuickBackend(app.getApplicationContext());
} catch (final Exception ignored) {
}
}
if (backend == null)
backend = new GoBackend(app.getApplicationContext());
app.backend = backend;
}
return app.backend;
}
}
@FunctionalInterface
public interface BackendCallback {
void callback(final Backend backend);
}
public static void onHaveBackend(final BackendCallback callback) {
final Application app = get();
synchronized (app.haveBackendCallbacksLock) {
if (app.haveBackendCallbacks == null) {
Objects.requireNonNull(app.backend, "Backend still null in onHaveBackend call");
callback.callback(app.backend);
} else {
app.haveBackendCallbacks.add(callback);
}
}
public static CompletableFuture<Backend> getBackendAsync() {
return get().futureBackend;
}
public static RootShell getRootShell() {
@@ -115,11 +169,7 @@ public class Application extends android.app.Application {
public void onCreate() {
super.onCreate();
handler = new Handler(Looper.getMainLooper());
final Executor executor = AsyncTask.SERIAL_EXECUTOR;
final ConfigStore configStore = new FileConfigStore(getApplicationContext());
asyncWorker = new AsyncWorker(executor, handler);
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
rootShell = new RootShell(getApplicationContext());
toolsInstaller = new ToolsInstaller(getApplicationContext());
@@ -128,8 +178,16 @@ public class Application extends android.app.Application {
sharedPreferences.getBoolean("dark_theme", false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
tunnelManager = new TunnelManager(configStore);
asyncWorker.runAsync(Application::getBackend);
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
tunnelManager.onCreate();
asyncWorker.supplyAsync(Application::getBackend).thenAccept(backend -> {
futureBackend.complete(backend);
if (ACRA.isInitialised()) {
ACRA.getErrorReporter().putCustomData("backend", backend.getClass().getSimpleName());
asyncWorker.supplyAsync(backend::getVersion).thenAccept(version ->
ACRA.getErrorReporter().putCustomData("backendVersion", version));
}
});
}
}
@@ -20,7 +20,7 @@ public class BootShutdownReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
if (!(backend instanceof WgQuickBackend))
return;
final String action = intent.getAction();
@@ -14,6 +14,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.IBinder;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.support.annotation.Nullable;
@@ -44,10 +45,26 @@ public class QuickTileService extends TileService {
@Nullable private Icon iconOn;
@Nullable private Icon iconOff;
@SuppressWarnings("deprecation")
/* This works around an annoying unsolved frameworks bug some people are hitting. */
@Override
@Nullable
public IBinder onBind(final Intent intent) {
IBinder ret = null;
try {
ret = super.onBind(intent);
} catch (final Exception e) {
Log.d(TAG, "Failed to bind to TileService", e);
}
return ret;
}
@Override
public void onCreate() {
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
return;
}
final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile, Application.get().getTheme()));
icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
icon.setSlashed(false);
Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
@@ -67,8 +84,10 @@ public class QuickTileService extends TileService {
public void onClick() {
if (tunnel != null) {
final Tile tile = getQsTile();
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
tile.updateTile();
if (tile != null) {
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
tile.updateTile();
}
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
} else {
final Intent intent = new Intent(this, MainActivity.class);
@@ -13,7 +13,6 @@ import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.TunnelManager;
import java.util.Objects;
@@ -39,15 +38,17 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
String savedTunnelName = null;
final String savedTunnelName;
if (savedInstanceState != null)
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
else if (getIntent() != null)
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
if (savedTunnelName != null) {
final TunnelManager tunnelManager = Application.getTunnelManager();
selectedTunnel = tunnelManager.getTunnels().get(savedTunnelName);
}
else
savedTunnelName = null;
if (savedTunnelName != null)
Application.getTunnelManager().getTunnels()
.thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
// The selected tunnel must be set before the superclass method recreates fragments.
super.onCreate(savedInstanceState);
@@ -10,12 +10,13 @@ import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import com.wireguard.android.R;
import com.wireguard.android.fragment.TunnelDetailFragment;
@@ -23,71 +24,46 @@ import com.wireguard.android.fragment.TunnelEditorFragment;
import com.wireguard.android.fragment.TunnelListFragment;
import com.wireguard.android.model.Tunnel;
import java.util.List;
import java9.util.stream.Stream;
/**
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
* WireGuard application, and contains several fragments for listing, viewing details of, and
* editing the configuration and interface state of WireGuard tunnels.
*/
public class MainActivity extends BaseActivity {
private static final String KEY_STATE = "fragment_state";
private static final String TAG = "WireGuard/" + MainActivity.class.getSimpleName();
private State state = State.EMPTY;
private boolean moveToState(final State nextState) {
if (state == nextState)
return false;
final FragmentManager fragmentManager = getSupportFragmentManager();
Log.i(TAG, "Moving from " + state.name() + " to " + nextState.name());
if (nextState.layer > state.layer + 1) {
moveToState(State.ofLayer(state.layer + 1));
moveToState(nextState);
return true;
} else if (nextState.layer == state.layer + 1) {
final Fragment fragment = Fragment.instantiate(this, nextState.fragment);
final FragmentTransaction transaction = fragmentManager.beginTransaction()
.replace(R.id.master_fragment, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (state.layer > 0)
transaction.addToBackStack(null);
transaction.commit();
} else if (nextState.layer == state.layer - 1) {
if (fragmentManager.getBackStackEntryCount() == 0)
return false;
fragmentManager.popBackStack();
} else if (nextState.layer < state.layer - 1) {
moveToState(State.ofLayer(state.layer - 1));
moveToState(nextState);
return true;
}
state = nextState;
if (state.layer <= State.LIST.layer)
setSelectedTunnel(null);
updateActionBar();
return true;
}
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Nullable private ActionBar actionBar;
private boolean isTwoPaneLayout;
@Nullable private TunnelListFragment listFragment;
@Override
public void onBackPressed() {
final List<Fragment> fragments = getSupportFragmentManager().getFragments();
boolean handled = false;
if (!fragments.isEmpty() && fragments.get(0) instanceof TunnelListFragment) {
handled = ((TunnelListFragment) fragments.get(0)).collapseActionMenu();
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
// If the action menu is visible and expanded, collapse it instead of navigating back.
if (isTwoPaneLayout || backStackEntries == 0) {
if (listFragment != null && listFragment.collapseActionMenu())
return;
}
if (!handled) {
handled = moveToState(State.ofLayer(state.layer - 1));
// If the two-pane layout does not have an editor open, going back should exit the app.
if (isTwoPaneLayout && backStackEntries <= 1) {
finish();
return;
}
if (!handled) {
super.onBackPressed();
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
if (!isTwoPaneLayout && backStackEntries == 1) {
setSelectedTunnel(null);
return;
}
super.onBackPressed();
}
@Override public void onBackStackChanged() {
if (actionBar == null)
return;
// Do not show the home menu when the two-pane layout is at the detail view (see above).
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
}
// We use onTouchListener here to avoid the UI click sound, hence
@@ -97,25 +73,14 @@ public class MainActivity extends BaseActivity {
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
if (savedInstanceState != null && savedInstanceState.getString(KEY_STATE) != null)
state = State.valueOf(savedInstanceState.getString(KEY_STATE));
if (state == State.EMPTY) {
State initialState = getSelectedTunnel() != null ? State.DETAIL : State.LIST;
if (getIntent() != null && getIntent().getStringExtra(KEY_STATE) != null)
initialState = State.valueOf(getIntent().getStringExtra(KEY_STATE));
moveToState(initialState);
}
updateActionBar();
final int actionBarId = getResources().getIdentifier("action_bar", "id", getPackageName());
if (actionBarId != 0 && findViewById(actionBarId) != null) {
findViewById(actionBarId).setOnTouchListener((v, e) -> {
final List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (!fragments.isEmpty() && fragments.get(0) instanceof TunnelListFragment) {
((TunnelListFragment) fragments.get(0)).collapseActionMenu();
}
return false;
});
}
actionBar = getSupportActionBar();
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
getSupportFragmentManager().addOnBackStackChangedListener(this);
onBackStackChanged();
final View actionBarView = findViewById(R.id.action_bar);
if (actionBarView != null)
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
}
@Override
@@ -129,11 +94,14 @@ public class MainActivity extends BaseActivity {
switch (item.getItemId()) {
case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
moveToState(State.ofLayer(state.layer - 1));
onBackPressed();
return true;
case R.id.menu_action_edit:
if (getSelectedTunnel() != null)
moveToState(State.EDITOR);
getSupportFragmentManager().beginTransaction()
.replace(R.id.detail_container, new TunnelEditorFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
return true;
case R.id.menu_action_save:
// This menu item is handled by the editor fragment.
@@ -147,37 +115,26 @@ public class MainActivity extends BaseActivity {
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
outState.putString(KEY_STATE, state.name());
super.onSaveInstanceState(outState);
}
@Override
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
moveToState(newTunnel != null ? State.DETAIL : State.LIST);
}
private void updateActionBar() {
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(state.layer > State.LIST.layer);
}
private enum State {
EMPTY(null, 0),
LIST(TunnelListFragment.class, 1),
DETAIL(TunnelDetailFragment.class, 2),
EDITOR(TunnelEditorFragment.class, 3);
@Nullable private final String fragment;
private final int layer;
State(@Nullable final Class<? extends Fragment> fragment, final int layer) {
this.fragment = fragment != null ? fragment.getName() : null;
this.layer = layer;
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
@Nullable final Tunnel newTunnel) {
final FragmentManager fragmentManager = getSupportFragmentManager();
final int backStackEntries = fragmentManager.getBackStackEntryCount();
if (newTunnel == null) {
// Clear everything off the back stack (all editors and detail fragments).
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
return;
}
private static State ofLayer(final int layer) {
return Stream.of(State.values()).filter(s -> s.layer == layer).findFirst().get();
if (backStackEntries == 2) {
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
// method to avoid the editor picking up the new tunnel while it is still visible.
fragmentManager.popBackStackImmediate();
} else if (backStackEntries == 0) {
// Create and show a new detail fragment.
fragmentManager.beginTransaction()
.add(R.id.detail_container, new TunnelDetailFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(null)
.commit();
}
}
}
@@ -99,7 +99,7 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
for (final Preference pref : wgQuickOnlyPrefs)
pref.setVisible(false);
final PreferenceScreen screen = getPreferenceScreen();
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
for (final Preference pref : wgQuickOnlyPrefs) {
if (backend instanceof WgQuickBackend)
pref.setVisible(true);
@@ -240,10 +240,13 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
for (final Tunnel tunnel : Application.getTunnelManager().getTunnels()) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (final Tunnel tunnel : tunnels) {
if (tunnel != null && tunnel.getState() != State.DOWN)
tunnel.setState(State.DOWN);
}
});
vpnService = vpnService.newIncompleteFuture();
super.onDestroy();
}
@@ -8,7 +8,6 @@ package com.wireguard.android.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -22,10 +21,8 @@ import android.widget.Toast;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.android.activity.BaseActivity;
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
import com.wireguard.android.model.ApplicationData;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableKeyedArrayList;
import com.wireguard.android.util.ObservableKeyedList;
@@ -39,7 +36,7 @@ public class AppListDialogFragment extends DialogFragment {
private static final String KEY_EXCLUDED_APPS = "excludedApps";
private final List<String> currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS));
private List<String> currentlyExcludedApps;
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
public static <T extends Fragment & AppExclusionListener> AppListDialogFragment newInstance(final String[] excludedApps, final T target) {
@@ -51,6 +48,13 @@ public class AppListDialogFragment extends DialogFragment {
return fragment;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentlyExcludedApps = Arrays.asList(getArguments().getStringArray(KEY_EXCLUDED_APPS));
}
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
@@ -94,7 +94,7 @@ public abstract class BaseFragment extends Fragment implements OnSelectedTunnelC
if (tunnel == null)
return;
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
if (backend instanceof GoBackend) {
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
if (intent != null) {
@@ -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.import_from_qrcode);
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);
}
}
}
@@ -36,6 +36,8 @@ import com.wireguard.config.Attribute;
import com.wireguard.config.Config;
import com.wireguard.config.Peer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -96,14 +98,17 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
@Override
public void onItemRangeInserted(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
@Override
public void onItemRangeRemoved(final ObservableList<Peer.Observable> sender, final int positionStart, final int itemCount) {
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
if (binding != null)
breakObjectOrientedLayeringHandler.onPropertyChanged(binding.getConfig(), BR.peers);
}
};
private final Collection<Object> breakObjectOrientedLayeringHandlerReceivers = new ArrayList<>();
private final Observable.OnPropertyChangedCallback breakObjectOrientedLayeringHandler = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(final Observable sender, final int propertyId) {
@@ -114,8 +119,11 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
return;
if (propertyId == BR.config) {
config.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config);
config.getInterfaceSection().addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getInterfaceSection());
config.getPeers().addOnListChangedCallback(breakObjectListOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(config.getPeers());
} else if (propertyId == BR.dnses || propertyId == BR.peers)
;
else
@@ -134,13 +142,21 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
super.onCreateView(inflater, container, savedInstanceState);
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
binding.addOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
breakObjectOrientedLayeringHandlerReceivers.add(binding);
binding.executePendingBindings();
return binding.getRoot();
}
@SuppressWarnings("unchecked")
@Override
public void onDestroyView() {
binding = null;
for (final Object o : breakObjectOrientedLayeringHandlerReceivers) {
if (o instanceof Observable)
((Observable)o).removeOnPropertyChangedCallback(breakObjectOrientedLayeringHandler);
else if (o instanceof ObservableList)
((ObservableList)o).removeOnListChangedCallback(breakObjectListOrientedLayeringHandler);
}
super.onDestroyView();
}
@@ -158,6 +174,7 @@ public class TunnelEditorFragment extends BaseFragment implements AppExclusionLi
}
// 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 Tunnel savedTunnel = tunnel;
if (savedTunnel == getSelectedTunnel())
@@ -18,8 +18,10 @@ 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.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -27,6 +29,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;
@@ -35,9 +39,13 @@ import com.wireguard.android.databinding.TunnelListFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.android.widget.MultiselectableRelativeLayout;
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
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;
@@ -71,6 +79,20 @@ 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)
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
} catch (final Exception exception) {
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
}
}
private void importTunnel(@Nullable final Uri uri) {
final Activity activity = getActivity();
if (activity == null || uri == null)
@@ -171,11 +193,18 @@ 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);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@@ -189,6 +218,7 @@ public class TunnelListFragment extends BaseFragment {
}
return false;
});
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
binding.executePendingBindings();
return binding.getRoot();
}
@@ -214,6 +244,17 @@ 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.setBeepEnabled(false);
intentIntegrator.setPrompt(getString(R.string.qrcode_hint));
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
if (binding != null)
binding.createMenu.collapse();
}
@Override
public void onPause() {
if (binding != null) {
@@ -222,9 +263,20 @@ public class TunnelListFragment extends BaseFragment {
super.onPause();
}
private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
return (MultiselectableRelativeLayout)binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
}
@Override
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
// Do nothing.
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) {
@@ -266,6 +318,26 @@ public class TunnelListFragment extends BaseFragment {
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
}
@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 onViewStateRestored(@Nullable final Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
@@ -275,7 +347,7 @@ public class TunnelListFragment extends BaseFragment {
}
binding.setFragment(this);
binding.setTunnels(Application.getTunnelManager().getTunnels());
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
binding.setFragment(this);
binding.getRoot().setOnClickListener(clicked -> {
@@ -290,7 +362,10 @@ public class TunnelListFragment extends BaseFragment {
return true;
});
binding.getRoot().setActivated(actionModeListener.checkedItems.contains(position));
if (actionMode != null)
((MultiselectableRelativeLayout)binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
else
((MultiselectableRelativeLayout)binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
});
}
@@ -303,25 +378,29 @@ public class TunnelListFragment extends BaseFragment {
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_action_delete:
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : checkedItems) {
tunnelsToDelete.add(Application.getTunnelManager().getTunnels().get(position));
}
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
for (final Integer position : copyCheckedItems)
tunnelsToDelete.add(tunnels.get(position));
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::delete)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures)
.thenApply(x -> futures.length)
.whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
.map(Tunnel::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:
for (int i = 0; i < Application.getTunnelManager().getTunnels().size(); ++i) {
setItemChecked(i, true);
}
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
for (int i = 0; i < tunnels.size(); ++i) {
setItemChecked(i, true);
}
});
return true;
default:
return false;
@@ -335,6 +414,7 @@ public class TunnelListFragment extends BaseFragment {
resources = getActivity().getResources();
}
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
binding.tunnelList.getAdapter().notifyDataSetChanged();
return true;
}
@@ -342,7 +422,6 @@ public class TunnelListFragment extends BaseFragment {
public void onDestroyActionMode(final ActionMode mode) {
actionMode = null;
resources = null;
checkedItems.clear();
binding.tunnelList.getAdapter().notifyDataSetChanged();
}
@@ -351,6 +430,10 @@ public class TunnelListFragment extends BaseFragment {
setItemChecked(position, !checkedItems.contains(position));
}
public ArrayList<Integer> getCheckedItems() {
return new ArrayList<>(checkedItems);
}
void setItemChecked(final int position, final boolean checked) {
if (checked) {
checkedItems.add(position);
@@ -358,15 +441,16 @@ public class TunnelListFragment extends BaseFragment {
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 (binding != null) {
binding.tunnelList.getAdapter().notifyItemChanged(position);
}
if (adapter != null)
adapter.notifyItemChanged(position);
updateTitle(actionMode);
}
@@ -15,11 +15,11 @@ import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.android.util.ExceptionLoggers;
import com.wireguard.android.util.ObservableKeyedList;
import com.wireguard.android.util.ObservableSortedKeyedArrayList;
import com.wireguard.android.util.ObservableSortedKeyedList;
import com.wireguard.config.Config;
@@ -47,6 +47,8 @@ public final class TunnelManager extends BaseObservable {
private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
private final ConfigStore configStore;
private final Context context = Application.get();
private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
@Nullable private Tunnel lastUsedTunnel;
private boolean haveLoaded;
@@ -64,9 +66,9 @@ public final class TunnelManager extends BaseObservable {
public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
@@ -121,8 +123,8 @@ public final class TunnelManager extends BaseObservable {
.thenApply(tunnel::onStatisticsChanged);
}
public ObservableKeyedList<String, Tunnel> getTunnels() {
return tunnels;
public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
return completableTunnels;
}
public void onCreate() {
@@ -152,6 +154,8 @@ public final class TunnelManager extends BaseObservable {
f.completeExceptionally(t);
}
});
completableTunnels.complete(tunnels);
}
public void refreshTunnelStates() {
@@ -210,9 +214,9 @@ public final class TunnelManager extends BaseObservable {
CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
if (Tunnel.isNameInvalid(name))
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid name"));
return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
if (tunnels.containsKey(name)) {
final String message = "Tunnel " + name + " already exists";
final String message = context.getString(R.string.tunnel_error_already_exists, name);
return CompletableFuture.failedFuture(new IllegalArgumentException(message));
}
final State originalState = tunnel.getState();
@@ -285,10 +289,12 @@ public final class TunnelManager extends BaseObservable {
final String tunnelName = intent.getStringExtra("tunnel");
if (tunnelName == null)
return;
final Tunnel tunnel = manager.getTunnels().get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
manager.getTunnels().thenAccept(tunnels -> {
final Tunnel tunnel = tunnels.get(tunnelName);
if (tunnel == null)
return;
manager.setTunnelState(tunnel, state);
});
}
}
}
@@ -23,7 +23,7 @@ public class VersionPreference extends Preference {
public VersionPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
Application.onHaveBackend(backend -> {
Application.getBackendAsync().thenAccept(backend -> {
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypeName().toLowerCase());
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
versionSummary = exception == null
@@ -48,7 +48,10 @@ public class ZipExporterPreference extends Preference {
}
private void exportZip() {
final List<Tunnel> tunnels = new ArrayList<>(Application.getTunnelManager().getTunnels());
Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
}
private void exportZip(final List<Tunnel> tunnels) {
final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
for (final Tunnel tunnel : tunnels)
futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
@@ -11,7 +11,11 @@ import android.view.ContextThemeWrapper;
import com.wireguard.android.activity.SettingsActivity;
public class FragmentUtils {
public final class FragmentUtils {
private FragmentUtils() {
// Prevent instantiation
}
public static SettingsActivity getPrefActivity(final Preference preference) {
final Context context = preference.getContext();
@@ -10,6 +10,7 @@ import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import com.wireguard.android.BuildConfig;
import com.wireguard.android.R;
import java.io.BufferedReader;
@@ -45,8 +46,8 @@ public class RootShell {
final File cacheDir = context.getCacheDir();
localBinaryDir = new File(cacheDir, "bin");
localTemporaryDir = new File(cacheDir, "tmp");
preamble = String.format("export CALLING_PACKAGE=com.wireguard.android PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
localBinaryDir, localTemporaryDir);
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
}
private static boolean isExecutableInPath(final String name) {
@@ -0,0 +1,57 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import com.wireguard.android.R;
public class MultiselectableRelativeLayout extends RelativeLayout {
public MultiselectableRelativeLayout(final Context context) {
super(context);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private static final int[] STATE_MULTISELECTED = { R.attr.state_multiselected };
private boolean multiselected;
@Override
protected int[] onCreateDrawableState(final int extraSpace) {
if (multiselected) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, STATE_MULTISELECTED);
return drawableState;
}
return super.onCreateDrawableState(extraSpace);
}
public void setMultiSelected(final boolean on) {
if (!multiselected) {
multiselected = true;
refreshDrawableState();
}
setActivated(on);
}
public void setSingleSelected(final boolean on) {
if (multiselected) {
multiselected = false;
refreshDrawableState();
}
setActivated(on);
}
}
@@ -0,0 +1,67 @@
/*
* Copyright © 2018 Harsh Shandilya <msfjarvis@gmail.com>
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
private static final long ANIMATION_DURATION = 250;
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
final float origin = child.getBehaviorYTranslation();
if (Math.abs(destination - origin) < fullSpan / 2) {
child.setBehaviorYTranslation(destination);
return;
}
final ValueAnimator animator = new ValueAnimator();
animator.setFloatValues(origin, destination);
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration((long)(ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator a) {
child.setBehaviorYTranslation(destination);
}
});
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float)a.getAnimatedValue()));
animator.start();
}
@Override
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
return true;
}
@Override
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
final View dependency) {
animateChange(child, 0, dependency.getMeasuredHeight());
}
}
@@ -17,6 +17,7 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Keep;
@@ -67,6 +68,8 @@ public class FloatingActionsMenu extends ViewGroup {
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
private final Rect touchArea = new Rect(0, 0, 0, 0);
private float scrollYTranslation;
private float behaviorYTranslation;
public FloatingActionsMenu(final Context context) {
this(context, null);
@@ -103,6 +106,24 @@ public class FloatingActionsMenu extends ViewGroup {
createAddButton(context);
}
public float getScrollYTranslation() {
return scrollYTranslation;
}
public void setScrollYTranslation(final float scrollYTranslation) {
this.scrollYTranslation = scrollYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public float getBehaviorYTranslation() {
return behaviorYTranslation;
}
public void setBehaviorYTranslation(final float behaviorYTranslation) {
this.behaviorYTranslation = behaviorYTranslation;
setTranslationY(behaviorYTranslation + scrollYTranslation);
}
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
mListener = listener;
}
@@ -112,7 +133,7 @@ public class FloatingActionsMenu extends ViewGroup {
}
private void createAddButton(final Context context) {
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_inverse, context.getTheme()));
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
mRotatingDrawable = rotatingDrawable;
final TimeInterpolator interpolator = new OvershootInterpolator();
@@ -372,8 +393,10 @@ public class FloatingActionsMenu extends ViewGroup {
}
}
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equals("ASUS");
private void createLabels() {
final Context context = new ContextThemeWrapper(getContext(), mLabelsStyle);
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
for (int i = 0; i < mButtonsCount; i++) {
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
@@ -382,7 +405,8 @@ public class FloatingActionsMenu extends ViewGroup {
final String title = ((LabeledFloatingActionButton) button).getTitle();
final AppCompatTextView label = new AppCompatTextView(context);
label.setTextAppearance(context, mLabelsStyle);
if (!BROKEN_LABEL_STYLE)
label.setTextAppearance(context, mLabelsStyle);
label.setText(title);
addView(label);
@@ -0,0 +1,27 @@
/*
* Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget.fab;
import android.support.v7.widget.RecyclerView;
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
private static final float SCALE_FACTOR = 1.5f;
private final FloatingActionsMenu menu;
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
this.menu = menu;
}
private static float bound(final float min, final float proposal, final float max) {
return Math.min(max, Math.max(min, proposal));
}
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
}
}
@@ -6,11 +6,11 @@
package com.wireguard.config;
import android.annotation.SuppressLint;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -56,6 +56,7 @@ public enum Attribute {
return TextUtils.join(", ", iterable);
}
@Nullable
public static Attribute match(final CharSequence line) {
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
}
@@ -66,12 +67,14 @@ public enum Attribute {
return LIST_SEPARATOR_PATTERN.split(string.trim());
}
@SuppressLint("DefaultLocale")
public String composeWith(@Nullable final Object value) {
return String.format("%s = %s%n", token, value);
}
@SuppressLint("DefaultLocale")
public String composeWith(final int value) {
return String.format(Locale.getDefault(), "%s = %d%n", token, value);
return String.format("%s = %d%n", token, value);
}
public <T> String composeWith(final Iterable<T> value) {
@@ -6,6 +6,7 @@
package com.wireguard.config;
import android.content.Context;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.ObservableArrayList;
@@ -15,11 +16,14 @@ import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
import com.wireguard.android.Application;
import com.wireguard.android.R;
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,12 +36,17 @@ 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)));
}
public static Config from(final BufferedReader reader) throws IOException {
final Config config = new Config();
final Context context = Application.get();
Peer currentPeer = null;
String line;
boolean inInterfaceSection = false;
@@ -60,11 +69,11 @@ public class Config {
} else if (currentPeer != null) {
currentPeer.parse(line);
} else {
throw new IllegalArgumentException("Invalid configuration line: " + line);
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, line));
}
}
if (!inInterfaceSection && currentPeer == null) {
throw new IllegalArgumentException("Could not find any config information");
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
}
return config;
}
@@ -8,6 +8,9 @@ package com.wireguard.config;
import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
@@ -30,7 +33,7 @@ public final class InetAddresses {
public static InetAddress parse(@Nullable final String address) {
if (address == null || address.isEmpty())
throw new IllegalArgumentException("Empty address");
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_inetaddress));
try {
return (InetAddress) PARSER_METHOD.invoke(null, address);
} catch (final IllegalAccessException | InvocationTargetException e) {
@@ -6,13 +6,16 @@
package com.wireguard.config;
import android.content.Context;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.wireguard.android.Application;
import com.wireguard.android.BR;
import com.wireguard.android.R;
import com.wireguard.crypto.Keypair;
import java.net.InetAddress;
@@ -31,6 +34,7 @@ public class Interface {
@Nullable private Keypair keypair;
private int listenPort;
private int mtu;
private final Context context = Application.get();
public Interface() {
addressList = new ArrayList<>();
@@ -42,7 +46,7 @@ public class Interface {
if (addresses != null && addresses.length > 0) {
for (final String addr : addresses) {
if (addr.isEmpty())
throw new IllegalArgumentException("Address is empty");
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_empty_interface_address));
addressList.add(new InetNetwork(addr));
}
}
@@ -140,6 +144,8 @@ public class Interface {
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(String.format(context.getString(R.string.tunnel_error_interface_parse_failed), line));
switch (key) {
case ADDRESS:
addAddresses(key.parseList(line));
@@ -6,6 +6,8 @@
package com.wireguard.config;
import android.annotation.SuppressLint;
import android.content.Context;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.os.Parcel;
@@ -13,6 +15,8 @@ import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.android.databinding.library.baseAdapters.BR;
import com.wireguard.android.Application;
import com.wireguard.android.R;
import com.wireguard.crypto.KeyEncoding;
import java.net.Inet6Address;
@@ -25,7 +29,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java9.lang.Iterables;
@@ -39,6 +42,7 @@ public class Peer {
private int persistentKeepalive;
@Nullable private String preSharedKey;
@Nullable private String publicKey;
private final Context context = Application.get();
public Peer() {
allowedIPsList = new ArrayList<>();
@@ -68,11 +72,15 @@ public class Peer {
return endpoint;
}
@SuppressLint("DefaultLocale")
@Nullable
private String getEndpointString() {
if (endpoint == null)
return null;
return String.format(Locale.getDefault(), "%s:%d", endpoint.getHostString(), endpoint.getPort());
if (endpoint.getHostString().contains(":") && !endpoint.getHostString().contains("["))
return String.format("[%s]:%d", endpoint.getHostString(), endpoint.getPort());
else
return String.format("%s:%d", endpoint.getHostString(), endpoint.getPort());
}
public int getPersistentKeepalive() {
@@ -96,6 +104,7 @@ public class Peer {
return publicKey;
}
@SuppressLint("DefaultLocale")
public String getResolvedEndpointString() throws UnknownHostException {
if (endpoint == null)
throw new UnknownHostException("{empty}");
@@ -104,18 +113,18 @@ public class Peer {
if (endpoint.isUnresolved())
throw new UnknownHostException(endpoint.getHostString());
if (endpoint.getAddress() instanceof Inet6Address)
return String.format(Locale.getDefault(),
"[%s]:%d",
return String.format("[%s]:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
return String.format(Locale.getDefault(),
"%s:%d",
return String.format("%s:%d",
endpoint.getAddress().getHostAddress(),
endpoint.getPort());
}
public void parse(final String line) {
final Attribute key = Attribute.match(line);
if (key == null)
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_interface_parse_failed, line));
switch (key) {
case ALLOWED_IPS:
addAllowedIPs(key.parseList(line));
@@ -150,7 +159,7 @@ public class Peer {
if (endpoint != null && !endpoint.isEmpty()) {
final InetSocketAddress constructedEndpoint;
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
throw new IllegalArgumentException("Forbidden characters in endpoint");
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_forbidden_endpoint_chars));
final URI uri;
try {
uri = new URI("wg://" + endpoint);
@@ -251,7 +260,7 @@ public class Peer {
parent.setPreSharedKey(preSharedKey);
parent.setPublicKey(publicKey);
if (parent.getPublicKey() == null)
throw new IllegalArgumentException("Peer public key may not be empty");
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_peer_public_key));
loadData(parent);
notifyChange();
}
@@ -5,6 +5,9 @@
package com.wireguard.crypto;
import com.wireguard.android.Application;
import com.wireguard.android.R;
/**
* This is a specialized constant-time base64 and hex implementation that resists side-channel attacks.
*/
@@ -15,11 +18,11 @@ public final class KeyEncoding {
public static final int KEY_LENGTH_BASE64 = 44;
public static final int KEY_LENGTH_HEX = 64;
private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE =
"WireGuard base64 keys must be 44 characters encoding 32 bytes";
Application.get().getString(R.string.key_length_base64_exception_message);
private static final String KEY_LENGTH_EXCEPTION_MESSAGE =
"WireGuard keys must be 32 bytes";
Application.get().getString(R.string.key_length_exception_message);
private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE =
"WireGuard hex keys must be 64 characters encoding 32 bytes";
Application.get().getString(R.string.key_length_hex_exception_message);
private KeyEncoding() {
// Prevent instantiation.
@@ -5,6 +5,8 @@
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.List;
@@ -18,8 +20,10 @@ public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
boolean containsKey(K key);
@Nullable
E get(K key);
@Nullable
E getLast(K key);
int indexOfKey(K key);
@@ -5,6 +5,8 @@
package com.wireguard.util;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
@@ -17,10 +19,12 @@ import java.util.Set;
public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
Comparator<? super K> comparator();
@Nullable
K firstKey();
Set<K> keySet();
@Nullable
K lastKey();
Collection<E> values();
@@ -4,6 +4,6 @@
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?android:attr/colorBackground"
android:fillColor="#ffffff"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
@@ -4,6 +4,6 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorBackground"
android:fillColor="#ffffff"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
@@ -4,6 +4,6 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="?android:attr/colorBackground"
android:fillColor="#ffffff"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
@@ -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>
@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/list_item_activated" android:state_activated="true" /> <!-- TODO(msf): themeify this -->
<item android:drawable="@android:color/transparent" /> <!-- TODO(msf): themeify this -->
</selector>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item>
<selector>
<item app:state_multiselected="true" android:state_activated="true">
<color android:color="?attr/colorControlActivated" />
</item>
<item app:state_multiselected="false" android:state_activated="true">
<color android:color="?attr/colorControlHighlight" />
</item>
</selector>
</item>
<item android:drawable="?attr/selectableItemBackground" />
</layer-list>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/list_item_ripple"> <!-- TODO(msf): themeify this -->
<item android:drawable="@drawable/list_item_background" />
<item android:id="@android:id/mask" android:drawable="@android:color/white" />
</ripple>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:divider="?attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
+1 -1
View File
@@ -22,7 +22,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background_anim"
android:background="@drawable/list_item_background"
android:padding="16dp"
android:orientation="horizontal"
android:gravity="center_vertical"
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.wireguard.android.widget.NameInputFilter" />
</data>
<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:inputType="textNoSuggestions|textVisiblePassword"
app:filter="@{NameInputFilter.newInstance()}" />
</android.support.design.widget.TextInputLayout>
</FrameLayout>
</layout>
+14 -2
View File
@@ -1,7 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/master_fragment"
android:id="@+id/master_detail_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
tools:context=".activity.MainActivity">
<fragment
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="LIST" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
@@ -55,7 +55,6 @@
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toStartOf="@+id/tunnel_switch"
android:text="@string/interface_title" />
<com.wireguard.android.widget.ToggleSwitch
@@ -120,7 +120,7 @@
<TextView
android:id="@+id/public_key_text"
style="?android:attr/editTextStyle"
style="?attr/editTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/public_key_label"
@@ -30,20 +30,65 @@
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
android:clipToPadding="false"
android:choiceMode="multipleChoiceModal"
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
app:items="@{tunnels}"
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:alpha="0.3333333"
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"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_margin="@dimen/fab_margin"
app:fab_labelStyle="@style/fab_label"
android:clipChildren="false"
app:fab_labelsPosition="left" >
app:fab_labelsPosition="@integer/label_position"
app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior" >
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/create_from_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestImportConfig}"
app:srcCompat="@drawable/ic_action_open_white"
app:fabSize="mini"
app:fab_title="@string/create_from_file" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/scan_qr_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestScanQRCode}"
app:srcCompat="@drawable/ic_action_scan_qr_code_white"
app:fabSize="mini"
app:fab_title="@string/create_from_qrcode" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/create_empty"
@@ -51,17 +96,9 @@
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestCreateConfig}"
app:fabSize="mini"
app:srcCompat="@drawable/ic_action_edit_inverse"
app:srcCompat="@drawable/ic_action_edit_white"
app:fab_title="@string/create_empty" />
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
android:id="@+id/create_from_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{fragment::onRequestImportConfig}"
app:srcCompat="@drawable/ic_action_open_inverse"
app:fabSize="mini"
app:fab_title="@string/create_from_file" />
</com.wireguard.android.widget.fab.FloatingActionsMenu>
</android.support.design.widget.CoordinatorLayout>
</layout>
+3 -4
View File
@@ -25,10 +25,10 @@
type="com.wireguard.android.fragment.TunnelListFragment" />
</data>
<RelativeLayout
<com.wireguard.android.widget.MultiselectableRelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_background_anim"
android:background="@drawable/list_item_background"
android:descendantFocusability="blocksDescendants"
android:padding="16dp">
@@ -39,7 +39,6 @@
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toStartOf="@+id/tunnel_switch"
android:ellipsize="end"
android:maxLines="1"
android:text="@{key}" />
@@ -52,5 +51,5 @@
android:layout_alignParentEnd="true"
app:checked="@{item.state == State.UP}"
app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
</RelativeLayout>
</com.wireguard.android.widget.MultiselectableRelativeLayout>
</layout>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="label_position">1</integer>
</resources>
+2 -4
View File
@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
<color name="list_item_activated">#64cfd8dc</color>
<color name="list_item_ripple">#a0808e95</color>
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#000000</color>
<color name="fab_label_background_color">#bbbbbb</color>
<color name="accent">#BBDEFB</color>
<color name="accent">#5e97f6</color>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean"/>
</declare-styleable>
</resources>
+1 -3
View File
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO(msf): remove these 4 hard-coded colors and replace with theme colors -->
<color name="list_item_activated">#cfd8dc</color> <!-- Blue Grey 200 -->
<color name="list_item_ripple">#808e95</color> <!-- Blue Grey 200 dark -->
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
<color name="fab_label_text_color">#ffffff</color>
<color name="fab_label_background_color">#444444</color>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>
+1
View File
@@ -27,4 +27,5 @@
<enum name="right" value="3"/>
</attr>
</declare-styleable>
<integer name="label_position">0</integer>
</resources>
+18
View File
@@ -30,6 +30,8 @@
<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_from_qrcode">Create from QR code</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>
@@ -50,6 +52,7 @@
<string name="hint_generated">(generated)</string>
<string name="hint_optional">(optional)</string>
<string name="hint_random">(random)</string>
<string name="import_from_qrcode">Import Tunnel from QR Code</string>
<string name="import_error">Unable to import tunnel: %s</string>
<string name="import_success">Imported “%s”</string>
<string name="interface_title">Interface</string>
@@ -68,6 +71,7 @@
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
<string name="public_key_description">Public key</string>
<string name="qrcode_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</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="save">Save</string>
@@ -89,8 +93,22 @@
<string name="tools_installer_title">Install command line tools</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_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="tunnel_error_invalid_name">Invalid name</string>
<string name="tunnel_error_already_exists">Tunnel %s already exists</string>
<string name="tunnel_error_empty_inetaddress">Empty address</string>
<string name="tunnel_error_empty_interface_address">Address is empty</string>
<string name="tunnel_error_interface_parse_failed">Unable to parse line: “%s”</string>
<string name="tunnel_error_forbidden_endpoint_chars">Forbidden characters in endpoint</string>
<string name="tunnel_error_empty_peer_public_key">Peer public key may not be empty</string>
<string name="tunnel_error_invalid_config_line">Invalid configuration line: %s</string>
<string name="tunnel_error_no_config_information">Could not find any config information</string>
<string name="key_length_base64_exception_message">WireGuard base64 keys must be 44 characters encoding 32 bytes</string>
<string name="key_length_exception_message">WireGuard keys must be 32 bytes</string>
<string name="key_length_hex_exception_message">WireGuard hex keys must be 64 characters encoding 32 bytes</string>
<string name="version_title">WireGuard for Android v%s"</string>
<string name="version_summary">%s backend v%s</string>
<string name="version_summary_checking">Checking %s backend version</string>
+5 -4
View File
@@ -8,12 +8,12 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
# Work around https://github.com/android-ndk/ndk/issues/602
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold")
add_executable(libwg-quick.so wireguard/src/tools/wg-quick/android.c)
target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -pedantic -Wno-missing-field-initializers)
add_executable(libwg-quick.so wireguard/src/tools/wg-quick/android.c ndk-compat/compat.c)
target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -pedantic -Wno-missing-field-initializers -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c)
file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c ndk-compat/compat.c)
add_executable(libwg.so ${WG_SOURCES})
target_compile_options(libwg.so PUBLIC -idirafter "${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" -I "${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"\\\"/data/data/com.wireguard.android/cache\\\"\" -Wno-pointer-arith -Wno-unused-parameter)
target_compile_options(libwg.so PUBLIC -idirafter "${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" -I "${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\" -Wno-pointer-arith -Wno-unused-parameter)
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
@@ -21,6 +21,7 @@ add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib
ANDROID_TOOLCHAIN_ROOT=${ANDROID_TOOLCHAIN_ROOT}
ANDROID_LLVM_TRIPLE=${ANDROID_LLVM_TRIPLE}
ANDROID_SYSROOT=${ANDROID_SYSROOT}
ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
CFLAGS=${CMAKE_C_FLAGS}\ -Wno-unused-command-line-argument
LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}\ -fuse-ld=gold
DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+2 -2
View File
@@ -23,7 +23,7 @@ default: $(DESTDIR)/libwg-go.so
GOBUILDARCH := $(NDK_GO_ARCH_MAP_$(shell uname -m))
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
GOBUILDVERSION := 1.10.2
GOBUILDVERSION := 1.11
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
@@ -54,5 +54,5 @@ $(DESTDIR)/libwg-go.so: $(FILES) src/git.zx2c4.com/wireguard-go/api-android.go s
mkdir -p $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILES)))
$(foreach FILE,$(FILES),ln -sfrt $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILE))) $(FILE);)
GOPATH=$(PWD) go get -v -d git.zx2c4.com/wireguard-go
GOPATH=$(PWD) go build -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared git.zx2c4.com/wireguard-go
GOPATH=$(PWD) go build -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared git.zx2c4.com/wireguard-go
go version > .gobuildversion
@@ -1,7 +1,28 @@
diff -ru b/src/runtime/sys_linux_386.s a/src/runtime/sys_linux_386.s
--- b/src/runtime/sys_linux_386.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_386.s 2018-05-09 17:09:42.319531743 +0200
@@ -287,13 +287,13 @@
From 6f345a38d9533db388009d91d29981673dcf95b4 Mon Sep 17 00:00:00 2001
From: Jason A. Donenfeld <Jason@zx2c4.com>
Date: Fri, 04 May 2018 03:48:23 +0200
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
This makes timers account for having expired while a computer was
asleep, which is quite common on mobile devices. Note that BOOTTIME is
identical to MONOTONIC, except that it takes into account time spent
in suspend. In Linux 4.17, the kernel will actually make MONOTONIC act
like BOOTTIME anyway, so this switch will additionally unify the
timer behavior across kernels.
BOOTTIME was introduced into Linux 2.6.39-rc1 with 70a08cca1227d in
2011.
Fixes #24595
Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
---
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
index 8d5a4ff..bfad064 100644
--- a/src/runtime/sys_linux_386.s
+++ b/src/runtime/sys_linux_386.s
@@ -285,13 +285,13 @@
LEAL 8(SP), BX // &ts (struct timespec)
MOVL BX, 4(SP)
@@ -17,11 +38,12 @@ diff -ru b/src/runtime/sys_linux_386.s a/src/runtime/sys_linux_386.s
LEAL 8(SP), CX
INVOKE_SYSCALL
diff -ru b/src/runtime/sys_linux_amd64.s a/src/runtime/sys_linux_amd64.s
--- b/src/runtime/sys_linux_amd64.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_amd64.s 2018-05-09 17:09:42.319531743 +0200
@@ -258,7 +258,7 @@
MOVQ runtime·__vdso_clock_gettime_sym(SB), AX
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
index 62d8024..8510be4 100644
--- a/src/runtime/sys_linux_amd64.s
+++ b/src/runtime/sys_linux_amd64.s
@@ -257,7 +257,7 @@
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
- MOVL $1, DI // CLOCK_MONOTONIC
@@ -29,34 +51,55 @@ diff -ru b/src/runtime/sys_linux_amd64.s a/src/runtime/sys_linux_amd64.s
LEAQ 0(SP), SI
CALL AX
MOVQ 0(SP), AX // sec
diff -ru b/src/runtime/sys_linux_arm64.s a/src/runtime/sys_linux_arm64.s
--- b/src/runtime/sys_linux_arm64.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_arm64.s 2018-05-09 17:12:47.844591628 +0200
@@ -201,7 +201,7 @@
RET
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
index aa39732..d29cb88 100644
--- a/src/runtime/sys_linux_arm.s
+++ b/src/runtime/sys_linux_arm.s
@@ -11,7 +11,7 @@
#include "textflag.h"
TEXT runtime·nanotime(SB),NOSPLIT,$24-8
- MOVW $1, R0 // CLOCK_MONOTONIC
+ MOVW $7, R0 // CLOCK_BOOTTIME
MOVD RSP, R1
MOVD $SYS_clock_gettime, R8
SVC
diff -ru b/src/runtime/sys_linux_arm.s a/src/runtime/sys_linux_arm.s
--- b/src/runtime/sys_linux_arm.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_arm.s 2018-05-09 17:13:18.983281049 +0200
@@ -235,7 +235,7 @@
#define CLOCK_REALTIME 0
-#define CLOCK_MONOTONIC 1
+#define CLOCK_BOOTTIME 7
// int64 nanotime(void)
TEXT runtime·nanotime(SB),NOSPLIT,$32
- MOVW $1, R0 // CLOCK_MONOTONIC
+ MOVW $7, R0 // CLOCK_BOOTTIME
MOVW $8(R13), R1 // timespec
MOVW $SYS_clock_gettime, R7
SWI $0
diff -ru b/src/runtime/sys_linux_mips64x.s a/src/runtime/sys_linux_mips64x.s
--- b/src/runtime/sys_linux_mips64x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_mips64x.s 2018-05-09 17:09:42.321531721 +0200
@@ -197,7 +197,7 @@
// for EABI, as we don't support OABI
#define SYS_BASE 0x0
@@ -287,7 +287,7 @@
SUB $24, R13 // Space for results
BIC $0x7, R13 // Align for C code
- MOVW $CLOCK_MONOTONIC, R0
+ MOVW $CLOCK_BOOTTIME, R0
MOVW $8(R13), R1 // timespec
MOVW runtime·vdsoClockgettimeSym(SB), R11
CMP $0, R11
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
index c6afd76..24294e8 100644
--- a/src/runtime/sys_linux_arm64.s
+++ b/src/runtime/sys_linux_arm64.s
@@ -13,7 +13,7 @@
#define AT_FDCWD -100
#define CLOCK_REALTIME 0
-#define CLOCK_MONOTONIC 1
+#define CLOCK_BOOTTIME 7
#define SYS_exit 93
#define SYS_read 63
@@ -243,7 +243,7 @@
BIC $15, R1
MOVD R1, RSP
- MOVW $CLOCK_MONOTONIC, R0
+ MOVW $CLOCK_BOOTTIME, R0
MOVD runtime·vdsoClockgettimeSym(SB), R2
CBZ R2, fallback
BL (R2)
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
index 8e64f1c..521062f 100644
--- a/src/runtime/sys_linux_mips64x.s
+++ b/src/runtime/sys_linux_mips64x.s
@@ -185,7 +185,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
@@ -65,10 +108,11 @@ diff -ru b/src/runtime/sys_linux_mips64x.s a/src/runtime/sys_linux_mips64x.s
MOVV $0(R29), R5
MOVV $SYS_clock_gettime, R2
SYSCALL
diff -ru b/src/runtime/sys_linux_mipsx.s a/src/runtime/sys_linux_mipsx.s
--- b/src/runtime/sys_linux_mipsx.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_mipsx.s 2018-05-09 17:09:42.321531721 +0200
@@ -202,7 +202,7 @@
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
index a6bca3b..920ed9a 100644
--- a/src/runtime/sys_linux_mipsx.s
+++ b/src/runtime/sys_linux_mipsx.s
@@ -189,7 +189,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$8-8
@@ -77,10 +121,11 @@ diff -ru b/src/runtime/sys_linux_mipsx.s a/src/runtime/sys_linux_mipsx.s
MOVW $4(R29), R5
MOVW $SYS_clock_gettime, R2
SYSCALL
diff -ru b/src/runtime/sys_linux_ppc64x.s a/src/runtime/sys_linux_ppc64x.s
--- b/src/runtime/sys_linux_ppc64x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_ppc64x.s 2018-05-09 17:09:42.322531710 +0200
@@ -176,7 +176,7 @@
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
index 483cb8e..11f3c49 100644
--- a/src/runtime/sys_linux_ppc64x.s
+++ b/src/runtime/sys_linux_ppc64x.s
@@ -164,7 +164,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
@@ -89,10 +134,11 @@ diff -ru b/src/runtime/sys_linux_ppc64x.s a/src/runtime/sys_linux_ppc64x.s
MOVD $0(R1), R4
SYSCALL $SYS_clock_gettime
MOVD 0(R1), R3 // sec
diff -ru b/src/runtime/sys_linux_s390x.s a/src/runtime/sys_linux_s390x.s
--- b/src/runtime/sys_linux_s390x.s 2018-04-30 22:26:39.000000000 +0200
+++ a/src/runtime/sys_linux_s390x.s 2018-05-09 17:09:42.322531710 +0200
@@ -188,7 +188,7 @@
diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s
index 1ff110c..e9049e0 100644
--- a/src/runtime/sys_linux_s390x.s
+++ b/src/runtime/sys_linux_s390x.s
@@ -176,7 +176,7 @@
RET
TEXT runtime·nanotime(SB),NOSPLIT,$16
@@ -16,6 +16,7 @@ import (
"io/ioutil"
"log"
"math"
"net"
"os"
"os/signal"
"runtime"
@@ -33,11 +34,16 @@ func (l AndroidLogger) Write(p []byte) (int, error) {
return len(p), nil
}
var tunnelHandles map[int32]*Device
type TunnelHandle struct {
device *Device
uapi net.Listener
}
var tunnelHandles map[int32]TunnelHandle
func init() {
roamingDisabled = true
tunnelHandles = make(map[int32]*Device)
tunnelHandles = make(map[int32]TunnelHandle)
signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2)
go func() {
@@ -85,6 +91,29 @@ func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
return -1
}
var uapi net.Listener;
uapiFile, err := UAPIOpen(name)
if err != nil {
logger.Error.Println(err)
} else {
uapi, err = UAPIListen(name, uapiFile)
if err != nil {
uapiFile.Close()
logger.Error.Println(err)
} else {
go func() {
for {
conn, err := uapi.Accept()
if err != nil {
return
}
go ipcHandle(device, conn)
}
}()
}
}
device.Up()
logger.Info.Println("Device started")
@@ -98,27 +127,30 @@ func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
unix.Close(int(tun_fd))
return -1
}
tunnelHandles[i] = device
tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
return i
}
//export wgTurnOff
func wgTurnOff(tunnelHandle int32) {
device, ok := tunnelHandles[tunnelHandle]
handle, ok := tunnelHandles[tunnelHandle]
if !ok {
return
}
delete(tunnelHandles, tunnelHandle)
device.Close()
if handle.uapi != nil {
handle.uapi.Close()
}
handle.device.Close()
}
//export wgGetSocketV4
func wgGetSocketV4(tunnelHandle int32) int32 {
device, ok := tunnelHandles[tunnelHandle]
handle, ok := tunnelHandles[tunnelHandle]
if !ok {
return -1
}
native, ok := device.net.bind.(*NativeBind)
native, ok := handle.device.net.bind.(*NativeBind)
if !ok {
return -1
}
@@ -138,11 +170,11 @@ func wgGetSocketV4(tunnelHandle int32) int32 {
//export wgGetSocketV6
func wgGetSocketV6(tunnelHandle int32) int32 {
device, ok := tunnelHandles[tunnelHandle]
handle, ok := tunnelHandles[tunnelHandle]
if !ok {
return -1
}
native, ok := device.net.bind.(*NativeBind)
native, ok := handle.device.net.bind.(*NativeBind)
if !ok {
return -1
}
+77
View File
@@ -0,0 +1,77 @@
/* SPDX-License-Identifier: BSD
*
* Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
*/
#define FILE_IS_EMPTY
#if defined(__ANDROID_API__) && __ANDROID_API__ < 18
#undef FILE_IS_EMPTY
#include <stdio.h>
#include <stdlib.h>
ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp)
{
char *ptr, *eptr;
if (*buf == NULL || *bufsiz == 0) {
*bufsiz = BUFSIZ;
if ((*buf = malloc(*bufsiz)) == NULL)
return -1;
}
for (ptr = *buf, eptr = *buf + *bufsiz;;) {
int c = fgetc(fp);
if (c == -1) {
if (feof(fp)) {
ssize_t diff = (ssize_t)(ptr - *buf);
if (diff != 0) {
*ptr = '\0';
return diff;
}
}
return -1;
}
*ptr++ = c;
if (c == delimiter) {
*ptr = '\0';
return ptr - *buf;
}
if (ptr + 2 >= eptr) {
char *nbuf;
size_t nbufsiz = *bufsiz * 2;
ssize_t d = ptr - *buf;
if ((nbuf = realloc(*buf, nbufsiz)) == NULL)
return -1;
*buf = nbuf;
*bufsiz = nbufsiz;
eptr = nbuf + nbufsiz;
ptr = nbuf + d;
}
}
}
ssize_t getline(char **buf, size_t *bufsiz, FILE *fp)
{
return getdelim(buf, bufsiz, '\n', fp);
}
#endif
#if defined(__ANDROID_API__) && __ANDROID_API__ < 24
#undef FILE_IS_EMPTY
#include <string.h>
char *strchrnul(const char *s, int c)
{
char *x = strchr(s, c);
if (!x)
return (char *)s + strlen(s);
return x;
}
#endif
#ifdef FILE_IS_EMPTY
#undef FILE_IS_EMPTY
static char ____x __attribute__((unused));
#endif
+16
View File
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: BSD
*
* Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
*/
#if defined(__ANDROID_API__) && __ANDROID_API__ < 18
#include <stdio.h>
ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp);
ssize_t getline(char **buf, size_t *bufsiz, FILE *fp);
#endif
#if defined(__ANDROID_API__) && __ANDROID_API__ < 24
char *strchrnul(const char *s, int c);
#endif
+1 -1
View File
@@ -7,7 +7,7 @@ allprojects {
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath 'com.android.tools.build:gradle:3.1.4'
}
repositories {
google()