mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-06-02 06:23:39 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4116c83624 | |||
| 762e82c71f | |||
| 8db4f52988 | |||
| 728c92916f | |||
| 9b3f6c0e60 | |||
| 1377eff79c | |||
| 108ae68256 | |||
| dd8c98db16 | |||
| c4b568a619 | |||
| 24ef24e68b | |||
| 704964c22a | |||
| 7aea733671 | |||
| ade016a11c | |||
| c1af1fb8d6 | |||
| 2e9f9d3650 | |||
| f207d7bfd7 | |||
| e71423f46d | |||
| 511397af2f | |||
| 6d654aff91 |
@@ -0,0 +1,141 @@
|
||||
name: Android build (signed AAB / debug APK)
|
||||
run-name: "Android • ${{ inputs.target }} • ${{ inputs.ref || github.ref_name }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: "What to build"
|
||||
type: choice
|
||||
required: true
|
||||
default: release_aab
|
||||
options:
|
||||
- release_aab
|
||||
- debug_apk
|
||||
ref:
|
||||
description: "Git ref (branch/tag/sha)"
|
||||
required: false
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build (${{ inputs.target }})"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Configure build
|
||||
id: cfg
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ inputs.target }}" in
|
||||
release_aab)
|
||||
echo "gradle_task=bundleRelease" >> "$GITHUB_OUTPUT"
|
||||
echo "kind=aab" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_name=release-aab" >> "$GITHUB_OUTPUT"
|
||||
echo "needs_signing=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
debug_apk)
|
||||
echo "gradle_task=assembleDebug" >> "$GITHUB_OUTPUT"
|
||||
echo "kind=apk" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_name=debug-apk" >> "$GITHUB_OUTPUT"
|
||||
echo "needs_signing=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown target: ${{ inputs.target }}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Decode keystore from secret
|
||||
if: ${{ steps.cfg.outputs.needs_signing == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${KEYSTORE_B64}" ]]; then
|
||||
echo "Missing secret ANDROID_KEYSTORE_BASE64" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${KEYSTORE_B64}" | base64 --decode > release.keystore.jks
|
||||
echo "KEYSTORE_PATH=${PWD}/release.keystore.jks" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chmod +x ./gradlew
|
||||
./gradlew --no-daemon --stacktrace ${{ steps.cfg.outputs.gradle_task }}
|
||||
|
||||
- name: Locate built artifact
|
||||
id: locate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ steps.cfg.outputs.kind }}" == "aab" ]]; then
|
||||
FILE="./ui/build/outputs/bundle/release/ui-release.aab"
|
||||
else
|
||||
FILE="./ui/build/outputs/apk/debug/ui-debug.apk"
|
||||
fi
|
||||
|
||||
if [[ -z "${FILE}" ]]; then
|
||||
echo "Built file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "built_file=${FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sign AAB
|
||||
if: ${{ steps.cfg.outputs.needs_signing == 'true' }}
|
||||
id: sign
|
||||
shell: bash
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
AAB="${{ steps.locate.outputs.built_file }}"
|
||||
SIGNED_AAB="${AAB%.aab}-signed.aab"
|
||||
|
||||
if [[ -z "${KEYSTORE_PASSWORD}" || -z "${KEY_ALIAS}" ]]; then
|
||||
echo "Missing one of secrets: ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jarsigner \
|
||||
-keystore "${KEYSTORE_PATH}" \
|
||||
-storepass "${KEYSTORE_PASSWORD}" \
|
||||
-signedjar "${SIGNED_AAB}" \
|
||||
"${AAB}" \
|
||||
"${KEY_ALIAS}"
|
||||
|
||||
echo "final_file=${SIGNED_AAB}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.cfg.outputs.artifact_name }}
|
||||
path: ${{ steps.sign.outputs.final_file || steps.locate.outputs.built_file }}
|
||||
if-no-files-found: error
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
amneziawgVersionCode=4
|
||||
amneziawgVersionName=1.1.1
|
||||
amneziawgVersionCode=10
|
||||
amneziawgVersionName=2.0.0
|
||||
amneziawgPackageName=org.amnezia.awg
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
@@ -17,7 +17,14 @@ android.useAndroidX=true
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
org.gradle.jvmargs=-Xmx1536m --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
|
||||
# Use Java 17+ for Gradle. Set JAVA_HOME or org.gradle.java.home if needed.
|
||||
# On macOS with Homebrew: org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
||||
# On Linux: org.gradle.java.home=/usr/lib/jvm/java-17-openjdk
|
||||
|
||||
# Kotlin daemon JVM args to allow KAPT access to internal JDK modules
|
||||
kotlin.daemon.jvmargs=-Xmx1536m --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
|
||||
|
||||
# Turn off AP discovery in compile path to enable compile avoidance
|
||||
kapt.include.compile.classpath=false
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ include(":tunnel")
|
||||
include(":ui")
|
||||
|
||||
configure<SettingsExtension> {
|
||||
buildToolsVersion = "34.0.0"
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "35.0.0"
|
||||
compileSdk = 35
|
||||
minSdk = 24
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ public final class AwgQuickBackend implements Backend {
|
||||
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
|
||||
private final ToolsInstaller toolsInstaller;
|
||||
private boolean multipleTunnels;
|
||||
@Nullable private Thread statusThread;
|
||||
@Nullable private StatusCallback statusCallback;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
|
||||
public AwgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
@@ -78,6 +81,106 @@ public final class AwgQuickBackend implements Backend {
|
||||
return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastHandshake(final Tunnel tunnel) {
|
||||
if (getState(tunnel) != State.UP) {
|
||||
return -3; // Tunnel not active
|
||||
}
|
||||
final Collection<String> output = new ArrayList<>();
|
||||
try {
|
||||
if (rootShell.run(output, String.format("awg show '%s' latest-handshakes", tunnel.getName())) != 0) {
|
||||
Log.e(TAG, "Failed to get latest handshakes");
|
||||
return -2;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to get latest handshakes", e);
|
||||
return -2;
|
||||
}
|
||||
for (final String line : output) {
|
||||
final String[] parts = line.split("\\t");
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
return Long.parseLong(parts[1]);
|
||||
} catch (final NumberFormatException ignored) {
|
||||
Log.e(TAG, "Failed to parse handshake time");
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "No handshake time found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback to be notified when connection status changes.
|
||||
*
|
||||
* @param callback The callback to invoke on status change
|
||||
*/
|
||||
public void setStatusCallback(@Nullable final StatusCallback callback) {
|
||||
this.statusCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a background thread to poll handshake status and determine connection state.
|
||||
* This is called after tunnel creation to wait for the first successful handshake.
|
||||
*/
|
||||
private void launchStatusJob() {
|
||||
stopStatusJob();
|
||||
Log.d(TAG, "Launch status job");
|
||||
statusThread = new Thread(() -> {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
final long lastHandshake = getLastHandshake(currentTunnel);
|
||||
|
||||
// Check if tunnel is no longer active (race condition protection)
|
||||
if (lastHandshake == -3L) {
|
||||
Log.d(TAG, "Tunnel is no longer active, stopping status job");
|
||||
break;
|
||||
}
|
||||
|
||||
// 0 means no handshake yet, wait and retry
|
||||
if (lastHandshake == 0L) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only positive handshake time indicates successful connection
|
||||
// -1 may be returned if unable to parse output (doesn't mean no connection)
|
||||
// -2 indicates command execution error (also doesn't mean no connection)
|
||||
if (lastHandshake > 0L) {
|
||||
if (statusCallback != null) {
|
||||
statusCallback.onStatusChanged(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// For -1 or -2, retry after delay instead of reporting disconnected
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
statusThread = null;
|
||||
}, "StatusJob");
|
||||
statusThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the status polling thread if running.
|
||||
*/
|
||||
private void stopStatusJob() {
|
||||
if (statusThread != null) {
|
||||
statusThread.interrupt();
|
||||
statusThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
@@ -185,10 +288,15 @@ public final class AwgQuickBackend implements Backend {
|
||||
if (result != 0)
|
||||
throw new BackendException(Reason.AWG_QUICK_CONFIG_ERROR_CODE, result);
|
||||
|
||||
if (state == State.UP)
|
||||
if (state == State.UP) {
|
||||
runningConfigs.put(tunnel, config);
|
||||
else
|
||||
currentTunnel = tunnel;
|
||||
launchStatusJob();
|
||||
} else {
|
||||
stopStatusJob();
|
||||
runningConfigs.remove(tunnel);
|
||||
currentTunnel = null;
|
||||
}
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ public interface Backend {
|
||||
*/
|
||||
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Get the last handshake time for a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to examine.
|
||||
* @return Last handshake time in seconds (>=0 means valid handshake time, 0 means no handshake yet),
|
||||
* -1 if parsing failed, -2 on command execution error, -3 if tunnel not active.
|
||||
* @throws Exception Exception raised when retrieving handshake time.
|
||||
*/
|
||||
long getLastHandshake(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
|
||||
* statistics object will be filled with zero values.
|
||||
@@ -64,4 +74,11 @@ public interface Backend {
|
||||
* @throws Exception Exception raised while changing state.
|
||||
*/
|
||||
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||
|
||||
/**
|
||||
* Set the callback for status changes (e.g. handshake / connection state).
|
||||
*
|
||||
* @param callback The callback to invoke on status changes, or null to clear.
|
||||
*/
|
||||
void setStatusCallback(@Nullable StatusCallback callback);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ public final class GoBackend implements Backend {
|
||||
@Nullable private Config currentConfig;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
@Nullable private Thread statusThread;
|
||||
@Nullable private StatusCallback statusCallback;
|
||||
|
||||
/**
|
||||
* Public constructor for GoBackend.
|
||||
@@ -169,6 +171,108 @@ public final class GoBackend implements Backend {
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the last handshake time for a given {@link Tunnel}.
|
||||
*
|
||||
* @param tunnel The tunnel to retrieve the last handshake time for.
|
||||
* @return Last handshake time in seconds (>=0), -1 if no handshake found, -2 on error, -3 if tunnel not active.
|
||||
*/
|
||||
@Override
|
||||
public long getLastHandshake(final Tunnel tunnel) {
|
||||
if (tunnel != currentTunnel || currentTunnelHandle == -1)
|
||||
return -3; // Tunnel not active
|
||||
final String config = awgGetConfig(currentTunnelHandle);
|
||||
if (config == null) {
|
||||
Log.e(TAG, "Failed to get tunnel config");
|
||||
return -2;
|
||||
}
|
||||
|
||||
for (final String line : config.split("\\n")) {
|
||||
if (line.startsWith("last_handshake_time_sec=")) {
|
||||
try {
|
||||
return Long.parseLong(line.substring(24));
|
||||
} catch (final NumberFormatException ignored) {
|
||||
Log.e(TAG, "Failed to parse last_handshake_time_sec");
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "Failed to get last_handshake_time_sec");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback to be notified when connection status changes.
|
||||
*
|
||||
* @param callback The callback to invoke on status change
|
||||
*/
|
||||
public void setStatusCallback(@Nullable final StatusCallback callback) {
|
||||
this.statusCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a background thread to poll handshake status and determine connection state.
|
||||
* This is called after tunnel creation to wait for the first successful handshake.
|
||||
*/
|
||||
private void launchStatusJob() {
|
||||
stopStatusJob();
|
||||
Log.d(TAG, "Launch status job");
|
||||
statusThread = new Thread(() -> {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
final long lastHandshake = getLastHandshake(currentTunnel);
|
||||
|
||||
// Check if tunnel is no longer active (race condition protection)
|
||||
if (lastHandshake == -3L) {
|
||||
Log.d(TAG, "Tunnel is no longer active, stopping status job");
|
||||
break;
|
||||
}
|
||||
|
||||
// 0 means no handshake yet, wait and retry
|
||||
if (lastHandshake == 0L) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only positive handshake time indicates successful connection
|
||||
// -1 may be returned if unable to parse output (doesn't mean no connection)
|
||||
// -2 indicates command execution error (also doesn't mean no connection)
|
||||
if (lastHandshake > 0L) {
|
||||
if (statusCallback != null) {
|
||||
statusCallback.onStatusChanged(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// For -1 or -2, retry after delay instead of reporting disconnected
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
statusThread = null;
|
||||
}, "StatusJob");
|
||||
statusThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the status polling thread if running.
|
||||
*/
|
||||
private void stopStatusJob() {
|
||||
if (statusThread != null) {
|
||||
statusThread.interrupt();
|
||||
statusThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the underlying amneziawg-go library.
|
||||
*
|
||||
@@ -324,11 +428,14 @@ public final class GoBackend implements Backend {
|
||||
|
||||
service.protect(awgGetSocketV4(currentTunnelHandle));
|
||||
service.protect(awgGetSocketV6(currentTunnelHandle));
|
||||
|
||||
launchStatusJob();
|
||||
} else {
|
||||
if (currentTunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down");
|
||||
return;
|
||||
}
|
||||
stopStatusJob();
|
||||
int handleToClose = currentTunnelHandle;
|
||||
currentTunnel = null;
|
||||
currentTunnelHandle = -1;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.amnezia.awg.backend;
|
||||
|
||||
/**
|
||||
* Callback for status changes detected by the status polling job.
|
||||
*/
|
||||
public interface StatusCallback {
|
||||
/**
|
||||
* Called when connection status is determined.
|
||||
*
|
||||
* @param connected true if handshake was successful (connected), false if disconnected
|
||||
*/
|
||||
void onStatusChanged(boolean connected);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
@NonNullForAll
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*(.*?)\\s*$");
|
||||
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
|
||||
|
||||
private final String key;
|
||||
|
||||
@@ -84,10 +84,17 @@ public class BadConfigException extends Exception {
|
||||
JUNK_PACKET_MAX_SIZE("JunkPacketMaxSize"),
|
||||
INIT_PACKET_JUNK_SIZE("InitPacketJunkSize"),
|
||||
RESPONSE_PACKET_JUNK_SIZE("ResponsePacketJunkSize"),
|
||||
COOKIE_REPLY_PACKET_JUNK_SIZE("CookieReplyPacketJunkSize"),
|
||||
TRANSPORT_PACKET_JUNK_SIZE("TransportPacketJunkSize"),
|
||||
INIT_PACKET_MAGIC_HEADER("InitPacketMagicHeader"),
|
||||
RESPONSE_PACKET_MAGIC_HEADER("ResponsePacketMagicHeader"),
|
||||
UNDERLOAD_PACKET_MAGIC_HEADER("UnderloadPacketMagicHeader"),
|
||||
TRANSPORT_PACKET_MAGIC_HEADER("TransportPacketMagicHeader");
|
||||
TRANSPORT_PACKET_MAGIC_HEADER("TransportPacketMagicHeader"),
|
||||
SPECIAL_JUNK_I1("SpecialJunkI1"),
|
||||
SPECIAL_JUNK_I2("SpecialJunkI2"),
|
||||
SPECIAL_JUNK_I3("SpecialJunkI3"),
|
||||
SPECIAL_JUNK_I4("SpecialJunkI4"),
|
||||
SPECIAL_JUNK_I5("SpecialJunkI5");
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
@@ -51,10 +51,17 @@ public final class Interface {
|
||||
private final Optional<Integer> junkPacketMaxSize;
|
||||
private final Optional<Integer> initPacketJunkSize;
|
||||
private final Optional<Integer> responsePacketJunkSize;
|
||||
private final Optional<Long> initPacketMagicHeader;
|
||||
private final Optional<Long> responsePacketMagicHeader;
|
||||
private final Optional<Long> underloadPacketMagicHeader;
|
||||
private final Optional<Long> transportPacketMagicHeader;
|
||||
private final Optional<Integer> cookieReplyPacketJunkSize;
|
||||
private final Optional<Integer> transportPacketJunkSize;
|
||||
private final Optional<String> initPacketMagicHeader;
|
||||
private final Optional<String> responsePacketMagicHeader;
|
||||
private final Optional<String> underloadPacketMagicHeader;
|
||||
private final Optional<String> transportPacketMagicHeader;
|
||||
private final Optional<String> specialJunkI1;
|
||||
private final Optional<String> specialJunkI2;
|
||||
private final Optional<String> specialJunkI3;
|
||||
private final Optional<String> specialJunkI4;
|
||||
private final Optional<String> specialJunkI5;
|
||||
|
||||
private Interface(final Builder builder) {
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
@@ -71,10 +78,17 @@ public final class Interface {
|
||||
junkPacketMaxSize = builder.junkPacketMaxSize;
|
||||
initPacketJunkSize = builder.initPacketJunkSize;
|
||||
responsePacketJunkSize = builder.responsePacketJunkSize;
|
||||
cookieReplyPacketJunkSize = builder.cookieReplyPacketJunkSize;
|
||||
transportPacketJunkSize = builder.transportPacketJunkSize;
|
||||
initPacketMagicHeader = builder.initPacketMagicHeader;
|
||||
responsePacketMagicHeader = builder.responsePacketMagicHeader;
|
||||
underloadPacketMagicHeader = builder.underloadPacketMagicHeader;
|
||||
transportPacketMagicHeader = builder.transportPacketMagicHeader;
|
||||
specialJunkI1 = builder.specialJunkI1;
|
||||
specialJunkI2 = builder.specialJunkI2;
|
||||
specialJunkI3 = builder.specialJunkI3;
|
||||
specialJunkI4 = builder.specialJunkI4;
|
||||
specialJunkI5 = builder.specialJunkI5;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +142,12 @@ public final class Interface {
|
||||
case "s2":
|
||||
builder.parseResponsePacketJunkSize(attribute.getValue());
|
||||
break;
|
||||
case "s3":
|
||||
builder.parseCookieReplyPacketJunkSize(attribute.getValue());
|
||||
break;
|
||||
case "s4":
|
||||
builder.parseTransportPacketJunkSize(attribute.getValue());
|
||||
break;
|
||||
case "h1":
|
||||
builder.parseInitPacketMagicHeader(attribute.getValue());
|
||||
break;
|
||||
@@ -140,6 +160,21 @@ public final class Interface {
|
||||
case "h4":
|
||||
builder.parseTransportPacketMagicHeader(attribute.getValue());
|
||||
break;
|
||||
case "i1":
|
||||
builder.parseSpecialJunkI1(attribute.getValue());
|
||||
break;
|
||||
case "i2":
|
||||
builder.parseSpecialJunkI2(attribute.getValue());
|
||||
break;
|
||||
case "i3":
|
||||
builder.parseSpecialJunkI3(attribute.getValue());
|
||||
break;
|
||||
case "i4":
|
||||
builder.parseSpecialJunkI4(attribute.getValue());
|
||||
break;
|
||||
case "i5":
|
||||
builder.parseSpecialJunkI5(attribute.getValue());
|
||||
break;
|
||||
default:
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
|
||||
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
|
||||
@@ -166,10 +201,17 @@ public final class Interface {
|
||||
&& junkPacketMaxSize.equals(other.junkPacketMaxSize)
|
||||
&& initPacketJunkSize.equals(other.initPacketJunkSize)
|
||||
&& responsePacketJunkSize.equals(other.responsePacketJunkSize)
|
||||
&& cookieReplyPacketJunkSize.equals(other.cookieReplyPacketJunkSize)
|
||||
&& transportPacketJunkSize.equals(other.transportPacketJunkSize)
|
||||
&& initPacketMagicHeader.equals(other.initPacketMagicHeader)
|
||||
&& responsePacketMagicHeader.equals(other.responsePacketMagicHeader)
|
||||
&& underloadPacketMagicHeader.equals(other.underloadPacketMagicHeader)
|
||||
&& transportPacketMagicHeader.equals(other.transportPacketMagicHeader);
|
||||
&& transportPacketMagicHeader.equals(other.transportPacketMagicHeader)
|
||||
&& specialJunkI1.equals(other.specialJunkI1)
|
||||
&& specialJunkI2.equals(other.specialJunkI2)
|
||||
&& specialJunkI3.equals(other.specialJunkI3)
|
||||
&& specialJunkI4.equals(other.specialJunkI4)
|
||||
&& specialJunkI5.equals(other.specialJunkI5);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,12 +336,30 @@ public final class Interface {
|
||||
return responsePacketJunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cookieReplyPacketJunkSize used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the cookieReplyPacketJunkSize, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getCookieReplyPacketJunkSize() {
|
||||
return cookieReplyPacketJunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transportPacketJunkSize used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the transportPacketJunkSize, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getTransportPacketJunkSize() {
|
||||
return transportPacketJunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initPacketMagicHeader used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the initPacketMagicHeader, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Long> getInitPacketMagicHeader() {
|
||||
public Optional<String> getInitPacketMagicHeader() {
|
||||
return initPacketMagicHeader;
|
||||
}
|
||||
|
||||
@@ -308,7 +368,7 @@ public final class Interface {
|
||||
*
|
||||
* @return the responsePacketMagicHeader, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Long> getResponsePacketMagicHeader() {
|
||||
public Optional<String> getResponsePacketMagicHeader() {
|
||||
return responsePacketMagicHeader;
|
||||
}
|
||||
|
||||
@@ -317,7 +377,7 @@ public final class Interface {
|
||||
*
|
||||
* @return the underloadPacketMagicHeader, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Long> getUnderloadPacketMagicHeader() {
|
||||
public Optional<String> getUnderloadPacketMagicHeader() {
|
||||
return underloadPacketMagicHeader;
|
||||
}
|
||||
|
||||
@@ -326,10 +386,55 @@ public final class Interface {
|
||||
*
|
||||
* @return the transportPacketMagicHeader, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Long> getTransportPacketMagicHeader() {
|
||||
public Optional<String> getTransportPacketMagicHeader() {
|
||||
return transportPacketMagicHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specialJunkI1 used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the specialJunkI1, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<String> getSpecialJunkI1() {
|
||||
return specialJunkI1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specialJunkI2 used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the specialJunkI2, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<String> getSpecialJunkI2() {
|
||||
return specialJunkI2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specialJunkI3 used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the specialJunkI3, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<String> getSpecialJunkI3() {
|
||||
return specialJunkI3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specialJunkI4 used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the specialJunkI4, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<String> getSpecialJunkI4() {
|
||||
return specialJunkI4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specialJunkI5 used for the AmneziaWG interface.
|
||||
*
|
||||
* @return the specialJunkI5, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<String> getSpecialJunkI5() {
|
||||
return specialJunkI5;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
@@ -346,10 +451,17 @@ public final class Interface {
|
||||
hash = 31 * hash + junkPacketMaxSize.hashCode();
|
||||
hash = 31 * hash + initPacketJunkSize.hashCode();
|
||||
hash = 31 * hash + responsePacketJunkSize.hashCode();
|
||||
hash = 31 * hash + cookieReplyPacketJunkSize.hashCode();
|
||||
hash = 31 * hash + transportPacketJunkSize.hashCode();
|
||||
hash = 31 * hash + initPacketMagicHeader.hashCode();
|
||||
hash = 31 * hash + responsePacketMagicHeader.hashCode();
|
||||
hash = 31 * hash + underloadPacketMagicHeader.hashCode();
|
||||
hash = 31 * hash + transportPacketMagicHeader.hashCode();
|
||||
hash = 31 * hash + specialJunkI1.hashCode();
|
||||
hash = 31 * hash + specialJunkI2.hashCode();
|
||||
hash = 31 * hash + specialJunkI3.hashCode();
|
||||
hash = 31 * hash + specialJunkI4.hashCode();
|
||||
hash = 31 * hash + specialJunkI5.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -394,10 +506,17 @@ public final class Interface {
|
||||
junkPacketMaxSize.ifPresent(jmax -> sb.append("Jmax = ").append(jmax).append('\n'));
|
||||
initPacketJunkSize.ifPresent(s1 -> sb.append("S1 = ").append(s1).append('\n'));
|
||||
responsePacketJunkSize.ifPresent(s2 -> sb.append("S2 = ").append(s2).append('\n'));
|
||||
cookieReplyPacketJunkSize.ifPresent(s3 -> sb.append("S3 = ").append(s3).append('\n'));
|
||||
transportPacketJunkSize.ifPresent(s4 -> sb.append("S4 = ").append(s4).append('\n'));
|
||||
initPacketMagicHeader.ifPresent(h1 -> sb.append("H1 = ").append(h1).append('\n'));
|
||||
responsePacketMagicHeader.ifPresent(h2 -> sb.append("H2 = ").append(h2).append('\n'));
|
||||
underloadPacketMagicHeader.ifPresent(h3 -> sb.append("H3 = ").append(h3).append('\n'));
|
||||
transportPacketMagicHeader.ifPresent(h4 -> sb.append("H4 = ").append(h4).append('\n'));
|
||||
specialJunkI1.ifPresent(i1 -> sb.append("I1 = ").append(i1).append('\n'));
|
||||
specialJunkI2.ifPresent(i2 -> sb.append("I2 = ").append(i2).append('\n'));
|
||||
specialJunkI3.ifPresent(i3 -> sb.append("I3 = ").append(i3).append('\n'));
|
||||
specialJunkI4.ifPresent(i4 -> sb.append("I4 = ").append(i4).append('\n'));
|
||||
specialJunkI5.ifPresent(i5 -> sb.append("I5 = ").append(i5).append('\n'));
|
||||
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -417,10 +536,17 @@ public final class Interface {
|
||||
junkPacketMaxSize.ifPresent(jmax -> sb.append("jmax=").append(jmax).append('\n'));
|
||||
initPacketJunkSize.ifPresent(s1 -> sb.append("s1=").append(s1).append('\n'));
|
||||
responsePacketJunkSize.ifPresent(s2 -> sb.append("s2=").append(s2).append('\n'));
|
||||
cookieReplyPacketJunkSize.ifPresent(s3 -> sb.append("s3=").append(s3).append('\n'));
|
||||
transportPacketJunkSize.ifPresent(s4 -> sb.append("s4=").append(s4).append('\n'));
|
||||
initPacketMagicHeader.ifPresent(h1 -> sb.append("h1=").append(h1).append('\n'));
|
||||
responsePacketMagicHeader.ifPresent(h2 -> sb.append("h2=").append(h2).append('\n'));
|
||||
underloadPacketMagicHeader.ifPresent(h3 -> sb.append("h3=").append(h3).append('\n'));
|
||||
transportPacketMagicHeader.ifPresent(h4 -> sb.append("h4=").append(h4).append('\n'));
|
||||
specialJunkI1.ifPresent(i1 -> sb.append("i1=").append(i1).append('\n'));
|
||||
specialJunkI2.ifPresent(i2 -> sb.append("i2=").append(i2).append('\n'));
|
||||
specialJunkI3.ifPresent(i3 -> sb.append("i3=").append(i3).append('\n'));
|
||||
specialJunkI4.ifPresent(i4 -> sb.append("i4=").append(i4).append('\n'));
|
||||
specialJunkI5.ifPresent(i5 -> sb.append("i5=").append(i5).append('\n'));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -453,13 +579,27 @@ public final class Interface {
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> responsePacketJunkSize = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Long> initPacketMagicHeader = Optional.empty();
|
||||
private Optional<Integer> cookieReplyPacketJunkSize = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Long> responsePacketMagicHeader = Optional.empty();
|
||||
private Optional<Integer> transportPacketJunkSize = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Long> underloadPacketMagicHeader = Optional.empty();
|
||||
private Optional<String> initPacketMagicHeader = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Long> transportPacketMagicHeader = Optional.empty();
|
||||
private Optional<String> responsePacketMagicHeader = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> underloadPacketMagicHeader = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> transportPacketMagicHeader = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> specialJunkI1 = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> specialJunkI2 = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> specialJunkI3 = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> specialJunkI4 = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<String> specialJunkI5 = Optional.empty();
|
||||
|
||||
|
||||
public Builder addAddress(final InetNetwork address) {
|
||||
@@ -613,37 +753,101 @@ public final class Interface {
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseInitPacketMagicHeader(final String initPacketMagicHeader) throws BadConfigException {
|
||||
public Builder parseCookieReplyPacketJunkSize(final String cookieReplyPacketJunkSize) throws BadConfigException {
|
||||
try {
|
||||
return setInitPacketMagicHeader(Long.parseLong(initPacketMagicHeader));
|
||||
return setCookieReplyPacketJunkSize(Integer.parseInt(cookieReplyPacketJunkSize));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.INIT_PACKET_MAGIC_HEADER, initPacketMagicHeader, e);
|
||||
throw new BadConfigException(Section.INTERFACE, Location.COOKIE_REPLY_PACKET_JUNK_SIZE, cookieReplyPacketJunkSize, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseTransportPacketJunkSize(final String transportPacketJunkSize) throws BadConfigException {
|
||||
try {
|
||||
return setTransportPacketJunkSize(Integer.parseInt(transportPacketJunkSize));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TRANSPORT_PACKET_JUNK_SIZE, transportPacketJunkSize, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseInitPacketMagicHeader(final String initPacketMagicHeader) throws BadConfigException {
|
||||
if (initPacketMagicHeader == null || initPacketMagicHeader.trim().isEmpty()) {
|
||||
this.initPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.initPacketMagicHeader = Optional.of(initPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseResponsePacketMagicHeader(final String responsePacketMagicHeader) throws BadConfigException {
|
||||
try {
|
||||
|
||||
return setResponsePacketMagicHeader(Long.parseLong(responsePacketMagicHeader));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.RESPONSE_PACKET_MAGIC_HEADER, responsePacketMagicHeader, e);
|
||||
if (responsePacketMagicHeader == null || responsePacketMagicHeader.trim().isEmpty()) {
|
||||
this.responsePacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.responsePacketMagicHeader = Optional.of(responsePacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseUnderloadPacketMagicHeader(final String underloadPacketMagicHeader) throws BadConfigException {
|
||||
try {
|
||||
return setUnderloadPacketMagicHeader(Long.parseLong(underloadPacketMagicHeader));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.UNDERLOAD_PACKET_MAGIC_HEADER, underloadPacketMagicHeader, e);
|
||||
if (underloadPacketMagicHeader == null || underloadPacketMagicHeader.trim().isEmpty()) {
|
||||
this.underloadPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.underloadPacketMagicHeader = Optional.of(underloadPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseTransportPacketMagicHeader(final String transportPacketMagicHeader) throws BadConfigException {
|
||||
try {
|
||||
return setTransportPacketMagicHeader(Long.parseLong(transportPacketMagicHeader));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TRANSPORT_PACKET_MAGIC_HEADER, transportPacketMagicHeader, e);
|
||||
if (transportPacketMagicHeader == null || transportPacketMagicHeader.trim().isEmpty()) {
|
||||
this.transportPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.transportPacketMagicHeader = Optional.of(transportPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseSpecialJunkI1(final String specialJunkI1) throws BadConfigException {
|
||||
if (specialJunkI1 == null || specialJunkI1.trim().isEmpty()) {
|
||||
this.specialJunkI1 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI1 = Optional.of(specialJunkI1.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseSpecialJunkI2(final String specialJunkI2) throws BadConfigException {
|
||||
if (specialJunkI2 == null || specialJunkI2.trim().isEmpty()) {
|
||||
this.specialJunkI2 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI2 = Optional.of(specialJunkI2.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseSpecialJunkI3(final String specialJunkI3) throws BadConfigException {
|
||||
if (specialJunkI3 == null || specialJunkI3.trim().isEmpty()) {
|
||||
this.specialJunkI3 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI3 = Optional.of(specialJunkI3.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseSpecialJunkI4(final String specialJunkI4) throws BadConfigException {
|
||||
if (specialJunkI4 == null || specialJunkI4.trim().isEmpty()) {
|
||||
this.specialJunkI4 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI4 = Optional.of(specialJunkI4.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseSpecialJunkI5(final String specialJunkI5) throws BadConfigException {
|
||||
if (specialJunkI5 == null || specialJunkI5.trim().isEmpty()) {
|
||||
this.specialJunkI5 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI5 = Optional.of(specialJunkI5.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
|
||||
@@ -715,35 +919,100 @@ public final class Interface {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setInitPacketMagicHeader(final long initPacketMagicHeader) throws BadConfigException {
|
||||
if (initPacketMagicHeader < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.INIT_PACKET_MAGIC_HEADER,
|
||||
Reason.INVALID_VALUE, String.valueOf(initPacketMagicHeader));
|
||||
this.initPacketMagicHeader = initPacketMagicHeader == 0 ? Optional.empty() : Optional.of(initPacketMagicHeader);
|
||||
public Builder setCookieReplyPacketJunkSize(final int cookieReplyPacketJunkSize) throws BadConfigException {
|
||||
if (cookieReplyPacketJunkSize < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.COOKIE_REPLY_PACKET_JUNK_SIZE,
|
||||
Reason.INVALID_VALUE, String.valueOf(cookieReplyPacketJunkSize));
|
||||
this.cookieReplyPacketJunkSize = cookieReplyPacketJunkSize == 0 ? Optional.empty() : Optional.of(cookieReplyPacketJunkSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setResponsePacketMagicHeader(final long responsePacketMagicHeader) throws BadConfigException {
|
||||
if (responsePacketMagicHeader < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.RESPONSE_PACKET_MAGIC_HEADER,
|
||||
Reason.INVALID_VALUE, String.valueOf(responsePacketMagicHeader));
|
||||
this.responsePacketMagicHeader = responsePacketMagicHeader == 0 ? Optional.empty() : Optional.of(responsePacketMagicHeader);
|
||||
public Builder setTransportPacketJunkSize(final int transportPacketJunkSize) throws BadConfigException {
|
||||
if (transportPacketJunkSize < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TRANSPORT_PACKET_JUNK_SIZE,
|
||||
Reason.INVALID_VALUE, String.valueOf(transportPacketJunkSize));
|
||||
this.transportPacketJunkSize = transportPacketJunkSize == 0 ? Optional.empty() : Optional.of(transportPacketJunkSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnderloadPacketMagicHeader(final long underloadPacketMagicHeader) throws BadConfigException {
|
||||
if (underloadPacketMagicHeader < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.UNDERLOAD_PACKET_MAGIC_HEADER,
|
||||
Reason.INVALID_VALUE, String.valueOf(underloadPacketMagicHeader));
|
||||
this.underloadPacketMagicHeader = underloadPacketMagicHeader == 0 ? Optional.empty() : Optional.of(underloadPacketMagicHeader);
|
||||
public Builder setInitPacketMagicHeader(final String initPacketMagicHeader) throws BadConfigException {
|
||||
if (initPacketMagicHeader == null || initPacketMagicHeader.trim().isEmpty()) {
|
||||
this.initPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.initPacketMagicHeader = Optional.of(initPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTransportPacketMagicHeader(final long transportPacketMagicHeader) throws BadConfigException {
|
||||
if (transportPacketMagicHeader < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TRANSPORT_PACKET_MAGIC_HEADER,
|
||||
Reason.INVALID_VALUE, String.valueOf(transportPacketMagicHeader));
|
||||
this.transportPacketMagicHeader = transportPacketMagicHeader == 0 ? Optional.empty() : Optional.of(transportPacketMagicHeader);
|
||||
public Builder setResponsePacketMagicHeader(final String responsePacketMagicHeader) throws BadConfigException {
|
||||
if (responsePacketMagicHeader == null || responsePacketMagicHeader.trim().isEmpty()) {
|
||||
this.responsePacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.responsePacketMagicHeader = Optional.of(responsePacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnderloadPacketMagicHeader(final String underloadPacketMagicHeader) throws BadConfigException {
|
||||
if (underloadPacketMagicHeader == null || underloadPacketMagicHeader.trim().isEmpty()) {
|
||||
this.underloadPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.underloadPacketMagicHeader = Optional.of(underloadPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTransportPacketMagicHeader(final String transportPacketMagicHeader) throws BadConfigException {
|
||||
if (transportPacketMagicHeader == null || transportPacketMagicHeader.trim().isEmpty()) {
|
||||
this.transportPacketMagicHeader = Optional.empty();
|
||||
} else {
|
||||
this.transportPacketMagicHeader = Optional.of(transportPacketMagicHeader.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpecialJunkI1(final String specialJunkI1) throws BadConfigException {
|
||||
if (specialJunkI1 == null || specialJunkI1.trim().isEmpty()) {
|
||||
this.specialJunkI1 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI1 = Optional.of(specialJunkI1.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpecialJunkI2(final String specialJunkI2) throws BadConfigException {
|
||||
if (specialJunkI2 == null || specialJunkI2.trim().isEmpty()) {
|
||||
this.specialJunkI2 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI2 = Optional.of(specialJunkI2.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpecialJunkI3(final String specialJunkI3) throws BadConfigException {
|
||||
if (specialJunkI3 == null || specialJunkI3.trim().isEmpty()) {
|
||||
this.specialJunkI3 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI3 = Optional.of(specialJunkI3.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpecialJunkI4(final String specialJunkI4) throws BadConfigException {
|
||||
if (specialJunkI4 == null || specialJunkI4.trim().isEmpty()) {
|
||||
this.specialJunkI4 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI4 = Optional.of(specialJunkI4.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpecialJunkI5(final String specialJunkI5) throws BadConfigException {
|
||||
if (specialJunkI5 == null || specialJunkI5.trim().isEmpty()) {
|
||||
this.specialJunkI5 = Optional.empty();
|
||||
} else {
|
||||
this.specialJunkI5 = Optional.of(specialJunkI5.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Submodule tunnel/tools/amneziawg-tools updated: d33c4b6936...5d6179a6d0
@@ -20,12 +20,12 @@ export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
GO_VERSION := 1.22.3
|
||||
GO_VERSION := 1.24.2
|
||||
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
|
||||
GO_HASH_darwin-amd64 := 610e48c1df4d2f852de8bc2e7fd2dc1521aac216f0c0026625db12f67f192024
|
||||
GO_HASH_darwin-arm64 := 02abeab3f4b8981232237ebd88f0a9bad933bc9621791cd7720a9ca29eacbe9d
|
||||
GO_HASH_linux-amd64 := 8920ea521bad8f6b7bc377b4824982e011c19af27df88a815e3586ea895f1b36
|
||||
GO_HASH_darwin-amd64 := 238d9c065d09ff6af229d2e3b8b5e85e688318d69f4006fb85a96e41c216ea83
|
||||
GO_HASH_darwin-arm64 := b70f8b3c5b4ccb0ad4ffa5ee91cd38075df20fdbd953a1daedd47f50fbcff47a
|
||||
GO_HASH_linux-amd64 := 68097bd680839cbc9d464a0edce4f7c333975e27a90246890e9f1078c7e702ad
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
module github.com/amnezia-vpn/amneziawg-android
|
||||
|
||||
go 1.22.3
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.11
|
||||
golang.org/x/sys v0.18.0
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tevino/abool/v2 v2.1.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.11 h1:JNGsxX4NIaYC/PpbUdwk23534HgS2SRMPgru6/K+364=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.11/go.mod h1:d7WpNfzCRLy7ufGElJBYpD58WRmNjyLyt3IDHPY8AmM=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
|
||||
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16 h1:XY6HOq/xtqH8ZXMncRWkjFs85EKdN10NLNnw23kTpE0=
|
||||
github.com/amnezia-vpn/amneziawg-go v0.2.16/go.mod h1:nRkPpIzjCxMW8pZKXTRkpqAQVlmFJdVOGkeQSC7wbms=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ android {
|
||||
namespace = pkg
|
||||
defaultConfig {
|
||||
applicationId = pkg
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
versionCode = providers.gradleProperty("amneziawgVersionCode").get().toInt()
|
||||
versionName = providers.gradleProperty("amneziawgVersionName").get()
|
||||
buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SYSTEM_ALERT_WINDOW"
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.amnezia.awg.backend.GoBackend
|
||||
import org.amnezia.awg.backend.AwgQuickBackend
|
||||
import org.amnezia.awg.configStore.FileConfigStore
|
||||
import org.amnezia.awg.model.TunnelManager
|
||||
import org.amnezia.awg.util.NetworkState
|
||||
import org.amnezia.awg.util.NetworkType
|
||||
import org.amnezia.awg.util.RootShell
|
||||
import org.amnezia.awg.util.ToolsInstaller
|
||||
import org.amnezia.awg.util.UserKnobs
|
||||
@@ -47,10 +49,12 @@ class Application : android.app.Application() {
|
||||
private lateinit var preferencesDataStore: DataStore<Preferences>
|
||||
private lateinit var toolsInstaller: ToolsInstaller
|
||||
private lateinit var tunnelManager: TunnelManager
|
||||
private lateinit var networkState: NetworkState
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
super.attachBaseContext(context)
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
@Suppress("UnsafeImplicitIntentLaunch")
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_HOME)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
@@ -107,10 +111,18 @@ class Application : android.app.Application() {
|
||||
}
|
||||
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||
tunnelManager.onCreate()
|
||||
|
||||
// Initialize network state monitor for auto-reconnection
|
||||
networkState = NetworkState(applicationContext) { oldType, newType ->
|
||||
Log.i(TAG, "NetworkState callback: Network changed: $oldType -> $newType")
|
||||
onNetworkChange(oldType, newType)
|
||||
}
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
backend = determineBackend()
|
||||
futureBackend.complete(backend!!)
|
||||
networkState.bindNetworkListener()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
@@ -123,10 +135,55 @@ class Application : android.app.Application() {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
networkState.unbindNetworkListener()
|
||||
coroutineScope.cancel()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when network changes (e.g., WiFi to Mobile or vice versa).
|
||||
* Reconnects active tunnels to ensure VPN connection works on new network.
|
||||
*/
|
||||
private fun onNetworkChange(oldType: NetworkType, newType: NetworkType) {
|
||||
Log.i(TAG, "onNetworkChange called: $oldType -> $newType")
|
||||
|
||||
if (newType == NetworkType.NONE) {
|
||||
Log.i(TAG, "Network lost, waiting for new connection...")
|
||||
return
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val activeTunnels = tunnelManager.getTunnels().filter {
|
||||
it.state == org.amnezia.awg.backend.Tunnel.State.UP
|
||||
}
|
||||
|
||||
if (activeTunnels.isEmpty()) {
|
||||
Log.d(TAG, "No active tunnels, skipping reconnection")
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.i(TAG, "Reconnecting ${activeTunnels.size} tunnel(s) after network change: $oldType -> $newType")
|
||||
|
||||
for (tunnel in activeTunnels) {
|
||||
try {
|
||||
Log.d(TAG, "Disconnecting tunnel: ${tunnel.name}")
|
||||
// Toggle tunnel off and on to reconnect
|
||||
tunnel.setStateAsync(org.amnezia.awg.backend.Tunnel.State.DOWN)
|
||||
kotlinx.coroutines.delay(500) // Small delay for cleanup
|
||||
Log.d(TAG, "Reconnecting tunnel: ${tunnel.name}")
|
||||
tunnel.setStateAsync(org.amnezia.awg.backend.Tunnel.State.UP)
|
||||
Log.i(TAG, "Successfully reconnected tunnel: ${tunnel.name}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to reconnect tunnel ${tunnel.name}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during network change handling", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val USER_AGENT = String.format(Locale.ENGLISH, "AmneziaWG/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT)
|
||||
private const val TAG = "AmneziaWG/Application"
|
||||
@@ -147,6 +204,8 @@ class Application : android.app.Application() {
|
||||
fun getTunnelManager() = get().tunnelManager
|
||||
|
||||
fun getCoroutineScope() = get().coroutineScope
|
||||
|
||||
fun getNetworkState() = get().networkState
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -59,7 +59,7 @@ class QuickTileService : TileService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress("DEPRECATION", "StartActivityAndCollapseDeprecated")
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class TvMainActivity : AppCompatActivity() {
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(object : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
private val tunnelFileImportResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||
@@ -209,12 +209,15 @@ class TvMainActivity : AppCompatActivity() {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
tunnelFileImportResultLauncher.launch(arrayOf("*/*"))
|
||||
} catch (_: Throwable) {
|
||||
MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
|
||||
startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("https://play.google.com/store/apps/details?id=com.cxinventor.file.explorer")
|
||||
setPackage("com.android.vending")
|
||||
})
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}.show()
|
||||
|
||||
@@ -7,11 +7,13 @@ package org.amnezia.awg.fragment
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
@@ -42,6 +44,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
qrcode.isEnabled = false
|
||||
qrcode.visibility = View.GONE
|
||||
}
|
||||
view.findViewById<TextView>(R.id.disclaimer)?.let { it.movementMethod = LinkMovementMethod.getInstance() }
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class AppListDialogFragment : DialogFragment() {
|
||||
val packageName = it.packageName
|
||||
val appInfo = it.applicationInfo
|
||||
val appData =
|
||||
ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||
ApplicationData(appInfo?.loadIcon(pm) ?: pm.defaultActivityIcon, appInfo?.loadLabel(pm)?.toString() ?: packageName, packageName, currentlySelectedApps.contains(packageName))
|
||||
applicationData.add(appData)
|
||||
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
|
||||
@@ -50,17 +50,42 @@ class ObservableTunnel internal constructor(
|
||||
var state = state
|
||||
private set
|
||||
|
||||
@get:Bindable
|
||||
var connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED
|
||||
private set
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
onStateChanged(newState)
|
||||
}
|
||||
|
||||
fun onStateChanged(state: Tunnel.State): Tunnel.State {
|
||||
if (state != Tunnel.State.UP) onStatisticsChanged(null)
|
||||
if (state != Tunnel.State.UP) {
|
||||
onStatisticsChanged(null)
|
||||
onConnectionStatusChanged(ConnectionStatus.DISCONNECTED)
|
||||
} else if (connectionStatus == ConnectionStatus.DISCONNECTED) {
|
||||
// When state changes to UP, set to CONNECTING until handshake confirms
|
||||
onConnectionStatusChanged(ConnectionStatus.CONNECTING)
|
||||
}
|
||||
this.state = state
|
||||
notifyPropertyChanged(BR.state)
|
||||
return state
|
||||
}
|
||||
|
||||
fun onConnectionStatusChanged(status: ConnectionStatus): ConnectionStatus {
|
||||
if (status != this.connectionStatus) {
|
||||
this.connectionStatus = status
|
||||
notifyPropertyChanged(BR.connectionStatus)
|
||||
Log.d(TAG, "Connection status changed for $name: $status")
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
enum class ConnectionStatus {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED
|
||||
}
|
||||
|
||||
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||
if (state != this@ObservableTunnel.state)
|
||||
manager.setTunnelState(this@ObservableTunnel, state)
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.amnezia.awg.Application.Companion.getTunnelManager
|
||||
import org.amnezia.awg.BR
|
||||
import org.amnezia.awg.R
|
||||
import org.amnezia.awg.backend.Statistics
|
||||
import org.amnezia.awg.backend.StatusCallback
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import org.amnezia.awg.configStore.ConfigStore
|
||||
import org.amnezia.awg.databinding.ObservableSortedKeyedArrayList
|
||||
@@ -102,12 +103,41 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
|
||||
setupStatusCallbacks()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupStatusCallbacks() {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
val backend = getBackend()
|
||||
val statusCallback = object : StatusCallback {
|
||||
override fun onStatusChanged(connected: Boolean) {
|
||||
applicationScope.launch(Dispatchers.Main) {
|
||||
// Find the currently active tunnel
|
||||
val activeTunnel = tunnelMap.firstOrNull { it.state == Tunnel.State.UP }
|
||||
if (activeTunnel != null) {
|
||||
val newStatus = if (connected) {
|
||||
ObservableTunnel.ConnectionStatus.CONNECTED
|
||||
} else {
|
||||
ObservableTunnel.ConnectionStatus.CONNECTING
|
||||
}
|
||||
activeTunnel.onConnectionStatusChanged(newStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backend.setStatusCallback(statusCallback)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to setup status callbacks", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||
for (name in present)
|
||||
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright © 2025 AmneziaWG. All Rights conneserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.amnezia.awg.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val TAG = "AmneziaWG/NetworkState"
|
||||
private const val BIND_NETWORK_RETRY_ATTEMPTS = 5
|
||||
|
||||
enum class NetworkType {
|
||||
NONE, WIFI, CELLULAR, OTHER
|
||||
}
|
||||
|
||||
class NetworkState(
|
||||
private val context: Context,
|
||||
private val onNetworkChange: (NetworkType, NetworkType) -> Unit
|
||||
) {
|
||||
private var currentNetwork: Network? = null
|
||||
private var currentNetworkType: NetworkType = NetworkType.NONE
|
||||
private var validated: Boolean = false
|
||||
private var isListenerBound = false
|
||||
|
||||
private val handler: Handler by lazy {
|
||||
Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
private val connectivityManager: ConnectivityManager by lazy {
|
||||
context.getSystemService<ConnectivityManager>()!!
|
||||
}
|
||||
|
||||
private val networkRequest: NetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(TRANSPORT_WIFI)
|
||||
.addTransportType(TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val networkCallback: NetworkCallback by lazy {
|
||||
object : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d(TAG, "onAvailable: $network")
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
val networkType = getNetworkType(networkCapabilities)
|
||||
val isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)
|
||||
Log.d(TAG, "onCapabilitiesChanged: network=$network, type=$networkType, validated=$isValidated")
|
||||
checkNetworkState(network, networkCapabilities)
|
||||
}
|
||||
|
||||
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
val newNetworkType = getNetworkType(networkCapabilities)
|
||||
val isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)
|
||||
|
||||
if (currentNetwork == null) {
|
||||
// First network connection
|
||||
currentNetwork = network
|
||||
currentNetworkType = newNetworkType
|
||||
validated = isValidated
|
||||
Log.d(TAG, "Initial network: $newNetworkType, validated: $validated")
|
||||
} else {
|
||||
if (currentNetwork != network || currentNetworkType != newNetworkType) {
|
||||
// Network changed (e.g., WiFi to Cellular or vice versa)
|
||||
val oldNetworkType = currentNetworkType
|
||||
currentNetwork = network
|
||||
currentNetworkType = newNetworkType
|
||||
validated = false
|
||||
|
||||
Log.d(TAG, "Network changed: $oldNetworkType -> $newNetworkType")
|
||||
|
||||
if (isValidated) {
|
||||
validated = true
|
||||
handler.post {
|
||||
onNetworkChange(oldNetworkType, newNetworkType)
|
||||
}
|
||||
}
|
||||
} else if (!validated && isValidated) {
|
||||
// Same network became validated
|
||||
validated = true
|
||||
Log.d(TAG, "Network validated: $newNetworkType")
|
||||
handler.post {
|
||||
onNetworkChange(currentNetworkType, newNetworkType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNetworkType(capabilities: NetworkCapabilities): NetworkType {
|
||||
return when {
|
||||
capabilities.hasTransport(TRANSPORT_WIFI) -> NetworkType.WIFI
|
||||
capabilities.hasTransport(TRANSPORT_CELLULAR) -> NetworkType.CELLULAR
|
||||
else -> NetworkType.OTHER
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "onLost: $network, currentNetwork: $currentNetwork")
|
||||
if (currentNetwork == network) {
|
||||
val oldType = currentNetworkType
|
||||
currentNetwork = null
|
||||
currentNetworkType = NetworkType.NONE
|
||||
validated = false
|
||||
Log.d(TAG, "Network lost: $oldType -> NONE")
|
||||
handler.post {
|
||||
onNetworkChange(oldType, NetworkType.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bindNetworkListener() {
|
||||
if (isListenerBound) {
|
||||
Log.d(TAG, "Network listener already bound")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have the required permission
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "ACCESS_NETWORK_STATE permission not granted, cannot bind network listener")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Binding network listener (SDK ${Build.VERSION.SDK_INT})")
|
||||
|
||||
var attemptCount = 0
|
||||
while (true) {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
|
||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback, handler)
|
||||
}
|
||||
else -> {
|
||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||
}
|
||||
}
|
||||
isListenerBound = true
|
||||
Log.i(TAG, "Network listener bound successfully")
|
||||
break
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to bind network listener: $e")
|
||||
// Android 11 bug: https://issuetracker.google.com/issues/175055271
|
||||
if (e.message?.startsWith("Package android does not belong to") == true) {
|
||||
if (++attemptCount >= BIND_NETWORK_RETRY_ATTEMPTS) {
|
||||
throw e
|
||||
}
|
||||
delay(1000)
|
||||
continue
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to bind network listener", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unbindNetworkListener() {
|
||||
if (!isListenerBound) {
|
||||
Log.d(TAG, "Network listener not bound, nothing to unbind")
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "Unbind network listener")
|
||||
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
Log.d(TAG, "Network listener unbound successfully")
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "SecurityException while unbinding network listener", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Callback was not registered, ignore
|
||||
Log.w(TAG, "Callback was not registered", e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to unbind network listener", e)
|
||||
}
|
||||
|
||||
isListenerBound = false
|
||||
currentNetwork = null
|
||||
currentNetworkType = NetworkType.NONE
|
||||
validated = false
|
||||
}
|
||||
|
||||
fun getCurrentNetworkType(): NetworkType = currentNetworkType
|
||||
|
||||
fun isConnected(): Boolean = validated && currentNetworkType != NetworkType.NONE
|
||||
}
|
||||
|
||||
@@ -88,6 +88,20 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
notifyPropertyChanged(BR.responsePacketJunkSize)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var cookieReplyPacketJunkSize: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.cookieReplyPacketJunkSize)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var transportPacketJunkSize: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.transportPacketJunkSize)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var initPacketMagicHeader: String = ""
|
||||
set(value) {
|
||||
@@ -116,6 +130,41 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
notifyPropertyChanged(BR.transportPacketMagicHeader)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var specialJunkI1: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.specialJunkI1)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var specialJunkI2: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.specialJunkI2)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var specialJunkI3: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.specialJunkI3)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var specialJunkI4: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.specialJunkI4)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var specialJunkI5: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
notifyPropertyChanged(BR.specialJunkI5)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var privateKey: String = ""
|
||||
set(value) {
|
||||
@@ -144,10 +193,17 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
junkPacketMaxSize = parcel.readString() ?: ""
|
||||
initPacketJunkSize = parcel.readString() ?: ""
|
||||
responsePacketJunkSize = parcel.readString() ?: ""
|
||||
cookieReplyPacketJunkSize = parcel.readString() ?: ""
|
||||
transportPacketJunkSize = parcel.readString() ?: ""
|
||||
initPacketMagicHeader = parcel.readString() ?: ""
|
||||
responsePacketMagicHeader = parcel.readString() ?: ""
|
||||
underloadPacketMagicHeader = parcel.readString() ?: ""
|
||||
transportPacketMagicHeader = parcel.readString() ?: ""
|
||||
specialJunkI1 = parcel.readString() ?: ""
|
||||
specialJunkI2 = parcel.readString() ?: ""
|
||||
specialJunkI3 = parcel.readString() ?: ""
|
||||
specialJunkI4 = parcel.readString() ?: ""
|
||||
specialJunkI5 = parcel.readString() ?: ""
|
||||
privateKey = parcel.readString() ?: ""
|
||||
}
|
||||
|
||||
@@ -164,10 +220,17 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
junkPacketMaxSize = other.junkPacketMaxSize.map { it.toString() }.orElse("")
|
||||
initPacketJunkSize = other.initPacketJunkSize.map { it.toString() }.orElse("")
|
||||
responsePacketJunkSize = other.responsePacketJunkSize.map { it.toString() }.orElse("")
|
||||
initPacketMagicHeader = other.initPacketMagicHeader.map { it.toString() }.orElse("")
|
||||
responsePacketMagicHeader = other.responsePacketMagicHeader.map { it.toString() }.orElse("")
|
||||
underloadPacketMagicHeader = other.underloadPacketMagicHeader.map { it.toString() }.orElse("")
|
||||
transportPacketMagicHeader = other.transportPacketMagicHeader.map { it.toString() }.orElse("")
|
||||
cookieReplyPacketJunkSize = other.cookieReplyPacketJunkSize.map { it.toString() }.orElse("")
|
||||
transportPacketJunkSize = other.transportPacketJunkSize.map { it.toString() }.orElse("")
|
||||
initPacketMagicHeader = other.initPacketMagicHeader.orElse("")
|
||||
responsePacketMagicHeader = other.responsePacketMagicHeader.orElse("")
|
||||
underloadPacketMagicHeader = other.underloadPacketMagicHeader.orElse("")
|
||||
transportPacketMagicHeader = other.transportPacketMagicHeader.orElse("")
|
||||
specialJunkI1 = other.specialJunkI1.orElse("")
|
||||
specialJunkI2 = other.specialJunkI2.orElse("")
|
||||
specialJunkI3 = other.specialJunkI3.orElse("")
|
||||
specialJunkI4 = other.specialJunkI4.orElse("")
|
||||
specialJunkI5 = other.specialJunkI5.orElse("")
|
||||
val keyPair = other.keyPair
|
||||
privateKey = keyPair.privateKey.toBase64()
|
||||
}
|
||||
@@ -197,10 +260,17 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
if (junkPacketMaxSize.isNotEmpty()) builder.parseJunkPacketMaxSize(junkPacketMaxSize)
|
||||
if (initPacketJunkSize.isNotEmpty()) builder.parseInitPacketJunkSize(initPacketJunkSize)
|
||||
if (responsePacketJunkSize.isNotEmpty()) builder.parseResponsePacketJunkSize(responsePacketJunkSize)
|
||||
if (cookieReplyPacketJunkSize.isNotEmpty()) builder.parseCookieReplyPacketJunkSize(cookieReplyPacketJunkSize)
|
||||
if (transportPacketJunkSize.isNotEmpty()) builder.parseTransportPacketJunkSize(transportPacketJunkSize)
|
||||
if (initPacketMagicHeader.isNotEmpty()) builder.parseInitPacketMagicHeader(initPacketMagicHeader)
|
||||
if (responsePacketMagicHeader.isNotEmpty()) builder.parseResponsePacketMagicHeader(responsePacketMagicHeader)
|
||||
if (underloadPacketMagicHeader.isNotEmpty()) builder.parseUnderloadPacketMagicHeader(underloadPacketMagicHeader)
|
||||
if (transportPacketMagicHeader.isNotEmpty()) builder.parseTransportPacketMagicHeader(transportPacketMagicHeader)
|
||||
if (specialJunkI1.isNotEmpty()) builder.parseSpecialJunkI1(specialJunkI1)
|
||||
if (specialJunkI2.isNotEmpty()) builder.parseSpecialJunkI2(specialJunkI2)
|
||||
if (specialJunkI3.isNotEmpty()) builder.parseSpecialJunkI3(specialJunkI3)
|
||||
if (specialJunkI4.isNotEmpty()) builder.parseSpecialJunkI4(specialJunkI4)
|
||||
if (specialJunkI5.isNotEmpty()) builder.parseSpecialJunkI5(specialJunkI5)
|
||||
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
|
||||
return builder.build()
|
||||
}
|
||||
@@ -217,10 +287,17 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
||||
dest.writeString(junkPacketMaxSize)
|
||||
dest.writeString(initPacketJunkSize)
|
||||
dest.writeString(responsePacketJunkSize)
|
||||
dest.writeString(cookieReplyPacketJunkSize)
|
||||
dest.writeString(transportPacketJunkSize)
|
||||
dest.writeString(initPacketMagicHeader)
|
||||
dest.writeString(responsePacketMagicHeader)
|
||||
dest.writeString(underloadPacketMagicHeader)
|
||||
dest.writeString(transportPacketMagicHeader)
|
||||
dest.writeString(specialJunkI1)
|
||||
dest.writeString(specialJunkI2)
|
||||
dest.writeString(specialJunkI3)
|
||||
dest.writeString(specialJunkI4)
|
||||
dest.writeString(specialJunkI5)
|
||||
dest.writeString(privateKey)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,16 +63,53 @@
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:nextFocusUp="@id/create_from_qrcode"
|
||||
android:nextFocusDown="@id/disclaimer"
|
||||
android:nextFocusForward="@id/disclaimer"
|
||||
android:text="@string/create_empty"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:icon="@drawable/ic_action_edit"
|
||||
app:iconPadding="@dimen/bottom_sheet_icon_padding"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/delimiter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/create_from_qrcode"
|
||||
app:rippleColor="?attr/colorSecondary" />
|
||||
|
||||
<View
|
||||
android:id="@+id/delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="@dimen/medium_margin"
|
||||
android:layout_marginLeft="@dimen/medium_margin"
|
||||
android:layout_marginEnd="@dimen/medium_margin"
|
||||
android:layout_marginRight="@dimen/medium_margin"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:background="?attr/colorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/disclaimer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/create_empty" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disclaimer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_margin"
|
||||
android:layout_marginLeft="@dimen/medium_margin"
|
||||
android:layout_marginEnd="@dimen/medium_margin"
|
||||
android:layout_marginRight="@dimen/medium_margin"
|
||||
android:nextFocusUp="@id/create_empty"
|
||||
android:text="@string/import_disclaimer"
|
||||
android:linksClickable="true"
|
||||
android:focusable="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textColorLink="?android:attr/textColorLink"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/delimiter" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -390,6 +390,52 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/response_packet_junk_size_label"
|
||||
tools:text="18" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cookie_reply_packet_junk_size_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/cookie_reply_packet_junk_size_text"
|
||||
android:text="@string/cookie_reply_packet_junk_size"
|
||||
android:visibility="@{!config.interface.cookieReplyPacketJunkSize.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/response_packet_junk_size_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cookie_reply_packet_junk_size_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{config.interface.cookieReplyPacketJunkSize}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.cookieReplyPacketJunkSize.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cookie_reply_packet_junk_size_label"
|
||||
tools:text="20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transport_packet_junk_size_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/transport_packet_junk_size_text"
|
||||
android:text="@string/transport_packet_junk_size"
|
||||
android:visibility="@{!config.interface.transportPacketJunkSize.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cookie_reply_packet_junk_size_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transport_packet_junk_size_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{config.interface.transportPacketJunkSize}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.transportPacketJunkSize.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_junk_size_label"
|
||||
tools:text="22" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/init_packet_magic_header_label"
|
||||
android:layout_width="0dp"
|
||||
@@ -399,7 +445,7 @@
|
||||
android:text="@string/init_packet_magic_header"
|
||||
android:visibility="@{!config.interface.initPacketMagicHeader.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/response_packet_junk_size_text" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_junk_size_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/init_packet_magic_header_text"
|
||||
@@ -482,12 +528,152 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_magic_header_label"
|
||||
tools:text="1766607858" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i1_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/special_junk_i1_text"
|
||||
android:text="@string/special_junk_i1"
|
||||
android:visibility="@{!config.interface.specialJunkI1.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_magic_header_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i1_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/special_junk_i1"
|
||||
android:nextFocusUp="@id/transport_packet_magic_header_text"
|
||||
android:nextFocusDown="@id/special_junk_i2_text"
|
||||
android:nextFocusForward="@id/special_junk_i2_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.specialJunkI1}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.specialJunkI1.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i1_label"
|
||||
tools:text="<b 0x10001c00c00050001000000df00210473746c730561646f626506636f6d2d636e09656467657375697465036e657400c02b00050001000001e200350473746c730561646f626506636f6d2d636e09656467657375697465036e65740b676c6f62616c726564697206616b61646e73c047c0580005000100>" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i2_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/special_junk_i2_text"
|
||||
android:text="@string/special_junk_i2"
|
||||
android:visibility="@{!config.interface.specialJunkI2.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i1_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i2_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/special_junk_i2"
|
||||
android:nextFocusUp="@id/special_junk_i1_text"
|
||||
android:nextFocusDown="@id/special_junk_i3_text"
|
||||
android:nextFocusForward="@id/special_junk_i3_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.specialJunkI2}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.specialJunkI2.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i2_label"
|
||||
tools:text="<b 0x1234>" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i3_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/special_junk_i3_text"
|
||||
android:text="@string/special_junk_i3"
|
||||
android:visibility="@{!config.interface.specialJunkI3.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i2_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i3_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/special_junk_i3"
|
||||
android:nextFocusUp="@id/special_junk_i2_text"
|
||||
android:nextFocusDown="@id/special_junk_i4_text"
|
||||
android:nextFocusForward="@id/special_junk_i4_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.specialJunkI3}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.specialJunkI3.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i3_label"
|
||||
tools:text="<b 0x1234567890>" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i4_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/special_junk_i4_text"
|
||||
android:text="@string/special_junk_i4"
|
||||
android:visibility="@{!config.interface.specialJunkI4.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i3_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i4_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/special_junk_i4"
|
||||
android:nextFocusUp="@id/special_junk_i3_text"
|
||||
android:nextFocusDown="@id/special_junk_i5_text"
|
||||
android:nextFocusForward="@id/special_junk_i5_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.specialJunkI4}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.specialJunkI4.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i4_label"
|
||||
tools:text="<b 0xdeadbeef>" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i5_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:labelFor="@+id/special_junk_i5_text"
|
||||
android:text="@string/special_junk_i5"
|
||||
android:visibility="@{!config.interface.specialJunkI5.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i4_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/special_junk_i5_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/special_junk_i5"
|
||||
android:nextFocusUp="@id/special_junk_i4_text"
|
||||
android:nextFocusDown="@id/applications_text"
|
||||
android:nextFocusForward="@id/applications_text"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
android:text="@{config.interface.specialJunkI5}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:visibility="@{!config.interface.specialJunkI5.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i5_label"
|
||||
tools:text="<rc 12><t><rd 18>" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/amnezia_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="transport_packet_magic_header_text" />
|
||||
app:constraint_referenced_ids="transport_packet_magic_header_text,special_junk_i1_text,special_junk_i2_text,special_junk_i3_text,special_junk_i4_text,special_junk_i5_text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/applications_label"
|
||||
@@ -504,7 +690,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/applications"
|
||||
android:nextFocusUp="@id/mtu_text"
|
||||
android:nextFocusUp="@id/special_junk_i5_text"
|
||||
android:nextFocusDown="@id/peers_layout"
|
||||
android:nextFocusForward="@id/peers_layout"
|
||||
android:onClick="@{ClipboardUtils::copyTextView}"
|
||||
|
||||
@@ -346,12 +346,56 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/junk_packet_max_size_text"
|
||||
android:nextFocusDown="@id/response_packet_magic_header_text"
|
||||
android:nextFocusForward="@id/init_packet_magic_header_text"
|
||||
android:nextFocusUp="@id/init_packet_junk_size_text"
|
||||
android:nextFocusDown="@id/cookie_reply_packet_junk_size_text"
|
||||
android:nextFocusForward="@id/cookie_reply_packet_junk_size_text"
|
||||
android:text="@={config.interface.responsePacketJunkSize}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/cookie_reply_packet_junk_size_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/cookie_reply_packet_junk_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/response_packet_junk_size_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/cookie_reply_packet_junk_size_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/response_packet_junk_size_text"
|
||||
android:nextFocusDown="@id/transport_packet_junk_size_text"
|
||||
android:nextFocusForward="@id/transport_packet_junk_size_text"
|
||||
android:text="@={config.interface.cookieReplyPacketJunkSize}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/transport_packet_junk_size_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/transport_packet_junk_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cookie_reply_packet_junk_size_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/transport_packet_junk_size_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/cookie_reply_packet_junk_size_text"
|
||||
android:nextFocusDown="@id/init_packet_magic_header_text"
|
||||
android:nextFocusForward="@id/init_packet_magic_header_text"
|
||||
android:text="@={config.interface.transportPacketJunkSize}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/init_packet_magic_header_layout"
|
||||
android:layout_width="0dp"
|
||||
@@ -359,7 +403,7 @@
|
||||
android:hint="@string/init_packet_magic_header"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/response_packet_junk_size_layout"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_junk_size_layout"
|
||||
android:layout_margin="4dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
@@ -367,8 +411,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:nextFocusUp="@id/init_packet_junk_size_text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/transport_packet_junk_size_text"
|
||||
android:nextFocusDown="@id/underload_packet_magic_header_text"
|
||||
android:nextFocusForward="@id/response_packet_magic_header_text"
|
||||
android:text="@={config.interface.initPacketMagicHeader}" />
|
||||
@@ -389,7 +433,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/response_packet_junk_size_text"
|
||||
android:nextFocusDown="@id/transport_packet_magic_header_text"
|
||||
android:nextFocusForward="@id/underload_packet_magic_header_text"
|
||||
@@ -411,9 +455,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="number"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/init_packet_magic_header_text"
|
||||
android:nextFocusDown="@id/set_excluded_applications"
|
||||
android:nextFocusDown="@id/special_junk_i1_text"
|
||||
android:nextFocusForward="@id/transport_packet_magic_header_text"
|
||||
android:text="@={config.interface.underloadPacketMagicHeader}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -432,12 +476,122 @@
|
||||
android:id="@+id/transport_packet_magic_header_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="number"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/response_packet_magic_header_text"
|
||||
android:nextFocusDown="@id/special_junk_i1_text"
|
||||
android:nextFocusForward="@id/special_junk_i1_text"
|
||||
android:text="@={config.interface.transportPacketMagicHeader}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/special_junk_i1_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/special_junk_i1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transport_packet_magic_header_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/special_junk_i1_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/transport_packet_magic_header_text"
|
||||
android:nextFocusDown="@id/special_junk_i2_text"
|
||||
android:nextFocusForward="@id/special_junk_i2_text"
|
||||
android:text="@={config.interface.specialJunkI1}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/special_junk_i2_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/special_junk_i2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i1_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/special_junk_i2_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/special_junk_i1_text"
|
||||
android:nextFocusDown="@id/special_junk_i3_text"
|
||||
android:nextFocusForward="@id/special_junk_i3_text"
|
||||
android:text="@={config.interface.specialJunkI2}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/special_junk_i3_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/special_junk_i3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i2_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/special_junk_i3_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/special_junk_i2_text"
|
||||
android:nextFocusDown="@id/special_junk_i4_text"
|
||||
android:nextFocusForward="@id/special_junk_i4_text"
|
||||
android:text="@={config.interface.specialJunkI3}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/special_junk_i4_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/special_junk_i4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i3_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/special_junk_i4_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/special_junk_i3_text"
|
||||
android:nextFocusDown="@id/special_junk_i5_text"
|
||||
android:nextFocusForward="@id/special_junk_i5_text"
|
||||
android:text="@={config.interface.specialJunkI4}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/special_junk_i5_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:hint="@string/special_junk_i5"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_junk_i4_layout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/special_junk_i5_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:nextFocusUp="@id/special_junk_i4_text"
|
||||
android:nextFocusDown="@id/set_excluded_applications"
|
||||
android:nextFocusForward="@id/set_excluded_applications"
|
||||
android:text="@={config.interface.transportPacketMagicHeader}" />
|
||||
android:text="@={config.interface.specialJunkI5}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
@@ -446,7 +600,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:nextFocusUp="@id/dns_servers_text"
|
||||
android:nextFocusUp="@id/special_junk_i5_text"
|
||||
android:nextFocusDown="@id/peers_layout"
|
||||
android:nextFocusForward="@id/peers_layout"
|
||||
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
|
||||
@@ -455,7 +609,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/transport_packet_magic_header_layout"
|
||||
app:layout_constraintTop_toBottomOf="@id/special_junk_i5_layout"
|
||||
app:rippleColor="?attr/colorSecondary"
|
||||
tools:text="4 excluded applications" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
<import type="org.amnezia.awg.model.ObservableTunnel" />
|
||||
|
||||
<import type="org.amnezia.awg.model.ObservableTunnel.ConnectionStatus" />
|
||||
|
||||
<import type="org.amnezia.awg.backend.Tunnel.State" />
|
||||
|
||||
<variable
|
||||
@@ -37,17 +39,37 @@
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tunnel_name"
|
||||
<LinearLayout
|
||||
android:id="@+id/tunnel_info_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{key}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
tools:text="@sample/interface_names.json/names/names/name" />
|
||||
android:layout_toStartOf="@+id/tunnel_switch"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tunnel_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{key}"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
tools:text="@sample/interface_names.json/names/names/name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tunnel_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{item.connectionStatus == ConnectionStatus.CONNECTED ? @string/tunnel_status_connected : @string/tunnel_status_connecting}"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="@{item.connectionStatus == ConnectionStatus.CONNECTED ? @color/tunnel_status_connected : @color/tunnel_status_connecting}"
|
||||
android:visibility="@{item.connectionStatus == ConnectionStatus.DISCONNECTED ? android.view.View.GONE : android.view.View.VISIBLE}"
|
||||
tools:text="Подключено"
|
||||
tools:textColor="@color/tunnel_status_connected" />
|
||||
</LinearLayout>
|
||||
|
||||
<org.amnezia.awg.widget.ToggleSwitch
|
||||
android:id="@+id/tunnel_switch"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Status colors for dark theme -->
|
||||
<color name="tunnel_status_connected">#66BB6A</color>
|
||||
<color name="tunnel_status_connecting">#FFB74D</color>
|
||||
<color name="tunnel_status_disconnected">#EF5350</color>
|
||||
</resources>
|
||||
|
||||
@@ -269,4 +269,7 @@
|
||||
<string name="biometric_prompt_private_key_title">Аутентификация для просмотра приватного ключа</string>
|
||||
<string name="biometric_auth_error">Ошибка аутентификации</string>
|
||||
<string name="biometric_auth_error_reason">Ошибка аутентификации: %s</string>
|
||||
<string name="import_disclaimer">Убедитесь, что вы получили файл конфигурации в надёжном источнике.\n\nОфициальные сервисы Amnezia доступны только на сайте <a href="https://storage.googleapis.com/amnezia/amnezia.org">amnezia.org</a>\n</string>
|
||||
<string name="tunnel_status_connected">Подключено</string>
|
||||
<string name="tunnel_status_connecting">Подключение…</string>
|
||||
</resources>
|
||||
|
||||
@@ -60,4 +60,7 @@
|
||||
<color name="md_theme_dark_surfaceTint">#ADC7FF</color>
|
||||
<color name="md_theme_dark_outlineVariant">#44474F</color>
|
||||
<color name="md_theme_dark_scrim">#000000</color>
|
||||
<color name="tunnel_status_connected">#4CAF50</color>
|
||||
<color name="tunnel_status_connecting">#FF9800</color>
|
||||
<color name="tunnel_status_disconnected">#F44336</color>
|
||||
</resources>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
<dimen name="bottom_sheet_item_height">56dp</dimen>
|
||||
<dimen name="normal_margin">8dp</dimen>
|
||||
<dimen name="medium_margin">24dp</dimen>
|
||||
<dimen name="bottom_sheet_top_padding">8dp</dimen>
|
||||
<dimen name="bottom_sheet_icon_padding">16dp</dimen>
|
||||
<dimen name="tunnel_list_placeholder_margin">16dp</dimen>
|
||||
|
||||
@@ -165,10 +165,17 @@
|
||||
<string name="junk_packet_max_size">Junk packet maximum size</string>
|
||||
<string name="init_packet_junk_size">Init packet junk size</string>
|
||||
<string name="response_packet_junk_size">Response packet junk size</string>
|
||||
<string name="cookie_reply_packet_junk_size">Cookie reply packet junk size</string>
|
||||
<string name="transport_packet_junk_size">Transport packet junk size</string>
|
||||
<string name="init_packet_magic_header">Init packet magic header</string>
|
||||
<string name="response_packet_magic_header">Response packet magic header</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
<string name="transport_packet_magic_header">Transport packet magic header</string>
|
||||
<string name="special_junk_i1">Special junk I1</string>
|
||||
<string name="special_junk_i2">Special junk I2</string>
|
||||
<string name="special_junk_i3">Special junk I3</string>
|
||||
<string name="special_junk_i4">Special junk I4</string>
|
||||
<string name="special_junk_i5">Special junk I5</string>
|
||||
<string name="multiple_tunnels_summary_off">Turning on one tunnel will turn off others</string>
|
||||
<string name="multiple_tunnels_summary_on">Multiple tunnels may be turned on simultaneously</string>
|
||||
<string name="multiple_tunnels_title">Allow multiple simultaneous tunnels</string>
|
||||
@@ -253,4 +260,7 @@
|
||||
<string name="biometric_prompt_private_key_title">Authenticate to view private key</string>
|
||||
<string name="biometric_auth_error">Authentication failure</string>
|
||||
<string name="biometric_auth_error_reason">Authentication failure: %s</string>
|
||||
<string name="import_disclaimer">Ensure that you obtained the configuration file from a trusted source.\n\nOfficial Amnezia services are available only at <a href="https://amnezia.org">amnezia.org</a>\n</string>
|
||||
<string name="tunnel_status_connected">Connected</string>
|
||||
<string name="tunnel_status_connecting">Connecting…</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user