mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-03 14:07:39 +02:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 914e3e2331 | |||
| c589bc9f5d | |||
| c3d92355b1 | |||
| 48704a2711 | |||
| 6a6b1230fe | |||
| 969aa7ad60 | |||
| 3ec1812da9 | |||
| 6714643c3f | |||
| 7f9fb04554 | |||
| fd492cfa9b | |||
| 48f01132fb | |||
| 690d92f236 | |||
| 33efa56f25 | |||
| 0fb854aedb | |||
| 7781258930 | |||
| cff17836b1 | |||
| f461b66abe | |||
| 88445dc8c4 | |||
| 291263f96f | |||
| 22fcb51a80 | |||
| 930e227a9e | |||
| bdaa56f734 | |||
| c430cca538 | |||
| fc8dfce90d | |||
| beca12ae40 | |||
| 109512d83e | |||
| 1fb21cfbfc | |||
| ff5fc4cd2a | |||
| 3f600c0088 | |||
| adc07a2b6a | |||
| 7e3134cdbb | |||
| 95b3b0eae3 | |||
| 61c27af17c | |||
| 2fa82a05d9 | |||
| f8408e863a | |||
| 3c0ac8170d | |||
| 591e0fea80 | |||
| 9a67d2684e | |||
| 2d624b3b59 | |||
| cc93898c60 | |||
| 3f265b899e | |||
| 799c3ec6e6 | |||
| 32b355a54e | |||
| 82e831f6d8 | |||
| a19e69ae61 | |||
| 995a60c503 | |||
| 0bb4ad2fbe |
@@ -9,8 +9,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.5.0
|
||||
QIF_VERSION: 4.5
|
||||
|
||||
steps:
|
||||
- name: 'Install Qt'
|
||||
@@ -63,8 +63,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.5.0
|
||||
QIF_VERSION: 4.5
|
||||
BUILD_ARCH: 64
|
||||
|
||||
steps:
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QT_VERSION: 6.5.0
|
||||
|
||||
steps:
|
||||
- name: 'Setup xcode'
|
||||
@@ -199,8 +199,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.5.0
|
||||
QIF_VERSION: 4.5
|
||||
|
||||
steps:
|
||||
- name: 'Setup xcode'
|
||||
@@ -257,8 +257,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ['android_x86_64', 'android_x86'] #, 'android_armv7', 'android_arm64_v8a']
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QT_VERSION: 6.5.0
|
||||
|
||||
steps:
|
||||
- name: 'Install desktop Qt'
|
||||
@@ -280,7 +284,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'android'
|
||||
arch: 'android_x86_64'
|
||||
arch: ${{ matrix.arch }}
|
||||
modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
@@ -314,12 +318,12 @@ jobs:
|
||||
export ANDROID_NDK_PLATFORM=android-23
|
||||
export ANDROID_NDK_HOME=${{ runner.temp }}/android-ndk-r${NDK_VERSION}
|
||||
export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME
|
||||
export ANDROID_CURRENT_ARCH=android_x86_64
|
||||
export ANDROID_CURRENT_ARCH=${{ matrix.arch }}
|
||||
|
||||
if [ ! -f $ANDROID_NDK_ROOT/ndk-build ]; then
|
||||
wget https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip -qO ${{ runner.temp }}/ndk.zip &&
|
||||
unzip -q -d ${{ runner.temp }} ${{ runner.temp }}/ndk.zip ;
|
||||
fi
|
||||
|
||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64/bin
|
||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/${{ matrix.arch }}/bin
|
||||
bash deploy/build_android.sh
|
||||
|
||||
@@ -58,6 +58,3 @@
|
||||
[submodule "client/3rd/mbedtls"]
|
||||
path = client/3rd/mbedtls
|
||||
url = https://github.com/Mbed-TLS/mbedtls.git
|
||||
[submodule "client/3rd-prebuilt"]
|
||||
path = client/3rd-prebuilt
|
||||
url = https://github.com/amnezia-vpn/3rd-prebuilt
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 3.0.7.5
|
||||
project(${PROJECT} VERSION 3.0.7.4
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
|
||||
@@ -4,22 +4,14 @@
|
||||
[](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
|
||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
Amnezia is a VPN client with the key feature of deploying your own VPN server on you virtual server.
|
||||
|
||||
## Features
|
||||
- Very easy to use - enter your ip address, ssh login and password, and Amnezia will automatically install VPN docker containers to your server and connect to VPN.
|
||||
- Very easy to use - enter your ip address, ssh login and password, and Amnezia client will automatically install VPN docker containers to your server and connect to VPN.
|
||||
- OpenVPN, ShadowSocks, WireGuard, IKEv2 protocols support.
|
||||
- Masking VPN with OpenVPN over Cloak plugin
|
||||
- Split tunneling support - add any sites to client to enable VPN only for them (only for desktops)
|
||||
- Windows, MacOS, Linux, Android, iOS releases.
|
||||
|
||||
## Links
|
||||
[https://amnezia.org](https://amnezia.org) - project website
|
||||
[https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
[https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
[https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
[https://signal.group/...](https://signal.group/#CjQKIB2gUf8QH_IXnOJMGQWMDjYz9cNfmRQipGWLFiIgc4MwEhAKBONrSiWHvoUFbbD0xwdh) - Signal channel
|
||||
|
||||
## Tech
|
||||
|
||||
AmneziaVPN uses a number of open source projects to work:
|
||||
@@ -129,6 +121,12 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
||||
## License
|
||||
GPL v.3
|
||||
|
||||
## Contacts
|
||||
[https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
[https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
[https://signal.group/...](https://signal.group/#CjQKIB2gUf8QH_IXnOJMGQWMDjYz9cNfmRQipGWLFiIgc4MwEhAKBONrSiWHvoUFbbD0xwdh) - Signal channel
|
||||
[https://amnezia.org](https://amnezia.org) - project website
|
||||
|
||||
## Donate
|
||||
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
||||
|
||||
Submodule client/3rd-prebuilt deleted from 62992af4fe
@@ -1 +0,0 @@
|
||||
../../../../client/3rd-prebuilt/3rd-prebuilt/openssl/android
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
../../../../client/3rd-prebuilt/3rd-prebuilt/openssl/linux
|
||||
@@ -1 +0,0 @@
|
||||
../../../../client/3rd-prebuilt/3rd-prebuilt/openssl/windows
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -494,6 +494,7 @@ if(ANDROID)
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/AmneziaApp.kt
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/PackageManagerHelper.java
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNActivity.kt
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNApplication.java
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNClientBinder.kt
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNPermissionHelper.kt
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
@@ -516,14 +517,7 @@ if(ANDROID)
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg-go.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg-quick.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libck-ovpn-plugin.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libopenvpn.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libovpn3.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libovpnexec.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libovpnutil.so
|
||||
${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/librsapss.so
|
||||
)
|
||||
|
||||
endforeach()
|
||||
|
||||
endif()
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "amnezia_application.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFontDatabase>
|
||||
#include <QMimeData>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
|
||||
@@ -54,7 +51,7 @@
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Fix config file permissions
|
||||
#ifdef Q_OS_LINUX && !defined(Q_OS_ANDROID)
|
||||
#ifdef Q_OS_LINUX
|
||||
{
|
||||
QSettings s(ORGANIZATION_NAME, APPLICATION_NAME);
|
||||
s.setValue("permFixed", true);
|
||||
@@ -67,6 +64,7 @@
|
||||
QString configLoc2 = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation).first() + "/"
|
||||
+ ORGANIZATION_NAME + "/" + APPLICATION_NAME + "/" + APPLICATION_NAME + ".conf";
|
||||
QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
|
||||
#endif
|
||||
|
||||
m_settings = std::shared_ptr<Settings>(new Settings);
|
||||
@@ -137,19 +135,6 @@ void AmneziaApplication::init()
|
||||
}
|
||||
#endif
|
||||
|
||||
// Android TextField clipboard workaround
|
||||
// https://bugreports.qt.io/browse/QTBUG-113461
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(qApp, &QApplication::applicationStateChanged, [](Qt::ApplicationState state) {
|
||||
if (state == Qt::ApplicationActive) {
|
||||
if (qApp->clipboard()->mimeData()->formats().contains("text/html")) {
|
||||
QTextDocument doc;
|
||||
doc.setHtml(qApp->clipboard()->mimeData()->html());
|
||||
qApp->clipboard()->setText(doc.toPlainText());
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void AmneziaApplication::registerTypes()
|
||||
|
||||
@@ -19,10 +19,11 @@ buildscript {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1'
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
||||
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
|
||||
@@ -40,7 +41,7 @@ apply plugin: 'kotlin-kapt'
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation group: 'org.json', name: 'json', version: '20220924'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||
@@ -64,6 +65,13 @@ dependencies {
|
||||
|
||||
def camerax_ml_version = "1.2.0-beta02"
|
||||
def ml_kit_version = "17.0.3"
|
||||
def work_version = "2.7.1"
|
||||
|
||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
||||
implementation "com.google.code.gson:gson:2.10.1"
|
||||
implementation "com.google.firebase:firebase-analytics-ktx:21.2.0"
|
||||
implementation "com.google.firebase:firebase-crashlytics:18.3.3"
|
||||
implementation "androidx.work:work-runtime-ktx:${work_version}"
|
||||
implementation("androidx.camera:camera-mlkit-vision:${camerax_ml_version}")
|
||||
implementation("com.google.mlkit:barcode-scanning:${ml_kit_version}")
|
||||
}
|
||||
@@ -137,9 +145,9 @@ android {
|
||||
defaultConfig {
|
||||
resConfig "en"
|
||||
minSdkVersion = 24
|
||||
targetSdkVersion = 34
|
||||
versionCode 22 // Change to a higher number
|
||||
versionName "3.0.7" // Change to a higher number
|
||||
targetSdkVersion = 31
|
||||
versionCode 10 // Change to a higher number
|
||||
versionName "2.0.10" // Change to a higher number
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments = [
|
||||
"room.schemaLocation": "${qtAndroidDir}/schemas".toString()
|
||||
@@ -174,6 +182,18 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// externalNativeBuild {
|
||||
// cmake {
|
||||
// path 'wireguard/CMakeLists.txt'
|
||||
// }
|
||||
// }
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version cmakeMinVersion
|
||||
path "${projectDir}/cpp/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
Submodule client/android/cpp/cloak updated: 28890e1c69...6d19a801bc
@@ -1 +0,0 @@
|
||||
../../3rd-prebuilt/3rd-prebuilt/openvpn/android
|
||||
@@ -1 +0,0 @@
|
||||
../../../client/3rd-prebuilt/3rd-prebuilt/wireguard/android
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,6 @@ pluginManagement {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +13,6 @@ dependencyResolutionManagement {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
//apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
//apply plugin: 'com.novoda.bintray-release'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
@@ -28,6 +29,9 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
@@ -42,29 +46,44 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30-M1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
implementation "androidx.room:room-runtime:$roomVersion" // runtime
|
||||
implementation "androidx.preference:preference:1.1.0"
|
||||
implementation "androidx.room:room-runtime:2.5.0" // runtime
|
||||
implementation "androidx.preference:preference:1.2.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
implementation "androidx.browser:browser:1.3.0-alpha01"
|
||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
implementation "com.google.android.material:material:1.2.0-alpha05"
|
||||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
implementation "androidx.work:work-multiprocess:2.7.1"
|
||||
|
||||
implementation "com.android.support:support-compat:27.0.2"
|
||||
implementation "androidx.activity:activity:1.6.1"
|
||||
implementation "androidx.fragment:fragment:1.3.0"
|
||||
implementation "com.jakewharton.timber:timber:5.0.1"
|
||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
||||
implementation "com.google.code.gson:gson:2.10.1"
|
||||
implementation "com.google.firebase:firebase-analytics-ktx:21.2.0"
|
||||
implementation "com.google.firebase:firebase-crashlytics:18.3.3"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.0-rc02"
|
||||
implementation "androidx.core:core-ktx:1.9.0"
|
||||
|
||||
implementation "dnsjava:dnsjava:2.1.9"
|
||||
implementation "com.github.kruton:jsocks:1.0.0"
|
||||
implementation "dnsjava:dnsjava:3.5.2"
|
||||
implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
||||
implementation "com.afollestad.material-dialogs:core:2.6.0"
|
||||
// api "com.takisoft.preferencex:preferencex:1.0.0"
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
api 'com.github.kruton:jsocks:1.0.0'
|
||||
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
api 'org.connectbot.jsocks:jsocks:1.0.0'
|
||||
api 'androidx.fragment:fragment-ktx:1.5.5'
|
||||
|
||||
kapt "androidx.room:room-compiler:2.5.0"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:2.4.0"
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "94322785672",
|
||||
"firebase_url": "https://amnezia-c6715-default-rtdb.europe-west1.firebasedatabase.app",
|
||||
"project_id": "amnezia-c6715",
|
||||
"storage_bucket": "amnezia-c6715.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:94322785672:android:ed42ab188d6edecf754e3d",
|
||||
"android_client_info": {
|
||||
"package_name": "org.amnezia.vpn"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "94322785672-p3q726tro36fr9nluj45l35gqv6nhjsq.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCvblU4w_NZbWk9bNQc-KGmg-WODjbb308"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "94322785672-p3q726tro36fr9nluj45l35gqv6nhjsq.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:94322785672:android:95e8779ff76d6641754e3d",
|
||||
"android_client_info": {
|
||||
"package_name": "org.amnezia.vpn.shadowsocks.core"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "94322785672-p3q726tro36fr9nluj45l35gqv6nhjsq.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCvblU4w_NZbWk9bNQc-KGmg-WODjbb308"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "94322785672-p3q726tro36fr9nluj45l35gqv6nhjsq.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,80 @@
|
||||
* 2.0.1:
|
||||
* Moved `AlertDialogFragment` and related utilities to `fragment` package, with support for Fragment Result API from AndroidX Fragment 1.3.
|
||||
* Dependency updates:
|
||||
- `androidx.fragment:fragment-ktx:1.3.3`;
|
||||
- `com.google.android.material:material:1.3.0`;
|
||||
- `org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32`.
|
||||
* 2.0.0:
|
||||
* Deprecated passing `-V` and `--fast-open` to plugin.
|
||||
Please find `__android_vpn` option passed via plugin options.
|
||||
* Dependency updates:
|
||||
- `androidx.core:core-ktx:1.3.2`;
|
||||
- `androidx.drawerlayout:drawerlayout:1.1.1`;
|
||||
- `com.google.android.material:material:1.2.1`;
|
||||
- `org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10`.
|
||||
* 1.3.4:
|
||||
* Optional new metadata `com.github.shadowsocks.plugin.id.aliases` for plugin ID aliases;
|
||||
(see doc for `PluginContract.METADATA_KEY_ID_ALIASES` and main documentation "Plugin ID Aliasing" for more information)
|
||||
* Please use `android:path` instead of `android:pathPrefix`, sample code in documentations have been updated to reflect this recommendation.
|
||||
* Added missing documentation regarding direct boot support.
|
||||
Please add `android:directBootAware="true"` with proper support for your `provider` if possible.
|
||||
* You can now use `android:resources` on `meta-data` tags. (main/host app update required, however, you should never use dynamic resources)
|
||||
* Fix occasional crash in `AlertDialogFragment`.
|
||||
* Translation updates.
|
||||
* Dependency updates:
|
||||
- `androidx.core:core-ktx:1.2.0`;
|
||||
- `com.google.android.material:material:1.1.0`.
|
||||
* 1.3.3:
|
||||
* Fix a build script issue.
|
||||
* 1.3.2:
|
||||
* Fix first key-value pair disappearing with null value. (#2391)
|
||||
* Dependency updates:
|
||||
- `androidx.core:core-ktx:1.1.0`;
|
||||
- `com.google.android.material:material:1.1.0-rc01`;
|
||||
- `org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61`.
|
||||
* 1.3.1:
|
||||
* New theme resource `Theme.Shadowsocks.Immersive` for better Android Q-esque translucent navigation bars.
|
||||
This is an opt-in feature.
|
||||
Please add `android:theme="@style/Theme.Shadowsocks.Immersive"` to your `<activity>` to enable this theme.
|
||||
* New color resources `light_*` and `dark_*` for passing to custom tabs;
|
||||
* Dependency updates:
|
||||
- `androidx.core:core-ktx:1.1.0-rc03`;
|
||||
- `androidx.drawerlayout:drawerlayout:1.1.0-alpha03`;
|
||||
- `com.google.android.material:material:1.1.0-alpha09`;
|
||||
- `org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.41`.
|
||||
* 1.3.0:
|
||||
* Optional new metadata `com.github.shadowsocks.plugin.executable_path` for even faster initialization;
|
||||
(see doc for `PluginContract.METADATA_KEY_EXECUTABLE_PATH` for more information)
|
||||
* Breaking API change: `val AlertDialogFragment.ret: Ret?` => `fun AlertDialogFragment.ret(which: Int): Ret?`;
|
||||
(nothing needs to be done if you are not using this API)
|
||||
* Dependency updates:
|
||||
- Now targeting API 29;
|
||||
- `androidx.core:core-ktx:1.1.0-rc01`;
|
||||
- `com.google.android.material:material:1.1.0-alpha07`;
|
||||
- `org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.40`.
|
||||
* 1.2.0:
|
||||
* New helper class `AlertDialogFragment` for creating `AlertDialog` that persists through configuration changes;
|
||||
* Dependency update: `com.google.android.material:material:1.1.0-alpha03`.
|
||||
* 1.1.0:
|
||||
* Having control characters in plugin options is no longer allowed.
|
||||
If this breaks your plugin, you are doing it wrong.
|
||||
* New helper method: `PluginOptions.putWithDefault`.
|
||||
* 1.0.0:
|
||||
* BREAKING CHANGE: Plugins developed using this version and forward require shadowsocks-android 4.6.5 or higher.
|
||||
* `PathProvider` now takes `Int` instead of `String` for file modes;
|
||||
* Refactor to AndroidX;
|
||||
* No longer depends on preference libraries.
|
||||
* 0.1.1:
|
||||
* Rewritten in Kotlin;
|
||||
* Fix assert not working;
|
||||
* Min API 21;
|
||||
* Update support library version to 27.1.1.
|
||||
* 0.0.4:
|
||||
* Enlarge text size of number pickers;
|
||||
* Update support library version to 26.0.0.
|
||||
* 0.0.3:
|
||||
* Update support library version to 25.2.0.
|
||||
* 0.0.2:
|
||||
* Add `getOrDefault` to `PluginOptions`;
|
||||
* Update support library version to 25.1.1.
|
||||
* 0.0.1: Initial release.
|
||||
@@ -0,0 +1,167 @@
|
||||
# shadowsocks-android plugin framework
|
||||
|
||||
[Documentation](doc.md) | [Change log](CHANGES.md)
|
||||
|
||||
Support library for easier development on [shadowsocks
|
||||
plugin](https://github.com/shadowsocks/shadowsocks-org/issues/28) for Android. Also includes some
|
||||
useful resources to easily get consistent styling with the main app.
|
||||
|
||||
## Official plugins
|
||||
|
||||
These are some plugins ready to use on shadowsocks-android.
|
||||
|
||||
* [v2ray](https://github.com/shadowsocks/v2ray-plugin-android)
|
||||
* [kcptun](https://github.com/shadowsocks/kcptun-android/releases)
|
||||
* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases)
|
||||
|
||||
## Developer's guide
|
||||
|
||||
This library is designed with Java interoperability in mind so theoretically you can use this
|
||||
library with other languages and/or build tools but there isn't documentation for that yet. This
|
||||
guide is written for Scala + SBT. Contributions are welcome.
|
||||
|
||||
### Package name
|
||||
|
||||
There are no arbitrary restrictions/requirements on package name, component name and content
|
||||
provider authority, but you're suggested to follow the format in this documentations. For package
|
||||
name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to
|
||||
prevent duplicated plugins. In some places hyphens are not accepted, for example package name. In
|
||||
that case, hyphens `-` should be changed into underscores `_`. For example, the package name for
|
||||
`obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`.
|
||||
|
||||
### Add dependency
|
||||
|
||||
First you need to add this library to your dependencies.
|
||||
This library is written mostly in Kotlin but can also work with Java-only projects:
|
||||
|
||||
```gradle
|
||||
implementation 'com.github.shadowsocks:plugin:$LATEST_VERSION'
|
||||
```
|
||||
|
||||
### Native binary configuration
|
||||
|
||||
First you need to get your native binary compiling on Android platform.
|
||||
|
||||
* [Sample project for C](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615);
|
||||
* [Sample project for Go](https://github.com/shadowsocks/v2ray-plugin-android/tree/172bd4cec0276112828614482fb646b79dbf1540).
|
||||
|
||||
In addition to functionalities of a normal plugin, it has to support these additional options:
|
||||
|
||||
* `__android_vpn`: VPN mode.
|
||||
In this case, the plugin should pass all file descriptors that needs protecting from VPN connections (i.e. its traffic will not be forwarded through the VPN) through an ancillary message to `./protect_path`.
|
||||
|
||||
### Implement a binary provider
|
||||
|
||||
You just need to implement two or three methods. For example for `v2ray`:
|
||||
|
||||
```kotlin
|
||||
class BinaryProvider : NativePluginProvider() {
|
||||
override fun populateFiles(provider: PathProvider) {
|
||||
provider.addPath("v2ray", 0b111101101)
|
||||
// add additional files here
|
||||
}
|
||||
|
||||
// remove this method to disable fast mode, read more in the documentation
|
||||
override fun getExecutable() = context!!.applicationInfo.nativeLibraryDir + "/libv2ray.so"
|
||||
|
||||
override fun openFile(uri: Uri): ParcelFileDescriptor = when (uri.path) {
|
||||
"/v2ray" -> ParcelFileDescriptor.open(File(getExecutable()), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
// handle additional files here
|
||||
else -> throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then add it to your manifest:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<provider android:name=".BinaryProvider"
|
||||
android:exported="true"
|
||||
android:directBootAware="true"
|
||||
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.id"
|
||||
android:value="$PLUGIN_ID"/>
|
||||
<!-- Optional: default is empty -->
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
|
||||
android:value="dummy=default;plugin=options"/>
|
||||
<!-- Optional: remove to disable faster mode, read more in the documentation -->
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.executable_path"
|
||||
android:value="$PATH_TO_EXECUTABLE_RELATIVE_TO_NATIVE_LIB_DIR"/>
|
||||
</provider>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### Add user interfaces
|
||||
|
||||
You should add to your plugin app a configuration activity or a help activity or both if you're
|
||||
going to use `ConfigurationActivity.fallbackToManualEditor`.
|
||||
|
||||
#### Configuration activity
|
||||
|
||||
This is used if found instead of a manual input dialog when user clicks "Configure..." in the main
|
||||
app. This gives you maximum freedom of the user interface. To implement this, you need to extend
|
||||
`ConfigurationActivity` and you will get current options via
|
||||
`onInitializePluginOptions(PluginOptions)` and you can invoke `saveChanges(PluginOptions)` or
|
||||
`discardChanges()` before `finish()` or `fallbackToManualEditor()`. Then add it to your manifest:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<activity android:name=".ConfigActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
#### Help activity/callback
|
||||
|
||||
This is started when user taps "?" in manual editor. To implement this, you need to extend
|
||||
`HelpCallback` if you want a simple dialog with help message as `CharSequence` or `HelpActivity`
|
||||
if you want to provide custom user interface, implement the required methods, then add it to your
|
||||
manifest:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<activity android:name=".HelpActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
Great. Now your plugin is ready to use.
|
||||
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("com.vanniktech.maven.publish")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
setupCommon()
|
||||
|
||||
android {
|
||||
namespace = "com.github.shadowsocks.plugin"
|
||||
lint.informational += "GradleDependency"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(kotlin("stdlib-jdk8"))
|
||||
api("androidx.core:core-ktx:1.7.0")
|
||||
api("androidx.fragment:fragment-ktx:1.5.5")
|
||||
api("com.google.android.material:material:1.6.0")
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
# Overview
|
||||
|
||||
Plugin should be bundled as an apk. `$PLUGIN_ID` in this documentation corresponds to the
|
||||
executable name for the plugin in order to be cross-platform, e.g. `obfs-local`. An apk can have
|
||||
more than one plugins bundled. We don't care as long as they have different `$PLUGIN_ID`. For
|
||||
duplicated plugin ID, host should refuse to start.
|
||||
|
||||
There are no arbitrary restrictions/requirements on package name, component name and content
|
||||
provider authority, but you're suggested to follow the format in this documentations. For package
|
||||
name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to prevent
|
||||
duplicated plugins. In some places hyphens are not accepted, for example package name. In that
|
||||
case, hyphens `-` should be changed into underscores `_`. For example, the package name for
|
||||
`obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`.
|
||||
|
||||
It's advised to use this library for easier development, but you're free to start from scratch following this
|
||||
documentation.
|
||||
|
||||
# Plugin configuration
|
||||
|
||||
Plugins get their args configured via one of the following two options:
|
||||
|
||||
* A configuration activity;
|
||||
([example](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615))
|
||||
* If no configuration activity is found or the activity requests the fallback mode, the fallback
|
||||
mode will be used: user manual input and optional help message.
|
||||
([example](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c))
|
||||
|
||||
Your user interface need not be consistent with shadowsocks-android styling - you don't need to use
|
||||
preferences UI at all if you don't feel like it - however it's recommended to use Material Design
|
||||
at minimum.
|
||||
|
||||
## Configuration activity
|
||||
|
||||
If the plugin provides a configuration activity, it will be started when user picks your plugin and
|
||||
taps configure. It:
|
||||
|
||||
* MUST have action: `com.github.shadowsocks.plugin.ACTION_CONFIGURE`;
|
||||
* MUST have category: `android.intent.category.DEFAULT`;
|
||||
* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
|
||||
* SHOULD parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` (all options as a single
|
||||
string) and display the current options;
|
||||
* SHOULD distinguish between server settings and feature settings in some way, e.g. for
|
||||
`obfs-local`, `obfs` is a server setting and `obfs_host` is a feature setting;
|
||||
* On finish, it SHOULD return one of the following results:
|
||||
- `RESULT_OK = 0`: In this case it MUST return the data Intent with the new
|
||||
`com.github.shadowsocks.plugin.EXTRA_OPTIONS`;
|
||||
- `RESULT_CANCELED = -1`: Nothing will be changed;
|
||||
- `RESULT_FALLBACK = 1`: Fallback mode is requested and the host should display the fallback
|
||||
editor.
|
||||
|
||||
This corresponds to `com.github.shadowsocks.plugin.ConfigurationActivity` in the plugin library.
|
||||
Here's what a proper configuration activity usually should look like in `AndroidManifest.xml`:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<activity android:name=".ConfigActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
## Help activity/callback
|
||||
|
||||
If the plugin doesn't provide a configuration activity, it's highly recommended to provide a help
|
||||
message in the form of an Activity. It:
|
||||
|
||||
* MUST have action: `com.github.shadowsocks.plugin.ACTION_HELP`;
|
||||
* MUST have category: `android.intent.category.DEFAULT`;
|
||||
* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`;
|
||||
* CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more
|
||||
relevant information;
|
||||
* SHOULD parse `@NightMode` int extra `com.github.shadowsocks.plugin.EXTRA_NIGHT_MODE` and act
|
||||
accordingly;
|
||||
* SHOULD either:
|
||||
- Be invisible and return help message with CharSequence extra
|
||||
`com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE` in the data intent with `RESULT_OK`; (in this
|
||||
case, a simple dialog will be shown containing the message)
|
||||
- Be visible and return `RESULT_CANCELED`.
|
||||
* SHOULD distinguish between server settings and feature settings in some way, e.g. for
|
||||
`simple_obfs`, `obfs` is a server setting and `obfs_host` is a feature setting.
|
||||
|
||||
This corresponds to `com.github.shadowsocks.plugin.HelpActivity` or
|
||||
`com.github.shadowsocks.plugin.HelpCallback` in the plugin library. Here's what a proper help
|
||||
activity/callback usually should look like in `AndroidManifest.xml`:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<activity android:name=".HelpActivity">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
# Plugin implementation
|
||||
|
||||
Every plugin can be either in native mode or JVM mode.
|
||||
|
||||
## Native mode
|
||||
|
||||
In native mode, plugins are provided as native executables and `shadowsocks-libev`'s plugin mode
|
||||
will be used.
|
||||
|
||||
Every native mode plugin MUST have a content provider to provide the native executables (since they
|
||||
can exceed 1M which is the limit of Intent size) that:
|
||||
|
||||
* MUST have `android:label` and `android:icon`; (may be inherited from parent `application`)
|
||||
* SHOULD have `android:directBootAware="true"` with proper support if possible;
|
||||
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`;
|
||||
(used for discovering plugins)
|
||||
* MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID` or a string resource;
|
||||
* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` and
|
||||
data `plugin://com.github.shadowsocks/$PLUGIN_ID`; (used for configuring plugin)
|
||||
* CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value or a string resource, default is empty;
|
||||
* MUST implement `query` that returns the file list which MUST include `$PLUGIN_ID` when having
|
||||
these as arguments:
|
||||
- `uri = "content://$authority_of_your_provider`;
|
||||
- `projection = ["path", "mode"]`; (relative path, for example `obfs-local`; file mode as integer, for
|
||||
example `0b110100100`)
|
||||
- `selection = null`;
|
||||
- `selectionArgs = null`;
|
||||
- `sortOrder = null`;
|
||||
* MUST implement `openFile` that for files returned in `query`, `openFile` with `mode = "r"` returns
|
||||
a valid `ParcelFileDescriptor` for reading. For example, `uri` can be
|
||||
`content://com.github.shadowsocks.plugin.kcptun/kcptun`.
|
||||
|
||||
This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the plugin library.
|
||||
Here's what a proper native plugin provider usually should look like in `AndroidManifest.xml`:
|
||||
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<provider android:name=".BinaryProvider"
|
||||
android:exported="true"
|
||||
android:directBootAware="true"
|
||||
android:authorities="$FULLY_QUALIFIED_NAME_OF_YOUR_CONTENTPROVIDER"
|
||||
tools:ignore="ExportedContentProvider">
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.id"
|
||||
android:value="$PLUGIN_ID"/>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.default_config"
|
||||
android:value="dummy=default;plugin=options"/>
|
||||
</provider>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
## Native mode without binary copying
|
||||
|
||||
If your plugin binary executable can run in place, you can support native mode without binary
|
||||
copying. To support this mode, your `ContentProvider` must first support native mode with binary
|
||||
copying (this will be used if the fast routine fails) and:
|
||||
|
||||
* MUST implement `call` that returns absolute path to the entry executable as
|
||||
`com.github.shadowsocks.plugin.EXTRA_ENTRY` when having `method = "shadowsocks:getExecutable"`;
|
||||
(`com.github.shadowsocks.plugin.EXTRA_OPTIONS` is provided in extras as well just in case you
|
||||
need them)
|
||||
* SHOULD define `android:installLocation="internalOnly"` for `<manifest>` in AndroidManifest.xml;
|
||||
* SHOULD define `android:extractNativeLibs="true"` for `<application>` in AndroidManifest.xml;
|
||||
|
||||
If you don't plan to support this mode, you can just throw `UnsupportedOperationException` when
|
||||
being invoked. It will fallback to the slow routine automatically.
|
||||
|
||||
### Native mode without binary copying and setup
|
||||
|
||||
Additionally, if your plugin only needs to supply the path of your executable without doing any extra setup work,
|
||||
you can use an additional `meta-data` with name `com.github.shadowsocks.plugin.executable_path`
|
||||
to supply executable path to your native binary.
|
||||
This allows the host app to launch your plugin without ever launching your app.
|
||||
|
||||
## JVM mode
|
||||
|
||||
This feature hasn't been implemented yet.
|
||||
Please open an issue if you need this.
|
||||
|
||||
# Plugin security
|
||||
|
||||
Plugins are certified using package signatures and shadowsocks-android will consider these
|
||||
signatures as trusted:
|
||||
|
||||
* Signatures by [trusted sources](/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt#L39)
|
||||
which includes:
|
||||
- @madeye, i.e. the signer of the main repo;
|
||||
- The main repo doesn't contain any other trusted signatures. Third-party forks should add their
|
||||
signatures to this trusted sources if they have plugins signed by them before publishing their
|
||||
source code.
|
||||
* Current package signature, which means:
|
||||
- If you get apk from shadowsocks-android releases or Google Play, this means only apk signed by
|
||||
@madeye will be recognized as trusted.
|
||||
- If you get apk from a third-party fork, all plugins from that developer will get recognized as
|
||||
trusted automatically even if its source code isn't available anywhere online.
|
||||
|
||||
A warning will be shown for untrusted plugins. No arbitrary restrictions will be applied.
|
||||
|
||||
# Plugin platform versioning
|
||||
|
||||
In order to be able to identify compatible and incompatible plugins, [Semantic
|
||||
Versioning](http://semver.org/) will be used.
|
||||
|
||||
>Given a version number MAJOR.MINOR.PATCH, increment the:
|
||||
>
|
||||
>1. MAJOR version when you make incompatible API changes,
|
||||
>2. MINOR version when you add functionality in a backwards-compatible manner, and
|
||||
>3. PATCH version when you make backwards-compatible bug fixes.
|
||||
|
||||
Plugin app must include this in their application tag: (which should be automatically included if
|
||||
you are using our library)
|
||||
|
||||
```
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.version"
|
||||
android:value="1.0.0"/>
|
||||
```
|
||||
|
||||
# Plugin ID Aliasing
|
||||
|
||||
To implement plugin ID aliasing, you:
|
||||
|
||||
* MUST define meta-data `com.github.shadowsocks.plugin.id.aliases` in your plugin content provider with `android:value="alias"`,
|
||||
or use `android:resources` to specify a string resource or string array resource for multiple aliases.
|
||||
* MUST be able to be matched by `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` when invoked on alias.
|
||||
To do this, you SHOULD use multiple `intent-filter` and use a different `android:path` for each alias.
|
||||
Alternatively, you MAY also use a single `intent-filter` and use `android:pathPattern` to match all your aliases at once.
|
||||
You MUST NOT use `android:pathPrefix` or allow `android:pathPattern` to match undeclared plugin ID/alias as it might create a conflict with other plugins.
|
||||
* SHOULD NOT add or change `intent-filter` for activities to include your aliases -- your plugin ID will always be used.
|
||||
|
||||
For example:
|
||||
```xml
|
||||
<manifest>
|
||||
...
|
||||
<application>
|
||||
...
|
||||
<provider>
|
||||
...
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ID"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"/>
|
||||
<data android:scheme="plugin"
|
||||
android:host="com.github.shadowsocks"
|
||||
android:path="/$PLUGIN_ALIAS"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.id"
|
||||
android:value="$PLUGIN_ID"/>
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.aliases"
|
||||
android:value="$PLUGIN_ALIAS"/>
|
||||
...
|
||||
</provider>
|
||||
...
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
# Android TV
|
||||
|
||||
Android TV client does not invoke configuration activities. Therefore your plugins should automatically work with them.
|
||||
@@ -0,0 +1,21 @@
|
||||
GROUP=com.github.shadowsocks
|
||||
VERSION_NAME=2.0.1
|
||||
|
||||
POM_ARTIFACT_ID=plugin
|
||||
POM_NAME=Shadowsocks Plugin
|
||||
POM_PACKAGING=aar
|
||||
|
||||
POM_DESCRIPTION=SIP003 plugin for Shadowsocks
|
||||
POM_INCEPTION_YEAR=2018
|
||||
|
||||
POM_URL=https://github.com/shadowsocks/shadowsocks-android
|
||||
POM_SCM_URL=https://github.com/shadowsocks/shadowsocks-android
|
||||
POM_SCM_CONNECTION=scm:git:git://github.com/shadowsocks/shadowsocks-android.git
|
||||
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/shadowsocks/shadowsocks-android.git
|
||||
|
||||
POM_LICENCE_NAME=The GNU General Public License v3.0
|
||||
POM_LICENCE_URL=https://www.gnu.org/licenses/gpl-3.0.html
|
||||
POM_LICENCE_DIST=repo
|
||||
|
||||
POM_DEVELOPER_ID=Mygod
|
||||
POM_DEVELOPER_NAME=Mygod Studio
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:theme="@style/Theme.Shadowsocks">
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.version"
|
||||
android:value="2.0.1"/>
|
||||
</application>
|
||||
</manifest>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Related APIs are deprecated in AndroidX", ReplaceWith("fragment.AlertDialogFragment"))
|
||||
abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
|
||||
AppCompatDialogFragment(), DialogInterface.OnClickListener {
|
||||
companion object {
|
||||
private const val KEY_ARG = "arg"
|
||||
private const val KEY_RET = "ret"
|
||||
fun <T : Parcelable> getRet(data: Intent) = data.extras!!.getParcelable<T>(KEY_RET)!!
|
||||
}
|
||||
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
|
||||
|
||||
protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }
|
||||
protected open fun ret(which: Int): Ret? = null
|
||||
fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
|
||||
AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
targetFragment?.onActivityResult(targetRequestCode, which, ret(which)?.let {
|
||||
Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
onClick(dialog, Activity.RESULT_CANCELED)
|
||||
}
|
||||
|
||||
fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
|
||||
setTargetFragment(target, requestCode)
|
||||
showAllowingStateLoss(target.fragmentManager ?: return, tag)
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Base class for configuration activity. A configuration activity is started when user wishes to configure the
|
||||
* selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
|
||||
* `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
|
||||
*
|
||||
* <pre class="prettyprint"><manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <activity android:name=".ConfigureActivity">
|
||||
* <intent-filter>
|
||||
* <action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/>
|
||||
* <category android:name="android.intent.category.DEFAULT"/>
|
||||
* <data android:scheme="plugin"
|
||||
* android:host="com.github.shadowsocks"
|
||||
* android:path="/$PLUGIN_ID"/>
|
||||
* </intent-filter>
|
||||
* </activity>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></pre>
|
||||
*/
|
||||
abstract class ConfigurationActivity : OptionsCapableActivity() {
|
||||
/**
|
||||
* Equivalent to setResult(RESULT_CANCELED).
|
||||
*/
|
||||
fun discardChanges() = setResult(Activity.RESULT_CANCELED)
|
||||
|
||||
/**
|
||||
* Equivalent to setResult(RESULT_OK, args_with_correct_format).
|
||||
*
|
||||
* @param options PluginOptions to save.
|
||||
*/
|
||||
fun saveChanges(options: PluginOptions) =
|
||||
setResult(Activity.RESULT_OK, Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString()))
|
||||
|
||||
/**
|
||||
* Finish this activity and request manual editor to pop up instead.
|
||||
*/
|
||||
fun fallbackToManualEditor() {
|
||||
setResult(PluginContract.RESULT_FALLBACK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
/**
|
||||
* Base class for a help activity. A help activity is started when user taps help when configuring options for your
|
||||
* plugin. To create a help activity, just extend this class, and add it to your manifest like this:
|
||||
*
|
||||
* <pre class="prettyprint"><manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <activity android:name=".HelpActivity">
|
||||
* <intent-filter>
|
||||
* <action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/>
|
||||
* <category android:name="android.intent.category.DEFAULT"/>
|
||||
* <data android:scheme="plugin"
|
||||
* android:host="com.github.shadowsocks"
|
||||
* android:path="/$PLUGIN_ID"/>
|
||||
* </intent-filter>
|
||||
* </activity>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></pre>
|
||||
*/
|
||||
abstract class HelpActivity : OptionsCapableActivity() {
|
||||
override fun onInitializePluginOptions(options: PluginOptions) { }
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
|
||||
* provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
|
||||
* manifest following the same procedure as adding a HelpActivity.
|
||||
*/
|
||||
abstract class HelpCallback : HelpActivity() {
|
||||
abstract fun produceHelpMessage(options: PluginOptions): CharSequence
|
||||
|
||||
override fun onInitializePluginOptions(options: PluginOptions) {
|
||||
setResult(RESULT_OK, Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.os.bundleOf
|
||||
|
||||
/**
|
||||
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
|
||||
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
|
||||
* class, implement the abstract methods, and add it to your manifest like this:
|
||||
*
|
||||
* <pre class="prettyprint"><manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
|
||||
* android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
|
||||
* <intent-filter>
|
||||
* <category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||
* </intent-filter>
|
||||
* </provider>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></pre>
|
||||
*/
|
||||
abstract class NativePluginProvider : ContentProvider() {
|
||||
override fun getType(uri: Uri): String? = "application/x-elf"
|
||||
|
||||
override fun onCreate(): Boolean = true
|
||||
|
||||
/**
|
||||
* Provide all files needed for native plugin.
|
||||
*
|
||||
* @param provider A helper object to use to add files.
|
||||
*/
|
||||
protected abstract fun populateFiles(provider: PathProvider)
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
|
||||
sortOrder: String?): Cursor? {
|
||||
check(selection == null && selectionArgs == null && sortOrder == null)
|
||||
val result = MatrixCursor(projection)
|
||||
populateFiles(PathProvider(uri, result))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns executable entry absolute path.
|
||||
* This is used for fast mode initialization where ss-local launches your native binary at the path given directly.
|
||||
* In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
|
||||
* - android:installLocation="internalOnly" for <manifest>
|
||||
* - android:extractNativeLibs="true" for <application>
|
||||
*
|
||||
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
|
||||
* default behavior.
|
||||
*
|
||||
* @return Absolute path for executable entry.
|
||||
*/
|
||||
open fun getExecutable(): String = throw UnsupportedOperationException()
|
||||
|
||||
abstract fun openFile(uri: Uri): ParcelFileDescriptor
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
|
||||
check(mode == "r")
|
||||
return openFile(uri)
|
||||
}
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
|
||||
PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
|
||||
else -> super.call(method, arg, extras)
|
||||
}
|
||||
|
||||
// Methods that should not be used
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = throw UnsupportedOperationException()
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
|
||||
throw UnsupportedOperationException()
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
/**
|
||||
* Activity that's capable of getting EXTRA_OPTIONS input.
|
||||
*/
|
||||
abstract class OptionsCapableActivity : AppCompatActivity() {
|
||||
protected fun pluginOptions(intent: Intent = this.intent) = try {
|
||||
PluginOptions("", intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
|
||||
} catch (exc: IllegalArgumentException) {
|
||||
Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
|
||||
PluginOptions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate args to your user interface.
|
||||
*
|
||||
* @param options PluginOptions parsed.
|
||||
*/
|
||||
protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) onInitializePluginOptions()
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Helper class to provide relative paths of files to copy.
|
||||
*/
|
||||
class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
|
||||
private val basePath = baseUri.path?.trim('/') ?: ""
|
||||
|
||||
fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
|
||||
val trimmed = path.trim('/')
|
||||
if (trimmed.startsWith(basePath)) cursor.newRow()
|
||||
.add(PluginContract.COLUMN_PATH, trimmed)
|
||||
.add(PluginContract.COLUMN_MODE, mode)
|
||||
return this
|
||||
}
|
||||
fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
|
||||
var sub = to + file.name
|
||||
if (basePath.startsWith(sub)) if (file.isDirectory) {
|
||||
sub += '/'
|
||||
file.listFiles()!!.forEach { addTo(it, sub, mode) }
|
||||
} else addPath(sub, mode)
|
||||
return this
|
||||
}
|
||||
fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
|
||||
if (basePath.startsWith(at)) {
|
||||
if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
/**
|
||||
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
|
||||
*
|
||||
* This class is written in Java to keep Java interoperability.
|
||||
*/
|
||||
object PluginContract {
|
||||
/**
|
||||
* ContentProvider Action: Used for NativePluginProvider.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
*/
|
||||
const val ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
|
||||
/**
|
||||
* Activity Action: Used for ConfigurationActivity.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
*/
|
||||
const val ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
/**
|
||||
* Activity Action: Used for HelpActivity or HelpCallback.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP"
|
||||
*/
|
||||
const val ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"
|
||||
|
||||
/**
|
||||
* The lookup key for a string that provides the plugin entry binary.
|
||||
*
|
||||
* Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
*/
|
||||
const val EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
/**
|
||||
* The lookup key for a string that provides the options as a string.
|
||||
*
|
||||
* Example: "obfs=http;obfs-host=www.baidu.com"
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
*/
|
||||
const val EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
/**
|
||||
* The lookup key for a CharSequence that provides user relevant help message.
|
||||
*
|
||||
* Example: "obfs=<http></http>|tls> Enable obfuscating: HTTP or TLS (Experimental).
|
||||
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
</host_name> */
|
||||
const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
|
||||
/**
|
||||
* The metadata key to retrieve plugin version. Required for plugin applications.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.version"
|
||||
*/
|
||||
const val METADATA_KEY_VERSION = "com.github.shadowsocks.plugin.version"
|
||||
|
||||
/**
|
||||
* The metadata key to retrieve plugin id. Required for plugins.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.id"
|
||||
*/
|
||||
const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id"
|
||||
/**
|
||||
* The metadata key to retrieve plugin id aliases.
|
||||
* Can be a string (representing one alias) or a resource to a string or string array.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.id.aliases"
|
||||
*/
|
||||
const val METADATA_KEY_ID_ALIASES = "com.github.shadowsocks.plugin.id.aliases"
|
||||
/**
|
||||
* The metadata key to retrieve default configuration. Default value is empty.
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.default_config"
|
||||
*/
|
||||
const val METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"
|
||||
/**
|
||||
* The metadata key to retrieve executable path to your native binary.
|
||||
* This path should be relative to your application's nativeLibraryDir.
|
||||
*
|
||||
* If this is set, the host app will prefer this value and (probably) not launch your app at all (aka faster mode).
|
||||
* In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
|
||||
* - android:installLocation="internalOnly" for <manifest>
|
||||
* - android:extractNativeLibs="true" for <application>
|
||||
*
|
||||
* Do not use this if you plan to do some setup work before giving away your binary path,
|
||||
* or your native binary is not at a fixed location relative to your application's nativeLibraryDir.
|
||||
*
|
||||
* Since plugin lib: 1.3.0
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.executable_path"
|
||||
*/
|
||||
const val METADATA_KEY_EXECUTABLE_PATH = "com.github.shadowsocks.plugin.executable_path"
|
||||
|
||||
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
|
||||
|
||||
/** ConfigurationActivity result: fallback to manual edit mode. */
|
||||
const val RESULT_FALLBACK = 1
|
||||
|
||||
/**
|
||||
* Relative to the file to be copied. This column is required.
|
||||
*
|
||||
* Example: "kcptun", "doc/help.txt"
|
||||
*
|
||||
* Type: String
|
||||
*/
|
||||
const val COLUMN_PATH = "path"
|
||||
/**
|
||||
* File mode bits. Default value is 644 in octal.
|
||||
*
|
||||
* Example: 0b110100100 (for 755 in octal)
|
||||
*
|
||||
* Type: Int or String (deprecated)
|
||||
*/
|
||||
const val COLUMN_MODE = "mode"
|
||||
|
||||
/**
|
||||
* The scheme for general plugin actions.
|
||||
*/
|
||||
const val SCHEME = "plugin"
|
||||
/**
|
||||
* The authority for general plugin actions.
|
||||
*/
|
||||
const val AUTHORITY = "com.github.shadowsocks"
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Helper class for processing plugin options.
|
||||
*
|
||||
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
|
||||
*/
|
||||
class PluginOptions : HashMap<String, String?> {
|
||||
var id = ""
|
||||
|
||||
constructor() : super()
|
||||
constructor(initialCapacity: Int) : super(initialCapacity)
|
||||
constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
|
||||
|
||||
private constructor(options: String?, parseId: Boolean) : this() {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
var parseId = parseId
|
||||
if (options.isNullOrEmpty()) return
|
||||
check(options.all { !it.isISOControl() }) { "No control characters allowed." }
|
||||
val tokenizer = StringTokenizer("$options;", "\\=;", true)
|
||||
val current = StringBuilder()
|
||||
var key: String? = null
|
||||
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
|
||||
"\\" -> current.append(tokenizer.nextToken())
|
||||
"=" -> if (key == null) {
|
||||
key = current.toString()
|
||||
current.setLength(0)
|
||||
} else current.append(nextToken)
|
||||
";" -> {
|
||||
if (key != null) {
|
||||
put(key, current.toString())
|
||||
key = null
|
||||
} else if (current.isNotEmpty()) {
|
||||
if (parseId) id = current.toString() else put(current.toString(), null)
|
||||
}
|
||||
current.setLength(0)
|
||||
parseId = false
|
||||
}
|
||||
else -> current.append(nextToken)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(options: String?) : this(options, true)
|
||||
constructor(id: String, options: String?) : this(options, false) {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
/**
|
||||
* Put but if value is null or default, the entry is deleted.
|
||||
*
|
||||
* @return Old value before put.
|
||||
*/
|
||||
fun putWithDefault(key: String, value: String?, default: String? = null) =
|
||||
if (value == null || value == default) remove(key) else put(key, value)
|
||||
|
||||
private fun append(result: StringBuilder, str: String) = str.indices.map { str[it] }.forEach {
|
||||
when (it) {
|
||||
'\\', '=', ';' -> {
|
||||
result.append('\\') // intentionally no break
|
||||
result.append(it)
|
||||
}
|
||||
else -> result.append(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun toString(trimId: Boolean): String {
|
||||
val result = StringBuilder()
|
||||
if (!trimId) if (id.isEmpty()) return "" else append(result, id)
|
||||
for ((key, value) in entries) {
|
||||
if (result.isNotEmpty()) result.append(';')
|
||||
append(result, key)
|
||||
if (value != null) {
|
||||
result.append('=')
|
||||
append(result, value)
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
override fun toString(): String = toString(true)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
|
||||
}
|
||||
override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
@file:JvmName("Utils")
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class Empty : Parcelable
|
||||
|
||||
@JvmOverloads
|
||||
@Deprecated("Moved to fragment package", ReplaceWith("fragment.showAllowingStateLoss"))
|
||||
fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {
|
||||
if (!fragmentManager.isStateSaved) show(fragmentManager, tag)
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
|
||||
*/
|
||||
abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable?> :
|
||||
AppCompatDialogFragment(), DialogInterface.OnClickListener {
|
||||
companion object {
|
||||
private const val KEY_RESULT = "result"
|
||||
private const val KEY_ARG = "arg"
|
||||
private const val KEY_RET = "ret"
|
||||
private const val KEY_WHICH = "which"
|
||||
|
||||
fun <Ret : Parcelable> setResultListener(fragment: Fragment, requestKey: String,
|
||||
listener: (Int, Ret?) -> Unit) {
|
||||
fragment.setFragmentResultListener(requestKey) { _, bundle ->
|
||||
listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET))
|
||||
}
|
||||
}
|
||||
inline fun <reified T : AlertDialogFragment<*, Ret>, Ret : Parcelable?> setResultListener(
|
||||
fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) =
|
||||
setResultListener(fragment, T::class.java.name, listener)
|
||||
}
|
||||
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
|
||||
|
||||
private val resultKey get() = requireArguments().getString(KEY_RESULT)
|
||||
protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }
|
||||
protected open fun ret(which: Int): Ret? = null
|
||||
|
||||
private fun args() = arguments ?: Bundle().also { arguments = it }
|
||||
fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg)
|
||||
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
|
||||
MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
setFragmentResult(resultKey ?: return, Bundle().apply {
|
||||
putInt(KEY_WHICH, which)
|
||||
putParcelable(KEY_RET, ret(which) ?: return@apply)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
onClick(null, Activity.RESULT_CANCELED)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
@file:JvmName("Utils")
|
||||
|
||||
package com.github.shadowsocks.plugin.fragment
|
||||
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
||||
typealias Empty = com.github.shadowsocks.plugin.Empty
|
||||
|
||||
@JvmOverloads
|
||||
fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {
|
||||
if (!fragmentManager.isStateSaved) show(fragmentManager, tag)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Source: https://github.com/material-components/material-components-android/blob/2de39fafe0285aab7e6e101549c4bc93f184a7e5/lib/java/com/google/android/material/button/res/color/mtrl_text_btn_text_color_selector.xml
|
||||
Copyright 2017 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="1.00" android:color="@color/color_primary_text" android:state_checkable="true" android:state_checked="true" android:state_enabled="true"/>
|
||||
<item android:alpha="0.60" android:color="?attr/colorOnSurface" android:state_checkable="true" android:state_checked="false" android:state_enabled="true"/>
|
||||
<item android:alpha="1.00" android:color="@color/color_primary_text" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="?attr/colorOnSurface"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
|
||||
android:id="@+id/toolbar" />
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Servereinstellungen"</string>
|
||||
<string name="feature_cat">"Funktionseinstellungen"</string>
|
||||
<string name="unsaved_changes_prompt">"Änderungen nicht gespeichert. Speichern?"</string>
|
||||
<string name="yes">"Ja"</string>
|
||||
<string name="no">"Nein"</string>
|
||||
<string name="apply">"Anwenden"</string>
|
||||
<string name="file_manager_missing">"Bitte installiere einen Dateimanager, z.B. MiXplorer"</string>
|
||||
<string name="browse">"Durchsuchen..."</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Propiedades del Servidor"</string>
|
||||
<string name="yes">"Sí"</string>
|
||||
<string name="no">"No"</string>
|
||||
<string name="apply">"Aplicar"</string>
|
||||
<string name="file_manager_missing">"Por favor, instala un explorador de archivos como MiXplorer"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"تنظیمات سرور"</string>
|
||||
<string name="feature_cat">"تنظیمات ویژگیها"</string>
|
||||
<string name="unsaved_changes_prompt">"تغییرات ذخیره نشدهاند. ذخیره شوند؟"</string>
|
||||
<string name="yes">"بله"</string>
|
||||
<string name="no">"خیر"</string>
|
||||
<string name="apply">"تایید"</string>
|
||||
<string name="file_manager_missing">"لطفاً یک فایل منیجر مانند MiXplorer نصب کنید"</string>
|
||||
<string name="browse">"مرور کردن..."</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Paramètres du Serveur"</string>
|
||||
<string name="feature_cat">"Paramètres des Fonctionnalités"</string>
|
||||
<string name="unsaved_changes_prompt">"Changements non enregistrés. Voulez-vous enregistrer ?"</string>
|
||||
<string name="yes">"Oui"</string>
|
||||
<string name="no">"Non"</string>
|
||||
<string name="apply">"Appliquer"</string>
|
||||
<string name="file_manager_missing">"Veuillez installer un gestionnaire de fichier tel que MiXplorer"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"サーバー設定"</string>
|
||||
<string name="feature_cat">"機能設定"</string>
|
||||
<string name="unsaved_changes_prompt">"変更は保存されておりません、保存しますか?"</string>
|
||||
<string name="yes">"はい"</string>
|
||||
<string name="no">"いいえ"</string>
|
||||
<string name="apply">"適応"</string>
|
||||
<string name="file_manager_missing">"ファイルマネージャーをインストールしてください(MiXplorerなど)"</string>
|
||||
<string name="browse">"参照…"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"서버 설정"</string>
|
||||
<string name="feature_cat">"기능 설정"</string>
|
||||
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
|
||||
<string name="yes">"예"</string>
|
||||
<string name="no">"아니오"</string>
|
||||
<string name="apply">"적용"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="color_primary">@color/dark_color_primary</color>
|
||||
<color name="color_primary_dark">@color/dark_color_primary_dark</color>
|
||||
<color name="color_primary_text">@color/dark_color_primary_text</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Настройки сервера"</string>
|
||||
<string name="feature_cat">"Функции"</string>
|
||||
<string name="unsaved_changes_prompt">"Сохранить изменения?"</string>
|
||||
<string name="yes">"Да"</string>
|
||||
<string name="no">"Нет"</string>
|
||||
<string name="apply">"Применить"</string>
|
||||
<string name="file_manager_missing">"Установите файловый менеджер (например, MiXplorer)"</string>
|
||||
<string name="browse">"Открыть…"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Sunucu Ayarları"</string>
|
||||
<string name="feature_cat">"Özellik Ayarları"</string>
|
||||
<string name="unsaved_changes_prompt">"Değişiklikler kaydedilmedi. Kaydetmek ister misiniz?"</string>
|
||||
<string name="yes">"Evet"</string>
|
||||
<string name="no">"Hayır"</string>
|
||||
<string name="apply">"Uygula"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"Налаштування сервера"</string>
|
||||
<string name="feature_cat">"Налаштування функцій"</string>
|
||||
<string name="unsaved_changes_prompt">"Зміни не збережено. Зберегти?"</string>
|
||||
<string name="yes">"Так"</string>
|
||||
<string name="no">"Ні"</string>
|
||||
<string name="apply">"Застосувати"</string>
|
||||
<string name="file_manager_missing">"Будь ласка, встановіть менеджер файлів, наприклад, MiXplorer"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Shadowsocks.Immersive">
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"服务器设置"</string>
|
||||
<string name="feature_cat">"功能设置"</string>
|
||||
<string name="unsaved_changes_prompt">"是否要保存修改?"</string>
|
||||
<string name="yes">"是"</string>
|
||||
<string name="no">"否"</string>
|
||||
<string name="apply">"应用"</string>
|
||||
<string name="file_manager_missing">"请安装文件管理器,如 MiXplorer"</string>
|
||||
<string name="browse">"浏览…"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DO NOT EDIT: This file was automagically generated by script.
|
||||
If you are looking for contributing a translation, read this:
|
||||
https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="proxy_cat">"伺服器設定"</string>
|
||||
<string name="feature_cat">"功能設定"</string>
|
||||
<string name="unsaved_changes_prompt">"要儲存變更嗎?"</string>
|
||||
<string name="yes">"是"</string>
|
||||
<string name="no">"否"</string>
|
||||
<string name="apply">"套用"</string>
|
||||
<string name="file_manager_missing">"請安裝文件管理器,如 MiXplorer"</string>
|
||||
<string name="browse">"瀏覽…"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
|
||||
<color name="material_green_700">#388E3C</color>
|
||||
<color name="material_green_a700">#00C853</color>
|
||||
<color name="material_blue_grey_100">#CFD8DC</color>
|
||||
<color name="material_blue_grey_300">#90A4AE</color>
|
||||
<color name="material_blue_grey_500">#607D8B</color>
|
||||
<color name="material_blue_grey_600">#546E7A</color>
|
||||
<color name="material_blue_grey_700">#455A64</color>
|
||||
<color name="material_primary_100">@color/material_blue_grey_100</color>
|
||||
<color name="material_primary_300">@color/material_blue_grey_300</color>
|
||||
<color name="material_primary_500">@color/material_blue_grey_500</color>
|
||||
<color name="material_primary_600">@color/material_blue_grey_600</color>
|
||||
<color name="material_primary_700">@color/material_blue_grey_700</color>
|
||||
<color name="material_primary_800">@color/material_blue_grey_800</color>
|
||||
<color name="material_primary_900">@color/material_blue_grey_900</color>
|
||||
<color name="material_accent_200">@color/material_green_a700</color>
|
||||
|
||||
<color name="light_color_primary">@color/material_primary_500</color>
|
||||
<color name="light_color_primary_dark">@color/material_primary_700</color>
|
||||
<color name="light_color_primary_text">@color/material_primary_500</color>
|
||||
<color name="dark_color_primary">@color/material_primary_800</color>
|
||||
<color name="dark_color_primary_dark">@color/material_primary_900</color>
|
||||
<color name="dark_color_primary_text">@color/material_primary_300</color>
|
||||
|
||||
<color name="color_primary">@color/light_color_primary</color>
|
||||
<color name="color_primary_dark">@color/light_color_primary_dark</color>
|
||||
<color name="color_primary_text">@color/light_color_primary_text</color>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="proxy_cat">Server Settings</string>
|
||||
<string name="feature_cat">Feature Settings</string>
|
||||
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="browse">Browse…</string>
|
||||
<string name="file_manager_missing">Please install a file manager like MiXplorer</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Shadowsocks" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="android:navigationBarColor">@color/color_primary_dark</item>
|
||||
<item name="actionBarStyle">@style/Widget.MaterialComponents.Light.ActionBar.Solid</item>
|
||||
<item name="actionModeCloseDrawable">@drawable/ic_navigation_close</item>
|
||||
<item name="colorAccent">@color/material_accent_200</item>
|
||||
<item name="colorButtonNormal">@color/material_accent_200</item>
|
||||
<item name="colorPrimary">@color/color_primary</item>
|
||||
<item name="colorPrimaryDark">@color/color_primary_dark</item>
|
||||
<item name="windowActionModeOverlay">true</item>
|
||||
|
||||
<!-- Remove ActionBar but keep styles and themes -->
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
<style name="Theme.Shadowsocks.ActionBar">
|
||||
<item name="windowActionBar">true</item>
|
||||
<item name="windowNoTitle">false</item>
|
||||
</style>
|
||||
<style name="Theme.Shadowsocks.Immersive">
|
||||
<item name="android:navigationBarColor">#6000</item>
|
||||
</style>
|
||||
<style name="Theme.Shadowsocks.Translucent" parent="Theme.MaterialComponents.Dialog">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowFrame">@null</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowCloseOnTouchOutside">false</item>
|
||||
<item name="colorAccent">@color/material_accent_200</item>
|
||||
<item name="colorButtonNormal">@color/material_accent_200</item>
|
||||
<item name="colorPrimary">@color/color_primary</item>
|
||||
<item name="colorPrimaryDark">@color/color_primary_dark</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.Dialog">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowFrame">@null</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowCloseOnTouchOutside">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class PluginOptionsTest {
|
||||
@Test
|
||||
fun basic() {
|
||||
val o1 = PluginOptions("obfs-local;obfs=http;obfs-host=localhost")
|
||||
val o2 = PluginOptions("obfs-local", "obfs-host=localhost;obfs=http")
|
||||
Assert.assertEquals(o1.hashCode(), o2.hashCode())
|
||||
Assert.assertEquals(true, o1 == o2)
|
||||
val o3 = PluginOptions(o1.toString(false))
|
||||
Assert.assertEquals(true, o2 == o3)
|
||||
val o4 = PluginOptions(o2.id, o2.toString())
|
||||
Assert.assertEquals(true, o3 == o4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullValues() {
|
||||
val o = PluginOptions("", "a;b;c;d=3")
|
||||
Assert.assertEquals(true, o == PluginOptions("", o.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun escape() {
|
||||
val options = PluginOptions("escapeTest")
|
||||
options["subject"] = "value;semicolon"
|
||||
Assert.assertEquals(true, options == PluginOptions(options.toString(false)))
|
||||
options["key;semicolon"] = "object"
|
||||
Assert.assertEquals(true, options == PluginOptions(options.toString(false)))
|
||||
options["subject"] = "value=equals"
|
||||
Assert.assertEquals(true, options == PluginOptions(options.toString(false)))
|
||||
options["key=equals"] = "object"
|
||||
Assert.assertEquals(true, options == PluginOptions(options.toString(false)))
|
||||
options["advanced\\=;test"] = "in;=\\progress"
|
||||
Assert.assertEquals(true, options == PluginOptions(options.toString(false)))
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,23 @@
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
<application
|
||||
@@ -75,6 +86,7 @@
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
<!-- https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/work/workmanager/src/main/AndroidManifest.xml -->
|
||||
<provider
|
||||
@@ -128,5 +140,18 @@
|
||||
android:directBootAware="true"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
|
||||
<service android:name="com.google.firebase.components.ComponentDiscoveryService"
|
||||
android:process=":QtOnlyProcess"
|
||||
android:directBootAware="true"/>
|
||||
|
||||
<provider android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:node="remove"/>
|
||||
|
||||
<service android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
android:process=":bg"/>
|
||||
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
+13
-9
@@ -25,27 +25,31 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
private val componentName by lazy { ComponentName(app, org.amnezia.vpn.shadowsocks.core.BootReceiver::class.java) }
|
||||
private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
|
||||
var enabled: Boolean
|
||||
get() = app.packageManager.getComponentEnabledSetting(org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName) ==
|
||||
get() = app.packageManager.getComponentEnabledSetting(componentName) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
set(value) = app.packageManager.setComponentEnabledSetting(
|
||||
org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName,
|
||||
set(value) = app.packageManager.setComponentEnabledSetting(componentName,
|
||||
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val locked = when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> false
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
|
||||
else -> return
|
||||
if (!DataStore.persistAcrossReboot) { // sanity check
|
||||
enabled = false
|
||||
return
|
||||
}
|
||||
if (DataStore.directBootAware == locked) org.amnezia.vpn.shadowsocks.core.Core.startService()
|
||||
val doStart = when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> !DataStore.directBootAware
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED -> DataStore.directBootAware
|
||||
else -> DataStore.directBootAware || Build.VERSION.SDK_INT >= 24 && Core.user.isUserUnlocked
|
||||
}
|
||||
if (doStart) Core.startService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,60 +20,72 @@
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.*
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.*
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.UserManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.persistableBundleOf
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
|
||||
import org.amnezia.vpn.shadowsocks.core.BuildConfig
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.*
|
||||
import org.amnezia.vpn.shadowsocks.core.subscription.SubscriptionService
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DeviceStorageApp
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.google.firebase.ktx.initialize
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object Core {
|
||||
const val TAG = "Core"
|
||||
|
||||
object Core : Configuration.Provider {
|
||||
lateinit var app: Application
|
||||
@VisibleForTesting set
|
||||
lateinit var configureIntent: (Context) -> PendingIntent
|
||||
val activity by lazy { app.getSystemService<ActivityManager>()!! }
|
||||
val clipboard by lazy { app.getSystemService<ClipboardManager>()!! }
|
||||
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
|
||||
val notification by lazy { app.getSystemService<NotificationManager>()!! }
|
||||
val user by lazy { app.getSystemService<UserManager>()!! }
|
||||
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
|
||||
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
|
||||
val directBootSupported by lazy {
|
||||
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
|
||||
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
|
||||
Build.VERSION.SDK_INT >= 24 && try {
|
||||
app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
|
||||
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
|
||||
} catch (_: RuntimeException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val activeProfileIds
|
||||
get() = ProfileManager.getProfile(DataStore.profileId).let {
|
||||
if (it == null) emptyList() else listOfNotNull(it.id, it.udpFallback)
|
||||
}
|
||||
val currentProfile: Pair<Profile, Profile?>?
|
||||
get() {
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId)
|
||||
?: return null)
|
||||
}
|
||||
val activeProfileIds get() = ProfileManager.getProfile(DataStore.profileId).let {
|
||||
if (it == null) emptyList() else listOfNotNull(it.id, it.udpFallback)
|
||||
}
|
||||
val currentProfile: ProfileManager.ExpandedProfile? get() {
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile()?.apply { return this }
|
||||
return ProfileManager.expand(ProfileManager.getProfile(DataStore.profileId) ?: return null)
|
||||
}
|
||||
|
||||
fun switchProfile(id: Long): Profile {
|
||||
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
||||
@@ -82,78 +94,93 @@ object Core {
|
||||
}
|
||||
|
||||
fun init(app: Application, configureClass: KClass<out Any>) {
|
||||
Core.app = app
|
||||
configureIntent = {
|
||||
PendingIntent.getActivity(it, 0,
|
||||
Intent(it, configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
this.app = app
|
||||
this.configureIntent = {
|
||||
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES_USER, app)
|
||||
if (old.canRead()) {
|
||||
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||
Acl.getFile(Acl.CUSTOM_RULES_USER).writeText(old.readText())
|
||||
old.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
|
||||
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||
/* Firebase.initialize(deviceStorage) // multiple processes needs manual set-up
|
||||
Timber.plant(object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (t == null) {
|
||||
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)
|
||||
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
|
||||
} else {
|
||||
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
|
||||
if (priority >= Log.INFO) FirebaseCrashlytics.getInstance().recordException(t)
|
||||
}
|
||||
}
|
||||
})*/
|
||||
|
||||
// handle data restored/crash
|
||||
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
|
||||
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
|
||||
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
|
||||
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware && user.isUserUnlocked) {
|
||||
DirectBoot.flushTrafficStats()
|
||||
}
|
||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
|
||||
val assetManager = app.assets
|
||||
try {
|
||||
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
|
||||
File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
|
||||
File(deviceStorage.noBackupFilesDir, file).outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
printLog(e)
|
||||
Timber.w(e)
|
||||
}
|
||||
DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime)
|
||||
}
|
||||
updateNotificationChannels()
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration() = Configuration.Builder().apply {
|
||||
setDefaultProcessName(app.packageName + ":bg")
|
||||
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
|
||||
setExecutor { GlobalScope.launch { it.run() } }
|
||||
setTaskExecutor { GlobalScope.launch { it.run() } }
|
||||
}.build()
|
||||
|
||||
fun updateNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
val nm = app.getSystemService<NotificationManager>()!!
|
||||
nm.createNotificationChannels(listOf(
|
||||
/* if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
notification.createNotificationChannels(listOf(
|
||||
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
|
||||
else NotificationManager.IMPORTANCE_LOW), // #1355
|
||||
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
|
||||
NotificationManager.IMPORTANCE_LOW)))
|
||||
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
||||
}
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
SubscriptionService.notificationChannel))
|
||||
notification.deleteNotificationChannel("service-nat") // NAT mode is gone for good
|
||||
} */
|
||||
}
|
||||
|
||||
fun getPackageInfo(packageName: String) = app.packageManager.getPackageInfo(packageName,
|
||||
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
|
||||
|
||||
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
|
||||
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
|
||||
|
||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
|
||||
init {
|
||||
app.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
|
||||
callback()
|
||||
if (onetime) app.unregisterReceiver(this)
|
||||
}
|
||||
fun trySetPrimaryClip(clip: String, isSensitive: Boolean = false) = try {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, clip).apply {
|
||||
if (isSensitive && Build.VERSION.SDK_INT >= 24) {
|
||||
//description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
|
||||
}
|
||||
})
|
||||
true
|
||||
} catch (e: RuntimeException) {
|
||||
Timber.d(e)
|
||||
false
|
||||
}
|
||||
|
||||
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
|
||||
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD).setPackage(app.packageName))
|
||||
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE).setPackage(app.packageName))
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.plugin.fragment.AlertDialogFragment
|
||||
import org.amnezia.vpn.shadowsocks.plugin.fragment.Empty
|
||||
import org.amnezia.vpn.shadowsocks.plugin.fragment.showAllowingStateLoss
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class UrlImportActivity : AppCompatActivity() {
|
||||
@Parcelize
|
||||
data class ProfilesArg(val profiles: List<Profile>) : Parcelable
|
||||
class ImportProfilesDialogFragment : AlertDialogFragment<ProfilesArg, Empty>() {
|
||||
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
|
||||
setTitle(R.string.add_profile_dialog)
|
||||
setPositiveButton(R.string.yes, listener)
|
||||
setNegativeButton(R.string.no, listener)
|
||||
setMessage(arg.profiles.joinToString("\n"))
|
||||
}
|
||||
|
||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) arg.profiles.forEach { ProfileManager.createProfile(it) }
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (val dialog = handleShareIntent()) {
|
||||
null -> {
|
||||
Toast.makeText(this, R.string.profile_invalid_input, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
else -> dialog.showAllowingStateLoss(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShareIntent() = intent.data?.toString()?.let { sharedStr ->
|
||||
val profiles = Profile.findAllUrls(sharedStr, Core.currentProfile?.main).toList()
|
||||
if (profiles.isEmpty()) null else ImportProfilesDialogFragment().apply {
|
||||
arg(ProfilesArg(profiles))
|
||||
key()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -18,8 +18,7 @@ class VpnManager private constructor() {
|
||||
|
||||
var state = BaseService.State.Idle
|
||||
private var context: Context? = null
|
||||
private val handler = Handler()
|
||||
private val connection = ShadowsocksConnection(handler, true)
|
||||
private val connection = ShadowsocksConnection(true)
|
||||
private var listener: OnStatusChangeListener? = null
|
||||
private val callback: ShadowsocksConnection.Callback = object : ShadowsocksConnection.Callback {
|
||||
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {
|
||||
@@ -69,7 +68,7 @@ class VpnManager private constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
fun init(context: Context){
|
||||
fun init(context: Context) {
|
||||
this.context=context
|
||||
connect()
|
||||
}
|
||||
|
||||
+6
-19
@@ -24,22 +24,17 @@ import android.app.KeyguardManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.StartService
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
|
||||
class VpnRequestActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
private const val REQUEST_CONNECT = 1
|
||||
}
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -49,21 +44,13 @@ class VpnRequestActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
|
||||
receiver = broadcastReceiver { _, _ -> request() }
|
||||
receiver = broadcastReceiver { _, _ -> connect.launch(null) }
|
||||
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
|
||||
} else request()
|
||||
} else connect.launch(null)
|
||||
}
|
||||
|
||||
private fun request() {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == RESULT_OK) Core.startService() else {
|
||||
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
private val connect = registerForActivityResult(StartService()) {
|
||||
if (it) Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
+84
-64
@@ -1,21 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.BaseSorter
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.URLSorter
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class Acl {
|
||||
companion object {
|
||||
const val TAG = "Acl"
|
||||
const val ALL = "all"
|
||||
const val BYPASS_LAN = "bypass-lan"
|
||||
const val BYPASS_CHN = "bypass-china"
|
||||
@@ -23,48 +48,73 @@ class Acl {
|
||||
const val GFWLIST = "gfwlist"
|
||||
const val CHINALIST = "china-list"
|
||||
const val CUSTOM_RULES = "custom-rules"
|
||||
const val CUSTOM_RULES_USER = "custom-rules-user"
|
||||
|
||||
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
|
||||
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
|
||||
|
||||
var customRules: Acl
|
||||
get() {
|
||||
val acl = Acl()
|
||||
val str = DataStore.publicStore.getString(CUSTOM_RULES)
|
||||
if (str != null) acl.fromReader(str.reader(), true)
|
||||
val file = getFile(CUSTOM_RULES_USER)
|
||||
if (file.canRead()) acl.fromReader(file.reader(), true)
|
||||
if (!acl.bypass) {
|
||||
acl.bypass = true
|
||||
acl.subnets.clear()
|
||||
}
|
||||
return acl
|
||||
}
|
||||
set(value) = DataStore.publicStore.putString(CUSTOM_RULES,
|
||||
if ((!value.bypass || value.subnets.size() == 0) && value.bypassHostnames.size() == 0 &&
|
||||
value.proxyHostnames.size() == 0 && value.urls.size() == 0) null else value.toString())
|
||||
set(value) = getFile(CUSTOM_RULES_USER).writeText(value.toString())
|
||||
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||
|
||||
suspend fun <T> parse(reader: Reader, bypassHostnames: (String) -> T, proxyHostnames: (String) -> T,
|
||||
urls: ((URL) -> T)? = null, defaultBypass: Boolean = false): Pair<Boolean, List<Subnet>> {
|
||||
var bypass = defaultBypass
|
||||
val bypassSubnets = mutableListOf<Subnet>()
|
||||
val proxySubnets = mutableListOf<Subnet>()
|
||||
var hostnames: ((String) -> T)? = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||
var subnets: MutableList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
|
||||
reader.useLines {
|
||||
for (line in it) {
|
||||
coroutineContext[Job]!!.ensureActive()
|
||||
val input = (if (urls == null) line else {
|
||||
val blocks = line.split('#', limit = 2)
|
||||
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
|
||||
if (url != null) urls(URL(url))
|
||||
blocks[0]
|
||||
}).trim()
|
||||
if (input.getOrNull(0) == '[') when (input) {
|
||||
"[outbound_block_list]" -> {
|
||||
hostnames = null
|
||||
subnets = null
|
||||
}
|
||||
"[black_list]", "[bypass_list]" -> {
|
||||
hostnames = bypassHostnames
|
||||
subnets = bypassSubnets
|
||||
}
|
||||
"[white_list]", "[proxy_list]" -> {
|
||||
hostnames = proxyHostnames
|
||||
subnets = proxySubnets
|
||||
}
|
||||
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||
else -> error("Unrecognized block: $input")
|
||||
} else if (subnets != null && input.isNotEmpty()) {
|
||||
val subnet = Subnet.fromString(input)
|
||||
if (subnet == null) hostnames!!(input) else subnets!!.add(subnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bypass to if (bypass) proxySubnets else bypassSubnets
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
override fun onInserted(position: Int, count: Int) { }
|
||||
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) { }
|
||||
override fun onChanged(position: Int, count: Int) { }
|
||||
override fun onRemoved(position: Int, count: Int) { }
|
||||
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
|
||||
override fun compare(o1: T?, o2: T?): Int =
|
||||
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
|
||||
abstract fun compareNonNull(o1: T, o2: T): Int
|
||||
}
|
||||
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
|
||||
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
|
||||
}
|
||||
private object StringSorter : DefaultSorter<String>()
|
||||
private object SubnetSorter : DefaultSorter<Subnet>()
|
||||
private object URLSorter : BaseSorter<URL>() {
|
||||
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
|
||||
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
|
||||
}
|
||||
|
||||
val bypassHostnames = SortedList(String::class.java, StringSorter)
|
||||
val proxyHostnames = SortedList(String::class.java, StringSorter)
|
||||
@@ -89,40 +139,11 @@ class Acl {
|
||||
proxyHostnames.clear()
|
||||
subnets.clear()
|
||||
urls.clear()
|
||||
bypass = defaultBypass
|
||||
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
|
||||
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
|
||||
reader.useLines {
|
||||
for (line in it) {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
val blocks = (line as java.lang.String).split("#", 2)
|
||||
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
|
||||
if (url != null) urls.add(URL(url))
|
||||
when (val input = blocks[0].trim()) {
|
||||
"[outbound_block_list]" -> {
|
||||
hostnames = null
|
||||
subnets = null
|
||||
}
|
||||
"[black_list]", "[bypass_list]" -> {
|
||||
hostnames = bypassHostnames
|
||||
subnets = bypassSubnets
|
||||
}
|
||||
"[white_list]", "[proxy_list]" -> {
|
||||
hostnames = proxyHostnames
|
||||
subnets = proxySubnets
|
||||
}
|
||||
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||
else -> if (subnets != null && input.isNotEmpty()) {
|
||||
val subnet = Subnet.fromString(input)
|
||||
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
val (bypass, subnets) = runBlocking {
|
||||
parse(reader, bypassHostnames::add, proxyHostnames::add, urls::add, defaultBypass)
|
||||
}
|
||||
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
|
||||
this.bypass = bypass
|
||||
for (item in subnets) this.subnets.add(item)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -132,14 +153,13 @@ class Acl {
|
||||
|
||||
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
|
||||
if (depth > 0) for (url in urls.asIterable()) {
|
||||
val child = Acl()
|
||||
try {
|
||||
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
continue
|
||||
}
|
||||
val child = Acl().fromReader(connect(url).also {
|
||||
(it as? HttpURLConnection)?.instanceFollowRedirects = true
|
||||
}.getInputStream().bufferedReader(), bypass)
|
||||
child.flatten(depth - 1, connect)
|
||||
if (bypass != child.bypass) {
|
||||
Timber.w("Imported network ACL has a conflicting mode set. " +
|
||||
"This will probably not work as intended. URL: $url")
|
||||
child.subnets.clear() // subnets for the different mode are discarded
|
||||
child.bypass = bypass
|
||||
}
|
||||
|
||||
+43
-17
@@ -1,9 +1,34 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.useCancellable
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -11,28 +36,29 @@ class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWor
|
||||
companion object {
|
||||
private const val KEY_ROUTE = "route"
|
||||
|
||||
fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
|
||||
OneTimeWorkRequestBuilder<AclSyncer>().run {
|
||||
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
|
||||
setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresCharging(true)
|
||||
.build())
|
||||
setInitialDelay(10, TimeUnit.SECONDS)
|
||||
build()
|
||||
})
|
||||
fun schedule(route: String) {
|
||||
if (Build.VERSION.SDK_INT >= 24 && !Core.user.isUserUnlocked) return // work does not support this
|
||||
WorkManager.getInstance(app).enqueueUniqueWork(
|
||||
route, ExistingWorkPolicy.REPLACE, OneTimeWorkRequestBuilder<AclSyncer>().run {
|
||||
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
|
||||
setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresCharging(true)
|
||||
.build())
|
||||
setInitialDelay(10, TimeUnit.SECONDS)
|
||||
build()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override val coroutineContext get() = Dispatchers.IO
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val route = inputData.getString(KEY_ROUTE)!!
|
||||
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
|
||||
.use { it.readText() }
|
||||
val connection = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openConnection() as HttpURLConnection
|
||||
val acl = connection.useCancellable { inputStream.bufferedReader().use { it.readText() } }
|
||||
Acl.getFile(route).printWriter().use { it.write(acl) }
|
||||
Result.success()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Result.retry()
|
||||
Timber.d(e)
|
||||
if (runAttemptCount > 5) Result.failure() else Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
+26
-22
@@ -24,28 +24,27 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.TransproxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.VpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* This object should be compact as it will not get GC-ed.
|
||||
*/
|
||||
class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
private var listenForDeath: Boolean = false) :
|
||||
ServiceConnection, IBinder.DeathRecipient {
|
||||
class ShadowsocksConnection(private var listenForDeath: Boolean = false) : ServiceConnection, IBinder.DeathRecipient {
|
||||
companion object {
|
||||
val serviceClass get() = when (DataStore.serviceMode) {
|
||||
Key.modeProxy -> ProxyService::class
|
||||
Key.modeVpn -> ShadowsocksVpnService::class
|
||||
Key.modeVpn -> VpnService::class
|
||||
Key.modeTransproxy -> TransproxyService::class
|
||||
else -> throw UnknownError()
|
||||
}.java
|
||||
@@ -70,38 +69,38 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
callback.stateChanged(BaseService.State.values()[state], profileName, msg)
|
||||
}
|
||||
}
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
val callback = callback ?: return
|
||||
handler.post {
|
||||
callback.trafficUpdated(profileId, stats)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) { callback.trafficUpdated(profileId, stats) }
|
||||
}
|
||||
override fun trafficPersisted(profileId: Long) {
|
||||
val callback = callback ?: return
|
||||
handler.post { callback.trafficPersisted(profileId) }
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) { callback.trafficPersisted(profileId) }
|
||||
}
|
||||
}
|
||||
private var binder: IBinder? = null
|
||||
|
||||
var bandwidthTimeout = 0L
|
||||
set(value) {
|
||||
val service = service
|
||||
if (bandwidthTimeout != value && service != null)
|
||||
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
|
||||
service.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: DeadObjectException) { }
|
||||
try {
|
||||
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
|
||||
else service?.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: RemoteException) { }
|
||||
field = value
|
||||
}
|
||||
var service: IShadowsocksService? = null
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||
this.binder = binder
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||
this.service = service
|
||||
if (!callbackRegistered) try {
|
||||
try {
|
||||
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||
check(!callbackRegistered)
|
||||
service.registerCallback(serviceCallback)
|
||||
callbackRegistered = true
|
||||
if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
|
||||
@@ -118,7 +117,8 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
|
||||
override fun binderDied() {
|
||||
service = null
|
||||
callback?.also { handler.post(it::onBinderDied) }
|
||||
callbackRegistered = false
|
||||
callback?.also { GlobalScope.launch(Dispatchers.Main.immediate) { it.onBinderDied() } }
|
||||
}
|
||||
|
||||
private fun unregisterCallback() {
|
||||
@@ -144,9 +144,13 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) { } // ignore
|
||||
connectionActive = false
|
||||
if (listenForDeath) binder?.unlinkToDeath(this, 0)
|
||||
if (listenForDeath) try {
|
||||
binder?.unlinkToDeath(this, 0)
|
||||
} catch (_: NoSuchElementException) { }
|
||||
binder = null
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
try {
|
||||
service?.stopListeningForBandwidth(serviceCallback)
|
||||
} catch (_: RemoteException) { }
|
||||
service = null
|
||||
callback = null
|
||||
}
|
||||
|
||||
+2
-15
@@ -20,9 +20,10 @@
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.aidl
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class TrafficStats(
|
||||
// Bytes per second
|
||||
var txRate: Long = 0L,
|
||||
@@ -35,18 +36,4 @@ data class TrafficStats(
|
||||
operator fun plus(other: TrafficStats) = TrafficStats(
|
||||
txRate + other.txRate, rxRate + other.rxRate,
|
||||
txTotal + other.txTotal, rxTotal + other.rxTotal)
|
||||
|
||||
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeLong(txRate)
|
||||
parcel.writeLong(rxRate)
|
||||
parcel.writeLong(txTotal)
|
||||
parcel.writeLong(rxTotal)
|
||||
}
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TrafficStats> {
|
||||
override fun createFromParcel(parcel: Parcel) = TrafficStats(parcel)
|
||||
override fun newArray(size: Int): Array<TrafficStats?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
+141
-132
@@ -24,26 +24,33 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.*
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteCallbackList
|
||||
import android.os.RemoteException
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.amnezia.vpn.shadowsocks.core.BootReceiver
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DnsResolverCompat
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.analytics.ktx.logEvent
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* This object uses WeakMap to simulate the effects of multi-inheritance.
|
||||
@@ -63,15 +70,20 @@ object BaseService {
|
||||
const val CONFIG_FILE = "shadowsocks.conf"
|
||||
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
|
||||
|
||||
class Data(private val service: Interface) {
|
||||
interface ExpectedException
|
||||
class ExpectedExceptionWrapper(e: Exception) : Exception(e.localizedMessage, e), ExpectedException
|
||||
|
||||
class Data (private val service: Interface) {
|
||||
var state = State.Stopped
|
||||
var processes: GuardedProcessPool? = null
|
||||
var proxy: ProxyInstance? = null
|
||||
var udpFallback: ProxyInstance? = null
|
||||
var localDns: LocalDnsWorker? = null
|
||||
|
||||
// var notification: ServiceNotification? = null
|
||||
// var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SHUTDOWN -> service.persistStats()
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
else -> service.stopRunner()
|
||||
}
|
||||
@@ -88,16 +100,16 @@ object BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
|
||||
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable {
|
||||
private val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
|
||||
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
|
||||
super.onCallbackDied(callback, cookie)
|
||||
stopListeningForBandwidth(callback ?: return)
|
||||
}
|
||||
}
|
||||
private val bandwidthListeners =
|
||||
mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
private val handler = Handler()
|
||||
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||
private var looper: Job? = null
|
||||
|
||||
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
|
||||
@@ -107,63 +119,63 @@ object BaseService {
|
||||
}
|
||||
|
||||
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
|
||||
repeat(callbacks.beginBroadcast()) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: DeadObjectException) {
|
||||
} catch (e: Exception) {
|
||||
printLog(e)
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
repeat(count) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: RemoteException) {
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
|
||||
private fun registerTimeout() {
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.minOrNull() ?: return)
|
||||
}
|
||||
|
||||
private fun onTimeout() {
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
private suspend fun loop() {
|
||||
while (true) {
|
||||
delay(bandwidthListeners.values.minOrNull() ?: return)
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) {
|
||||
stats.forEach { (id, stats) -> item.trafficUpdated(id, stats) }
|
||||
item.trafficUpdated(0, sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
registerTimeout()
|
||||
}
|
||||
|
||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
|
||||
val wasEmpty = bandwidthListeners.isEmpty()
|
||||
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
|
||||
if (wasEmpty) registerTimeout()
|
||||
if (data?.state != State.Connected) return
|
||||
launch {
|
||||
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(cb.asBinder(), timeout) == null)) {
|
||||
check(looper == null)
|
||||
looper = launch { loop() }
|
||||
}
|
||||
if (data?.state != State.Connected) return@launch
|
||||
var sum = TrafficStats()
|
||||
val data = data
|
||||
val proxy = data?.proxy ?: return
|
||||
val proxy = data?.proxy ?: return@launch
|
||||
proxy.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
})
|
||||
}
|
||||
data.udpFallback?.also { udpFallback ->
|
||||
udpFallback.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(
|
||||
udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
})
|
||||
}
|
||||
}
|
||||
cb.trafficUpdated(0, sum)
|
||||
@@ -171,8 +183,11 @@ object BaseService {
|
||||
}
|
||||
|
||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
launch {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
|
||||
looper!!.cancel()
|
||||
looper = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,12 +196,12 @@ object BaseService {
|
||||
callbacks.unregister(cb)
|
||||
}
|
||||
|
||||
fun stateChanged(s: State, msg: String?) {
|
||||
fun stateChanged(s: State, msg: String?) = launch {
|
||||
val profileName = profileName
|
||||
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
|
||||
}
|
||||
|
||||
fun trafficPersisted(ids: List<Long>) {
|
||||
fun trafficPersisted(ids: List<Long>) = launch {
|
||||
if (bandwidthListeners.isNotEmpty() && ids.isNotEmpty()) broadcast { item ->
|
||||
if (bandwidthListeners.contains(item.asBinder())) ids.forEach(item::trafficPersisted)
|
||||
}
|
||||
@@ -194,7 +209,7 @@ object BaseService {
|
||||
|
||||
override fun close() {
|
||||
callbacks.kill()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
cancel()
|
||||
data = null
|
||||
}
|
||||
}
|
||||
@@ -202,48 +217,36 @@ object BaseService {
|
||||
interface Interface {
|
||||
val data: Data
|
||||
val tag: String
|
||||
// fun createNotification(profileName: String): ServiceNotification
|
||||
//fun createNotification(profileName: String): ServiceNotification
|
||||
|
||||
fun onBind(intent: Intent): IBinder? =
|
||||
if (intent.action == Action.SERVICE) data.binder else null
|
||||
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun forceLoad() {
|
||||
val (profile, fallback) = Core.currentProfile
|
||||
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||
if (profile.host.isEmpty() || profile.password.isEmpty() ||
|
||||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())
|
||||
) {
|
||||
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||
return
|
||||
}
|
||||
val s = data.state
|
||||
when {
|
||||
s == State.Stopped -> startRunner()
|
||||
s.canStop -> stopRunner(true)
|
||||
else -> {}
|
||||
else -> Timber.w("Illegal state $s when invoking use")
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
||||
val isVpnService get() = false
|
||||
|
||||
suspend fun startProcesses() {
|
||||
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false
|
||||
) app else Core.deviceStorage).noBackupFilesDir
|
||||
val context = if (Build.VERSION.SDK_INT < 24 || Core.user.isUserUnlocked) app else Core.deviceStorage
|
||||
val configRoot = context.noBackupFilesDir
|
||||
val udpFallback = data.udpFallback
|
||||
data.proxy!!.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null) "-u" else null
|
||||
)
|
||||
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
|
||||
udpFallback?.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"-U"
|
||||
)
|
||||
data.proxy!!.start(this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null && data.proxy?.plugin == null) "tcp_and_udp" else "tcp_and_udp")
|
||||
if (udpFallback?.plugin != null) throw ExpectedExceptionWrapper(IllegalStateException(
|
||||
"UDP fallback cannot have plugins"))
|
||||
udpFallback?.start(this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"udp_only", false)
|
||||
data.localDns = LocalDnsWorker(this::rawResolver).apply { start() }
|
||||
}
|
||||
|
||||
fun startRunner() {
|
||||
@@ -257,6 +260,8 @@ object BaseService {
|
||||
close(scope)
|
||||
data.processes = null
|
||||
}
|
||||
data.localDns?.shutdown(scope)
|
||||
data.localDns = null
|
||||
}
|
||||
|
||||
fun stopRunner(restart: Boolean = false, msg: String? = null) {
|
||||
@@ -264,6 +269,7 @@ object BaseService {
|
||||
// change the state
|
||||
data.changeState(State.Stopping)
|
||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
//Firebase.analytics.logEvent("stop") { param(FirebaseAnalytics.Param.METHOD, tag) }
|
||||
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
||||
this@Interface as Service
|
||||
// we use a coroutineScope here to allow clean-up in parallel
|
||||
@@ -276,8 +282,8 @@ object BaseService {
|
||||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
// data.notification?.destroy()
|
||||
// data.notification = null
|
||||
// data.notification?.destroy()
|
||||
// data.notification = null
|
||||
|
||||
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
|
||||
it.shutdown(this)
|
||||
@@ -292,86 +298,89 @@ object BaseService {
|
||||
data.changeState(State.Stopped, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (restart) {
|
||||
startRunner()
|
||||
} else {
|
||||
Log.d("Aman", "Stop Self BaseService-------")
|
||||
// stopSelf()
|
||||
if (restart) startRunner() else {
|
||||
BootReceiver.enabled = false
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preInit() {}
|
||||
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||
fun persistStats() =
|
||||
listOfNotNull(data.proxy, data.udpFallback).forEach { it.trafficMonitor?.persistStats(it.profile.id) }
|
||||
|
||||
suspend fun preInit() { }
|
||||
suspend fun rawResolver(query: ByteArray) = DnsResolverCompat.resolveRawOnActiveNetwork(query)
|
||||
suspend fun openConnection(url: URL) = url.openConnection()
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val data = data
|
||||
if (data.state != State.Stopped) return Service.START_REDELIVER_INTENT
|
||||
val profilePair = Core.currentProfile
|
||||
if (data.state != State.Stopped) return Service.START_NOT_STICKY
|
||||
val expanded = Core.currentProfile
|
||||
this as Context
|
||||
if (profilePair == null) {
|
||||
if (expanded == null) {
|
||||
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
|
||||
// data.notification = createNotification("")
|
||||
// data.notification = createNotification("")
|
||||
stopRunner(false, getString(R.string.profile_empty))
|
||||
return Service.START_REDELIVER_INTENT
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
val (profile, fallback) = expanded
|
||||
try {
|
||||
data.proxy = ProxyInstance(profile)
|
||||
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// data.notification = createNotification("")
|
||||
stopRunner(false, e.message)
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
val (profile, fallback) = profilePair
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
val proxy = ProxyInstance(profile)
|
||||
data.proxy = proxy
|
||||
data.udpFallback =
|
||||
if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
|
||||
BootReceiver.enabled = DataStore.persistAcrossReboot
|
||||
if (!data.closeReceiverRegistered) {
|
||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||
ContextCompat.registerReceiver(this, data.closeReceiver, IntentFilter().apply {
|
||||
addAction(Action.RELOAD)
|
||||
addAction(Intent.ACTION_SHUTDOWN)
|
||||
addAction(Action.CLOSE)
|
||||
})
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
// data.notification = createNotification(profile.formattedName)
|
||||
// data.notification = createNotification(profile.formattedName)
|
||||
//Firebase.analytics.logEvent("start") { param(FirebaseAnalytics.Param.METHOD, tag) }
|
||||
|
||||
data.changeState(State.Connecting)
|
||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
Executable.killAll() // clean up old processes
|
||||
preInit()
|
||||
proxy.init(this@Interface)
|
||||
data.udpFallback?.init(this@Interface)
|
||||
if (profile.route == Acl.CUSTOM_RULES) try {
|
||||
withContext(Dispatchers.IO) {
|
||||
Acl.customRules.flatten(10, this@Interface::openConnection).also {
|
||||
Acl.save(Acl.CUSTOM_RULES, it)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw ExpectedExceptionWrapper(e)
|
||||
}
|
||||
|
||||
data.processes = GuardedProcessPool {
|
||||
printLog(it)
|
||||
Timber.w(it)
|
||||
stopRunner(false, it.readableMessage)
|
||||
}
|
||||
startProcesses()
|
||||
|
||||
proxy.scheduleUpdate()
|
||||
data.proxy!!.scheduleUpdate()
|
||||
data.udpFallback?.scheduleUpdate()
|
||||
|
||||
data.changeState(State.Connected)
|
||||
} catch (_: CancellationException) {
|
||||
// if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(false, getString(R.string.invalid_server))
|
||||
} catch (exc: Throwable) {
|
||||
if (exc !is PluginManager.PluginNotFoundException &&
|
||||
exc !is BindException &&
|
||||
exc !is ShadowsocksVpnService.NullConnectionException
|
||||
) {
|
||||
printLog(exc)
|
||||
}
|
||||
stopRunner(
|
||||
false,
|
||||
"${getString(R.string.service_failed)}: ${exc.readableMessage}"
|
||||
)
|
||||
if (exc is ExpectedException) Timber.d(exc) else Timber.w(exc)
|
||||
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
|
||||
} finally {
|
||||
data.connectingJob = null
|
||||
}
|
||||
}
|
||||
return Service.START_REDELIVER_INTENT
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -24,19 +24,19 @@ import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.text.TextUtils
|
||||
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object Executable {
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
const val SS_LOCAL = "libss-local.so"
|
||||
const val SS_LOCAL = "libsslocal.so"
|
||||
const val TUN2SOCKS = "libtun2socks.so"
|
||||
|
||||
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) } ?: return) {
|
||||
val exe = File(try {
|
||||
File(process, "cmdline").inputStream().bufferedReader().readText()
|
||||
} catch (_: IOException) {
|
||||
@@ -46,7 +46,8 @@ object Executable {
|
||||
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.ESRCH) {
|
||||
e.printStackTrace()
|
||||
Timber.w("SIGKILL ${exe.absolutePath} (${process.name}) failed")
|
||||
Timber.w(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-12
@@ -25,12 +25,13 @@ import android.os.SystemClock
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.MainThread
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Commandline
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -38,7 +39,6 @@ import kotlin.concurrent.thread
|
||||
|
||||
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
|
||||
companion object {
|
||||
private const val TAG = "GuardedProcessPool"
|
||||
private val pid by lazy {
|
||||
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid").apply { isAccessible = true }
|
||||
}
|
||||
@@ -49,8 +49,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||
|
||||
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
|
||||
input.bufferedReader().forEachLine(logger)
|
||||
} catch (_: IOException) {
|
||||
} // ignore
|
||||
} catch (_: IOException) { } // ignore
|
||||
|
||||
fun start() {
|
||||
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
|
||||
@@ -62,31 +61,40 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||
val exitChannel = Channel<Int>()
|
||||
try {
|
||||
while (true) {
|
||||
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
|
||||
thread(name = "stderr-$cmdName") {
|
||||
streamLogger(process.errorStream) { Log.e(cmdName, it) }
|
||||
}
|
||||
thread(name = "stdout-$cmdName") {
|
||||
streamLogger(process.inputStream) { Log.i(cmdName, it) }
|
||||
streamLogger(process.inputStream) { Log.e(cmdName, it) }
|
||||
// this thread also acts as a daemon thread for waitFor
|
||||
runBlocking { exitChannel.send(process.waitFor()) }
|
||||
}
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
val exitCode = exitChannel.receive()
|
||||
running = false
|
||||
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
|
||||
when {
|
||||
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException(
|
||||
"$cmdName exits too fast (exit code: $exitCode)")
|
||||
exitCode == 128 + OsConstants.SIGKILL -> Log.e(cmdName, "$cmdName was killed")
|
||||
else -> Log.e(cmdName, "$cmdName unexpectedly exits with code $exitCode")
|
||||
}
|
||||
Log.e(cmdName, "restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
|
||||
start()
|
||||
running = true
|
||||
onRestartCallback?.invoke()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(cmdName, "error occurred. stop guard: ${Commandline.toString(cmd)}")
|
||||
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
||||
} finally {
|
||||
if (running) withContext(NonCancellable) {
|
||||
// clean-up cannot be cancelled
|
||||
if (running) withContext(NonCancellable) { // clean-up cannot be cancelled
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
try {
|
||||
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.ESRCH) throw e
|
||||
if (e.errno != OsConstants.ESRCH) Log.e(cmdName, e.toString())
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
Log.e(cmdName, e.toString())
|
||||
}
|
||||
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
|
||||
}
|
||||
@@ -105,6 +113,7 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
|
||||
|
||||
@MainThread
|
||||
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
|
||||
Log.i("GuardedProcessPool", "start process: ${Commandline.toString(cmd)}")
|
||||
Guard(cmd).apply {
|
||||
start() // if start fails, IOException will be thrown directly
|
||||
launch { looper(onRestartCallback) }
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Socks5Endpoint
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
|
||||
object LocalDnsService {
|
||||
private val googleApisTester =
|
||||
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
|
||||
private val chinaIpList by lazy {
|
||||
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
|
||||
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
|
||||
}
|
||||
|
||||
private val servers = WeakHashMap<Interface, LocalDnsServer>()
|
||||
|
||||
interface Interface : BaseService.Interface {
|
||||
override suspend fun startProcesses() {
|
||||
super.startProcesses()
|
||||
val profile = data.proxy!!.profile
|
||||
val dns = URI("dns://${profile.remoteDns}")
|
||||
LocalDnsServer(this::resolver,
|
||||
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
|
||||
DataStore.proxyAddress).apply {
|
||||
tcp = !profile.udpdns
|
||||
when (profile.route) {
|
||||
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
|
||||
remoteDomainMatcher = googleApisTester
|
||||
localIpMatcher = chinaIpList
|
||||
}
|
||||
Acl.CHINALIST -> { }
|
||||
else -> forwardOnly = true
|
||||
}
|
||||
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
servers.remove(this)?.shutdown(scope)
|
||||
super.killProcesses(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.net.LocalSocket
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DnsResolverCompat
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xbill.DNS.Message
|
||||
import org.xbill.DNS.Rcode
|
||||
import timber.log.Timber
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class LocalDnsWorker(private val resolver: suspend (ByteArray) -> ByteArray) : ConcurrentLocalSocketListener(
|
||||
"LocalDnsThread", File(Core.deviceStorage.noBackupFilesDir, "local_dns_path")), CoroutineScope {
|
||||
override fun acceptInternal(socket: LocalSocket) = error("big no no")
|
||||
override fun accept(socket: LocalSocket) {
|
||||
launch {
|
||||
socket.use {
|
||||
val input = DataInputStream(socket.inputStream)
|
||||
val query = try {
|
||||
ByteArray(input.readUnsignedShort()).also { input.read(it) }
|
||||
} catch (e: IOException) { // connection early close possibly due to resolving timeout
|
||||
return@use Timber.d(e)
|
||||
}
|
||||
try {
|
||||
resolver(query)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TimeoutCancellationException -> Timber.w("Resolving timed out")
|
||||
is CancellationException -> { } // ignore
|
||||
is IOException -> Timber.d(e)
|
||||
is UnsupportedOperationException -> Timber.w(e.message)
|
||||
else -> Timber.w(e)
|
||||
}
|
||||
try {
|
||||
DnsResolverCompat.prepareDnsResponse(Message(query)).apply {
|
||||
header.rcode = Rcode.SERVFAIL
|
||||
}.toWire()
|
||||
} catch (_: IOException) {
|
||||
byteArrayOf() // return empty if cannot parse packet
|
||||
}
|
||||
}?.let { response ->
|
||||
try {
|
||||
val output = DataOutputStream(socket.outputStream)
|
||||
output.writeShort(response.size)
|
||||
output.write(response)
|
||||
} catch (e: IOException) {
|
||||
Timber.d(e.readableMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
-59
@@ -21,80 +21,109 @@
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.content.Context
|
||||
|
||||
import android.util.Base64
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.AclSyncer
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginConfiguration
|
||||
import org.amnezia.vpn.shadowsocks.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
/**
|
||||
* This class sets up environment for ss-local.
|
||||
*/
|
||||
class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
|
||||
private var configFile: File? = null
|
||||
var trafficMonitor: TrafficMonitor? = null
|
||||
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||
val pluginPath by lazy { PluginManager.init(plugin) }
|
||||
|
||||
suspend fun init(service: BaseService.Interface) {
|
||||
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
|
||||
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
|
||||
init {
|
||||
require(profile.host.isNotEmpty() && (profile.method == "none" || profile.password.isNotEmpty())) {
|
||||
app.getString(R.string.proxy_empty)
|
||||
}
|
||||
|
||||
// it's hard to resolve DNS on a specific interface so we'll do it here
|
||||
if (profile.host.parseNumericAddress() == null) {
|
||||
while (true) try {
|
||||
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
|
||||
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
|
||||
return
|
||||
} catch (e: UnknownHostException) {
|
||||
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
|
||||
if (!DataStore.hasArc0) throw e
|
||||
Thread.yield()
|
||||
}
|
||||
// check the crypto
|
||||
require(profile.method !in arrayOf("aes-192-gcm", "chacha20", "salsa20")) {
|
||||
"cipher ${profile.method} is deprecated."
|
||||
}
|
||||
// check the key format for aead-2022-cipher
|
||||
require(profile.method !in setOf(
|
||||
"2022-blake3-aes-128-gcm",
|
||||
"2022-blake3-aes-256-gcm",
|
||||
"2022-blake3-chacha20-poly1305",
|
||||
) || Base64.decode(profile.password, Base64.DEFAULT).size in arrayOf(16, 32)) {
|
||||
"The Base64 Key is invalid."
|
||||
}
|
||||
}
|
||||
|
||||
private var configFile: File? = null
|
||||
var trafficMonitor: TrafficMonitor? = null
|
||||
val plugin by lazy { PluginManager.init(PluginConfiguration(profile.plugin ?: "")) }
|
||||
|
||||
/**
|
||||
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
|
||||
* device storage, depending on which is currently available.
|
||||
*/
|
||||
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
|
||||
fun start(service: BaseService.Interface, stat: File, configFile: File, mode: String, dnsRelay: Boolean = true) {
|
||||
// setup traffic monitor path
|
||||
trafficMonitor = TrafficMonitor(stat)
|
||||
|
||||
// init JSON config
|
||||
this.configFile = configFile
|
||||
val config = profile.toJson()
|
||||
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString())
|
||||
plugin?.let { (path, opts, isV2) ->
|
||||
if (service.isVpnService) {
|
||||
if (isV2) opts["__android_vpn"] = "" else config.put("plugin_args", JSONArray(arrayOf("-V")))
|
||||
}
|
||||
config.put("plugin", path).put("plugin_opts", opts.toString())
|
||||
}
|
||||
config.put("dns", "system")
|
||||
config.put("locals", JSONArray().apply {
|
||||
// local SOCKS5 proxy
|
||||
put(JSONObject().apply {
|
||||
put("local_address", DataStore.listenAddress)
|
||||
put("local_port", DataStore.portProxy)
|
||||
put("local_udp_address", DataStore.listenAddress)
|
||||
put("local_udp_port", DataStore.portProxy)
|
||||
put("mode", mode)
|
||||
})
|
||||
|
||||
// local DNS proxy
|
||||
if (dnsRelay) try {
|
||||
URI("dns://${profile.remoteDns}")
|
||||
} catch (e: URISyntaxException) {
|
||||
throw BaseService.ExpectedExceptionWrapper(e)
|
||||
}.let { dns ->
|
||||
put(JSONObject().apply {
|
||||
put("local_address", DataStore.listenAddress)
|
||||
put("local_port", DataStore.portLocalDns)
|
||||
put("local_dns_address", "local_dns_path")
|
||||
put("remote_dns_address", dns.host ?: "0.0.0.0")
|
||||
put("remote_dns_port", if (dns.port < 0) 53 else dns.port)
|
||||
put("protocol", "dns")
|
||||
})
|
||||
}
|
||||
})
|
||||
configFile.writeText(config.toString())
|
||||
|
||||
val cmd = service.buildAdditionalArguments(arrayListOf(
|
||||
// build the command line
|
||||
val cmd = arrayListOf(
|
||||
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-b", DataStore.listenAddress,
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-S", stat.absolutePath,
|
||||
"-c", configFile.absolutePath))
|
||||
if (extraFlag != null) cmd.add(extraFlag)
|
||||
"--stat-path", stat.absolutePath,
|
||||
"-c", configFile.absolutePath,
|
||||
)
|
||||
|
||||
if (service.isVpnService) cmd += "--vpn"
|
||||
cmd += "--tcp-fast-open"
|
||||
|
||||
if (route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += Acl.getFile(route).absolutePath
|
||||
}
|
||||
|
||||
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
|
||||
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
|
||||
|
||||
if (DataStore.tcpFastOpen) cmd += "--fast-open"
|
||||
|
||||
service.data.processes!!.start(cmd)
|
||||
}
|
||||
|
||||
@@ -105,22 +134,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
|
||||
fun shutdown(scope: CoroutineScope) {
|
||||
trafficMonitor?.apply {
|
||||
thread.shutdown(scope)
|
||||
// Make sure update total traffic when stopping the runner
|
||||
try {
|
||||
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||
val profile = ProfileManager.getProfile(profile.id) ?: return
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
ProfileManager.updateProfile(profile)
|
||||
} catch (e: IOException) {
|
||||
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
||||
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
profile.dirty = true
|
||||
DirectBoot.update(profile)
|
||||
DirectBoot.listenForUnlock()
|
||||
}
|
||||
persistStats(profile.id) // Make sure update total traffic when stopping the runner
|
||||
}
|
||||
trafficMonitor = null
|
||||
configFile?.delete() // remove old config possibly in device storage
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ import android.content.Intent
|
||||
class ProxyService : Service(), BaseService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-proxy", true)
|
||||
fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
|
||||
+124
-145
@@ -1,145 +1,124 @@
|
||||
///*******************************************************************************
|
||||
// * *
|
||||
// * Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
// * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
// * *
|
||||
// * This program is free software: you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
// * *
|
||||
// *******************************************************************************/
|
||||
//
|
||||
//package org.amnezia.vpn.shadowsocks.core.bg
|
||||
//
|
||||
//import android.app.KeyguardManager
|
||||
//import android.app.NotificationManager
|
||||
//import android.app.PendingIntent
|
||||
//import android.app.Service
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.content.IntentFilter
|
||||
//import android.os.Build
|
||||
//import android.os.PowerManager
|
||||
//import android.text.format.Formatter
|
||||
//import androidx.core.app.NotificationCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.content.getSystemService
|
||||
//import org.amnezia.vpn.shadowsocks.core.Core
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
//import org.amnezia.vpn.shadowsocks.core.R
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
//
|
||||
///**
|
||||
// * Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
// * Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
// * Android 8+: always visible due to system limitations
|
||||
// * (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
// */
|
||||
//class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
// channel: String, private val visible: Boolean = false) {
|
||||
// private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||
// private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||
// private val callback: IShadowsocksServiceCallback by lazy {
|
||||
// object : IShadowsocksServiceCallback.Stub() {
|
||||
// override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
// when (state) {
|
||||
// BaseService.State.Connected.ordinal -> {
|
||||
// builder.setContentText("VPN Connected")
|
||||
// }
|
||||
// BaseService.State.Stopped.ordinal -> {
|
||||
// builder.setContentText("VPN Disconnected")
|
||||
// }
|
||||
// }
|
||||
// } // ignore
|
||||
// override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
//// if (profileId != 0L) return
|
||||
//// service as Context
|
||||
//// val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||
//// val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||
//// builder.setContentText("$txr↑\t$rxr↓")
|
||||
//// style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||
//// Formatter.formatFileSize(service, stats.txTotal),
|
||||
//// Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
//// show()
|
||||
// }
|
||||
// override fun trafficPersisted(profileId: Long) { }
|
||||
// }
|
||||
// }
|
||||
//// private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
// private var callbackRegistered = false
|
||||
//
|
||||
// private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
// .setWhen(0)
|
||||
// .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
// .setTicker(service.getString(R.string.forward_success))
|
||||
// .setContentTitle("AmneziaVPN -- testing")
|
||||
// .setContentIntent(Core.configureIntent(service))
|
||||
// .setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
// private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||
// private var isVisible = true
|
||||
//
|
||||
// init {
|
||||
// service as Context
|
||||
//// if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
//// service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
//// update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
//// Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
//// service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||
//// addAction(Intent.ACTION_SCREEN_ON)
|
||||
//// addAction(Intent.ACTION_SCREEN_OFF)
|
||||
//// if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||
//// })
|
||||
// }
|
||||
//
|
||||
//// private fun update(action: String?, forceShow: Boolean = false) {
|
||||
//// if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||
//// Intent.ACTION_SCREEN_OFF -> {
|
||||
//// setVisible(false, forceShow)
|
||||
//// unregisterCallback() // unregister callback to save battery
|
||||
//// }
|
||||
//// Intent.ACTION_SCREEN_ON -> {
|
||||
//// setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||
//// service.data.binder.registerCallback(callback)
|
||||
//// service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
//// callbackRegistered = true
|
||||
//// }
|
||||
//// Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// private fun unregisterCallback() {
|
||||
// if (callbackRegistered) {
|
||||
// service.data.binder.unregisterCallback(callback)
|
||||
// callbackRegistered = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
// if (isVisible != visible) {
|
||||
// isVisible = visible
|
||||
// builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
// show()
|
||||
// } else if (forceShow) show()
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private fun show() = (service as Service).startForeground(1337, builder.build())
|
||||
//
|
||||
// fun destroy() {
|
||||
//// (service as Service).unregisterReceiver(lockReceiver)
|
||||
// unregisterCallback()
|
||||
//// service.stopForeground(true)
|
||||
// nm.cancel(1337)
|
||||
// }
|
||||
//}
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.text.format.Formatter
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
|
||||
/**
|
||||
* User can customize visibility of notification since Android 8.
|
||||
* The default visibility:
|
||||
*
|
||||
* Android 8.x: always visible due to system limitations
|
||||
* VPN: always invisible because of VPN notification/icon
|
||||
* Other: always visible
|
||||
*
|
||||
* See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4
|
||||
*/
|
||||
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
channel: String, visible: Boolean = false) : BroadcastReceiver() {
|
||||
private val callback: IShadowsocksServiceCallback by lazy {
|
||||
object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
if (profileId != 0L) return
|
||||
builder.apply {
|
||||
// setContentText((service as Context).getString(R.string.traffic,
|
||||
// service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate)),
|
||||
// service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))))
|
||||
// setSubText(service.getString(R.string.traffic,
|
||||
// Formatter.formatFileSize(service, stats.txTotal),
|
||||
// Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
}
|
||||
show()
|
||||
}
|
||||
override fun trafficPersisted(profileId: Long) { }
|
||||
}
|
||||
}
|
||||
private var callbackRegistered = false
|
||||
|
||||
private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(Core.configureIntent(service))
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
|
||||
|
||||
init {
|
||||
service as Context
|
||||
val closeAction = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_navigation_close,
|
||||
service.getText(R.string.stop),
|
||||
PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE).setPackage(service.packageName),
|
||||
PendingIntent.FLAG_IMMUTABLE)).apply {
|
||||
setAuthenticationRequired(true)
|
||||
setShowsUserInterface(false)
|
||||
}.build()
|
||||
if (Build.VERSION.SDK_INT < 24) builder.addAction(closeAction) else builder.addInvisibleAction(closeAction)
|
||||
updateCallback(service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
service.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
})
|
||||
show()
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON)
|
||||
}
|
||||
|
||||
private fun updateCallback(screenOn: Boolean) {
|
||||
if (screenOn) {
|
||||
service.data.binder.registerCallback(callback)
|
||||
service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
callbackRegistered = true
|
||||
} else if (callbackRegistered) { // unregister callback to save battery
|
||||
service.data.binder.unregisterCallback(callback)
|
||||
callbackRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun show() = (service as Service).startForeground(1, builder.build())
|
||||
|
||||
fun destroy() {
|
||||
(service as Service).unregisterReceiver(this)
|
||||
updateCallback(false)
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -23,7 +23,10 @@ package org.amnezia.vpn.shadowsocks.core.bg
|
||||
import android.net.LocalSocket
|
||||
import android.os.SystemClock
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
@@ -34,7 +37,11 @@ class TrafficMonitor(statFile: File) {
|
||||
private val buffer = ByteArray(16)
|
||||
private val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||
override fun acceptInternal(socket: LocalSocket) {
|
||||
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
|
||||
when (val read = socket.inputStream.read(buffer)) {
|
||||
-1 -> return
|
||||
16 -> { }
|
||||
else -> throw IOException("Unexpected traffic stat length $read")
|
||||
}
|
||||
val tx = stat.getLong(0)
|
||||
val rx = stat.getLong(8)
|
||||
if (current.txTotal != tx) {
|
||||
@@ -52,6 +59,7 @@ class TrafficMonitor(statFile: File) {
|
||||
var out = TrafficStats()
|
||||
private var timestampLast = 0L
|
||||
private var dirty = false
|
||||
private var persisted: TrafficStats? = null
|
||||
|
||||
fun requestUpdate(): Pair<TrafficStats, Boolean> {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
@@ -79,4 +87,25 @@ class TrafficMonitor(statFile: File) {
|
||||
}
|
||||
return Pair(out, updated)
|
||||
}
|
||||
|
||||
fun persistStats(id: Long) {
|
||||
val current = current
|
||||
check(persisted == null || persisted == current) { "Data loss occurred" }
|
||||
persisted = current
|
||||
try {
|
||||
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||
val profile = ProfileManager.getProfile(id) ?: return
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
ProfileManager.updateProfile(profile)
|
||||
} catch (e: IOException) {
|
||||
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
|
||||
val profile = DirectBoot.getDeviceProfile()!!.toList().single { it.id == id }
|
||||
profile.tx += current.txTotal
|
||||
profile.rx += current.rxTotal
|
||||
profile.dirty = true
|
||||
DirectBoot.update(profile)
|
||||
DirectBoot.listenForUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-15
@@ -26,19 +26,18 @@ import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
class TransproxyService : Service(), BaseService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText(
|
||||
"""base {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
@@ -52,15 +51,9 @@ redsocks {
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
)
|
||||
data.processes!!.start(
|
||||
listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c",
|
||||
"redsocks.conf"
|
||||
)
|
||||
)
|
||||
""")
|
||||
data.processes!!.start(listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
|
||||
}
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user