mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07c85ee6f5 | |||
| c02dd9e040 | |||
| 7dae94976d | |||
| 38c360cb74 | |||
| edba640641 | |||
| cf25ae4448 | |||
| e86182af56 | |||
| 902a1ce46e | |||
| 55849cad65 | |||
| edb76af820 | |||
| 04d0b819f6 | |||
| 85aa5fbd46 | |||
| 1054e54c89 | |||
| 2fe5b92035 | |||
| 3a5a161c03 | |||
| 8451321a79 | |||
| 90050a0008 | |||
| 87c9efce4a | |||
| 240e049e46 | |||
| 0899b49bb3 | |||
| fc0660ca8d | |||
| b2ed5dbbc8 | |||
| 94c864503e | |||
| c387c6aebf | |||
| 55c9d39c13 | |||
| 6a3b143876 | |||
| 5f29abfa0d | |||
| ea3364ac12 | |||
| 2b31eac1af | |||
| 093139bc91 | |||
| 6c8a4a6a28 | |||
| f5d2fd6190 | |||
| 4d77bd8f25 | |||
| 1068adbee3 | |||
| 86fc518585 | |||
| 78377a5c67 | |||
| 56f2dcc073 | |||
| 48739b4141 | |||
| 704c344213 | |||
| ee263ba68e | |||
| 692b71af23 | |||
| 7e029f1db0 | |||
| d4875afe31 | |||
| de0e431d00 | |||
| 6b304391b5 | |||
| 8f85e4c88f | |||
| a3b9c3b884 | |||
| 021e16959f | |||
| 37adab76e1 | |||
| 5d342ee1ab | |||
| 697d131397 | |||
| 89d9e30025 | |||
| dfde86df76 | |||
| d55fb25a40 | |||
| 492fcce053 | |||
| d61f17dbd3 | |||
| 8996979dc2 | |||
| 31ddb242a6 | |||
| b7b5d96b3b | |||
| 2e55e5fd05 | |||
| 40ebf8006e | |||
| 8e8643122e | |||
| c00a0b12e4 | |||
| 0e21520fd1 | |||
| 0db233e5c7 | |||
| b9948085a4 | |||
| d62526fde6 | |||
| 134f9c014e | |||
| 056cf472d9 | |||
| 0c161cc0c2 | |||
| 7894894610 | |||
| 7d48bef70a | |||
| 6bc3e257f8 | |||
| adc613d801 | |||
| fd573f6c1c | |||
| 1235e966d2 | |||
| e6a2f049ea | |||
| d9e9dd04af | |||
| 3c2fa15dc2 | |||
| e8c9c20570 | |||
| 0db2578ca0 | |||
| e71d6157f1 | |||
| 7ca2a0df18 | |||
| 7a4af834c2 | |||
| 314a0d124d | |||
| 6f1e86e8a7 | |||
| 840c7ea560 | |||
| 1839730663 | |||
| afd75cc4cf | |||
| 453a1aaa65 | |||
| 4905185e61 | |||
| 0990430513 | |||
| 7df13a044f | |||
| 6135a1f60a | |||
| 5e94adc73a | |||
| 44fc0228a9 | |||
| 4d2bfe3ef3 | |||
| 7f37ff032f | |||
| 30d508464f | |||
| 75b0fed00d | |||
| 927cc1fcf5 | |||
| b3090e277a | |||
| 0b45151a3d | |||
| eb5bfa9b5d | |||
| 4267e94dcd | |||
| fac9e7612f | |||
| 01e8e535f4 | |||
| e5e2e7571f | |||
| c889a8c8de | |||
| cbf2ea7b48 | |||
| 8078347880 | |||
| dd8a802bec | |||
| 6f6602ddd1 | |||
| 33e69db436 | |||
| 13cbec28d6 | |||
| 826083adf6 | |||
| a62bd28e1f | |||
| da1188c6ee | |||
| e22cefbfe3 | |||
| 02ea696070 | |||
| d25702d99d | |||
| c554413327 | |||
| f8c5f238ea | |||
| 66b46c8618 |
@@ -15,4 +15,3 @@ build/
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
package-info.java
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
[submodule "app/tools/wireguard-tools"]
|
||||
path = app/tools/wireguard-tools
|
||||
[submodule "tunnel/tools/wireguard-tools"]
|
||||
path = tunnel/tools/wireguard-tools
|
||||
url = https://git.zx2c4.com/wireguard-tools
|
||||
|
||||
Generated
+9
-3
@@ -29,9 +29,6 @@
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||
@@ -59,6 +56,15 @@
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
|
||||
</JetCodeStyleSettings>
|
||||
<XML>
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
|
||||
Generated
+14
-13
@@ -9,7 +9,6 @@
|
||||
</option>
|
||||
<option name="nonThreadSafeTypes" value="" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -40,7 +39,9 @@
|
||||
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="exceptionsString" value="" />
|
||||
<option name="exceptions">
|
||||
@@ -59,6 +60,9 @@
|
||||
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreCatchBlocksWithComments" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -103,9 +107,6 @@
|
||||
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSideEffectConditions" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
@@ -196,7 +197,6 @@
|
||||
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreInvisibleFields" value="true" />
|
||||
@@ -215,7 +215,6 @@
|
||||
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
@@ -294,6 +293,7 @@
|
||||
<option name="ignoreForLoopDeclarations" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
@@ -347,7 +347,6 @@
|
||||
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
@@ -394,7 +393,13 @@
|
||||
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="allowedClasses">
|
||||
<set>
|
||||
<option value="org.junit.Assert" />
|
||||
</set>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
|
||||
@@ -432,7 +437,6 @@
|
||||
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
|
||||
<option name="assertKeywordIsAssertion" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
@@ -440,7 +444,6 @@
|
||||
<option name="ignoreRethrownExceptions" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_allowConstructorAsInitializer" value="false" />
|
||||
<option name="m_onlyLookAtBlocks" value="true" />
|
||||
@@ -466,10 +469,8 @@
|
||||
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSwitchBranches" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreJavadoc" value="false" />
|
||||
|
||||
@@ -11,3 +11,20 @@ $ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
|
||||
$ cd wireguard-android
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
## Embedding
|
||||
|
||||
The tunnel library is [on JCenter](https://bintray.com/wireguard/wireguard-android/wireguard-android/_latestVersion):
|
||||
|
||||
```
|
||||
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
|
||||
```
|
||||
|
||||
The library makes use of Java 8 features, so be sure to support those in your gradle configuration:
|
||||
|
||||
```
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
|
||||
group = "Copying"
|
||||
description = "Generate package-info.java classes"
|
||||
|
||||
def basePackage = "com" + File.separatorChar + "wireguard"
|
||||
def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
|
||||
def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"main" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"test" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage)
|
||||
def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
|
||||
.separatorChar +
|
||||
"androidTest" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
|
||||
generateInfoFiles(sourceDir, mainSrcPhrase);
|
||||
sourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainSrcPhrase)
|
||||
}
|
||||
if (file(testSourceDir).exists()) {
|
||||
generateInfoFiles(testSourceDir, mainTestSrcPhrase);
|
||||
testSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
if (file(androidTestSourceDir).exists()) {
|
||||
generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
|
||||
androidTestSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainAndroidTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
println "[SUCCESS] NonNull generator: package-info.java files checked"
|
||||
}
|
||||
|
||||
private void generateInfoFiles(File dir, String mainSrcPhrase) {
|
||||
def infoFileContentHeader = getFileContentHeader();
|
||||
def infoFileContentFooter = getFileContentFooter();
|
||||
def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
|
||||
|
||||
//file(infoFilePath).delete(); //do not use in production code
|
||||
if (!file(infoFilePath).exists()) {
|
||||
def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
|
||||
new File(infoFilePath).write(infoFileContentHeader +
|
||||
infoFileContentPackage + infoFileContentFooter)
|
||||
println "[dir] " + infoFilePath + " created";
|
||||
}
|
||||
}
|
||||
|
||||
def getFileContentPackage(String path, String mainSrcPhrase) {
|
||||
def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
|
||||
def output = path.substring(mainSrcPhraseIndex)
|
||||
|
||||
// Win hotfix
|
||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
output = output.replace("\\", "/")
|
||||
mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
|
||||
}
|
||||
|
||||
return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
|
||||
"/", ".") + ";\n"
|
||||
}
|
||||
|
||||
def getFileContentHeader() {
|
||||
return "/**\n" +
|
||||
" * Make all method parameters @NonNull by default.\n" +
|
||||
" */\n" +
|
||||
"@NonNullForAll\n"
|
||||
}
|
||||
|
||||
def getFileContentFooter() {
|
||||
return "\n" +
|
||||
"import com.wireguard.util.NonNullForAll;\n"
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import androidx.databinding.CallbackRegistry;
|
||||
import androidx.databinding.CallbackRegistry.NotifierCallback;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for activities that need to remember the currently-selected tunnel.
|
||||
*/
|
||||
|
||||
public abstract class BaseActivity extends ThemeChangeAwareActivity {
|
||||
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
|
||||
|
||||
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
|
||||
@Nullable private Tunnel selectedTunnel;
|
||||
|
||||
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.add(listener);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Tunnel getSelectedTunnel() {
|
||||
return selectedTunnel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
|
||||
final String savedTunnelName;
|
||||
if (savedInstanceState != null)
|
||||
savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
|
||||
else if (getIntent() != null)
|
||||
savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
if (selectedTunnel != null)
|
||||
outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
|
||||
public void removeOnSelectedTunnelChangedListener(
|
||||
final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.remove(listener);
|
||||
}
|
||||
|
||||
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
|
||||
final Tunnel oldTunnel = selectedTunnel;
|
||||
if (Objects.equals(oldTunnel, tunnel))
|
||||
return;
|
||||
selectedTunnel = tunnel;
|
||||
onSelectedTunnelChanged(oldTunnel, tunnel);
|
||||
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
|
||||
}
|
||||
|
||||
public interface OnSelectedTunnelChangedListener {
|
||||
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
}
|
||||
|
||||
private static final class SelectionChangeNotifier
|
||||
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
@Override
|
||||
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
|
||||
final Tunnel oldTunnel, final int ignored,
|
||||
final Tunnel newTunnel) {
|
||||
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SelectionChangeRegistry
|
||||
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
private SelectionChangeRegistry() {
|
||||
super(new SelectionChangeNotifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.appcompat.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;
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.fragment.TunnelListFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* 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
|
||||
implements FragmentManager.OnBackStackChangedListener {
|
||||
@Nullable private ActionBar actionBar;
|
||||
private boolean isTwoPaneLayout;
|
||||
@Nullable private TunnelListFragment listFragment;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
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 the two-pane layout does not have an editor open, going back should exit the app.
|
||||
if (isTwoPaneLayout && backStackEntries <= 1) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
// calling View#performClick defeats the purpose of it.
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
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
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main_activity, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// The back arrow in the action bar should act the same as the back button.
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_action_edit:
|
||||
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.
|
||||
return false;
|
||||
case R.id.menu_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
|
||||
public class SettingsActivity extends ThemeChangeAwareActivity {
|
||||
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
|
||||
private int permissionRequestCounter;
|
||||
|
||||
public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
|
||||
final List<String> needPermissions = new ArrayList<>(permissions.length);
|
||||
for (final String permission : permissions) {
|
||||
if (ContextCompat.checkSelfPermission(this, permission)
|
||||
!= PackageManager.PERMISSION_GRANTED)
|
||||
needPermissions.add(permission);
|
||||
}
|
||||
if (needPermissions.isEmpty()) {
|
||||
final int[] granted = new int[permissions.length];
|
||||
Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
|
||||
cb.done(permissions, granted);
|
||||
return;
|
||||
}
|
||||
final int idx = permissionRequestCounter++;
|
||||
permissionRequestCallbacks.put(idx, cb);
|
||||
ActivityCompat.requestPermissions(this,
|
||||
needPermissions.toArray(new String[needPermissions.size()]), idx);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
final String[] permissions,
|
||||
final int[] grantResults) {
|
||||
final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
|
||||
if (f != null) {
|
||||
permissionRequestCallbacks.remove(requestCode);
|
||||
f.done(permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
public interface PermissionRequestCallback {
|
||||
void done(String[] permissions, int[] grantResults);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
|
||||
|
||||
final Preference wgQuickOnlyPrefs[] = {
|
||||
getPreferenceManager().findPreference("tools_installer"),
|
||||
getPreferenceManager().findPreference("restore_on_boot")
|
||||
};
|
||||
for (final Preference pref : wgQuickOnlyPrefs)
|
||||
pref.setVisible(false);
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
for (final Preference pref : wgQuickOnlyPrefs) {
|
||||
if (backend instanceof WgQuickBackend)
|
||||
pref.setVisible(true);
|
||||
else
|
||||
screen.removePreference(pref);
|
||||
}
|
||||
});
|
||||
|
||||
final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader");
|
||||
moduleInstaller.setVisible(false);
|
||||
if (Application.getModuleLoader().isModuleLoaded()) {
|
||||
screen.removePreference(moduleInstaller);
|
||||
} else {
|
||||
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> {
|
||||
if (e == null)
|
||||
moduleInstaller.setVisible(true);
|
||||
else
|
||||
screen.removePreference(moduleInstaller);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
|
||||
private static boolean lastDarkMode;
|
||||
@Nullable private static Resources lastResources;
|
||||
|
||||
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
|
||||
if (resources == lastResources && darkMode == lastDarkMode)
|
||||
return;
|
||||
|
||||
try {
|
||||
Field f;
|
||||
Object o = resources;
|
||||
try {
|
||||
f = o.getClass().getDeclaredField("mResourcesImpl");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
f = o.getClass().getDeclaredField("mDrawableCache");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
try {
|
||||
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
|
||||
} catch (final Exception ignored) {
|
||||
o.getClass().getMethod("clear").invoke(o);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to flush drawable cache", e);
|
||||
}
|
||||
|
||||
lastResources = resources;
|
||||
lastDarkMode = darkMode;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
if ("dark_theme".equals(key)) {
|
||||
final boolean darkMode = sharedPreferences.getBoolean(key, false);
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean(key, false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES :
|
||||
AppCompatDelegate.MODE_NIGHT_NO);
|
||||
invalidateDrawableCache(getResources(), darkMode);
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* Standalone activity for creating tunnels.
|
||||
*/
|
||||
|
||||
public class TunnelCreatorActivity extends BaseActivity {
|
||||
@Override
|
||||
@SuppressWarnings("UnnecessaryFullyQualifiedName")
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(android.R.id.content, new TunnelEditorFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.QuickTileService;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
public class TunnelToggleActivity extends AppCompatActivity {
|
||||
private static final String TAG = "WireGuard/" + TunnelToggleActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Tunnel tunnel = Application.getTunnelManager().getLastUsedTunnel();
|
||||
if (tunnel == null)
|
||||
return;
|
||||
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {
|
||||
TileService.requestListeningState(this, new ComponentName(this, QuickTileService.class));
|
||||
onToggleFinished(t);
|
||||
finishAffinity();
|
||||
});
|
||||
}
|
||||
|
||||
private void onToggleFinished(@Nullable final Throwable throwable) {
|
||||
if (throwable == null)
|
||||
return;
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
final String message = getString(R.string.toggle_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
*/
|
||||
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
|
||||
private final File localTemporaryDir;
|
||||
private final Context context;
|
||||
|
||||
public WgQuickBackend(final Context context) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
final List<String> output = new ArrayList<>();
|
||||
// Don't throw an exception here or nothing will show up in the UI.
|
||||
try {
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||
return Collections.emptySet();
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to enumerate running tunnels", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
final Collection<String> output = new ArrayList<>();
|
||||
try {
|
||||
if (Application.getRootShell().run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
|
||||
return stats;
|
||||
} catch (final Exception ignored) {
|
||||
return stats;
|
||||
}
|
||||
for (final String line : output) {
|
||||
final String[] parts = line.split("\\t");
|
||||
if (parts.length != 3)
|
||||
continue;
|
||||
try {
|
||||
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypePrettyName() {
|
||||
return context.getString(R.string.type_name_kernel_module);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() throws Exception {
|
||||
final List<String> output = new ArrayList<>();
|
||||
if (Application.getRootShell()
|
||||
.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
|
||||
throw new Exception(context.getString(R.string.module_version_error));
|
||||
return output.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
return originalState;
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
return getState(tunnel);
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
|
||||
Objects.requireNonNull(config, "Trying to set state with a null config");
|
||||
|
||||
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
|
||||
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
|
||||
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String command = String.format("wg-quick %s '%s'",
|
||||
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
|
||||
if (state == State.UP)
|
||||
command = "cat /sys/module/wireguard/version && " + command;
|
||||
final int result = Application.getRootShell().run(null, command);
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
if (result != 0)
|
||||
throw new Exception(context.getString(R.string.tunnel_config_error, result));
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.configStore;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
|
||||
*/
|
||||
|
||||
public final class FileConfigStore implements ConfigStore {
|
||||
private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public FileConfigStore(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config create(final String name, final Config config) throws IOException {
|
||||
Log.d(TAG, "Creating configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.createNewFile())
|
||||
throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
|
||||
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final String name) throws IOException {
|
||||
Log.d(TAG, "Deleting configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.delete())
|
||||
throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
return Stream.of(context.fileList())
|
||||
.filter(name -> name.endsWith(".conf"))
|
||||
.map(name -> name.substring(0, name.length() - ".conf".length()))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private File fileFor(final String name) {
|
||||
return new File(context.getFilesDir(), name + ".conf");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config load(final String name) throws BadConfigException, IOException {
|
||||
try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
|
||||
return Config.parse(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(final String name, final String replacement) throws IOException {
|
||||
Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
|
||||
final File file = fileFor(name);
|
||||
final File replacementFile = fileFor(replacement);
|
||||
if (!replacementFile.createNewFile())
|
||||
throw new IOException(context.getString(R.string.config_exists_error, replacement));
|
||||
if (!file.renameTo(replacementFile)) {
|
||||
if (!replacementFile.delete())
|
||||
Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
|
||||
throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config save(final String name, final Config config) throws IOException {
|
||||
Log.d(TAG, "Saving configuration for tunnel " + name);
|
||||
final File file = fileFor(name);
|
||||
if (!file.isFile())
|
||||
throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
|
||||
try (final FileOutputStream stream = new FileOutputStream(file, false)) {
|
||||
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding;
|
||||
import com.wireguard.android.model.ApplicationData;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.util.ObservableKeyedArrayList;
|
||||
import com.wireguard.android.util.ObservableKeyedList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import java9.util.Comparators;
|
||||
|
||||
public class AppListDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String KEY_EXCLUDED_APPS = "excludedApps";
|
||||
private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
|
||||
@Nullable private List<String> currentlyExcludedApps;
|
||||
|
||||
public static <T extends Fragment & AppExclusionListener>
|
||||
AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
|
||||
final AppListDialogFragment fragment = new AppListDialogFragment();
|
||||
fragment.setTargetFragment(target, 0);
|
||||
fragment.setArguments(extras);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PackageManager pm = activity.getPackageManager();
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
|
||||
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
|
||||
|
||||
final List<ApplicationData> appData = new ArrayList<>();
|
||||
for (ResolveInfo resolveInfo : resolveInfos) {
|
||||
String packageName = resolveInfo.activityInfo.packageName;
|
||||
appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
|
||||
}
|
||||
|
||||
Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
|
||||
return appData;
|
||||
}).whenComplete(((data, throwable) -> {
|
||||
if (data != null) {
|
||||
appData.clear();
|
||||
appData.addAll(data);
|
||||
} else {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
final String message = activity.getString(R.string.error_fetching_apps, error);
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
alertDialogBuilder.setTitle(R.string.excluded_applications);
|
||||
|
||||
final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
|
||||
binding.executePendingBindings();
|
||||
alertDialogBuilder.setView(binding.getRoot());
|
||||
|
||||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
alertDialogBuilder.setNeutralButton(R.string.deselect_all, (dialog, which) -> {
|
||||
});
|
||||
|
||||
binding.setFragment(this);
|
||||
binding.setAppData(appData);
|
||||
|
||||
loadData();
|
||||
|
||||
final AlertDialog dialog = alertDialogBuilder.create();
|
||||
dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
|
||||
for (final ApplicationData app : appData)
|
||||
app.setExcludedFromTunnel(false);
|
||||
}));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
void setExclusionsAndDismiss() {
|
||||
final List<String> excludedApps = new ArrayList<>();
|
||||
for (final ApplicationData data : appData) {
|
||||
if (data.isExcludedFromTunnel()) {
|
||||
excludedApps.add(data.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
public interface AppExclusionListener {
|
||||
void onExcludedAppsSelected(List<String> excludedApps);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.databinding.ViewDataBinding;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.BaseActivity;
|
||||
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
|
||||
import com.wireguard.android.backend.GoBackend;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
|
||||
/**
|
||||
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||
* attached to a {@code BaseActivity}.
|
||||
*/
|
||||
|
||||
public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
|
||||
private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
|
||||
private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
|
||||
@Nullable private BaseActivity activity;
|
||||
@Nullable private Tunnel pendingTunnel;
|
||||
@Nullable private Boolean pendingTunnelUp;
|
||||
|
||||
@Nullable
|
||||
protected Tunnel getSelectedTunnel() {
|
||||
return activity != null ? activity.getSelectedTunnel() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
|
||||
if (pendingTunnel != null && pendingTunnelUp != null)
|
||||
setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
|
||||
pendingTunnel = null;
|
||||
pendingTunnelUp = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof BaseActivity) {
|
||||
activity = (BaseActivity) context;
|
||||
activity.addOnSelectedTunnelChangedListener(this);
|
||||
} else {
|
||||
activity = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
if (activity != null)
|
||||
activity.removeOnSelectedTunnelChangedListener(this);
|
||||
activity = null;
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
protected void setSelectedTunnel(@Nullable final Tunnel tunnel) {
|
||||
if (activity != null)
|
||||
activity.setSelectedTunnel(tunnel);
|
||||
}
|
||||
|
||||
public void setTunnelState(final View view, final boolean checked) {
|
||||
final ViewDataBinding binding = DataBindingUtil.findBinding(view);
|
||||
final Tunnel tunnel;
|
||||
if (binding instanceof TunnelDetailFragmentBinding)
|
||||
tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
|
||||
else if (binding instanceof TunnelListItemBinding)
|
||||
tunnel = ((TunnelListItemBinding) binding).getItem();
|
||||
else
|
||||
return;
|
||||
if (tunnel == null)
|
||||
return;
|
||||
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
if (backend instanceof GoBackend) {
|
||||
final Intent intent = GoBackend.VpnService.prepare(view.getContext());
|
||||
if (intent != null) {
|
||||
pendingTunnel = tunnel;
|
||||
pendingTunnelUp = checked;
|
||||
startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setTunnelStateWithPermissionsResult(tunnel, checked);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) {
|
||||
tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
|
||||
if (throwable == null)
|
||||
return;
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
final int messageResId = checked ? R.string.error_up : R.string.error_down;
|
||||
final String message = getContext().getString(messageResId, error);
|
||||
final View view = getView();
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
|
||||
else
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, message, throwable);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ConfigNamingDialogFragment extends DialogFragment {
|
||||
private static final String KEY_CONFIG_TEXT = "config_text";
|
||||
|
||||
@Nullable private ConfigNamingDialogFragmentBinding binding;
|
||||
@Nullable private Config config;
|
||||
@Nullable private InputMethodManager imm;
|
||||
|
||||
public static ConfigNamingDialogFragment newInstance(final String configText) {
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString(KEY_CONFIG_TEXT, configText);
|
||||
final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
|
||||
fragment.setArguments(extras);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void createTunnelAndDismiss() {
|
||||
if (binding != null) {
|
||||
final String name = binding.tunnelNameText.getText().toString();
|
||||
|
||||
Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
|
||||
if (tunnel != null) {
|
||||
dismiss();
|
||||
} else {
|
||||
binding.tunnelNameTextLayout.setError(throwable.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
setKeyboardVisible(false);
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Bundle arguments = getArguments();
|
||||
final String configText = arguments.getString(KEY_CONFIG_TEXT);
|
||||
final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
|
||||
try {
|
||||
config = Config.parse(new ByteArrayInputStream(configBytes));
|
||||
} catch (final BadConfigException | IOException e) {
|
||||
throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(final Bundle savedInstanceState) {
|
||||
final Activity activity = Objects.requireNonNull(getActivity());
|
||||
|
||||
imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
|
||||
alertDialogBuilder.setTitle(R.string.import_from_qr_code);
|
||||
|
||||
binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
|
||||
binding.executePendingBindings();
|
||||
alertDialogBuilder.setView(binding.getRoot());
|
||||
|
||||
alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
|
||||
alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
|
||||
|
||||
return alertDialogBuilder.create();
|
||||
}
|
||||
|
||||
@Override public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final AlertDialog dialog = (AlertDialog) getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
|
||||
|
||||
setKeyboardVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setKeyboardVisible(final boolean visible) {
|
||||
Objects.requireNonNull(imm);
|
||||
|
||||
if (visible) {
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (binding != null) {
|
||||
imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelDetailPeerBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
*/
|
||||
|
||||
public class TunnelDetailFragment extends BaseFragment {
|
||||
@Nullable private TunnelDetailFragmentBinding binding;
|
||||
@Nullable private Timer timer;
|
||||
@Nullable private State lastState = State.TOGGLE;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tunnel_detail, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateStats();
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
if (binding == null)
|
||||
return;
|
||||
binding.setTunnel(newTunnel);
|
||||
if (newTunnel == null)
|
||||
binding.setConfig(null);
|
||||
else
|
||||
newTunnel.getConfigAsync().thenAccept(binding::setConfig);
|
||||
lastState = State.TOGGLE;
|
||||
updateStats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
private String formatBytes(final long bytes) {
|
||||
if (bytes < 1024)
|
||||
return getContext().getString(R.string.transfer_bytes, bytes);
|
||||
else if (bytes < 1024*1024)
|
||||
return getContext().getString(R.string.transfer_kibibytes, bytes/1024.0);
|
||||
else if (bytes < 1024*1024*1024)
|
||||
return getContext().getString(R.string.transfer_mibibytes, bytes/(1024.0*1024.0));
|
||||
else if (bytes < 1024*1024*1024*1024)
|
||||
return getContext().getString(R.string.transfer_gibibytes, bytes/(1024.0*1024.0*1024.0));
|
||||
return getContext().getString(R.string.transfer_tibibytes, bytes/(1024.0*1024.0*1024.0)/1024.0);
|
||||
}
|
||||
|
||||
private void updateStats() {
|
||||
if (binding == null || !isResumed())
|
||||
return;
|
||||
final Tunnel tunnel = binding.getTunnel();
|
||||
if (tunnel == null)
|
||||
return;
|
||||
final State state = tunnel.getState();
|
||||
if (state != State.UP && lastState == state)
|
||||
return;
|
||||
lastState = state;
|
||||
tunnel.getStatisticsAsync().whenComplete((statistics, throwable) -> {
|
||||
if (throwable != null) {
|
||||
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
|
||||
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
|
||||
if (peer == null)
|
||||
continue;
|
||||
peer.transferLabel.setVisibility(View.GONE);
|
||||
peer.transferText.setVisibility(View.GONE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < binding.peersLayout.getChildCount(); ++i) {
|
||||
final TunnelDetailPeerBinding peer = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i));
|
||||
if (peer == null)
|
||||
continue;
|
||||
final Key publicKey = peer.getItem().getPublicKey();
|
||||
final long rx = statistics.peerRx(publicKey);
|
||||
final long tx = statistics.peerTx(publicKey);
|
||||
if (rx == 0 && tx == 0) {
|
||||
peer.transferLabel.setVisibility(View.GONE);
|
||||
peer.transferText.setVisibility(View.GONE);
|
||||
continue;
|
||||
}
|
||||
peer.transferText.setText(getContext().getString(R.string.transfer_rx_tx, formatBytes(rx), formatBytes(tx)));
|
||||
peer.transferLabel.setVisibility(View.VISIBLE);
|
||||
peer.transferText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import androidx.databinding.ObservableList;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
|
||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.viewmodel.ConfigProxy;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Fragment for editing a WireGuard configuration.
|
||||
*/
|
||||
|
||||
public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
|
||||
private static final String KEY_LOCAL_CONFIG = "local_config";
|
||||
private static final String KEY_ORIGINAL_NAME = "original_name";
|
||||
private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
|
||||
|
||||
@Nullable private TunnelEditorFragmentBinding binding;
|
||||
@Nullable private Tunnel tunnel;
|
||||
|
||||
private void onConfigLoaded(final Config config) {
|
||||
if (binding != null) {
|
||||
binding.setConfig(new ConfigProxy(config));
|
||||
}
|
||||
}
|
||||
|
||||
private void onConfigSaved(final Tunnel savedTunnel,
|
||||
@Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.config_save_success, savedTunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
onFinished();
|
||||
} else {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
message = getString(R.string.config_save_error, savedTunnel.getName(), error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.config_editor, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExcludedAppsSelected(final List<String> excludedApps) {
|
||||
Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
|
||||
final ObservableList<String> excludedApplications =
|
||||
binding.getConfig().getInterface().getExcludedApplications();
|
||||
excludedApplications.clear();
|
||||
excludedApplications.addAll(excludedApps);
|
||||
}
|
||||
|
||||
private void onFinished() {
|
||||
// Hide the keyboard; it rarely goes away on its own.
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null) return;
|
||||
final View focusedView = activity.getCurrentFocus();
|
||||
if (focusedView != null) {
|
||||
final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
final InputMethodManager inputManager = (InputMethodManager) service;
|
||||
if (inputManager != null)
|
||||
inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
}
|
||||
// Tell the activity to finish itself or go back to the detail view.
|
||||
getActivity().runOnUiThread(() -> {
|
||||
// TODO(smaeul): Remove this hack when fixing the Config ViewModel
|
||||
// The selected tunnel has to actually change, but we have to remember this one.
|
||||
final Tunnel savedTunnel = tunnel;
|
||||
if (savedTunnel == getSelectedTunnel())
|
||||
setSelectedTunnel(null);
|
||||
setSelectedTunnel(savedTunnel);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_save:
|
||||
if (binding == null)
|
||||
return false;
|
||||
final Config newConfig;
|
||||
try {
|
||||
newConfig = binding.getConfig().resolve();
|
||||
} catch (final Exception e) {
|
||||
final String error = ErrorMessages.get(e);
|
||||
final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
|
||||
final String message = getString(R.string.config_save_error, tunnelName, error);
|
||||
Log.e(TAG, message, e);
|
||||
Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
if (tunnel == null) {
|
||||
Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
|
||||
final TunnelManager manager = Application.getTunnelManager();
|
||||
manager.create(binding.getName(), newConfig)
|
||||
.whenComplete(this::onTunnelCreated);
|
||||
} else if (!tunnel.getName().equals(binding.getName())) {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
|
||||
tunnel.setName(binding.getName())
|
||||
.whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
|
||||
} else {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel.getName());
|
||||
tunnel.setConfig(newConfig)
|
||||
.whenComplete((a, b) -> onConfigSaved(tunnel, b));
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
|
||||
final FragmentManager fragmentManager = getFragmentManager();
|
||||
if (fragmentManager != null && binding != null) {
|
||||
final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
|
||||
final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
|
||||
fragment.show(fragmentManager, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
if (binding != null)
|
||||
outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
|
||||
outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
|
||||
@Nullable final Tunnel newTunnel) {
|
||||
tunnel = newTunnel;
|
||||
if (binding == null)
|
||||
return;
|
||||
binding.setConfig(new ConfigProxy());
|
||||
if (tunnel != null) {
|
||||
binding.setName(tunnel.getName());
|
||||
tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
|
||||
} else {
|
||||
binding.setName("");
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
tunnel = newTunnel;
|
||||
message = getString(R.string.tunnel_create_success, tunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
|
||||
onFinished();
|
||||
} else {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
message = getString(R.string.tunnel_create_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
|
||||
@Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
|
||||
Log.d(TAG, message);
|
||||
// Now save the rest of configuration changes.
|
||||
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
|
||||
renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
|
||||
} else {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
message = getString(R.string.tunnel_rename_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
if (binding != null) {
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
} else {
|
||||
tunnel = getSelectedTunnel();
|
||||
final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
|
||||
final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
|
||||
if (tunnel != null && !tunnel.getName().equals(originalName))
|
||||
onSelectedTunnelChanged(null, tunnel);
|
||||
else
|
||||
binding.setConfig(config);
|
||||
}
|
||||
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity;
|
||||
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
|
||||
import com.wireguard.android.databinding.TunnelListFragmentBinding;
|
||||
import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.widget.MultiselectableRelativeLayout;
|
||||
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||
*/
|
||||
|
||||
public class TunnelListFragment extends BaseFragment {
|
||||
private static final int REQUEST_IMPORT = 1;
|
||||
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
|
||||
|
||||
private final ActionModeListener actionModeListener = new ActionModeListener();
|
||||
@Nullable private ActionMode actionMode;
|
||||
@Nullable private TunnelListFragmentBinding binding;
|
||||
|
||||
public boolean collapseActionMenu() {
|
||||
if (binding != null && binding.createMenu.isExpanded()) {
|
||||
binding.createMenu.collapse();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void importTunnel(@NonNull final String configText) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
// Config text is valid, now create the tunnel…
|
||||
final FragmentManager fragmentManager = getFragmentManager();
|
||||
if (fragmentManager != null)
|
||||
ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
|
||||
} catch (final BadConfigException | IOException e) {
|
||||
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void importTunnel(@Nullable final Uri uri) {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null || uri == null)
|
||||
return;
|
||||
final ContentResolver contentResolver = activity.getContentResolver();
|
||||
|
||||
final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
|
||||
final List<Throwable> throwables = new ArrayList<>();
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
final String[] columns = {OpenableColumns.DISPLAY_NAME};
|
||||
String name = null;
|
||||
try (Cursor cursor = contentResolver.query(uri, columns,
|
||||
null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
|
||||
name = cursor.getString(0);
|
||||
}
|
||||
if (name == null)
|
||||
name = Uri.decode(uri.getLastPathSegment());
|
||||
int idx = name.lastIndexOf('/');
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length() - 1)
|
||||
throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
|
||||
name = name.substring(idx + 1);
|
||||
}
|
||||
boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
|
||||
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
|
||||
name = name.substring(0, name.length() - ".conf".length());
|
||||
else if (!isZip)
|
||||
throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
|
||||
|
||||
if (isZip) {
|
||||
try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
if (entry.isDirectory())
|
||||
continue;
|
||||
name = entry.getName();
|
||||
idx = name.lastIndexOf('/');
|
||||
if (idx >= 0) {
|
||||
if (idx >= name.length() - 1)
|
||||
continue;
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
|
||||
name = name.substring(0, name.length() - ".conf".length());
|
||||
else
|
||||
continue;
|
||||
Config config = null;
|
||||
try {
|
||||
config = Config.parse(reader);
|
||||
} catch (Exception e) {
|
||||
throwables.add(e);
|
||||
}
|
||||
if (config != null)
|
||||
futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
futureTunnels.add(Application.getTunnelManager().create(name,
|
||||
Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
|
||||
}
|
||||
|
||||
if (futureTunnels.isEmpty()) {
|
||||
if (throwables.size() == 1)
|
||||
throw throwables.get(0);
|
||||
else if (throwables.isEmpty())
|
||||
throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
|
||||
}).whenComplete((future, exception) -> {
|
||||
if (exception != null) {
|
||||
onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
|
||||
} else {
|
||||
future.whenComplete((ignored1, ignored2) -> {
|
||||
final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
|
||||
for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
|
||||
Tunnel tunnel = null;
|
||||
try {
|
||||
tunnel = futureTunnel.getNow(null);
|
||||
} catch (final Exception e) {
|
||||
throwables.add(e);
|
||||
}
|
||||
if (tunnel != null)
|
||||
tunnels.add(tunnel);
|
||||
}
|
||||
onTunnelImportFinished(tunnels, throwables);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
|
||||
if (checkedItems != null) {
|
||||
for (final Integer i : checkedItems)
|
||||
actionModeListener.setItemChecked(i, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_IMPORT:
|
||||
if (resultCode == Activity.RESULT_OK && data != null)
|
||||
importTunnel(data.getData());
|
||||
return;
|
||||
case IntentIntegrator.REQUEST_CODE:
|
||||
final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
|
||||
if (result != null && result.getContents() != null) {
|
||||
importTunnel(result.getContents());
|
||||
}
|
||||
return;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
|
||||
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
|
||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQUEST_IMPORT);
|
||||
if (binding != null)
|
||||
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.qr_code_hint));
|
||||
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
|
||||
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
if (binding == null)
|
||||
return;
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
if (newTunnel != null)
|
||||
viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
|
||||
if (oldTunnel != null)
|
||||
viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
message = getResources().getQuantityString(R.plurals.delete_success, count, count);
|
||||
} else {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
}
|
||||
if (binding != null) {
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
|
||||
String message = null;
|
||||
|
||||
for (final Throwable throwable : throwables) {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
message = getString(R.string.import_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
}
|
||||
|
||||
if (tunnels.size() == 1 && throwables.isEmpty())
|
||||
message = getString(R.string.import_success, tunnels.get(0).getName());
|
||||
else if (tunnels.isEmpty() && throwables.size() == 1)
|
||||
/* Use the exception message from above. */ ;
|
||||
else if (throwables.isEmpty())
|
||||
message = getResources().getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size(), tunnels.size());
|
||||
else if (!throwables.isEmpty())
|
||||
message = getResources().getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size() + throwables.size(),
|
||||
tunnels.size(), tunnels.size() + throwables.size());
|
||||
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
|
||||
binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
|
||||
binding.setFragment(this);
|
||||
binding.getRoot().setOnClickListener(clicked -> {
|
||||
if (actionMode == null) {
|
||||
setSelectedTunnel(tunnel);
|
||||
} else {
|
||||
actionModeListener.toggleItemChecked(position);
|
||||
}
|
||||
});
|
||||
binding.getRoot().setOnLongClickListener(clicked -> {
|
||||
actionModeListener.toggleItemChecked(position);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (actionMode != null)
|
||||
((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
|
||||
else
|
||||
((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
|
||||
});
|
||||
}
|
||||
|
||||
private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
|
||||
return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
|
||||
}
|
||||
|
||||
private final class ActionModeListener implements ActionMode.Callback {
|
||||
private final Collection<Integer> checkedItems = new HashSet<>();
|
||||
|
||||
@Nullable private Resources resources;
|
||||
|
||||
public ArrayList<Integer> getCheckedItems() {
|
||||
return new ArrayList<>(checkedItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_action_delete:
|
||||
final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
final Collection<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);
|
||||
|
||||
});
|
||||
checkedItems.clear();
|
||||
mode.finish();
|
||||
return true;
|
||||
case R.id.menu_action_select_all:
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
for (int i = 0; i < tunnels.size(); ++i) {
|
||||
setItemChecked(i, true);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
|
||||
actionMode = mode;
|
||||
if (getActivity() != null) {
|
||||
resources = getActivity().getResources();
|
||||
}
|
||||
mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
|
||||
binding.tunnelList.getAdapter().notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode mode) {
|
||||
actionMode = null;
|
||||
resources = null;
|
||||
checkedItems.clear();
|
||||
binding.tunnelList.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
|
||||
updateTitle(mode);
|
||||
return false;
|
||||
}
|
||||
|
||||
void setItemChecked(final int position, final boolean checked) {
|
||||
if (checked) {
|
||||
checkedItems.add(position);
|
||||
} else {
|
||||
checkedItems.remove(position);
|
||||
}
|
||||
|
||||
final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
|
||||
|
||||
if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
|
||||
((AppCompatActivity) getActivity()).startSupportActionMode(this);
|
||||
} else if (actionMode != null && checkedItems.isEmpty()) {
|
||||
actionMode.finish();
|
||||
}
|
||||
|
||||
if (adapter != null)
|
||||
adapter.notifyItemChanged(position);
|
||||
|
||||
updateTitle(actionMode);
|
||||
}
|
||||
|
||||
void toggleItemChecked(final int position) {
|
||||
setItemChecked(position, !checkedItems.contains(position));
|
||||
}
|
||||
|
||||
private void updateTitle(@Nullable final ActionMode mode) {
|
||||
if (mode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int count = checkedItems.size();
|
||||
if (count == 0) {
|
||||
mode.setTitle("");
|
||||
} else {
|
||||
mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.model;
|
||||
|
||||
import androidx.databinding.BaseObservable;
|
||||
import androidx.databinding.Bindable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.util.Keyed;
|
||||
|
||||
public class ApplicationData extends BaseObservable implements Keyed<String> {
|
||||
private final Drawable icon;
|
||||
private final String name;
|
||||
private final String packageName;
|
||||
private boolean excludedFromTunnel;
|
||||
|
||||
public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
|
||||
this.icon = icon;
|
||||
this.name = name;
|
||||
this.packageName = packageName;
|
||||
this.excludedFromTunnel = excludedFromTunnel;
|
||||
}
|
||||
|
||||
public Drawable getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isExcludedFromTunnel() {
|
||||
return excludedFromTunnel;
|
||||
}
|
||||
|
||||
public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
|
||||
this.excludedFromTunnel = excludedFromTunnel;
|
||||
notifyPropertyChanged(BR.excludedFromTunnel);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.DownloadsFileSaver;
|
||||
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.util.FragmentUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously exports logs.
|
||||
*/
|
||||
|
||||
public class LogExporterPreference extends Preference {
|
||||
private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
|
||||
|
||||
@Nullable private String exportedFilePath;
|
||||
|
||||
public LogExporterPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void exportLog() {
|
||||
Application.getAsyncWorker().supplyAsync(() -> {
|
||||
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true);
|
||||
try {
|
||||
final Process process = Runtime.getRuntime().exec(new String[]{
|
||||
"logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"});
|
||||
try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())))
|
||||
{
|
||||
String line;
|
||||
while ((line = stdout.readLine()) != null) {
|
||||
outputFile.getOutputStream().write(line.getBytes());
|
||||
outputFile.getOutputStream().write('\n');
|
||||
}
|
||||
outputFile.getOutputStream().close();
|
||||
stdout.close();
|
||||
if (process.waitFor() != 0) {
|
||||
final StringBuilder errors = new StringBuilder();
|
||||
errors.append(R.string.logcat_error);
|
||||
while ((line = stderr.readLine()) != null)
|
||||
errors.append(line);
|
||||
throw new Exception(errors.toString());
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
outputFile.delete();
|
||||
throw e;
|
||||
}
|
||||
return outputFile.getFileName();
|
||||
}).whenComplete(this::exportLogComplete);
|
||||
}
|
||||
|
||||
private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
final String message = getContext().getString(R.string.log_export_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show();
|
||||
setEnabled(true);
|
||||
} else {
|
||||
exportedFilePath = filePath;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return exportedFilePath == null ?
|
||||
getContext().getString(R.string.log_export_summary) :
|
||||
getContext().getString(R.string.log_export_success, exportedFilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.log_export_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
FragmentUtils.getPrefActivity(this).ensurePermissions(
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
(permissions, granted) -> {
|
||||
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
setEnabled(false);
|
||||
exportLog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.system.OsConstants;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.ModuleLoader;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
public class ModuleDownloaderPreference extends Preference {
|
||||
private State state = State.INITIAL;
|
||||
|
||||
public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return getContext().getString(state.messageResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.module_installer_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
setState(State.WORKING);
|
||||
Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult);
|
||||
}
|
||||
|
||||
private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
setState(State.FAILURE);
|
||||
Toast.makeText(getContext(), throwable.getMessage(), Toast.LENGTH_LONG).show();
|
||||
} else if (result == OsConstants.ENOENT)
|
||||
setState(State.NOTFOUND);
|
||||
else if (result == OsConstants.EXIT_SUCCESS) {
|
||||
setState(State.SUCCESS);
|
||||
Application.getAsyncWorker().runAsync(() -> {
|
||||
Thread.sleep(1000 * 5);
|
||||
Intent i = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
|
||||
if (i == null)
|
||||
return;
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
Application.get().startActivity(i);
|
||||
System.exit(0);
|
||||
});
|
||||
} else
|
||||
setState(State.FAILURE);
|
||||
}
|
||||
|
||||
private void setState(final State state) {
|
||||
if (this.state == state)
|
||||
return;
|
||||
this.state = state;
|
||||
if (isEnabled() != state.shouldEnableView)
|
||||
setEnabled(state.shouldEnableView);
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
private enum State {
|
||||
INITIAL(R.string.module_installer_initial, true),
|
||||
FAILURE(R.string.module_installer_error, true),
|
||||
WORKING(R.string.module_installer_working, false),
|
||||
SUCCESS(R.string.module_installer_success, false),
|
||||
NOTFOUND(R.string.module_installer_not_found, false);
|
||||
|
||||
private final int messageResourceId;
|
||||
private final boolean shouldEnableView;
|
||||
|
||||
State(final int messageResourceId, final boolean shouldEnableView) {
|
||||
this.messageResourceId = messageResourceId;
|
||||
this.shouldEnableView = shouldEnableView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
|
||||
* result as the preference summary.
|
||||
*/
|
||||
|
||||
public class ToolsInstallerPreference extends Preference {
|
||||
private State state = State.INITIAL;
|
||||
|
||||
public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return getContext().getString(state.messageResourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.tools_installer_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttached() {
|
||||
super.onAttached();
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
|
||||
}
|
||||
|
||||
private void onCheckResult(final int state, @Nullable final Throwable throwable) {
|
||||
if (throwable != null || state == ToolsInstaller.ERROR)
|
||||
setState(State.INITIAL);
|
||||
else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
|
||||
setState(State.ALREADY);
|
||||
else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
|
||||
setState(State.INITIAL_MAGISK);
|
||||
else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
|
||||
setState(State.INITIAL_SYSTEM);
|
||||
else
|
||||
setState(State.INITIAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
setState(State.WORKING);
|
||||
Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
|
||||
}
|
||||
|
||||
private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
|
||||
if (throwable != null)
|
||||
setState(State.FAILURE);
|
||||
else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
|
||||
setState(State.SUCCESS_MAGISK);
|
||||
else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
|
||||
setState(State.SUCCESS_SYSTEM);
|
||||
else
|
||||
setState(State.FAILURE);
|
||||
}
|
||||
|
||||
private void setState(final State state) {
|
||||
if (this.state == state)
|
||||
return;
|
||||
this.state = state;
|
||||
if (isEnabled() != state.shouldEnableView)
|
||||
setEnabled(state.shouldEnableView);
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
private enum State {
|
||||
INITIAL(R.string.tools_installer_initial, true),
|
||||
ALREADY(R.string.tools_installer_already, false),
|
||||
FAILURE(R.string.tools_installer_failure, true),
|
||||
WORKING(R.string.tools_installer_working, false),
|
||||
INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
|
||||
SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
|
||||
INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
|
||||
SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
|
||||
|
||||
private final int messageResourceId;
|
||||
private final boolean shouldEnableView;
|
||||
|
||||
State(final int messageResourceId, final boolean shouldEnableView) {
|
||||
this.messageResourceId = messageResourceId;
|
||||
this.shouldEnableView = shouldEnableView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class VersionPreference extends Preference {
|
||||
@Nullable private String versionSummary;
|
||||
|
||||
public VersionPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
|
||||
Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
|
||||
versionSummary = exception == null
|
||||
? getContext().getString(R.string.version_summary, backend.getTypePrettyName(), version)
|
||||
: getContext().getString(R.string.version_summary_unknown, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
|
||||
notifyChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return versionSummary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://www.wireguard.com/"));
|
||||
try {
|
||||
getContext().startActivity(intent);
|
||||
} catch (final ActivityNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.util.DownloadsFileSaver;
|
||||
import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.util.FragmentUtils;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Preference implementing a button that asynchronously exports config zips.
|
||||
*/
|
||||
|
||||
public class ZipExporterPreference extends Preference {
|
||||
private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
|
||||
|
||||
@Nullable private String exportedFilePath;
|
||||
|
||||
public ZipExporterPreference(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void exportZip() {
|
||||
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());
|
||||
if (futureConfigs.isEmpty()) {
|
||||
exportZipComplete(null, new IllegalArgumentException(
|
||||
getContext().getString(R.string.no_tunnels_error)));
|
||||
return;
|
||||
}
|
||||
CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
|
||||
.whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true);
|
||||
try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) {
|
||||
for (int i = 0; i < futureConfigs.size(); ++i) {
|
||||
zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
|
||||
zip.write(futureConfigs.get(i).getNow(null).
|
||||
toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
zip.closeEntry();
|
||||
} catch (final Exception e) {
|
||||
outputFile.delete();
|
||||
throw e;
|
||||
}
|
||||
return outputFile.getFileName();
|
||||
}).whenComplete(this::exportZipComplete));
|
||||
}
|
||||
|
||||
private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
final String error = ErrorMessages.get(throwable);
|
||||
final String message = getContext().getString(R.string.zip_export_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
Snackbar.make(
|
||||
FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show();
|
||||
setEnabled(true);
|
||||
} else {
|
||||
exportedFilePath = filePath;
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return exportedFilePath == null ?
|
||||
getContext().getString(R.string.zip_export_summary) :
|
||||
getContext().getString(R.string.zip_export_success, exportedFilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return getContext().getString(R.string.zip_export_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
FragmentUtils.getPrefActivity(this).ensurePermissions(
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
(permissions, granted) -> {
|
||||
if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
setEnabled(false);
|
||||
exportZip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.viewmodel;
|
||||
|
||||
import androidx.databinding.ObservableArrayList;
|
||||
import androidx.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.Peer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class ConfigProxy implements Parcelable {
|
||||
public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
|
||||
|
||||
private final InterfaceProxy interfaze;
|
||||
private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
|
||||
|
||||
private ConfigProxy(final Parcel in) {
|
||||
interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
|
||||
in.readTypedList(peers, PeerProxy.CREATOR);
|
||||
for (final PeerProxy proxy : peers)
|
||||
proxy.bind(this);
|
||||
}
|
||||
|
||||
public ConfigProxy(final Config other) {
|
||||
interfaze = new InterfaceProxy(other.getInterface());
|
||||
for (final Peer peer : other.getPeers()) {
|
||||
final PeerProxy proxy = new PeerProxy(peer);
|
||||
peers.add(proxy);
|
||||
proxy.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigProxy() {
|
||||
interfaze = new InterfaceProxy();
|
||||
}
|
||||
|
||||
public PeerProxy addPeer() {
|
||||
final PeerProxy proxy = new PeerProxy();
|
||||
peers.add(proxy);
|
||||
proxy.bind(this);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public InterfaceProxy getInterface() {
|
||||
return interfaze;
|
||||
}
|
||||
|
||||
public ObservableList<PeerProxy> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public Config resolve() throws BadConfigException {
|
||||
final Collection<Peer> resolvedPeers = new ArrayList<>();
|
||||
for (final PeerProxy proxy : peers)
|
||||
resolvedPeers.add(proxy.resolve());
|
||||
return new Config.Builder()
|
||||
.setInterface(interfaze.resolve())
|
||||
.addPeers(resolvedPeers)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeParcelable(interfaze, flags);
|
||||
dest.writeTypedList(peers);
|
||||
}
|
||||
|
||||
private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
|
||||
@Override
|
||||
public ConfigProxy createFromParcel(final Parcel in) {
|
||||
return new ConfigProxy(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigProxy[] newArray(final int size) {
|
||||
return new ConfigProxy[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.viewmodel;
|
||||
|
||||
import androidx.databinding.BaseObservable;
|
||||
import androidx.databinding.Bindable;
|
||||
import androidx.databinding.ObservableArrayList;
|
||||
import androidx.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.config.Attribute;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Interface;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.crypto.KeyPair;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
public class InterfaceProxy extends BaseObservable implements Parcelable {
|
||||
public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
|
||||
|
||||
private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
|
||||
private String addresses;
|
||||
private String dnsServers;
|
||||
private String listenPort;
|
||||
private String mtu;
|
||||
private String privateKey;
|
||||
private String publicKey;
|
||||
|
||||
private InterfaceProxy(final Parcel in) {
|
||||
addresses = in.readString();
|
||||
dnsServers = in.readString();
|
||||
in.readStringList(excludedApplications);
|
||||
listenPort = in.readString();
|
||||
mtu = in.readString();
|
||||
privateKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
}
|
||||
|
||||
public InterfaceProxy(final Interface other) {
|
||||
addresses = Attribute.join(other.getAddresses());
|
||||
final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
|
||||
.map(InetAddress::getHostAddress)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
dnsServers = Attribute.join(dnsServerStrings);
|
||||
excludedApplications.addAll(other.getExcludedApplications());
|
||||
listenPort = other.getListenPort().map(String::valueOf).orElse("");
|
||||
mtu = other.getMtu().map(String::valueOf).orElse("");
|
||||
final KeyPair keyPair = other.getKeyPair();
|
||||
privateKey = keyPair.getPrivateKey().toBase64();
|
||||
publicKey = keyPair.getPublicKey().toBase64();
|
||||
}
|
||||
|
||||
public InterfaceProxy() {
|
||||
addresses = "";
|
||||
dnsServers = "";
|
||||
listenPort = "";
|
||||
mtu = "";
|
||||
privateKey = "";
|
||||
publicKey = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void generateKeyPair() {
|
||||
final KeyPair keyPair = new KeyPair();
|
||||
privateKey = keyPair.getPrivateKey().toBase64();
|
||||
publicKey = keyPair.getPublicKey().toBase64();
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getAddresses() {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getDnsServers() {
|
||||
return dnsServers;
|
||||
}
|
||||
|
||||
public ObservableList<String> getExcludedApplications() {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public Interface resolve() throws BadConfigException {
|
||||
final Interface.Builder builder = new Interface.Builder();
|
||||
if (!addresses.isEmpty())
|
||||
builder.parseAddresses(addresses);
|
||||
if (!dnsServers.isEmpty())
|
||||
builder.parseDnsServers(dnsServers);
|
||||
if (!excludedApplications.isEmpty())
|
||||
builder.excludeApplications(excludedApplications);
|
||||
if (!listenPort.isEmpty())
|
||||
builder.parseListenPort(listenPort);
|
||||
if (!mtu.isEmpty())
|
||||
builder.parseMtu(mtu);
|
||||
if (!privateKey.isEmpty())
|
||||
builder.parsePrivateKey(privateKey);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public void setAddresses(final String addresses) {
|
||||
this.addresses = addresses;
|
||||
notifyPropertyChanged(BR.addresses);
|
||||
}
|
||||
|
||||
public void setDnsServers(final String dnsServers) {
|
||||
this.dnsServers = dnsServers;
|
||||
notifyPropertyChanged(BR.dnsServers);
|
||||
}
|
||||
|
||||
public void setListenPort(final String listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
notifyPropertyChanged(BR.listenPort);
|
||||
}
|
||||
|
||||
public void setMtu(final String mtu) {
|
||||
this.mtu = mtu;
|
||||
notifyPropertyChanged(BR.mtu);
|
||||
}
|
||||
|
||||
public void setPrivateKey(final String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
try {
|
||||
publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
|
||||
} catch (final KeyFormatException ignored) {
|
||||
publicKey = "";
|
||||
}
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(addresses);
|
||||
dest.writeString(dnsServers);
|
||||
dest.writeStringList(excludedApplications);
|
||||
dest.writeString(listenPort);
|
||||
dest.writeString(mtu);
|
||||
dest.writeString(privateKey);
|
||||
dest.writeString(publicKey);
|
||||
}
|
||||
|
||||
private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
|
||||
@Override
|
||||
public InterfaceProxy createFromParcel(final Parcel in) {
|
||||
return new InterfaceProxy(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InterfaceProxy[] newArray(final int size) {
|
||||
return new InterfaceProxy[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.viewmodel;
|
||||
|
||||
import androidx.databinding.BaseObservable;
|
||||
import androidx.databinding.Bindable;
|
||||
import androidx.databinding.Observable;
|
||||
import androidx.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.config.Attribute;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.InetEndpoint;
|
||||
import com.wireguard.config.Peer;
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.Lists;
|
||||
import java9.util.Sets;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
public class PeerProxy extends BaseObservable implements Parcelable {
|
||||
public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
|
||||
private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
|
||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
));
|
||||
private static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
|
||||
|
||||
private final List<String> dnsRoutes = new ArrayList<>();
|
||||
private String allowedIps;
|
||||
private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
|
||||
private String endpoint;
|
||||
@Nullable private InterfaceDnsListener interfaceDnsListener;
|
||||
@Nullable private ConfigProxy owner;
|
||||
@Nullable private PeerListListener peerListListener;
|
||||
private String persistentKeepalive;
|
||||
private String preSharedKey;
|
||||
private String publicKey;
|
||||
private int totalPeers;
|
||||
|
||||
private PeerProxy(final Parcel in) {
|
||||
allowedIps = in.readString();
|
||||
endpoint = in.readString();
|
||||
persistentKeepalive = in.readString();
|
||||
preSharedKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
}
|
||||
|
||||
public PeerProxy(final Peer other) {
|
||||
allowedIps = Attribute.join(other.getAllowedIps());
|
||||
endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
|
||||
persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
|
||||
preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
|
||||
publicKey = other.getPublicKey().toBase64();
|
||||
}
|
||||
|
||||
public PeerProxy() {
|
||||
allowedIps = "";
|
||||
endpoint = "";
|
||||
persistentKeepalive = "";
|
||||
preSharedKey = "";
|
||||
publicKey = "";
|
||||
}
|
||||
|
||||
public void bind(final ConfigProxy owner) {
|
||||
final InterfaceProxy interfaze = owner.getInterface();
|
||||
final ObservableList<PeerProxy> peers = owner.getPeers();
|
||||
if (interfaceDnsListener == null)
|
||||
interfaceDnsListener = new InterfaceDnsListener(this);
|
||||
interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
|
||||
setInterfaceDns(interfaze.getDnsServers());
|
||||
if (peerListListener == null)
|
||||
peerListListener = new PeerListListener(this);
|
||||
peers.addOnListChangedCallback(peerListListener);
|
||||
setTotalPeers(peers.size());
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
private void calculateAllowedIpsState() {
|
||||
final AllowedIpsState newState;
|
||||
if (totalPeers == 1) {
|
||||
// String comparison works because we only care if allowedIps is a superset of one of
|
||||
// the above sets of (valid) *networks*. We are not checking for a superset based on
|
||||
// the individual addresses in each set.
|
||||
final Collection<String> networkStrings = getAllowedIpsSet();
|
||||
// If allowedIps contains both the wildcard and the public networks, then private
|
||||
// networks aren't excluded!
|
||||
if (networkStrings.containsAll(IPV4_WILDCARD))
|
||||
newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
|
||||
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
|
||||
newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
|
||||
else
|
||||
newState = AllowedIpsState.OTHER;
|
||||
} else {
|
||||
newState = AllowedIpsState.INVALID;
|
||||
}
|
||||
if (newState != allowedIpsState) {
|
||||
allowedIpsState = newState;
|
||||
notifyPropertyChanged(BR.ableToExcludePrivateIps);
|
||||
notifyPropertyChanged(BR.excludingPrivateIps);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getAllowedIps() {
|
||||
return allowedIps;
|
||||
}
|
||||
|
||||
private Set<String> getAllowedIpsSet() {
|
||||
return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isAbleToExcludePrivateIps() {
|
||||
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|
||||
|| allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean isExcludingPrivateIps() {
|
||||
return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
|
||||
}
|
||||
|
||||
public Peer resolve() throws BadConfigException {
|
||||
final Peer.Builder builder = new Peer.Builder();
|
||||
if (!allowedIps.isEmpty())
|
||||
builder.parseAllowedIPs(allowedIps);
|
||||
if (!endpoint.isEmpty())
|
||||
builder.parseEndpoint(endpoint);
|
||||
if (!persistentKeepalive.isEmpty())
|
||||
builder.parsePersistentKeepalive(persistentKeepalive);
|
||||
if (!preSharedKey.isEmpty())
|
||||
builder.parsePreSharedKey(preSharedKey);
|
||||
if (!publicKey.isEmpty())
|
||||
builder.parsePublicKey(publicKey);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public void setAllowedIps(final String allowedIps) {
|
||||
this.allowedIps = allowedIps;
|
||||
notifyPropertyChanged(BR.allowedIps);
|
||||
calculateAllowedIpsState();
|
||||
}
|
||||
|
||||
public void setEndpoint(final String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
notifyPropertyChanged(BR.endpoint);
|
||||
}
|
||||
|
||||
public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
|
||||
if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
|
||||
return;
|
||||
final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
|
||||
final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
|
||||
final Collection<String> input = getAllowedIpsSet();
|
||||
final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
|
||||
final Collection<String> output = new LinkedHashSet<>(outputSize);
|
||||
boolean replaced = false;
|
||||
// Replace the first instance of the wildcard with the public network list, or vice versa.
|
||||
for (final String network : input) {
|
||||
if (oldNetworks.contains(network)) {
|
||||
if (!replaced) {
|
||||
for (final String replacement : newNetworks)
|
||||
if (!output.contains(replacement))
|
||||
output.add(replacement);
|
||||
replaced = true;
|
||||
}
|
||||
} else if (!output.contains(network)) {
|
||||
output.add(network);
|
||||
}
|
||||
}
|
||||
// DNS servers only need to handled specially when we're excluding private IPs.
|
||||
if (excludingPrivateIps)
|
||||
output.addAll(dnsRoutes);
|
||||
else
|
||||
output.removeAll(dnsRoutes);
|
||||
allowedIps = Attribute.join(output);
|
||||
allowedIpsState = excludingPrivateIps ?
|
||||
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
|
||||
notifyPropertyChanged(BR.allowedIps);
|
||||
notifyPropertyChanged(BR.excludingPrivateIps);
|
||||
}
|
||||
|
||||
private void setInterfaceDns(final CharSequence dnsServers) {
|
||||
final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
|
||||
.filter(server -> !server.contains(":"))
|
||||
.map(server -> server + "/32")
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
|
||||
final Collection<String> input = getAllowedIpsSet();
|
||||
final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
|
||||
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
|
||||
for (final String network : input)
|
||||
if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
|
||||
output.add(network);
|
||||
// Since output is a Set, this does the Right Thing™ (it does not duplicate networks).
|
||||
output.addAll(newDnsRoutes);
|
||||
// None of the public networks are /32s, so this cannot change the AllowedIPs state.
|
||||
allowedIps = Attribute.join(output);
|
||||
notifyPropertyChanged(BR.allowedIps);
|
||||
}
|
||||
dnsRoutes.clear();
|
||||
dnsRoutes.addAll(newDnsRoutes);
|
||||
}
|
||||
|
||||
public void setPersistentKeepalive(final String persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
notifyPropertyChanged(BR.persistentKeepalive);
|
||||
}
|
||||
|
||||
public void setPreSharedKey(final String preSharedKey) {
|
||||
this.preSharedKey = preSharedKey;
|
||||
notifyPropertyChanged(BR.preSharedKey);
|
||||
}
|
||||
|
||||
public void setPublicKey(final String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
private void setTotalPeers(final int totalPeers) {
|
||||
if (this.totalPeers == totalPeers)
|
||||
return;
|
||||
this.totalPeers = totalPeers;
|
||||
calculateAllowedIpsState();
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
if (owner == null)
|
||||
return;
|
||||
final InterfaceProxy interfaze = owner.getInterface();
|
||||
final ObservableList<PeerProxy> peers = owner.getPeers();
|
||||
if (interfaceDnsListener != null)
|
||||
interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
|
||||
if (peerListListener != null)
|
||||
peers.removeOnListChangedCallback(peerListListener);
|
||||
peers.remove(this);
|
||||
setInterfaceDns("");
|
||||
setTotalPeers(0);
|
||||
owner = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(allowedIps);
|
||||
dest.writeString(endpoint);
|
||||
dest.writeString(persistentKeepalive);
|
||||
dest.writeString(preSharedKey);
|
||||
dest.writeString(publicKey);
|
||||
}
|
||||
|
||||
private enum AllowedIpsState {
|
||||
CONTAINS_IPV4_PUBLIC_NETWORKS,
|
||||
CONTAINS_IPV4_WILDCARD,
|
||||
INVALID,
|
||||
OTHER
|
||||
}
|
||||
|
||||
private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
|
||||
private final WeakReference<PeerProxy> weakPeerProxy;
|
||||
|
||||
private InterfaceDnsListener(final PeerProxy peerProxy) {
|
||||
weakPeerProxy = new WeakReference<>(peerProxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertyChanged(final Observable sender, final int propertyId) {
|
||||
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
|
||||
if (peerProxy == null) {
|
||||
sender.removeOnPropertyChangedCallback(this);
|
||||
return;
|
||||
}
|
||||
// This shouldn't be possible, but try to avoid a ClassCastException anyway.
|
||||
if (!(sender instanceof InterfaceProxy))
|
||||
return;
|
||||
if (!(propertyId == BR._all || propertyId == BR.dnsServers))
|
||||
return;
|
||||
peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PeerListListener
|
||||
extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
|
||||
private final WeakReference<PeerProxy> weakPeerProxy;
|
||||
|
||||
private PeerListListener(final PeerProxy peerProxy) {
|
||||
weakPeerProxy = new WeakReference<>(peerProxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(final ObservableList<PeerProxy> sender) {
|
||||
@Nullable final PeerProxy peerProxy = weakPeerProxy.get();
|
||||
if (peerProxy == null) {
|
||||
sender.removeOnListChangedCallback(this);
|
||||
return;
|
||||
}
|
||||
peerProxy.setTotalPeers(sender.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
|
||||
final int positionStart, final int itemCount) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
|
||||
final int positionStart, final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
|
||||
final int fromPosition, final int toPosition,
|
||||
final int itemCount) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
|
||||
final int positionStart, final int itemCount) {
|
||||
onChanged(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
|
||||
@Override
|
||||
public PeerProxy createFromParcel(final Parcel in) {
|
||||
return new PeerProxy(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerProxy[] newArray(final int size) {
|
||||
return new PeerProxy[size];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.crypto.Key;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard private/public keys encoded with base64.
|
||||
*/
|
||||
|
||||
public class KeyInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new KeyInputFilter();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to the base64 character set.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
|
||||
(dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
|
||||
dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. 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 {
|
||||
private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
|
||||
private boolean multiselected;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
*/
|
||||
|
||||
public class NameInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new NameInputFilter();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 The Android Open Source Project
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Path.Direction;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.util.FloatProperty;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
public class SlashDrawable extends Drawable {
|
||||
|
||||
private static final float CENTER_X = 10.65f;
|
||||
private static final float CENTER_Y = 11.869239f;
|
||||
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
|
||||
// Draw the slash washington-monument style; rotate to no-u-turn style
|
||||
private static final float DEFAULT_ROTATION = -45f;
|
||||
private static final long QS_ANIM_LENGTH = 350;
|
||||
private static final float SCALE = 24f;
|
||||
private static final float SLASH_HEIGHT = 28f;
|
||||
// These values are derived in un-rotated (vertical) orientation
|
||||
private static final float SLASH_WIDTH = 1.8384776f;
|
||||
// Bottom is derived during animation
|
||||
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
|
||||
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
|
||||
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
|
||||
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
|
||||
@Override
|
||||
public Float get(final SlashDrawable object) {
|
||||
return object.mCurrentSlashLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(final SlashDrawable object, final float value) {
|
||||
object.mCurrentSlashLength = value;
|
||||
}
|
||||
};
|
||||
private final Drawable mDrawable;
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path mPath = new Path();
|
||||
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
|
||||
private boolean mAnimationEnabled = true;
|
||||
// Animate this value on change
|
||||
private float mCurrentSlashLength;
|
||||
private float mRotation;
|
||||
private boolean mSlashed;
|
||||
|
||||
public SlashDrawable(final Drawable d) {
|
||||
mDrawable = d;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
final Matrix m = new Matrix();
|
||||
final int width = getBounds().width();
|
||||
final int height = getBounds().height();
|
||||
final float radiusX = scale(CORNER_RADIUS, width);
|
||||
final float radiusY = scale(CORNER_RADIUS, height);
|
||||
updateRect(
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
);
|
||||
|
||||
mPath.reset();
|
||||
// Draw the slash vertically
|
||||
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
|
||||
// Rotate -45 + desired rotation
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
canvas.drawPath(mPath, mPaint);
|
||||
|
||||
// Rotate back to vertical
|
||||
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
// Draw another rect right next to the first, for clipping
|
||||
m.setTranslate(mSlashRect.width(), 0);
|
||||
mPath.transform(m);
|
||||
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
|
||||
else
|
||||
canvas.clipOutPath(mPath);
|
||||
|
||||
mDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(final Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
mDrawable.setBounds(bounds);
|
||||
}
|
||||
|
||||
private float scale(final float frac, final int width) {
|
||||
return frac * width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
|
||||
mDrawable.setAlpha(alpha);
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
public void setAnimationEnabled(final boolean enabled) {
|
||||
mAnimationEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
|
||||
mDrawable.setColorFilter(colorFilter);
|
||||
mPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
private void setDrawableTintList(@Nullable final ColorStateList tint) {
|
||||
mDrawable.setTintList(tint);
|
||||
}
|
||||
|
||||
public void setRotation(final float rotation) {
|
||||
if (mRotation == rotation)
|
||||
return;
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setSlashed(final boolean slashed) {
|
||||
if (mSlashed == slashed) return;
|
||||
|
||||
mSlashed = slashed;
|
||||
|
||||
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
|
||||
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
|
||||
|
||||
if (mAnimationEnabled) {
|
||||
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
|
||||
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
|
||||
anim.setDuration(QS_ANIM_LENGTH);
|
||||
anim.start();
|
||||
} else {
|
||||
mCurrentSlashLength = end;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTint(@ColorInt final int tintColor) {
|
||||
super.setTint(tintColor);
|
||||
mDrawable.setTint(tintColor);
|
||||
mPaint.setColor(tintColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintList(@Nullable final ColorStateList tint) {
|
||||
super.setTintList(tint);
|
||||
setDrawableTintList(tint);
|
||||
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintMode(final Mode tintMode) {
|
||||
super.setTintMode(tintMode);
|
||||
mDrawable.setTintMode(tintMode);
|
||||
}
|
||||
|
||||
private void updateRect(final float left, final float top, final float right, final float bottom) {
|
||||
mSlashRect.left = left;
|
||||
mSlashRect.top = top;
|
||||
mSlashRect.right = right;
|
||||
mSlashRect.bottom = bottom;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 The Android Open Source Project
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Switch;
|
||||
|
||||
public class ToggleSwitch extends Switch {
|
||||
private boolean isRestoringState;
|
||||
@Nullable private OnBeforeCheckedChangeListener listener;
|
||||
|
||||
public ToggleSwitch(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
|
||||
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
isRestoringState = true;
|
||||
super.onRestoreInstanceState(state);
|
||||
isRestoringState = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
if (checked == isChecked())
|
||||
return;
|
||||
if (isRestoringState || listener == null) {
|
||||
super.setChecked(checked);
|
||||
return;
|
||||
}
|
||||
setEnabled(false);
|
||||
listener.onBeforeCheckedChanged(this, checked);
|
||||
}
|
||||
|
||||
public void setCheckedInternal(final boolean checked) {
|
||||
super.setChecked(checked);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. 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 androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
|
||||
|
||||
private static final long ANIMATION_DURATION = 250;
|
||||
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
|
||||
|
||||
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
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 layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
@@ -1,629 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. 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.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
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 androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class FloatingActionsMenu extends ViewGroup {
|
||||
public static final int EXPAND_DOWN = 1;
|
||||
public static final int EXPAND_LEFT = 2;
|
||||
public static final int EXPAND_RIGHT = 3;
|
||||
public static final int EXPAND_UP = 0;
|
||||
public static final int LABELS_ON_LEFT_SIDE = 0;
|
||||
public static final int LABELS_ON_RIGHT_SIDE = 1;
|
||||
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equalsIgnoreCase("ASUS");
|
||||
private static final float COLLAPSED_PLUS_ROTATION = 0f;
|
||||
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
|
||||
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
|
||||
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
|
||||
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final Rect touchArea = new Rect(0, 0, 0, 0);
|
||||
private float behaviorYTranslation;
|
||||
@Nullable private FloatingActionButton mAddButton;
|
||||
private int mButtonSpacing;
|
||||
private int mButtonsCount;
|
||||
private int mExpandDirection;
|
||||
private boolean mExpanded;
|
||||
private int mLabelsMargin;
|
||||
private int mLabelsPosition;
|
||||
private int mLabelsStyle;
|
||||
private int mLabelsVerticalOffset;
|
||||
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
|
||||
private int mMaxButtonHeight;
|
||||
private int mMaxButtonWidth;
|
||||
@Nullable private RotatingDrawable mRotatingDrawable;
|
||||
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
|
||||
private float scrollYTranslation;
|
||||
|
||||
public FloatingActionsMenu(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private static int adjustForOvershoot(final int dimension) {
|
||||
return dimension * 12 / 10;
|
||||
}
|
||||
|
||||
public void addButton(final LabeledFloatingActionButton button) {
|
||||
addView(button, mButtonsCount - 1);
|
||||
mButtonsCount++;
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
collapse(false);
|
||||
}
|
||||
|
||||
private void collapse(final boolean immediately) {
|
||||
if (mExpanded) {
|
||||
mExpanded = false;
|
||||
mTouchDelegateGroup.setEnabled(false);
|
||||
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
|
||||
mCollapseAnimation.start();
|
||||
mExpandAnimation.cancel();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuCollapsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void collapseImmediately() {
|
||||
collapse(true);
|
||||
}
|
||||
|
||||
private void createAddButton(final Context context) {
|
||||
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
|
||||
mRotatingDrawable = rotatingDrawable;
|
||||
|
||||
final TimeInterpolator interpolator = new OvershootInterpolator();
|
||||
|
||||
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
|
||||
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
|
||||
|
||||
collapseAnimator.setInterpolator(interpolator);
|
||||
expandAnimator.setInterpolator(interpolator);
|
||||
|
||||
mExpandAnimation.play(expandAnimator);
|
||||
mCollapseAnimation.play(collapseAnimator);
|
||||
|
||||
mAddButton = new FloatingActionButton(context);
|
||||
mAddButton.setImageDrawable(rotatingDrawable);
|
||||
mAddButton.setId(R.id.fab_expand_menu_button);
|
||||
mAddButton.setOnClickListener(v -> toggle());
|
||||
|
||||
addView(mAddButton, super.generateDefaultLayoutParams());
|
||||
mButtonsCount++;
|
||||
}
|
||||
|
||||
private void createLabels() {
|
||||
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
|
||||
|
||||
if (button instanceof LabeledFloatingActionButton) {
|
||||
final String title = ((LabeledFloatingActionButton) button).getTitle();
|
||||
|
||||
final AppCompatTextView label = new AppCompatTextView(context);
|
||||
if (!BROKEN_LABEL_STYLE)
|
||||
label.setTextAppearance(context, mLabelsStyle);
|
||||
label.setText(title);
|
||||
addView(label);
|
||||
|
||||
button.setTag(R.id.fab_label, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
if (!mExpanded) {
|
||||
mExpanded = true;
|
||||
mTouchDelegateGroup.setEnabled(true);
|
||||
mCollapseAnimation.cancel();
|
||||
mExpandAnimation.start();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuExpanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean expandsHorizontally() {
|
||||
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(super.generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
|
||||
return new LayoutParams(super.generateLayoutParams(attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
|
||||
return new LayoutParams(super.generateLayoutParams(p));
|
||||
}
|
||||
|
||||
public float getBehaviorYTranslation() {
|
||||
return behaviorYTranslation;
|
||||
}
|
||||
|
||||
public float getScrollYTranslation() {
|
||||
return scrollYTranslation;
|
||||
}
|
||||
|
||||
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
|
||||
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
|
||||
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
|
||||
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
|
||||
|
||||
mTouchDelegateGroup = new TouchDelegateGroup(this);
|
||||
setTouchDelegate(mTouchDelegateGroup);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
|
||||
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
|
||||
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
|
||||
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
|
||||
attr.recycle();
|
||||
|
||||
if (mLabelsStyle != 0 && expandsHorizontally()) {
|
||||
throw new IllegalStateException("Action labels in horizontal expand orientation are not supported");
|
||||
}
|
||||
|
||||
createAddButton(context);
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return mExpanded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
bringChildToFront(mAddButton);
|
||||
mButtonsCount = getChildCount();
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
final boolean expandUp = mExpandDirection == EXPAND_UP;
|
||||
|
||||
if (changed) {
|
||||
mTouchDelegateGroup.clearTouchDelegates();
|
||||
}
|
||||
|
||||
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? r - l - mMaxButtonWidth / 2
|
||||
: mMaxButtonWidth / 2);
|
||||
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
|
||||
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
|
||||
|
||||
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
|
||||
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? buttonsHorizontalCenter - labelsOffset
|
||||
: buttonsHorizontalCenter + labelsOffset;
|
||||
|
||||
int nextY = expandUp ?
|
||||
addButtonY - mButtonSpacing :
|
||||
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
|
||||
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonY - childY;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
final View label = (View) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton - label.getMeasuredWidth()
|
||||
: labelsXNearButton + label.getMeasuredWidth();
|
||||
|
||||
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelXAwayFromButton
|
||||
: labelsXNearButton;
|
||||
|
||||
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton
|
||||
: labelXAwayFromButton;
|
||||
|
||||
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
|
||||
|
||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
|
||||
|
||||
touchArea.set(Math.min(childX, labelLeft),
|
||||
childY - mButtonSpacing / 2,
|
||||
Math.max(childX + child.getMeasuredWidth(), labelRight),
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
|
||||
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
|
||||
|
||||
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
label.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
|
||||
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
labelParams.setAnimationsTarget(label);
|
||||
}
|
||||
|
||||
nextY = expandUp ?
|
||||
childY - mButtonSpacing :
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing;
|
||||
}
|
||||
break;
|
||||
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
|
||||
|
||||
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
|
||||
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
|
||||
|
||||
int nextX = expandLeft ?
|
||||
addButtonX - mButtonSpacing :
|
||||
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
|
||||
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonX - childX;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
nextX = expandLeft ?
|
||||
childX - mButtonSpacing :
|
||||
childX + child.getMeasuredWidth() + mButtonSpacing;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
mMaxButtonWidth = 0;
|
||||
mMaxButtonHeight = 0;
|
||||
int maxLabelWidth = 0;
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child.getVisibility() == GONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
|
||||
height += child.getMeasuredHeight();
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += child.getMeasuredWidth();
|
||||
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
|
||||
break;
|
||||
}
|
||||
|
||||
if (!expandsHorizontally()) {
|
||||
final TextView label = (TextView) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expandsHorizontally()) {
|
||||
height = mMaxButtonHeight;
|
||||
} else {
|
||||
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
height += mButtonSpacing * (mButtonsCount - 1);
|
||||
height = adjustForOvershoot(height);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += mButtonSpacing * (mButtonsCount - 1);
|
||||
width = adjustForOvershoot(width);
|
||||
break;
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
if (state instanceof SavedState) {
|
||||
final SavedState savedState = (SavedState) state;
|
||||
mExpanded = savedState.mExpanded;
|
||||
mTouchDelegateGroup.setEnabled(mExpanded);
|
||||
|
||||
if (mRotatingDrawable != null) {
|
||||
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
|
||||
}
|
||||
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState savedState = new SavedState(superState);
|
||||
savedState.mExpanded = mExpanded;
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
public void removeButton(final LabeledFloatingActionButton button) {
|
||||
removeView(button.getLabelView());
|
||||
removeView(button);
|
||||
button.setTag(R.id.fab_label, null);
|
||||
mButtonsCount--;
|
||||
}
|
||||
|
||||
public void setBehaviorYTranslation(final float behaviorYTranslation) {
|
||||
this.behaviorYTranslation = behaviorYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
|
||||
mAddButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setScrollYTranslation(final float scrollYTranslation) {
|
||||
this.scrollYTranslation = scrollYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (mExpanded) {
|
||||
collapse();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnFloatingActionsMenuUpdateListener {
|
||||
void onMenuCollapsed();
|
||||
|
||||
void onMenuExpanded();
|
||||
}
|
||||
|
||||
private static class RotatingDrawable extends LayerDrawable {
|
||||
private float mRotation;
|
||||
|
||||
RotatingDrawable(final Drawable drawable) {
|
||||
super(new Drawable[]{drawable});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public float getRotation() {
|
||||
return mRotation;
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public void setRotation(final float rotation) {
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SavedState extends BaseSavedState {
|
||||
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||
|
||||
@Override
|
||||
public SavedState createFromParcel(final Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(final int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
private boolean mExpanded;
|
||||
|
||||
public SavedState(final Parcelable parcel) {
|
||||
super(parcel);
|
||||
}
|
||||
|
||||
private SavedState(final Parcel in) {
|
||||
super(in);
|
||||
mExpanded = in.readInt() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(mExpanded ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class LayoutParams extends ViewGroup.LayoutParams {
|
||||
|
||||
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandDir = new ObjectAnimator();
|
||||
private boolean animationsSetToPlay;
|
||||
|
||||
LayoutParams(final ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
|
||||
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
|
||||
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
|
||||
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
|
||||
mCollapseAlpha.setProperty(View.ALPHA);
|
||||
mCollapseAlpha.setFloatValues(1f, 0f);
|
||||
|
||||
mExpandAlpha.setProperty(View.ALPHA);
|
||||
mExpandAlpha.setFloatValues(0f, 1f);
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_Y);
|
||||
mExpandDir.setProperty(View.TRANSLATION_Y);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_X);
|
||||
mExpandDir.setProperty(View.TRANSLATION_X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void addLayerTypeListener(final Animator animator, final View view) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setAnimationsTarget(final View view) {
|
||||
mCollapseAlpha.setTarget(view);
|
||||
mCollapseDir.setTarget(view);
|
||||
mExpandAlpha.setTarget(view);
|
||||
mExpandDir.setTarget(view);
|
||||
|
||||
// Now that the animations have targets, set them to be played
|
||||
if (!animationsSetToPlay) {
|
||||
addLayerTypeListener(mExpandDir, view);
|
||||
addLayerTypeListener(mCollapseDir, view);
|
||||
|
||||
mCollapseAnimation.play(mCollapseAlpha);
|
||||
mCollapseAnimation.play(mCollapseDir);
|
||||
mExpandAnimation.play(mExpandAlpha);
|
||||
mExpandAnimation.play(mExpandDir);
|
||||
animationsSetToPlay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import androidx.recyclerview.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()));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class LabeledFloatingActionButton extends FloatingActionButton {
|
||||
|
||||
@Nullable private final String title;
|
||||
|
||||
public LabeledFloatingActionButton(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
|
||||
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
|
||||
attr.recycle();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TextView getLabelView() {
|
||||
return (TextView) getTag(R.id.fab_label);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(final int visibility) {
|
||||
final TextView label = getLabelView();
|
||||
if (label != null) {
|
||||
label.setVisibility(visibility);
|
||||
}
|
||||
|
||||
super.setVisibility(visibility);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class TouchDelegateGroup extends TouchDelegate {
|
||||
private static final Rect USELESS_HACKY_RECT = new Rect();
|
||||
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
|
||||
@Nullable private TouchDelegate mCurrentTouchDelegate;
|
||||
private boolean mEnabled;
|
||||
|
||||
public TouchDelegateGroup(final View uselessHackyView) {
|
||||
super(USELESS_HACKY_RECT, uselessHackyView);
|
||||
}
|
||||
|
||||
public void addTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.add(touchDelegate);
|
||||
}
|
||||
|
||||
public void clearTouchDelegates() {
|
||||
mTouchDelegates.clear();
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (!mEnabled)
|
||||
return false;
|
||||
|
||||
TouchDelegate delegate = null;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
for (final TouchDelegate touchDelegate : mTouchDelegates) {
|
||||
if (touchDelegate.onTouchEvent(event)) {
|
||||
mCurrentTouchDelegate = touchDelegate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
mCurrentTouchDelegate = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return delegate != null && delegate.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.remove(touchDelegate);
|
||||
if (mCurrentTouchDelegate == touchDelegate) {
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnabled(final boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="4dp" />
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
android:left="8dp"
|
||||
android:right="8dp"
|
||||
android:top="4dp" />
|
||||
<solid android:color="@color/fab_label_background_color" />
|
||||
</shape>
|
||||
@@ -1,9 +0,0 @@
|
||||
<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="?android:attr/colorForeground"
|
||||
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>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item>
|
||||
<selector>
|
||||
<item
|
||||
android:state_activated="true"
|
||||
app:state_multiselected="true">
|
||||
<color android:color="?attr/colorControlActivated" />
|
||||
</item>
|
||||
<item
|
||||
android:state_activated="true"
|
||||
app:state_multiselected="false">
|
||||
<color android:color="?attr/colorControlHighlight" />
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
<item android:drawable="?attr/selectableItemBackground" />
|
||||
</layer-list>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?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,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="label_position">1</integer>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 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">#5e97f6</color>
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="Multiselected">
|
||||
<attr name="state_multiselected" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 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>
|
||||
|
||||
<color name="accent">#2196F3</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
</resources>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="fab_expand_menu_button" type="id" />
|
||||
<item name="fab_label" type="id" />
|
||||
|
||||
<dimen name="fab_shadow_offset">3dp</dimen>
|
||||
<dimen name="fab_shadow_radius">9dp</dimen>
|
||||
|
||||
<dimen name="fab_stroke_width">1dp</dimen>
|
||||
|
||||
<dimen name="fab_actions_spacing">24dp</dimen>
|
||||
<dimen name="fab_labels_margin">8dp</dimen>
|
||||
|
||||
<declare-styleable name="LabeledFloatingActionButton">
|
||||
<attr name="fab_title" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="FloatingActionsMenu">
|
||||
<attr name="fab_labelStyle" format="reference" />
|
||||
<attr name="fab_labelsPosition" format="enum">
|
||||
<enum name="left" value="0" />
|
||||
<enum name="right" value="1" />
|
||||
</attr>
|
||||
<attr name="fab_expandDirection" format="enum">
|
||||
<enum name="up" value="0" />
|
||||
<enum name="down" value="1" />
|
||||
<enum name="left" value="2" />
|
||||
<enum name="right" value="3" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
<integer name="label_position">0</integer>
|
||||
</resources>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
</style>
|
||||
|
||||
<style name="NoBackgroundTheme" parent="AppTheme">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
<item name="colorPrimaryDark">@android:color/transparent</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
|
||||
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
|
||||
</style>
|
||||
|
||||
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
|
||||
<item name="android:background">@drawable/fab_label_background</item>
|
||||
<item name="android:textColor">@color/fab_label_text_color</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
Submodule app/tools/wireguard-tools deleted from e5b08c2849
+34
-2
@@ -6,8 +6,38 @@ allprojects {
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
cardviewVersion = '1.0.0'
|
||||
collectionVersion = '1.1.0'
|
||||
coreKtxVersion = '1.2.0'
|
||||
constraintLayoutVersion = '1.1.3'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
agpVersion = '3.6.1'
|
||||
fragmentVersion = '1.2.2'
|
||||
materialComponentsVersion = '1.1.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinVersion = '1.3.70'
|
||||
preferenceVersion = '1.1.0'
|
||||
streamsupportVersion = '1.7.1'
|
||||
threetenabpVersion = '1.2.2'
|
||||
// ZXING switched minSdk to 24 so we cannot upgrade to 4.0.2 without following suit.
|
||||
// If you choose to upgrade to minSDK 24 then you should also disable Jetifier from
|
||||
// gradle.properties.
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
eddsaVersion = '0.3.0'
|
||||
bintrayPluginVersion = '1.8.4'
|
||||
mavenPluginVersion = '2.1'
|
||||
junitVersion = '4.13'
|
||||
|
||||
groupName = 'com.wireguard.android'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "com.android.tools.build:gradle:$agpVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "com.github.dcendents:android-maven-gradle-plugin:$mavenPluginVersion"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$bintrayPluginVersion"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@@ -21,7 +51,9 @@ task clean(type: Delete) {
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.1"
|
||||
gradleVersion = "6.2.2"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "version.gradle"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
include ':app'
|
||||
include ':tunnel'
|
||||
include ':ui'
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
version wireguardVersionName
|
||||
group groupName
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 29
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
testOptions.unitTests.all {
|
||||
testLogging {
|
||||
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.collection:collection:$collectionVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
|
||||
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
}
|
||||
|
||||
apply from: "publish.gradle"
|
||||
@@ -0,0 +1,76 @@
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
|
||||
install {
|
||||
repositories.mavenInstaller {
|
||||
pom.project {
|
||||
name 'WireGuard Tunnel Library'
|
||||
description 'Embeddable tunnel library for WireGuard for Android'
|
||||
url 'https://www.wireguard.com/'
|
||||
|
||||
packaging 'aar'
|
||||
groupId groupName
|
||||
artifactId 'tunnel'
|
||||
version wireguardVersionName
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name 'The Apache Software License, Version 2.0'
|
||||
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution 'repo'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection 'scm:git:https://git.zx2c4.com/wireguard-android'
|
||||
url 'https://git.zx2c4.com/wireguard-android'
|
||||
}
|
||||
organization {
|
||||
name 'WireGuard'
|
||||
url 'https://www.wireguard.com/'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintray {
|
||||
user = hasProperty('BINTRAY_USER') ? getProperty('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
key = hasProperty('BINTRAY_KEY') ? getProperty('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
|
||||
configurations = [ 'archives' ]
|
||||
|
||||
pkg {
|
||||
repo = 'wireguard-android'
|
||||
name = 'wireguard-android'
|
||||
userOrg = 'wireguard'
|
||||
licenses = [ 'Apache-2.0' ]
|
||||
vcsUrl = 'https://git.zx2c4.com/wireguard-android'
|
||||
publish = true
|
||||
|
||||
version {
|
||||
name = wireguardVersionName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android.libraryVariants.all { variant ->
|
||||
if (variant.name == 'release') {
|
||||
task javadoc(type: Javadoc) {
|
||||
source = variant.javaCompileProvider.get().source
|
||||
classpath = files((android.bootClasspath.join(File.pathSeparator)))
|
||||
classpath += variant.javaCompileProvider.get().classpath
|
||||
title = 'Embeddable WireGuard Tunnel for Android v$wireguardVersionName'
|
||||
}
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
archiveClassifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
task sourcesJar(type: Jar) {
|
||||
archiveClassifier = 'sources'
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
}
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!--
|
||||
~ Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.wireguard.android.tunnel">
|
||||
<application>
|
||||
<service
|
||||
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
+12
-27
@@ -5,43 +5,33 @@
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Interface for implementations of the WireGuard secure network tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface Backend {
|
||||
/**
|
||||
* Update the volatile configuration of a running tunnel and return the resulting configuration.
|
||||
* If the tunnel is not up, return the configuration that would result (if known), or else
|
||||
* simply return the given configuration.
|
||||
*
|
||||
* @param tunnel The tunnel to apply the configuration to.
|
||||
* @param config The new configuration for this tunnel.
|
||||
* @return The updated configuration of the tunnel.
|
||||
*/
|
||||
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
|
||||
|
||||
/**
|
||||
* Enumerate the names of currently-running tunnels.
|
||||
* Enumerate names of currently-running tunnels.
|
||||
*
|
||||
* @return The set of running tunnel names.
|
||||
*/
|
||||
Set<String> enumerate();
|
||||
Set<String> getRunningTunnelNames();
|
||||
|
||||
/**
|
||||
* Get the actual state of a tunnel.
|
||||
* Get the state of a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return The state of the tunnel.
|
||||
*/
|
||||
State getState(Tunnel tunnel) throws Exception;
|
||||
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
|
||||
@@ -52,13 +42,6 @@ public interface Backend {
|
||||
*/
|
||||
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Determine type name of underlying backend.
|
||||
*
|
||||
* @return Type name
|
||||
*/
|
||||
String getTypePrettyName();
|
||||
|
||||
/**
|
||||
* Determine version of underlying backend.
|
||||
*
|
||||
@@ -68,12 +51,14 @@ public interface Backend {
|
||||
String getVersion() throws Exception;
|
||||
|
||||
/**
|
||||
* Set the state of a tunnel.
|
||||
* Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
|
||||
* may update the running configuration; config may be null when setting the tunnel down.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return The updated state of the tunnel.
|
||||
*/
|
||||
State setState(Tunnel tunnel, State state) throws Exception;
|
||||
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
@NonNullForAll
|
||||
public final class BackendException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
public BackendException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
UNKNOWN_KERNEL_MODULE_NAME,
|
||||
WG_QUICK_CONFIG_ERROR_CODE,
|
||||
TUNNEL_MISSING_CONFIG,
|
||||
VPN_NOT_AUTHORIZED,
|
||||
UNABLE_TO_START_VPN,
|
||||
TUN_CREATION_ERROR,
|
||||
GO_ACTIVATION_ERROR_CODE
|
||||
}
|
||||
}
|
||||
+102
-65
@@ -5,51 +5,52 @@
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.util.SharedLibraryLoader;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.config.Peer;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
@NonNullForAll
|
||||
public final class GoBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
|
||||
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
|
||||
|
||||
@Nullable private static AlwaysOnCallback alwaysOnCallback;
|
||||
private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
|
||||
private final Context context;
|
||||
@Nullable private Config currentConfig;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
|
||||
public GoBackend(final Context context) {
|
||||
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
|
||||
alwaysOnCallback = cb;
|
||||
}
|
||||
|
||||
private static native String wgGetConfig(int handle);
|
||||
|
||||
private static native int wgGetSocketV4(int handle);
|
||||
@@ -63,23 +64,7 @@ public final class GoBackend implements Backend {
|
||||
private static native String wgVersion();
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
if (currentTunnel != null) {
|
||||
final Set<String> runningTunnels = new ArraySet<>();
|
||||
runningTunnels.add(currentTunnel.getName());
|
||||
@@ -101,7 +86,8 @@ public final class GoBackend implements Backend {
|
||||
}
|
||||
final String config = wgGetConfig(currentTunnelHandle);
|
||||
Key key = null;
|
||||
long rx = 0, tx = 0;
|
||||
long rx = 0;
|
||||
long tx = 0;
|
||||
for (final String line : config.split("\\n")) {
|
||||
if (line.startsWith("public_key=")) {
|
||||
if (key != null)
|
||||
@@ -136,40 +122,47 @@ public final class GoBackend implements Backend {
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypePrettyName() {
|
||||
return context.getString(R.string.type_name_go_userspace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return wgVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
if (state == originalState && tunnel == currentTunnel && config == currentConfig)
|
||||
return originalState;
|
||||
if (state == State.UP && currentTunnel != null)
|
||||
throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error));
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
if (state == State.UP) {
|
||||
final Config originalConfig = currentConfig;
|
||||
final Tunnel originalTunnel = currentTunnel;
|
||||
if (currentTunnel != null)
|
||||
setStateInternal(currentTunnel, null, State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, state);
|
||||
} catch (final Exception e) {
|
||||
if (originalTunnel != null)
|
||||
setStateInternal(originalTunnel, originalConfig, State.UP);
|
||||
throw e;
|
||||
}
|
||||
} else if (state == State.DOWN && tunnel == currentTunnel) {
|
||||
setStateInternal(tunnel, null, State.DOWN);
|
||||
}
|
||||
return getState(tunnel);
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
|
||||
throws Exception {
|
||||
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
|
||||
|
||||
if (state == State.UP) {
|
||||
Log.i(TAG, "Bringing tunnel up");
|
||||
|
||||
Objects.requireNonNull(config, context.getString(R.string.no_config_error));
|
||||
if (config == null)
|
||||
throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
|
||||
|
||||
if (VpnService.prepare(context) != null)
|
||||
throw new Exception(context.getString(R.string.vpn_not_authorized_error));
|
||||
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
|
||||
|
||||
final VpnService service;
|
||||
if (!vpnService.isDone())
|
||||
@@ -178,8 +171,11 @@ public final class GoBackend implements Backend {
|
||||
try {
|
||||
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||
} catch (final TimeoutException e) {
|
||||
throw new Exception(context.getString(R.string.vpn_start_error), e);
|
||||
final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
|
||||
be.initCause(e);
|
||||
throw be;
|
||||
}
|
||||
service.setOwner(this);
|
||||
|
||||
if (currentTunnelHandle != -1) {
|
||||
Log.w(TAG, "Tunnel already up");
|
||||
@@ -193,10 +189,6 @@ public final class GoBackend implements Backend {
|
||||
final VpnService.Builder builder = service.getBuilder();
|
||||
builder.setSession(tunnel.getName());
|
||||
|
||||
final Intent configureIntent = new Intent(context, MainActivity.class);
|
||||
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
|
||||
|
||||
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||
builder.addDisallowedApplication(excludedApplication);
|
||||
|
||||
@@ -216,25 +208,24 @@ public final class GoBackend implements Backend {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
builder.setMetered(false);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
service.setUnderlyingNetworks(null);
|
||||
service.setUnderlyingNetworks(null);
|
||||
|
||||
builder.setBlocking(true);
|
||||
try (final ParcelFileDescriptor tun = builder.establish()) {
|
||||
if (tun == null)
|
||||
throw new Exception(context.getString(R.string.tun_create_error));
|
||||
throw new BackendException(Reason.TUN_CREATION_ERROR);
|
||||
Log.d(TAG, "Go backend v" + wgVersion());
|
||||
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
|
||||
}
|
||||
if (currentTunnelHandle < 0)
|
||||
throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
|
||||
throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
|
||||
|
||||
currentTunnel = tunnel;
|
||||
currentConfig = config;
|
||||
|
||||
service.protect(wgGetSocketV4(currentTunnelHandle));
|
||||
service.protect(wgGetSocketV6(currentTunnelHandle));
|
||||
} else {
|
||||
Log.i(TAG, "Bringing tunnel down");
|
||||
|
||||
if (currentTunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down");
|
||||
return;
|
||||
@@ -243,7 +234,10 @@ public final class GoBackend implements Backend {
|
||||
wgTurnOff(currentTunnelHandle);
|
||||
currentTunnel = null;
|
||||
currentTunnelHandle = -1;
|
||||
currentConfig = null;
|
||||
}
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
|
||||
private void startVpnService() {
|
||||
@@ -251,7 +245,42 @@ public final class GoBackend implements Backend {
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
public interface AlwaysOnCallback {
|
||||
void alwaysOnTriggered();
|
||||
}
|
||||
|
||||
// TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture.
|
||||
private static final class GhettoCompletableFuture<V> {
|
||||
private final LinkedBlockingQueue<V> completion = new LinkedBlockingQueue<>(1);
|
||||
private final FutureTask<V> result = new FutureTask<>(completion::peek);
|
||||
|
||||
public boolean complete(final V value) {
|
||||
final boolean offered = completion.offer(value);
|
||||
if (offered)
|
||||
result.run();
|
||||
return offered;
|
||||
}
|
||||
|
||||
public V get() throws ExecutionException, InterruptedException {
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
|
||||
return result.get(timeout, unit);
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return !completion.isEmpty();
|
||||
}
|
||||
|
||||
public GhettoCompletableFuture<V> newIncompleteFuture() {
|
||||
return new GhettoCompletableFuture<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class VpnService extends android.net.VpnService {
|
||||
@Nullable private GoBackend owner;
|
||||
|
||||
public Builder getBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
@@ -264,13 +293,17 @@ public final class GoBackend implements Backend {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
for (final Tunnel tunnel : tunnels) {
|
||||
if (tunnel != null && tunnel.getState() != State.DOWN)
|
||||
tunnel.setState(State.DOWN);
|
||||
if (owner != null) {
|
||||
final Tunnel tunnel = owner.currentTunnel;
|
||||
if (tunnel != null) {
|
||||
if (owner.currentTunnelHandle != -1)
|
||||
wgTurnOff(owner.currentTunnelHandle);
|
||||
owner.currentTunnel = null;
|
||||
owner.currentTunnelHandle = -1;
|
||||
owner.currentConfig = null;
|
||||
tunnel.onStateChange(State.DOWN);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
vpnService = vpnService.newIncompleteFuture();
|
||||
super.onDestroy();
|
||||
}
|
||||
@@ -280,10 +313,14 @@ public final class GoBackend implements Backend {
|
||||
vpnService.complete(this);
|
||||
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
|
||||
Log.d(TAG, "Service started by Always-on VPN feature");
|
||||
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
|
||||
if (alwaysOnCallback != null)
|
||||
alwaysOnCallback.alwaysOnTriggered();
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
public void setOwner(final GoBackend owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@NonNullForAll
|
||||
public class Statistics {
|
||||
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
|
||||
private long lastTouched = SystemClock.elapsedRealtime();
|
||||
|
||||
Statistics() {
|
||||
}
|
||||
|
||||
void add(final Key key, final long rx, final long tx) {
|
||||
peerBytes.put(key, Pair.create(rx, tx));
|
||||
lastTouched = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
public boolean isStale() {
|
||||
return SystemClock.elapsedRealtime() - lastTouched > 900;
|
||||
}
|
||||
|
||||
public long peerRx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).first;
|
||||
}
|
||||
|
||||
public long peerTx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).second;
|
||||
}
|
||||
|
||||
public Key[] peers() {
|
||||
return peerBytes.keySet().toArray(new Key[0]);
|
||||
}
|
||||
|
||||
public long totalRx() {
|
||||
long rx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
rx += val.first;
|
||||
}
|
||||
return rx;
|
||||
}
|
||||
|
||||
public long totalTx() {
|
||||
long tx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
tx += val.second;
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents a WireGuard tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface Tunnel {
|
||||
int NAME_MAX_LENGTH = 15;
|
||||
Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
|
||||
|
||||
static boolean isNameInvalid(final CharSequence name) {
|
||||
return !NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the tunnel, which should always pass the !isNameInvalid test.
|
||||
*
|
||||
* @return The name of the tunnel.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* React to a change in state of the tunnel. Should only be directly called by Backend.
|
||||
*
|
||||
* @param newState The new state of the tunnel.
|
||||
*/
|
||||
void onStateChange(State newState);
|
||||
|
||||
enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UP;
|
||||
|
||||
public static State of(final boolean running) {
|
||||
return running ? UP : DOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
private final File localTemporaryDir;
|
||||
private final RootShell rootShell;
|
||||
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
|
||||
private final ToolsInstaller toolsInstaller;
|
||||
private boolean multipleTunnels;
|
||||
|
||||
public WgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
this.rootShell = rootShell;
|
||||
this.toolsInstaller = toolsInstaller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
final List<String> output = new ArrayList<>();
|
||||
// Don't throw an exception here or nothing will show up in the UI.
|
||||
try {
|
||||
toolsInstaller.ensureToolsAvailable();
|
||||
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||
return Collections.emptySet();
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to enumerate running tunnels", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
final Collection<String> output = new ArrayList<>();
|
||||
try {
|
||||
if (rootShell.run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
|
||||
return stats;
|
||||
} catch (final Exception ignored) {
|
||||
return stats;
|
||||
}
|
||||
for (final String line : output) {
|
||||
final String[] parts = line.split("\\t");
|
||||
if (parts.length != 3)
|
||||
continue;
|
||||
try {
|
||||
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() throws Exception {
|
||||
final List<String> output = new ArrayList<>();
|
||||
if (rootShell.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
|
||||
throw new BackendException(Reason.UNKNOWN_KERNEL_MODULE_NAME);
|
||||
return output.get(0);
|
||||
}
|
||||
|
||||
public void setMultipleTunnels(final boolean on) {
|
||||
multipleTunnels = on;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
final Config originalConfig = runningConfigs.get(tunnel);
|
||||
final Map<Tunnel, Config> runningConfigsSnapshot = new HashMap<>(runningConfigs);
|
||||
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) ||
|
||||
(state == State.DOWN && originalState == State.DOWN))
|
||||
return originalState;
|
||||
if (state == State.UP) {
|
||||
toolsInstaller.ensureToolsAvailable();
|
||||
if (!multipleTunnels && originalState == State.DOWN) {
|
||||
final List<Pair<Tunnel, Config>> rewind = new LinkedList<>();
|
||||
try {
|
||||
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
|
||||
setStateInternal(entry.getKey(), entry.getValue(), State.DOWN);
|
||||
rewind.add(Pair.create(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
for (final Pair<Tunnel, Config> entry : rewind) {
|
||||
setStateInternal(entry.first, entry.second, State.UP);
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (originalState == State.UP)
|
||||
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
if (originalState == State.UP && originalConfig != null) {
|
||||
setStateInternal(tunnel, originalConfig, State.UP);
|
||||
}
|
||||
if (!multipleTunnels && originalState == State.DOWN) {
|
||||
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
|
||||
setStateInternal(entry.getKey(), entry.getValue(), State.UP);
|
||||
}
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} else if (state == State.DOWN) {
|
||||
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
|
||||
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
|
||||
|
||||
Objects.requireNonNull(config, "Trying to set state up with a null config");
|
||||
|
||||
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
|
||||
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
|
||||
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String command = String.format("wg-quick %s '%s'",
|
||||
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
|
||||
if (state == State.UP)
|
||||
command = "cat /sys/module/wireguard/version && " + command;
|
||||
final int result = rootShell.run(null, command);
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
if (result != 0)
|
||||
throw new BackendException(Reason.WG_QUICK_CONFIG_ERROR_CODE, result);
|
||||
|
||||
if (state == State.UP)
|
||||
runningConfigs.put(tunnel, config);
|
||||
else
|
||||
runningConfigs.remove(tunnel);
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
}
|
||||
+93
-87
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2019 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,8 @@ import android.content.Context;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.util.RootShell.NoRootException;
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
@@ -35,45 +35,98 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NonNullForAll
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public class ModuleLoader {
|
||||
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
|
||||
private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig";
|
||||
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
|
||||
private static final String MODULE_NAME = "wireguard-%s.ko";
|
||||
|
||||
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
|
||||
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
|
||||
private final File moduleDir;
|
||||
private final RootShell rootShell;
|
||||
private final File tmpDir;
|
||||
private final String userAgent;
|
||||
|
||||
public ModuleLoader(final Context context) {
|
||||
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
|
||||
moduleDir = new File(context.getCacheDir(), "kmod");
|
||||
tmpDir = new File(context.getCacheDir(), "tmp");
|
||||
this.rootShell = rootShell;
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public static boolean isModuleLoaded() {
|
||||
return new File("/sys/module/wireguard").exists();
|
||||
}
|
||||
|
||||
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
|
||||
final List<String> output = new ArrayList<>();
|
||||
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
|
||||
if (output.size() != 1 || output.get(0).length() != 64)
|
||||
throw new InvalidParameterException("Invalid sha256 of /proc/version");
|
||||
final String moduleName = String.format(MODULE_NAME, output.get(0));
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(MODULE_LIST_URL).openConnection();
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Hash list could not be found");
|
||||
final byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
|
||||
int len;
|
||||
try (final InputStream inputStream = connection.getInputStream()) {
|
||||
len = inputStream.read(input);
|
||||
}
|
||||
if (len <= 0)
|
||||
throw new IOException("Hash list was empty");
|
||||
final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
|
||||
if (modules == null)
|
||||
throw new InvalidParameterException("The signature did not verify or invalid hash list format");
|
||||
if (!modules.containsKey(moduleName))
|
||||
return OsConstants.ENOENT;
|
||||
connection = (HttpURLConnection) new URL(String.format(MODULE_URL, moduleName)).openConnection();
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Module file could not be found, despite being on hash list");
|
||||
|
||||
tmpDir.mkdirs();
|
||||
moduleDir.mkdir();
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (final InputStream inputStream = connection.getInputStream();
|
||||
final FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
int total = 0;
|
||||
while ((len = inputStream.read(input)) > 0) {
|
||||
total += len;
|
||||
if (total > 1024 * 1024 * 15 /* 15 MiB */)
|
||||
throw new IOException("File too big");
|
||||
outputStream.write(input, 0, len);
|
||||
digest.update(input, 0, len);
|
||||
}
|
||||
outputStream.getFD().sync();
|
||||
}
|
||||
if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
|
||||
throw new IOException("Incorrect file hash");
|
||||
|
||||
if (!tempFile.renameTo(new File(moduleDir, moduleName)))
|
||||
throw new IOException("Unable to rename to final destination");
|
||||
} finally {
|
||||
if (tempFile != null)
|
||||
tempFile.delete();
|
||||
}
|
||||
return OsConstants.EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
public void loadModule() throws IOException, RootShellException {
|
||||
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
|
||||
}
|
||||
|
||||
public boolean moduleMightExist() {
|
||||
return moduleDir.exists() && moduleDir.isDirectory();
|
||||
}
|
||||
|
||||
public void loadModule() throws IOException, NoRootException {
|
||||
Application.getRootShell().run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
|
||||
}
|
||||
|
||||
public boolean isModuleLoaded() {
|
||||
return new File("/sys/module/wireguard").exists();
|
||||
}
|
||||
|
||||
private static final class Sha256Digest {
|
||||
private byte[] bytes;
|
||||
private Sha256Digest(final String hex) {
|
||||
if (hex.length() != 64)
|
||||
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
|
||||
bytes = new byte[32];
|
||||
for (int i = 0; i < 32; ++i)
|
||||
bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
|
||||
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
@@ -96,9 +149,9 @@ public class ModuleLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
|
||||
Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
|
||||
byte[] rawPublicKeyBytes = new byte[32];
|
||||
final EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
|
||||
final Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
|
||||
final byte[] rawPublicKeyBytes = new byte[32];
|
||||
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
|
||||
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
|
||||
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
|
||||
@@ -108,9 +161,9 @@ public class ModuleLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Sha256Digest> hashes = new HashMap<>();
|
||||
final Map<String, Sha256Digest> hashes = new HashMap<>();
|
||||
for (final String line : lines[2].split("\n")) {
|
||||
final String[] components = line.split(" ", 2);
|
||||
final String[] components = line.split(" {2}", 2);
|
||||
if (components.length != 2)
|
||||
return null;
|
||||
try {
|
||||
@@ -122,62 +175,15 @@ public class ModuleLoader {
|
||||
return hashes;
|
||||
}
|
||||
|
||||
public Integer download() throws IOException, NoRootException, NoSuchAlgorithmException {
|
||||
final List<String> output = new ArrayList<>();
|
||||
Application.getRootShell().run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
|
||||
if (output.size() != 1 || output.get(0).length() != 64)
|
||||
throw new InvalidParameterException("Invalid sha256 of /proc/version");
|
||||
final String moduleName = String.format(MODULE_NAME, output.get(0));
|
||||
HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection();
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Hash list could not be found");
|
||||
byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
|
||||
int len;
|
||||
try (final InputStream inputStream = connection.getInputStream()) {
|
||||
len = inputStream.read(input);
|
||||
}
|
||||
if (len <= 0)
|
||||
throw new IOException("Hash list was empty");
|
||||
final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
|
||||
if (modules == null)
|
||||
throw new InvalidParameterException("The signature did not verify or invalid hash list format");
|
||||
if (!modules.containsKey(moduleName))
|
||||
return OsConstants.ENOENT;
|
||||
connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection();
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Module file could not be found, despite being on hash list");
|
||||
private static final class Sha256Digest {
|
||||
private final byte[] bytes;
|
||||
|
||||
tmpDir.mkdirs();
|
||||
moduleDir.mkdir();
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (final InputStream inputStream = connection.getInputStream();
|
||||
final FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
int total = 0;
|
||||
while ((len = inputStream.read(input)) > 0) {
|
||||
total += len;
|
||||
if (total > 1024 * 1024 * 15 /* 15 MiB */)
|
||||
throw new IOException("File too big");
|
||||
outputStream.write(input, 0, len);
|
||||
digest.update(input, 0, len);
|
||||
}
|
||||
outputStream.getFD().sync();
|
||||
}
|
||||
if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
|
||||
throw new IOException("Incorrect file hash");
|
||||
|
||||
if (!tempFile.renameTo(new File(moduleDir, moduleName)))
|
||||
throw new IOException("Unable to rename to final destination");
|
||||
} finally {
|
||||
if (tempFile != null)
|
||||
tempFile.delete();
|
||||
private Sha256Digest(final String hex) {
|
||||
if (hex.length() != 64)
|
||||
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
|
||||
bytes = new byte[32];
|
||||
for (int i = 0; i < 32; ++i)
|
||||
bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return OsConstants.EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
+45
-26
@@ -6,15 +6,13 @@
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.RootShell.RootShellException.Reason;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
@@ -22,16 +20,17 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Helper class for running commands as root.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class RootShell {
|
||||
private static final String SU = "su";
|
||||
private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final String deviceNotRootedMessage;
|
||||
private final File localBinaryDir;
|
||||
private final File localTemporaryDir;
|
||||
private final Object lock = new Object();
|
||||
@@ -42,12 +41,10 @@ public class RootShell {
|
||||
@Nullable private BufferedReader stdout;
|
||||
|
||||
public RootShell(final Context context) {
|
||||
deviceNotRootedMessage = context.getString(R.string.error_root);
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
|
||||
BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
|
||||
this.context = context;
|
||||
context.getPackageName(), localBinaryDir, localTemporaryDir);
|
||||
}
|
||||
|
||||
private static boolean isExecutableInPath(final String name) {
|
||||
@@ -83,7 +80,7 @@ public class RootShell {
|
||||
* @return The exit value of the command.
|
||||
*/
|
||||
public int run(@Nullable final Collection<String> output, final String command)
|
||||
throws IOException, NoRootException {
|
||||
throws IOException, RootShellException {
|
||||
synchronized (lock) {
|
||||
/* Start inside synchronized block to prevent a concurrent call to stop(). */
|
||||
start();
|
||||
@@ -122,24 +119,24 @@ public class RootShell {
|
||||
}
|
||||
}
|
||||
if (markersSeen != 4)
|
||||
throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen));
|
||||
throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
|
||||
if (errnoStdout != errnoStderr)
|
||||
throw new IOException(context.getString(R.string.shell_exit_status_read_error));
|
||||
throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
|
||||
Log.v(TAG, "exit: " + errnoStdout);
|
||||
return errnoStdout;
|
||||
}
|
||||
}
|
||||
|
||||
public void start() throws IOException, NoRootException {
|
||||
public void start() throws IOException, RootShellException {
|
||||
if (!isExecutableInPath(SU))
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
synchronized (lock) {
|
||||
if (isRunning())
|
||||
return;
|
||||
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
|
||||
throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error));
|
||||
throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
|
||||
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
|
||||
throw new FileNotFoundException(context.getString(R.string.create_temp_dir_error));
|
||||
throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
|
||||
try {
|
||||
final ProcessBuilder builder = new ProcessBuilder().command(SU);
|
||||
builder.environment().put("LC_ALL", "C");
|
||||
@@ -147,7 +144,9 @@ public class RootShell {
|
||||
process = builder.start();
|
||||
} catch (final IOException e) {
|
||||
// A failure at this stage means the device isn't rooted.
|
||||
throw new NoRootException(deviceNotRootedMessage, e);
|
||||
final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
rse.initCause(e);
|
||||
throw rse;
|
||||
}
|
||||
stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
|
||||
stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
|
||||
@@ -160,18 +159,18 @@ public class RootShell {
|
||||
final String uid = stdout.readLine();
|
||||
if (!"0".equals(uid)) {
|
||||
Log.w(TAG, "Root check did not return correct UID: " + uid);
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
}
|
||||
if (!isRunning()) {
|
||||
String line;
|
||||
while ((line = stderr.readLine()) != null) {
|
||||
Log.w(TAG, "Root check returned an error: " + line);
|
||||
if (line.contains("Permission denied"))
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
}
|
||||
throw new IOException(context.getString(R.string.shell_start_error, process.exitValue()));
|
||||
throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
|
||||
}
|
||||
} catch (final IOException | NoRootException e) {
|
||||
} catch (final IOException | RootShellException e) {
|
||||
stop();
|
||||
throw e;
|
||||
}
|
||||
@@ -187,13 +186,33 @@ public class RootShell {
|
||||
}
|
||||
}
|
||||
|
||||
public static class NoRootException extends Exception {
|
||||
public NoRootException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
public static class RootShellException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
public RootShellException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public NoRootException(final String message) {
|
||||
super(message);
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public boolean isIORelated() {
|
||||
return reason != Reason.NO_ROOT_ACCESS;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
NO_ROOT_ACCESS,
|
||||
SHELL_MARKER_COUNT_ERROR,
|
||||
SHELL_EXIT_STATUS_READ_ERROR,
|
||||
SHELL_START_ERROR,
|
||||
CREATE_BIN_DIR_ERROR,
|
||||
CREATE_TEMP_DIR_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-20
@@ -9,6 +9,8 @@ import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -19,6 +21,11 @@ import java.util.HashSet;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
|
||||
@NonNullForAll
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
|
||||
|
||||
@@ -34,27 +41,22 @@ public final class SharedLibraryLoader {
|
||||
|
||||
for (final String abi : Build.SUPPORTED_ABIS) {
|
||||
for (final String apk : apks) {
|
||||
final ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
|
||||
try (final FileOutputStream out = new FileOutputStream(destination);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
|
||||
try (final FileOutputStream out = new FileOutputStream(destination);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
out.getFD().sync();
|
||||
}
|
||||
out.getFD().sync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+75
-57
@@ -6,14 +6,11 @@
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.RootShell.NoRootException;
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -21,10 +18,15 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
|
||||
/**
|
||||
* Helper to install WireGuard tools to the system partition.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class ToolsInstaller {
|
||||
public static final int ERROR = 0x0;
|
||||
public static final int MAGISK = 0x4;
|
||||
@@ -42,12 +44,14 @@ public final class ToolsInstaller {
|
||||
private final Context context;
|
||||
private final File localBinaryDir;
|
||||
private final Object lock = new Object();
|
||||
private final RootShell rootShell;
|
||||
@Nullable private Boolean areToolsAvailable;
|
||||
@Nullable private Boolean installAsMagiskModule;
|
||||
|
||||
public ToolsInstaller(final Context context) {
|
||||
public ToolsInstaller(final Context context, final RootShell rootShell) {
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
this.context = context;
|
||||
this.rootShell = rootShell;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -63,7 +67,7 @@ public final class ToolsInstaller {
|
||||
return null;
|
||||
}
|
||||
|
||||
public int areInstalled() throws NoRootException {
|
||||
public int areInstalled() throws RootShellException {
|
||||
if (INSTALL_DIR == null)
|
||||
return ERROR;
|
||||
final StringBuilder script = new StringBuilder();
|
||||
@@ -74,13 +78,17 @@ public final class ToolsInstaller {
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append(';');
|
||||
try {
|
||||
final int ret = Application.getRootShell().run(null, script.toString());
|
||||
final int ret = rootShell.run(null, script.toString());
|
||||
if (ret == OsConstants.EALREADY)
|
||||
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
|
||||
else
|
||||
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,59 +105,14 @@ public final class ToolsInstaller {
|
||||
}
|
||||
}
|
||||
if (!areToolsAvailable)
|
||||
throw new FileNotFoundException(
|
||||
context.getString(R.string.tools_unavailable_error));
|
||||
}
|
||||
}
|
||||
|
||||
public int install() throws NoRootException, IOException {
|
||||
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
|
||||
}
|
||||
|
||||
private int installMagisk() throws NoRootException, IOException {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
script.append("trap - INT TERM EXIT;");
|
||||
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private int installSystem() throws NoRootException, IOException {
|
||||
if (INSTALL_DIR == null)
|
||||
return OsConstants.ENOENT;
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File(INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
throw new FileNotFoundException("Required tools unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean extract() throws IOException {
|
||||
localBinaryDir.mkdirs();
|
||||
final File files[] = new File[EXECUTABLES.length];
|
||||
final File tempFiles[] = new File[EXECUTABLES.length];
|
||||
final File[] files = new File[EXECUTABLES.length];
|
||||
final File[] tempFiles = new File[EXECUTABLES.length];
|
||||
boolean allExist = true;
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
files[i] = new File(localBinaryDir, EXECUTABLES[i]);
|
||||
@@ -169,11 +132,66 @@ public final class ToolsInstaller {
|
||||
return true;
|
||||
}
|
||||
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public int install() throws RootShellException, IOException {
|
||||
if (!context.getPackageName().startsWith("com.wireguard."))
|
||||
throw new SecurityException("The tools may only be installed system-wide from the main WireGuard app.");
|
||||
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
|
||||
}
|
||||
|
||||
private int installMagisk() throws RootShellException, IOException {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
|
||||
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
script.append("trap - INT TERM EXIT;");
|
||||
|
||||
try {
|
||||
return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private int installSystem() throws RootShellException, IOException {
|
||||
if (INSTALL_DIR == null)
|
||||
return OsConstants.ENOENT;
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File(INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
try {
|
||||
return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean willInstallAsMagiskModule() {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
+14
-2
@@ -5,13 +5,15 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.Optional;
|
||||
|
||||
@NonNullForAll
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
|
||||
@@ -25,7 +27,17 @@ public final class Attribute {
|
||||
}
|
||||
|
||||
public static String join(final Iterable<?> values) {
|
||||
return TextUtils.join(", ", values);
|
||||
final Iterator<?> it = values.iterator();
|
||||
if (!it.hasNext()) {
|
||||
return "";
|
||||
}
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(it.next());
|
||||
while (it.hasNext()) {
|
||||
sb.append(", ");
|
||||
sb.append(it.next());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Optional<Attribute> parse(final CharSequence line) {
|
||||
+4
-3
@@ -5,10 +5,12 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
|
||||
@NonNullForAll
|
||||
public class BadConfigException extends Exception {
|
||||
private final Location location;
|
||||
private final Reason reason;
|
||||
@@ -94,7 +96,6 @@ public class BadConfigException extends Exception {
|
||||
INVALID_VALUE,
|
||||
MISSING_ATTRIBUTE,
|
||||
MISSING_SECTION,
|
||||
MISSING_VALUE,
|
||||
SYNTAX_ERROR,
|
||||
UNKNOWN_ATTRIBUTE,
|
||||
UNKNOWN_SECTION
|
||||
+7
-3
@@ -5,11 +5,10 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -23,12 +22,15 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
|
||||
* sections (combined together), and zero or more "Peer" sections (treated individually).
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Config {
|
||||
private final Interface interfaze;
|
||||
private final List<Peer> peers;
|
||||
@@ -67,6 +69,7 @@ public final class Config {
|
||||
final Collection<String> peerLines = new ArrayList<>();
|
||||
boolean inInterfaceSection = false;
|
||||
boolean inPeerSection = false;
|
||||
boolean seenInterfaceSection = false;
|
||||
@Nullable String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final int commentIndex = line.indexOf('#');
|
||||
@@ -84,6 +87,7 @@ public final class Config {
|
||||
if ("[Interface]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = true;
|
||||
inPeerSection = false;
|
||||
seenInterfaceSection = true;
|
||||
} else if ("[Peer]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = false;
|
||||
inPeerSection = true;
|
||||
@@ -102,7 +106,7 @@ public final class Config {
|
||||
}
|
||||
if (inPeerSection)
|
||||
builder.parsePeer(peerLines);
|
||||
else if (!inInterfaceSection)
|
||||
if (!seenInterfaceSection)
|
||||
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
|
||||
Reason.MISSING_SECTION, null);
|
||||
// Combine all [Interface] sections in the file.
|
||||
+8
-5
@@ -5,7 +5,8 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.os.Build;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
@@ -13,11 +14,12 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for creating instances of {@link InetAddress}.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetAddresses {
|
||||
@Nullable private static final Method PARSER_METHOD;
|
||||
private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
|
||||
@@ -25,7 +27,7 @@ public final class InetAddresses {
|
||||
static {
|
||||
Method m = null;
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
|
||||
// noinspection JavaReflectionMemberAccess
|
||||
m = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final Exception ignored) {
|
||||
@@ -33,7 +35,8 @@ public final class InetAddresses {
|
||||
PARSER_METHOD = m;
|
||||
}
|
||||
|
||||
private InetAddresses() { }
|
||||
private InetAddresses() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
|
||||
@@ -45,7 +48,7 @@ public final class InetAddresses {
|
||||
if (address.isEmpty())
|
||||
throw new ParseException(InetAddress.class, address, "Empty address");
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
|
||||
return android.net.InetAddresses.parseNumericAddress(address);
|
||||
else if (PARSER_METHOD != null)
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
+4
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import org.threeten.bp.Duration;
|
||||
import org.threeten.bp.Instant;
|
||||
@@ -17,6 +17,7 @@ import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
|
||||
@@ -25,6 +26,7 @@ import java9.util.Optional;
|
||||
* <p>
|
||||
* Instances of this class are externally immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetEndpoint {
|
||||
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
|
||||
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
|
||||
@@ -49,7 +51,7 @@ public final class InetEndpoint {
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
throw new ParseException(InetEndpoint.class, endpoint, e);
|
||||
}
|
||||
if (uri.getPort() < 0 || uri.getPort() > 65535)
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
|
||||
+4
-1
@@ -5,6 +5,8 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@@ -13,6 +15,7 @@ import java.net.InetAddress;
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetNetwork {
|
||||
private final InetAddress address;
|
||||
private final int mask;
|
||||
@@ -44,7 +47,7 @@ public final class InetNetwork {
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
if (rawMask > maxMask)
|
||||
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
|
||||
final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
final int mask = rawMask >= 0 ? rawMask : maxMask;
|
||||
return new InetNetwork(address, mask);
|
||||
}
|
||||
|
||||
+3
-2
@@ -5,14 +5,13 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.crypto.KeyPair;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collection;
|
||||
@@ -23,6 +22,7 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Optional;
|
||||
import java9.util.stream.Collectors;
|
||||
@@ -35,6 +35,7 @@ import java9.util.stream.StreamSupport;
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Interface {
|
||||
private static final int MAX_UDP_PORT = 65535;
|
||||
private static final int MIN_UDP_PORT = 0;
|
||||
+4
@@ -5,10 +5,14 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class ParseException extends Exception {
|
||||
private final Class<?> parsingClass;
|
||||
private final CharSequence text;
|
||||
+3
-2
@@ -5,13 +5,12 @@
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -20,6 +19,7 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -28,6 +28,7 @@ import java9.util.Optional;
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Peer {
|
||||
private final Set<InetNetwork> allowedIps;
|
||||
private final Optional<InetEndpoint> endpoint;
|
||||
+5
-2
@@ -6,10 +6,12 @@
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of the Curve25519 elliptic curve algorithm.
|
||||
* <p>
|
||||
@@ -25,7 +27,8 @@ import java.util.Arrays;
|
||||
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
||||
*/
|
||||
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||
public final class Curve25519 {
|
||||
@NonNullForAll
|
||||
final class Curve25519 {
|
||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||
private static final int NUM_LIMBS_255BIT = 10;
|
||||
private static final int NUM_LIMBS_510BIT = 20;
|
||||
+20
-18
@@ -6,6 +6,7 @@
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException.Type;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
@@ -18,6 +19,7 @@ import java.util.Arrays;
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@SuppressWarnings("MagicNumber")
|
||||
@NonNullForAll
|
||||
public final class Key {
|
||||
private final byte[] key;
|
||||
|
||||
@@ -202,6 +204,16 @@ public final class Key {
|
||||
return new Key(publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (obj == null || obj.getClass() != getClass())
|
||||
return false;
|
||||
final Key other = (Key) obj;
|
||||
return MessageDigest.isEqual(key, other.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key as an array of bytes.
|
||||
*
|
||||
@@ -212,6 +224,14 @@ public final class Key {
|
||||
return Arrays.copyOf(key, key.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length / 4; ++i)
|
||||
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the key to base64.
|
||||
*
|
||||
@@ -248,24 +268,6 @@ public final class Key {
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length / 4; ++i)
|
||||
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (obj == null || obj.getClass() != getClass())
|
||||
return false;
|
||||
final Key other = (Key) obj;
|
||||
return MessageDigest.isEqual(key, other.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported formats for encoding a WireGuard key.
|
||||
*/
|
||||
+3
@@ -5,11 +5,14 @@
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
|
||||
* data inappropriate for the format). The format being parsed can be accessed with the
|
||||
* {@link #getFormat} method.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class KeyFormatException extends Exception {
|
||||
private final Key.Format format;
|
||||
private final Type type;
|
||||
+3
@@ -5,11 +5,14 @@
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* Represents a Curve25519 key pair as used by WireGuard.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class KeyPair {
|
||||
private final Key privateKey;
|
||||
private final Key publicKey;
|
||||
+5
-2
@@ -5,7 +5,6 @@
|
||||
|
||||
package com.wireguard.util;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -13,14 +12,18 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.meta.TypeQualifierDefault;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
|
||||
/**
|
||||
* This annotation can be applied to a package, class or method to indicate that all
|
||||
* class fields and method parameters and return values in that element are nonnull
|
||||
* by default unless overridden.
|
||||
*/
|
||||
@Documented
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
@Nonnull
|
||||
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
public @interface NonNullForAll {
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.config.BadConfigException.Location;
|
||||
import com.wireguard.config.BadConfigException.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BadConfigExceptionTest {
|
||||
private static final String[] CONFIG_NAMES = {
|
||||
"invalid-key",
|
||||
"invalid-number",
|
||||
"invalid-value",
|
||||
"missing-attribute",
|
||||
"missing-section",
|
||||
"syntax-error",
|
||||
"unknown-attribute",
|
||||
"unknown-section"
|
||||
};
|
||||
private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
|
||||
|
||||
@BeforeClass
|
||||
public static void readConfigs() {
|
||||
for (final String config: CONFIG_NAMES) {
|
||||
CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void closeStreams() {
|
||||
for (final InputStream inputStream : CONFIG_MAP.values()) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (final IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_INVALID_KEY_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("invalid-key"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.INVALID_KEY);
|
||||
assertEquals(e.getLocation(), Location.PUBLIC_KEY);
|
||||
assertEquals(e.getSection(), Section.PEER);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException thrown during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_INVALID_NUMBER_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("invalid-number"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.INVALID_NUMBER);
|
||||
assertEquals(e.getLocation(), Location.PERSISTENT_KEEPALIVE);
|
||||
assertEquals(e.getSection(), Section.PEER);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException thrown during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_INVALID_VALUE_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("invalid-value"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.INVALID_VALUE);
|
||||
assertEquals(e.getLocation(), Location.DNS);
|
||||
assertEquals(e.getSection(), Section.INTERFACE);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_MISSING_ATTRIBUTE_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("missing-attribute"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.MISSING_ATTRIBUTE);
|
||||
assertEquals(e.getLocation(), Location.PUBLIC_KEY);
|
||||
assertEquals(e.getSection(), Section.PEER);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_MISSING_SECTION_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("missing-section"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.MISSING_SECTION);
|
||||
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||
assertEquals(e.getSection(), Section.CONFIG);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_SYNTAX_ERROR_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("syntax-error"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.SYNTAX_ERROR);
|
||||
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||
assertEquals(e.getSection(), Section.PEER);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_UNKNOWN_ATTRIBUTE_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("unknown-attribute"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.UNKNOWN_ATTRIBUTE);
|
||||
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||
assertEquals(e.getSection(), Section.PEER);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throws_correctly_with_UNKNOWN_SECTION_reason() {
|
||||
try {
|
||||
Config.parse(CONFIG_MAP.get("unknown-section"));
|
||||
fail("Config parsing must fail in this test");
|
||||
} catch (final BadConfigException e) {
|
||||
assertEquals(e.getReason(), Reason.UNKNOWN_SECTION);
|
||||
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||
assertEquals(e.getSection(), Section.CONFIG);
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
fail("IOException throwing during test");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ConfigTest {
|
||||
|
||||
@Test
|
||||
public void valid_config_parses_correctly() throws IOException, ParseException {
|
||||
Config config = null;
|
||||
final Collection<InetNetwork> expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
|
||||
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
|
||||
config = Config.parse(is);
|
||||
} catch (final BadConfigException e) {
|
||||
fail("'working.conf' should never fail to parse");
|
||||
}
|
||||
assertNotNull("config cannot be null after parsing", config);
|
||||
assertTrue(
|
||||
"No applications should be excluded by default",
|
||||
config.getInterface().getExcludedApplications().isEmpty()
|
||||
);
|
||||
assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
|
||||
assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
|
||||
assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
|
||||
}
|
||||
|
||||
@Test(expected = BadConfigException.class)
|
||||
public void invalid_config_throws() throws IOException, BadConfigException {
|
||||
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
|
||||
Config.parse(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n=
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
|
||||
[Peer]
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
AllowedIPs = 0.0.0.0/0,::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0L
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0,yes
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,8 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
@@ -0,0 +1,5 @@
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint =
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
DontLetTheFeelingFade = 1
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peers]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -0,0 +1,9 @@
|
||||
[Interface]
|
||||
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||
DNS = 192.0.2.0
|
||||
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||
[Peer]
|
||||
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||
Endpoint = 192.0.2.1:51820
|
||||
PersistentKeepalive = 0
|
||||
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||
@@ -20,7 +20,7 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
DESIRED_GO_VERSION := 1.13.7
|
||||
DESIRED_GO_VERSION := 1.14.1
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
+28
-28
@@ -1,4 +1,4 @@
|
||||
From b19623e7673a4d6743745382d5d38751b64e011d Mon Sep 17 00:00:00 2001
|
||||
From e44f456f1d0e429e08afed64a161175ff493f3ac Mon Sep 17 00:00:00 2001
|
||||
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
|
||||
Date: Wed, 27 Feb 2019 05:05:44 +0100
|
||||
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
|
||||
@@ -28,10 +28,10 @@ Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
|
||||
8 files changed, 11 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
|
||||
index 72c43bd9da..daadfe32a9 100644
|
||||
index 1b28098ad9..46b7071ed8 100644
|
||||
--- a/src/runtime/sys_linux_386.s
|
||||
+++ b/src/runtime/sys_linux_386.s
|
||||
@@ -288,13 +288,13 @@ noswitch:
|
||||
@@ -317,13 +317,13 @@ noswitch:
|
||||
|
||||
LEAL 8(SP), BX // &ts (struct timespec)
|
||||
MOVL BX, 4(SP)
|
||||
@@ -48,10 +48,10 @@ index 72c43bd9da..daadfe32a9 100644
|
||||
INVOKE_SYSCALL
|
||||
|
||||
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
|
||||
index 5c300f553d..e4a6f12ec6 100644
|
||||
index 58d3bc54b4..4bb9bde3d0 100644
|
||||
--- a/src/runtime/sys_linux_amd64.s
|
||||
+++ b/src/runtime/sys_linux_amd64.s
|
||||
@@ -261,7 +261,7 @@ noswitch:
|
||||
@@ -293,7 +293,7 @@ noswitch:
|
||||
MOVQ runtime·vdsoClockgettimeSym(SB), AX
|
||||
CMPQ AX, $0
|
||||
JEQ fallback
|
||||
@@ -61,7 +61,7 @@ index 5c300f553d..e4a6f12ec6 100644
|
||||
CALL AX
|
||||
MOVQ 0(SP), AX // sec
|
||||
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
|
||||
index 9c7398451c..61b6cd91f6 100644
|
||||
index e103da56dc..0b872b90a6 100644
|
||||
--- a/src/runtime/sys_linux_arm.s
|
||||
+++ b/src/runtime/sys_linux_arm.s
|
||||
@@ -11,7 +11,7 @@
|
||||
@@ -73,17 +73,17 @@ index 9c7398451c..61b6cd91f6 100644
|
||||
|
||||
// for EABI, as we don't support OABI
|
||||
#define SYS_BASE 0x0
|
||||
@@ -291,7 +291,7 @@ noswitch:
|
||||
@@ -345,7 +345,7 @@ noswitch:
|
||||
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
|
||||
MOVW runtime·vdsoClockgettimeSym(SB), R2
|
||||
CMP $0, R2
|
||||
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
|
||||
index 2835b6ca1c..346ca9cfce 100644
|
||||
index b9588cec30..e444d50df4 100644
|
||||
--- a/src/runtime/sys_linux_arm64.s
|
||||
+++ b/src/runtime/sys_linux_arm64.s
|
||||
@@ -13,7 +13,7 @@
|
||||
@@ -95,7 +95,7 @@ index 2835b6ca1c..346ca9cfce 100644
|
||||
|
||||
#define SYS_exit 93
|
||||
#define SYS_read 63
|
||||
@@ -247,7 +247,7 @@ noswitch:
|
||||
@@ -297,7 +297,7 @@ noswitch:
|
||||
BIC $15, R1
|
||||
MOVD R1, RSP
|
||||
|
||||
@@ -103,59 +103,59 @@ index 2835b6ca1c..346ca9cfce 100644
|
||||
+ 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 33ed1050c2..59a5be179c 100644
|
||||
index 723cfe43d9..edd7a195eb 100644
|
||||
--- a/src/runtime/sys_linux_mips64x.s
|
||||
+++ b/src/runtime/sys_linux_mips64x.s
|
||||
@@ -189,7 +189,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
|
||||
RET
|
||||
@@ -278,7 +278,7 @@ noswitch:
|
||||
AND $~15, R1 // Align for C code
|
||||
MOVV R1, R29
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||
MOVV $0(R29), R5
|
||||
MOVV $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
|
||||
MOVV runtime·vdsoClockgettimeSym(SB), R25
|
||||
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
|
||||
index 6e539fbc6f..55b2bf7156 100644
|
||||
index 15893a7a28..f3edf9a83a 100644
|
||||
--- a/src/runtime/sys_linux_mipsx.s
|
||||
+++ b/src/runtime/sys_linux_mipsx.s
|
||||
@@ -194,7 +194,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
|
||||
@@ -235,7 +235,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$8-12
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$8-8
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
|
||||
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||
MOVW $4(R29), R5
|
||||
MOVW $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
|
||||
index 13d23156bd..f67e5062aa 100644
|
||||
index 8629fe3233..2402e2623a 100644
|
||||
--- a/src/runtime/sys_linux_ppc64x.s
|
||||
+++ b/src/runtime/sys_linux_ppc64x.s
|
||||
@@ -204,7 +204,7 @@ fallback:
|
||||
@@ -233,7 +233,7 @@ fallback:
|
||||
JMP finish
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16
|
||||
- MOVD $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVD $7, R3 // CLOCK_BOOTTIME
|
||||
|
||||
MOVD R1, R15 // R15 is unchanged by C code
|
||||
MOVD g_m(g), R21 // R21 = m
|
||||
diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s
|
||||
index 58b36dff0a..cb92e9a402 100644
|
||||
index c15a1d5364..f52c4d5098 100644
|
||||
--- a/src/runtime/sys_linux_s390x.s
|
||||
+++ b/src/runtime/sys_linux_s390x.s
|
||||
@@ -180,7 +180,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
|
||||
@@ -207,7 +207,7 @@ TEXT runtime·walltime1(SB),NOSPLIT,$16
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
TEXT runtime·nanotime1(SB),NOSPLIT,$16
|
||||
- MOVW $1, R2 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R2 // CLOCK_BOOTTIME
|
||||
MOVD $tp-16(SP), R3
|
||||
MOVW $SYS_clock_gettime, R1
|
||||
SYSCALL
|
||||
--
|
||||
2.23.0
|
||||
2.25.1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user