Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42bb5be390 | ||
|
|
37c051c30a | ||
|
|
582a6e441b | ||
|
|
6c89207eff | ||
|
|
0b2c9fbcfd | ||
|
|
a742512892 | ||
|
|
8b0c8168f8 | ||
|
|
020a2a5d43 | ||
|
|
7624f4ac52 | ||
|
|
ef519a8581 | ||
|
|
540b1d3710 | ||
|
|
6d6682d4a7 | ||
|
|
157a750e53 | ||
|
|
6408450b31 | ||
|
|
723097fc8c | ||
|
|
37ff7fddee | ||
|
|
536a1b4bbe | ||
|
|
2202041e80 | ||
|
|
37856ab4d2 | ||
|
|
3f30fc7b0c | ||
|
|
f381ad6300 | ||
|
|
b25e9a0905 | ||
|
|
922eb8b202 | ||
|
|
1010edc561 | ||
|
|
bc22b0c481 | ||
|
|
1bea09c5c6 | ||
|
|
5c252a2895 | ||
|
|
8ee9331222 | ||
|
|
870fd9e652 | ||
|
|
4e71722a75 | ||
|
|
e5cd145b24 | ||
|
|
f89088ef34 | ||
|
|
9b87d6a796 | ||
|
|
0ac113e7ef | ||
|
|
11a3bade54 | ||
|
|
11dd7112c3 | ||
|
|
612a34a690 | ||
|
|
f1934f876d | ||
|
|
6eec78a343 | ||
|
|
32d0789c94 | ||
|
|
d7a11eaa3b | ||
|
|
14ac54c2de | ||
|
|
8c28fffbdb | ||
|
|
96c86b2441 | ||
|
|
15373f8d44 | ||
|
|
30b56c97ea | ||
|
|
d081d06903 | ||
|
|
090851ed3b | ||
|
|
9352f2dfe7 | ||
|
|
9a2643ec2c | ||
|
|
5c2d548dee | ||
|
|
2fa8bc7f41 | ||
|
|
94527ea460 | ||
|
|
9259789fb9 | ||
|
|
8db642e721 | ||
|
|
fca0e87b6c | ||
|
|
b88f836616 | ||
|
|
c3bbd09ef3 | ||
|
|
ea7ede033d | ||
|
|
956443ef28 |
@@ -7,9 +7,14 @@ env:
|
||||
global:
|
||||
- GOROOT_BOOTSTRAP=$GOROOT
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
install:
|
||||
- pushd $ANDROID_HOME
|
||||
@@ -25,4 +30,4 @@ install:
|
||||
>/dev/null
|
||||
|
||||
script:
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleDebug check
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlinVersion = '1.2.0'
|
||||
kotlinVersion = '1.2.10'
|
||||
minSdkVersion = 19
|
||||
sdkVersion = 27
|
||||
buildToolsVersion = '27.0.2'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
@@ -43,8 +45,6 @@ task goClean(type: Exec) {
|
||||
args "-c", "src/overture/clean.bash"
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
tasks.whenTaskAdded { task ->
|
||||
if ((task.name == 'generateJsonModelDebug' ||
|
||||
task.name == 'generateJsonModelRelease') &&
|
||||
|
||||
@@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("com.github.shadowsockscorelibraries.test", appContext.getPackageName());
|
||||
assertEquals("com.github.shadowsocks.core.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,15 +39,19 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.system.ErrnoException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class JniHelper {
|
||||
static {
|
||||
System.loadLibrary("jni-helper");
|
||||
}
|
||||
|
||||
@Deprecated // Use Process.destroy() since API 24
|
||||
public static void sigtermCompat(Process process) throws Exception {
|
||||
public static void sigtermCompat(@NonNull Process process) throws Exception {
|
||||
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
|
||||
int errno = sigterm(process);
|
||||
if (errno != 0) throw Build.VERSION.SDK_INT >= 21
|
||||
@@ -55,7 +59,7 @@ public class JniHelper {
|
||||
}
|
||||
|
||||
@Deprecated // only implemented for before API 24
|
||||
public static boolean waitForCompat(Process process, long millis) throws Exception {
|
||||
public static boolean waitForCompat(@NonNull Process process, long millis) throws Exception {
|
||||
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
|
||||
final Object mutex = getExitValueMutex(process);
|
||||
synchronized (mutex) {
|
||||
@@ -68,6 +72,8 @@ public class JniHelper {
|
||||
private static native int sigterm(Process process);
|
||||
private static native Integer getExitValue(Process process);
|
||||
private static native Object getExitValueMutex(Process process);
|
||||
public static native int sendFd(int fd, String path);
|
||||
public static native int sendFd(int fd, @NonNull String path);
|
||||
public static native void close(int fd);
|
||||
@Nullable
|
||||
public static native byte[] parseNumericAddress(@NonNull String str);
|
||||
}
|
||||
|
||||
@@ -318,6 +318,8 @@ include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE:= jni-helper
|
||||
|
||||
LOCAL_CFLAGS := -std=c++11
|
||||
|
||||
LOCAL_C_INCLUDES:= $(LOCAL_PATH)/libancillary
|
||||
|
||||
LOCAL_SRC_FILES:= jni-helper.cpp
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86
|
||||
APP_PLATFORM := android-19
|
||||
APP_STL := c++_static
|
||||
NDK_TOOLCHAIN_VERSION := clang
|
||||
@@ -4,33 +4,33 @@
|
||||
#include <android/log.h>
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <ancillary.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0)
|
||||
#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0)
|
||||
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0)
|
||||
#define THROW(env, clazz, msg) do { env->ThrowNew(env->FindClass(clazz), msg); } while (0)
|
||||
|
||||
static int sdk_version;
|
||||
static jclass ProcessImpl;
|
||||
static jfieldID ProcessImpl_pid, ProcessImpl_exitValue, ProcessImpl_exitValueMutex;
|
||||
|
||||
static int sdk_version() {
|
||||
char version[PROP_VALUE_MAX + 1];
|
||||
__system_property_get("ro.build.version.sdk", version);
|
||||
return atoi(version);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
extern "C" {
|
||||
JNIEXPORT jint JNICALL Java_com_github_shadowsocks_JniHelper_sigkill(JNIEnv *env, jobject thiz, jint pid) {
|
||||
// Suppress "No such process" errors. We just want the process killed. It's fine if it's already killed.
|
||||
@@ -103,6 +103,22 @@ JNIEXPORT jint JNICALL
|
||||
env->ReleaseStringUTFChars(path, sock_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_github_shadowsocks_JniHelper_parseNumericAddress(JNIEnv *env, jobject thiz, jstring str) {
|
||||
const char *src = env->GetStringUTFChars(str, 0);
|
||||
jbyte dst[max(sizeof(in_addr), sizeof(in6_addr))];
|
||||
jbyteArray arr = nullptr;
|
||||
if (inet_pton(AF_INET, src, dst) == 1) {
|
||||
arr = env->NewByteArray(sizeof(in_addr));
|
||||
env->SetByteArrayRegion(arr, 0, sizeof(in_addr), dst);
|
||||
} else if (inet_pton(AF_INET6, src, dst) == 1) {
|
||||
arr = env->NewByteArray(sizeof(in6_addr));
|
||||
env->SetByteArrayRegion(arr, 0, sizeof(in6_addr), dst);
|
||||
}
|
||||
env->ReleaseStringUTFChars(str, src);
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -114,6 +130,7 @@ typedef union {
|
||||
void* venv;
|
||||
} UnionJNIEnvToVoid;
|
||||
|
||||
#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
|
||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
UnionJNIEnvToVoid uenv;
|
||||
uenv.venv = NULL;
|
||||
@@ -126,24 +143,27 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
}
|
||||
env = uenv.env;
|
||||
|
||||
if (sdk_version() < 24) {
|
||||
if (!(ProcessImpl = env->FindClass("java/lang/ProcessManager$ProcessImpl"))) {
|
||||
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl not found");
|
||||
goto bail;
|
||||
}
|
||||
ProcessImpl = (jclass) env->NewGlobalRef((jobject) ProcessImpl);
|
||||
if (!(ProcessImpl_pid = env->GetFieldID(ProcessImpl, "pid", "I"))) {
|
||||
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.pid not found");
|
||||
goto bail;
|
||||
}
|
||||
if (!(ProcessImpl_exitValue = env->GetFieldID(ProcessImpl, "exitValue", "Ljava/lang/Integer;"))) {
|
||||
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.exitValue not found");
|
||||
goto bail;
|
||||
}
|
||||
if (!(ProcessImpl_exitValueMutex = env->GetFieldID(ProcessImpl, "exitValueMutex", "Ljava/lang/Object;"))) {
|
||||
THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.exitValueMutex not found");
|
||||
goto bail;
|
||||
}
|
||||
char version[PROP_VALUE_MAX + 1];
|
||||
__system_property_get("ro.build.version.sdk", version);
|
||||
sdk_version = atoi(version);
|
||||
|
||||
#define FIND_CLASS(out, name) \
|
||||
if (!(out = env->FindClass(name))) { \
|
||||
THROW(env, "java/lang/RuntimeException", name " not found"); \
|
||||
goto bail; \
|
||||
} \
|
||||
out = reinterpret_cast<jclass>(env->NewGlobalRef(reinterpret_cast<jobject>(out)))
|
||||
#define GET_FIELD(out, clazz, name, sig) \
|
||||
if (!(out = env->GetFieldID(clazz, name, sig))) { \
|
||||
THROW(env, "java/lang/RuntimeException", "Field " #clazz "." name " with type " sig " not found"); \
|
||||
goto bail; \
|
||||
}
|
||||
|
||||
if (sdk_version < 24) {
|
||||
FIND_CLASS(ProcessImpl, "java/lang/ProcessManager$ProcessImpl");
|
||||
GET_FIELD(ProcessImpl_pid, ProcessImpl, "pid", "I")
|
||||
GET_FIELD(ProcessImpl_exitValue, ProcessImpl, "exitValue", "Ljava/lang/Integer;")
|
||||
GET_FIELD(ProcessImpl_exitValueMutex, ProcessImpl, "exitValueMutex", "Ljava/lang/Object;")
|
||||
}
|
||||
|
||||
result = JNI_VERSION_1_6;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
def getCurrentFlavor() {
|
||||
Matcher matcher = Pattern.compile("(assemble|generate)(\\w+)(Release|Debug)")
|
||||
.matcher(getGradle().getStartParameter().getTaskRequests().toString())
|
||||
String task = getGradle().getStartParameter().getTaskRequests().toString()
|
||||
Matcher matcher = Pattern.compile("(assemble|generate)\\w*(Release|Debug)").matcher(task)
|
||||
if (matcher.find()) return matcher.group(1).toLowerCase() else {
|
||||
println "NO MATCH FOUND"
|
||||
return ""
|
||||
println "Warning: No match found for $task"
|
||||
return "debug"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ android {
|
||||
applicationId "com.github.shadowsocks"
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
targetSdkVersion rootProject.sdkVersion
|
||||
versionCode 200
|
||||
versionName "4.4.0"
|
||||
versionCode 202
|
||||
versionName "4.4.2"
|
||||
testApplicationId "com.github.shadowsocks.test"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary true
|
||||
resConfigs "fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
|
||||
@@ -36,9 +37,6 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
sourceSets.main.jniLibs.srcDirs +=
|
||||
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
|
||||
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "src/overture/bin")
|
||||
@@ -51,7 +49,6 @@ dependencies {
|
||||
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
|
||||
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
|
||||
implementation 'com.evernote:android-job:1.2.1'
|
||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01'
|
||||
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
|
||||
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
|
||||
implementation "com.google.android.gms:play-services-gcm:$playServicesVersion"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="ormlite-core" />
|
||||
</issue>
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="informational" />
|
||||
</lint>
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("com.github.shadowsocks", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.acl
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class AclTest {
|
||||
companion object {
|
||||
private const val INPUT1 = """[proxy_all]
|
||||
[bypass_list]
|
||||
1.0.1.0/24
|
||||
(^|\.)4tern\.com${'$'}
|
||||
"""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parse() {
|
||||
Assert.assertEquals(INPUT1, Acl().fromReader(INPUT1.reader()).toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.database
|
||||
|
||||
import android.net.Uri
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class ProfileTest {
|
||||
@Test
|
||||
fun parsing() {
|
||||
val results = Profile.findAll("garble ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server garble")
|
||||
.toList()
|
||||
Assert.assertEquals(1, results.size)
|
||||
Assert.assertEquals(Uri.parse("ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888#example-server"),
|
||||
results.single().toUri())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import com.github.shadowsocks.utils.Subnet
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.net.InetAddress
|
||||
|
||||
object SubnetTest {
|
||||
class SubnetTest {
|
||||
@Test
|
||||
fun parsingAndEquals() {
|
||||
Assert.assertEquals(Subnet(InetAddress.getByName("1.10.11.12"), 25), Subnet.fromString("1.10.11.12/25"))
|
||||
@@ -17,16 +16,13 @@ object SubnetTest {
|
||||
Assert.assertNotEquals(Subnet.fromString("1.2.3.4/12"), Subnet.fromString("1.2.3.5/12"))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun invalidParsing1() {
|
||||
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129")
|
||||
}
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun invalidParsing2() {
|
||||
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/-99")
|
||||
}
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun invalidParsing3() {
|
||||
Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/1/0")
|
||||
@Test
|
||||
fun invalidParsings() {
|
||||
Assert.assertEquals(null, Subnet.fromString("1.2.3.456"))
|
||||
Assert.assertEquals(null, Subnet.fromString("ffff::f0000"))
|
||||
Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129"))
|
||||
Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129"))
|
||||
Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/-99"))
|
||||
Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/1/0"))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.github.shadowsocks">
|
||||
package="com.github.shadowsocks"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
@@ -20,10 +21,12 @@
|
||||
android:name=".App"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:backupAgent=".ConfigBackupHelper"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:fullBackupOnly="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:banner="@drawable/ic_start_connected">
|
||||
android:banner="@drawable/ic_service_active">
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="true" />
|
||||
@@ -31,9 +34,6 @@
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
<meta-data android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg"/>
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
android:resource="@array/preloaded_fonts"/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -111,6 +111,7 @@
|
||||
<service
|
||||
android:name=".bg.VpnService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="false">
|
||||
@@ -122,31 +123,41 @@
|
||||
<service
|
||||
android:name=".bg.TransproxyService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".bg.ProxyService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service android:name=".bg.TileService" android:label="@string/quick_toggle"
|
||||
android:process=":bg"
|
||||
android:icon="@drawable/ic_start_connected"
|
||||
android:directBootAware="true"
|
||||
android:icon="@drawable/ic_service_active"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
|
||||
<receiver android:name=".BootReceiver"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".tasker.ActionListener" android:process=":bg" tools:ignore="ExportedReceiver">
|
||||
<receiver android:name=".tasker.ActionListener"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
@@ -154,16 +165,22 @@
|
||||
|
||||
<!-- android-job components -->
|
||||
<service android:name="com.evernote.android.job.v21.PlatformJobService"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<service android:name="com.evernote.android.job.v14.PlatformAlarmService"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<service android:name="com.evernote.android.job.JobRescheduleService"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<receiver android:name="com.evernote.android.job.v14.PlatformAlarmReceiver"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
||||
android:process=":bg"/>
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -4425,8 +4425,6 @@
|
||||
223.255.252.0/23
|
||||
|
||||
[proxy_list]
|
||||
# manhuaren
|
||||
mangaapi.manhuaren.com
|
||||
# Telegram IPs$
|
||||
91.108.4.0/22
|
||||
91.108.56.0/22
|
||||
@@ -4492,6 +4490,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?lovetvshow\.com$
|
||||
^(.*\.)?macgamestore\.com$
|
||||
^(.*\.)?madonna-av\.com$
|
||||
^(.*\.)?manhuaren\.com$
|
||||
^(.*\.)?mangafox\.com$
|
||||
^(.*\.)?mangafox\.me$
|
||||
^(.*\.)?matome-plus\.com$
|
||||
@@ -4742,7 +4741,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?android\.com$
|
||||
^(.*\.)?androidify\.com$
|
||||
^(.*\.)?appspot\.com$
|
||||
^(.*\.)?blogspot(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?blogspot(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?certificate-transparency\.org$
|
||||
^(.*\.)?chrome\.com$
|
||||
^(.*\.)?chromecast\.com$
|
||||
@@ -4765,8 +4764,8 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?gmail\.com$
|
||||
^(.*\.)?gmodules\.com$
|
||||
^(.*\.)?goo\.gl$
|
||||
^(.*\.)?google(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?googleapis(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapps\.com$
|
||||
^(.*\.)?googleartproject\.com$
|
||||
^(.*\.)?googleblog\.com$
|
||||
|
||||
@@ -4439,8 +4439,6 @@
|
||||
223.255.252.0/23
|
||||
|
||||
[proxy_list]
|
||||
# manhuaren
|
||||
mangaapi.manhuaren.com
|
||||
# Telegram IPs$
|
||||
91.108.4.0/22
|
||||
91.108.56.0/22
|
||||
@@ -4506,6 +4504,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?lovetvshow\.com$
|
||||
^(.*\.)?macgamestore\.com$
|
||||
^(.*\.)?madonna-av\.com$
|
||||
^(.*\.)?manhuaren\.com$
|
||||
^(.*\.)?mangafox\.com$
|
||||
^(.*\.)?mangafox\.me$
|
||||
^(.*\.)?matome-plus\.com$
|
||||
@@ -4756,7 +4755,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?android\.com$
|
||||
^(.*\.)?androidify\.com$
|
||||
^(.*\.)?appspot\.com$
|
||||
^(.*\.)?blogspot(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?blogspot(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?certificate-transparency\.org$
|
||||
^(.*\.)?chrome\.com$
|
||||
^(.*\.)?chromecast\.com$
|
||||
@@ -4779,8 +4778,8 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?gmail\.com$
|
||||
^(.*\.)?gmodules\.com$
|
||||
^(.*\.)?goo\.gl$
|
||||
^(.*\.)?google(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?googleapis(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapps\.com$
|
||||
^(.*\.)?googleartproject\.com$
|
||||
^(.*\.)?googleblog\.com$
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
[bypass_all]
|
||||
|
||||
[proxy_list]
|
||||
# manhuaren
|
||||
mangaapi.manhuaren.com
|
||||
# Telegram IPs$
|
||||
91.108.4.0/22
|
||||
91.108.56.0/22
|
||||
@@ -70,6 +68,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?lovetvshow\.com$
|
||||
^(.*\.)?macgamestore\.com$
|
||||
^(.*\.)?madonna-av\.com$
|
||||
^(.*\.)?manhuaren\.com$
|
||||
^(.*\.)?mangafox\.com$
|
||||
^(.*\.)?mangafox\.me$
|
||||
^(.*\.)?matome-plus\.com$
|
||||
@@ -320,7 +319,7 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?android\.com$
|
||||
^(.*\.)?androidify\.com$
|
||||
^(.*\.)?appspot\.com$
|
||||
^(.*\.)?blogspot(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?blogspot(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?certificate-transparency\.org$
|
||||
^(.*\.)?chrome\.com$
|
||||
^(.*\.)?chromecast\.com$
|
||||
@@ -343,8 +342,8 @@ mangaapi.manhuaren.com
|
||||
^(.*\.)?gmail\.com$
|
||||
^(.*\.)?gmodules\.com$
|
||||
^(.*\.)?goo\.gl$
|
||||
^(.*\.)?google(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?googleapis(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-)){1,2}$
|
||||
^(.*\.)?google(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
|
||||
^(.*\.)?googleapps\.com$
|
||||
^(.*\.)?googleartproject\.com$
|
||||
^(.*\.)?googleblog\.com$
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.evernote.android.job
|
||||
|
||||
object JobConstants {
|
||||
const val DATABASE_NAME = JobStorage.DATABASE_NAME
|
||||
const val PREF_FILE_NAME = JobStorage.PREF_FILE_NAME
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -34,9 +35,12 @@ import android.os.Handler
|
||||
import android.os.LocaleList
|
||||
import android.os.Looper
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobConstants
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.acl.AclSyncJob
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
@@ -44,10 +48,7 @@ import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.BottomSheetPreferenceDialogFragment
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.preference.IconListPreference
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.TcpFastOpen
|
||||
import com.github.shadowsocks.utils.broadcastReceiver
|
||||
import com.github.shadowsocks.utils.*
|
||||
import com.google.android.gms.analytics.GoogleAnalytics
|
||||
import com.google.android.gms.analytics.HitBuilders
|
||||
import com.google.android.gms.analytics.StandardExceptionParser
|
||||
@@ -75,8 +76,9 @@ class App : Application() {
|
||||
}
|
||||
|
||||
val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
val deviceContext: Context by lazy { if (Build.VERSION.SDK_INT < 24) this else DeviceContext(this) }
|
||||
val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() }
|
||||
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker) }
|
||||
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
|
||||
val info: PackageInfo by lazy { packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) }
|
||||
|
||||
fun startService() {
|
||||
@@ -86,7 +88,8 @@ class App : Application() {
|
||||
fun reloadService() = sendBroadcast(Intent(Action.RELOAD))
|
||||
fun stopService() = sendBroadcast(Intent(Action.CLOSE))
|
||||
|
||||
val currentProfile: Profile? get() = ProfileManager.getProfile(DataStore.profileId)
|
||||
val currentProfile: Profile? get() =
|
||||
if (DataStore.directBootAware) DirectBoot.getDeviceProfile() else ProfileManager.getProfile(DataStore.profileId)
|
||||
|
||||
fun switchProfile(id: Int): Profile {
|
||||
val result = ProfileManager.getProfile(id) ?: ProfileManager.createProfile()
|
||||
@@ -162,31 +165,42 @@ class App : Application() {
|
||||
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
checkChineseLocale(resources.configuration)
|
||||
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
||||
BottomSheetPreferenceDialogFragment::class.java)
|
||||
|
||||
FirebaseApp.initializeApp(this)
|
||||
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
|
||||
deviceContext.moveDatabaseFrom(this, Key.DB_PUBLIC)
|
||||
deviceContext.moveDatabaseFrom(this, JobConstants.DATABASE_NAME)
|
||||
deviceContext.moveSharedPreferencesFrom(this, JobConstants.PREF_FILE_NAME)
|
||||
val old = Acl.getFile(Acl.CUSTOM_RULES, this)
|
||||
if (old.canRead()) {
|
||||
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
|
||||
old.delete()
|
||||
}
|
||||
}
|
||||
|
||||
FirebaseApp.initializeApp(deviceContext)
|
||||
remoteConfig.setDefaults(R.xml.default_configs)
|
||||
remoteConfig.fetch().addOnCompleteListener {
|
||||
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
|
||||
}
|
||||
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
|
||||
|
||||
JobManager.create(this).addJobCreator(AclSyncJob)
|
||||
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
||||
BottomSheetPreferenceDialogFragment::class.java)
|
||||
|
||||
TcpFastOpen.enabled(DataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
|
||||
if (DataStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
||||
// handle data restored
|
||||
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
|
||||
TcpFastOpen.enabled(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
||||
val assetManager = assets
|
||||
for (dir in arrayOf("acl", "overture"))
|
||||
try {
|
||||
for (file in assetManager.list(dir)) assetManager.open(dir + '/' + file).use { input ->
|
||||
File(filesDir, file).outputStream().use { output -> input.copyTo(output) }
|
||||
File(deviceContext.filesDir, file).outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.message)
|
||||
app.track(e)
|
||||
}
|
||||
DataStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
||||
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
|
||||
@@ -28,6 +28,8 @@ import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
@@ -47,37 +49,41 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.resolveResourceId
|
||||
import com.github.shadowsocks.utils.thread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
data class ProxiedApp(val name: String, val packageName: String, val icon: Drawable)
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var instance: AppManager? = null
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
private var cachedApps: Array<ProxiedApp>? = null
|
||||
private fun getApps(pm: PackageManager): Array<ProxiedApp> {
|
||||
private var cachedApps: List<PackageInfo>? = null
|
||||
private fun getApps(pm: PackageManager): List<ProxiedApp> {
|
||||
if (receiver == null) receiver = app.listenForPackageChanges {
|
||||
synchronized(AppManager) { cachedApps = null }
|
||||
AppManager.instance?.reloadApps()
|
||||
}
|
||||
return synchronized(AppManager) {
|
||||
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
|
||||
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||
.filter { it.packageName != app.packageName &&
|
||||
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
|
||||
.map { ProxiedApp(pm.getApplicationLabel(it.applicationInfo).toString(), it.packageName,
|
||||
it.applicationInfo.loadIcon(pm)) }
|
||||
.toTypedArray()
|
||||
this.cachedApps = cachedApps
|
||||
cachedApps
|
||||
}
|
||||
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) }
|
||||
}
|
||||
}
|
||||
|
||||
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
|
||||
val packageName: String) {
|
||||
val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting
|
||||
val icon: Drawable get() = appInfo.loadIcon(pm)
|
||||
}
|
||||
|
||||
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
private val icon = view.findViewById<ImageView>(R.id.itemicon)
|
||||
private val check = view.findViewById<Switch>(R.id.itemcheck)
|
||||
@@ -114,13 +120,8 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
private var apps = listOf<ProxiedApp>()
|
||||
|
||||
fun reload() {
|
||||
apps = getApps(packageManager).sortedWith(Comparator { a, b ->
|
||||
when (Pair(proxiedApps.contains(a.packageName), proxiedApps.contains(b.packageName))) {
|
||||
Pair(true, false) -> -1
|
||||
Pair(false, true) -> 1
|
||||
else -> a.name.compareTo(b.name, true)
|
||||
}
|
||||
})
|
||||
apps = getApps(packageManager)
|
||||
.sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() }))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(apps[position])
|
||||
@@ -180,7 +181,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
setContentView(R.layout.layout_apps)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.proxied_apps)
|
||||
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
|
||||
toolbar.setNavigationIcon(theme.resolveResourceId(R.attr.homeAsUpIndicator))
|
||||
toolbar.setNavigationOnClickListener {
|
||||
val intent = parentActivityIntent
|
||||
if (shouldUpRecreateTask(intent) || isTaskRoot)
|
||||
@@ -229,6 +230,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
it.individual = proxiedAppString
|
||||
ProfileManager.updateProfile(it)
|
||||
}
|
||||
if (DataStore.directBootAware) DirectBoot.update()
|
||||
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
|
||||
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
@@ -39,6 +40,11 @@ class BootReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) app.startService()
|
||||
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.directBootAware == locked) app.startService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ package com.github.shadowsocks
|
||||
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.FileBackupHelper
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.utils.Key
|
||||
|
||||
@Deprecated("Only used in API level < 23. For 6.0+, Auto Backup for Apps is used.")
|
||||
class ConfigBackupHelper : BackupAgentHelper() {
|
||||
override fun onCreate() = addHelper("com.github.shadowsocks.database.profile",
|
||||
FileBackupHelper(this, "../databases/" + Key.PROFILE, Acl.CUSTOM_RULES + ".acl"))
|
||||
override fun onCreate() = addHelper("com.github.shadowsocks.database.profile", FileBackupHelper(this,
|
||||
"../databases/" + Key.DB_PROFILE, "../databases/" + Key.DB_PUBLIC))
|
||||
}
|
||||
|
||||
@@ -20,27 +20,40 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.TcpFastOpen
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
|
||||
|
||||
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompatDividers() {
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.preferenceDataStore = DataStore
|
||||
preferenceManager.preferenceDataStore = DataStore.publicStore
|
||||
DataStore.initGlobal()
|
||||
addPreferencesFromResource(R.xml.pref_global)
|
||||
val switch = findPreference(Key.isAutoConnect) as SwitchPreference
|
||||
switch.setOnPreferenceChangeListener { _, value ->
|
||||
BootReceiver.enabled = value as Boolean
|
||||
val boot = findPreference(Key.isAutoConnect) as SwitchPreference
|
||||
val directBootAwareListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
|
||||
true
|
||||
}
|
||||
switch.isChecked = BootReceiver.enabled
|
||||
boot.setOnPreferenceChangeListener { _, value ->
|
||||
BootReceiver.enabled = value as Boolean
|
||||
directBootAwareListener.onPreferenceChange(null, DataStore.directBootAware)
|
||||
}
|
||||
boot.isChecked = BootReceiver.enabled
|
||||
|
||||
val dba = findPreference(Key.directBootAware)
|
||||
if (Build.VERSION.SDK_INT >= 24 && context!!.getSystemService(DevicePolicyManager::class.java)
|
||||
.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER)
|
||||
dba.onPreferenceChangeListener = directBootAwareListener else dba.parent!!.removePreference(dba)
|
||||
|
||||
val tfo = findPreference(Key.tfo) as SwitchPreference
|
||||
tfo.isChecked = TcpFastOpen.sendEnabled
|
||||
|
||||
@@ -31,19 +31,15 @@ import android.nfc.NfcAdapter
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.support.customtabs.CustomTabsIntent
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.content.res.AppCompatResources
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import android.support.v7.widget.TooltipCompat
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.acl.CustomRulesFragment
|
||||
@@ -59,6 +55,7 @@ import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.responseLength
|
||||
import com.github.shadowsocks.utils.thread
|
||||
import com.github.shadowsocks.widget.ServiceButton
|
||||
import com.mikepenz.crossfader.Crossfader
|
||||
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
|
||||
import com.mikepenz.materialdrawer.Drawer
|
||||
@@ -88,8 +85,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
|
||||
// UI
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var fabProgressCircle: FABProgressCircle
|
||||
private lateinit var fab: ServiceButton
|
||||
internal var crossfader: Crossfader<CrossFadeSlidingPaneLayout>? = null
|
||||
internal lateinit var drawer: Drawer
|
||||
private var previousSelectedDrawer: Long = 0 // it's actually lateinit
|
||||
@@ -111,19 +107,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
} catch (_: ActivityNotFoundException) { } // ignore
|
||||
fun launchUrl(uri: String) = launchUrl(Uri.parse(uri))
|
||||
|
||||
private val greyTint by lazy { ContextCompat.getColorStateList(this, R.color.material_primary_500) }
|
||||
private val greenTint by lazy { ContextCompat.getColorStateList(this, R.color.material_green_700) }
|
||||
|
||||
private fun hideCircle() = try {
|
||||
fabProgressCircle.hide()
|
||||
} catch (_: NullPointerException) { } // ignore
|
||||
|
||||
// service
|
||||
var state = BaseService.IDLE
|
||||
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
|
||||
object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
app.handler.post { changeState(state, msg) }
|
||||
app.handler.post { changeState(state, msg, true) }
|
||||
}
|
||||
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) }
|
||||
@@ -134,27 +123,13 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
}
|
||||
|
||||
fun changeState(state: Int, msg: String? = null) {
|
||||
fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
|
||||
fab.changeState(state, animate)
|
||||
when (state) {
|
||||
BaseService.CONNECTING -> {
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
fabProgressCircle.show()
|
||||
statusText.setText(R.string.connecting)
|
||||
}
|
||||
BaseService.CONNECTED -> {
|
||||
if (this.state == BaseService.CONNECTING) fabProgressCircle.beginFinalAnimation()
|
||||
else fabProgressCircle.postDelayed(this::hideCircle, 1000)
|
||||
fab.setImageResource(R.drawable.ic_start_connected)
|
||||
statusText.setText(R.string.vpn_connected)
|
||||
}
|
||||
BaseService.STOPPING -> {
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
if (this.state == BaseService.CONNECTED) fabProgressCircle.show() // ignore for stopped
|
||||
statusText.setText(R.string.stopping)
|
||||
}
|
||||
BaseService.CONNECTING -> statusText.setText(R.string.connecting)
|
||||
BaseService.CONNECTED -> statusText.setText(R.string.vpn_connected)
|
||||
BaseService.STOPPING -> statusText.setText(R.string.stopping)
|
||||
else -> {
|
||||
fab.setImageResource(R.drawable.ic_start_idle)
|
||||
fabProgressCircle.postDelayed(this::hideCircle, 1000)
|
||||
if (msg != null) {
|
||||
Snackbar.make(findViewById(R.id.snackbar),
|
||||
getString(R.string.vpn_error).format(Locale.ENGLISH, msg), Snackbar.LENGTH_LONG).show()
|
||||
@@ -164,20 +139,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
}
|
||||
this.state = state
|
||||
if (state == BaseService.CONNECTED) {
|
||||
fab.backgroundTintList = greenTint
|
||||
TooltipCompat.setTooltipText(fab, getString(R.string.stop))
|
||||
} else {
|
||||
fab.backgroundTintList = greyTint
|
||||
TooltipCompat.setTooltipText(fab, getString(R.string.connect))
|
||||
if (state != BaseService.CONNECTED) {
|
||||
updateTraffic(-1, 0, 0, 0, 0)
|
||||
testCount += 1 // suppress previous test messages
|
||||
}
|
||||
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
|
||||
stateListener?.invoke(state)
|
||||
fab.isEnabled = false
|
||||
if (state == BaseService.CONNECTED || state == BaseService.STOPPED) app.handler.postDelayed(
|
||||
{ fab.isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED }, 1000)
|
||||
}
|
||||
fun updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
txText.text = TrafficMonitor.formatTraffic(txTotal)
|
||||
@@ -225,6 +192,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
}
|
||||
|
||||
override val listenForDeath: Boolean get() = true
|
||||
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
|
||||
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
|
||||
override fun binderDied() {
|
||||
@@ -322,7 +290,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
|
||||
fab = findViewById(R.id.fab)
|
||||
fabProgressCircle = findViewById(R.id.fabProgressCircle)
|
||||
fab.setOnClickListener {
|
||||
if (state == BaseService.CONNECTED) app.stopService() else thread {
|
||||
if (BaseService.usingVpnMode) {
|
||||
@@ -335,7 +302,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
|
||||
changeState(BaseService.IDLE) // reset everything to init state
|
||||
app.handler.post { connection.connect() }
|
||||
DataStore.registerChangeListener(this)
|
||||
DataStore.publicStore.registerChangeListener(this)
|
||||
|
||||
val intent = this.intent
|
||||
if (intent != null) handleShareIntent(intent)
|
||||
@@ -355,7 +322,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (TextUtils.isEmpty(sharedStr)) return
|
||||
if (sharedStr.isNullOrEmpty()) return
|
||||
val profiles = Profile.findAll(sharedStr).toList()
|
||||
if (profiles.isEmpty()) {
|
||||
Snackbar.make(findViewById(R.id.snackbar), R.string.profile_invalid_input, Snackbar.LENGTH_LONG).show()
|
||||
@@ -404,7 +371,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
app.remoteConfig.fetch()
|
||||
if (state !in arrayOf(BaseService.STOPPING, BaseService.CONNECTING)) hideCircle()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -434,7 +400,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
DataStore.unregisterChangeListener(this)
|
||||
DataStore.publicStore.unregisterChangeListener(this)
|
||||
connection.disconnect()
|
||||
BackupManager(this).dataChanged()
|
||||
app.handler.removeCallbacksAndMessages(null)
|
||||
|
||||
@@ -33,7 +33,6 @@ import android.support.v7.preference.PreferenceDataStore
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.MenuItem
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.plugin.PluginConfiguration
|
||||
@@ -45,6 +44,7 @@ import com.github.shadowsocks.preference.IconListPreference
|
||||
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.takisoft.fix.support.v7.preference.EditTextPreference
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
|
||||
@@ -62,7 +62,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
private lateinit var pluginConfiguration: PluginConfiguration
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.preferenceDataStore = DataStore
|
||||
preferenceManager.preferenceDataStore = DataStore.privateStore
|
||||
val activity = activity!!
|
||||
val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
|
||||
if (profile == null) {
|
||||
@@ -77,15 +77,18 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
findPreference(Key.remotePort).summary = "1337"
|
||||
findPreference(Key.password).summary = "\u2022".repeat(32)
|
||||
}
|
||||
val serviceMode = DataStore.serviceMode
|
||||
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy
|
||||
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
|
||||
if (Build.VERSION.SDK_INT < 21) isProxyApps.parent!!.removePreference(isProxyApps) else {
|
||||
isProxyApps.isEnabled = BaseService.usingVpnMode
|
||||
isProxyApps.isEnabled = serviceMode == Key.modeVpn
|
||||
isProxyApps.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, AppManager::class.java))
|
||||
isProxyApps.isChecked = true
|
||||
false
|
||||
}
|
||||
}
|
||||
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy
|
||||
plugin = findPreference(Key.plugin) as IconListPreference
|
||||
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference
|
||||
plugin.unknownValueSummary = getString(R.string.plugin_unknown)
|
||||
@@ -102,7 +105,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
pluginConfigure.onPreferenceChangeListener = this
|
||||
initPlugins()
|
||||
app.listenForPackageChanges { initPlugins() }
|
||||
DataStore.registerChangeListener(this)
|
||||
DataStore.privateStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
private fun initPlugins() {
|
||||
@@ -130,6 +133,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
profile.deserialize()
|
||||
ProfileManager.updateProfile(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
|
||||
if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update()
|
||||
activity!!.finish()
|
||||
}
|
||||
|
||||
@@ -141,7 +145,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean = try {
|
||||
val selected = pluginConfiguration.selected
|
||||
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions +
|
||||
(pluginConfiguration.selected to PluginOptions(selected, newValue as String?)), selected)
|
||||
(pluginConfiguration.selected to PluginOptions(selected, newValue as? String?)), selected)
|
||||
DataStore.plugin = pluginConfiguration.toString()
|
||||
DataStore.dirty = true
|
||||
true
|
||||
@@ -201,7 +205,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
DataStore.unregisterChangeListener(this)
|
||||
DataStore.privateStore.unregisterChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,8 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
|
||||
@SuppressLint("ValidFragment")
|
||||
private class QRCodeDialog() : DialogFragment() {
|
||||
class QRCodeDialog() : DialogFragment() {
|
||||
|
||||
constructor(url: String) : this() {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_URL, url)
|
||||
@@ -104,9 +105,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
adapter?.setNdefPushMessage(null, activity)
|
||||
adapter = null
|
||||
super.onDetach()
|
||||
val activity = activity
|
||||
if (activity != null && !activity.isFinishing && !activity.isDestroyed)
|
||||
adapter?.setNdefPushMessage(null, activity)
|
||||
adapter = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +223,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
|
||||
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
|
||||
private val updated = HashSet<Profile>()
|
||||
|
||||
init {
|
||||
setHasStableIds(true) // see: http://stackoverflow.com/a/32488059/2245107
|
||||
@@ -249,13 +253,17 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
next.userOrder = previousOrder
|
||||
previousOrder = order
|
||||
profiles[i] = next
|
||||
ProfileManager.updateProfile(next)
|
||||
updated.add(next)
|
||||
}
|
||||
first.userOrder = previousOrder
|
||||
profiles[to] = first
|
||||
ProfileManager.updateProfile(first)
|
||||
updated.add(first)
|
||||
notifyItemMoved(from, to)
|
||||
}
|
||||
fun commitMove() {
|
||||
updated.forEach { ProfileManager.updateProfile(it) }
|
||||
updated.clear()
|
||||
}
|
||||
|
||||
fun remove(pos: Int) {
|
||||
profiles.removeAt(pos)
|
||||
@@ -344,6 +352,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
profilesAdapter.move(viewHolder.adapterPosition, target.adapterPosition)
|
||||
return true
|
||||
}
|
||||
override fun clearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
profilesAdapter.commitMove()
|
||||
}
|
||||
}).attachToRecyclerView(profilesList)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.support.v4.content.pm.ShortcutInfoCompat
|
||||
import android.support.v4.content.pm.ShortcutManagerCompat
|
||||
import android.support.v4.graphics.drawable.IconCompat
|
||||
@@ -34,8 +33,8 @@ import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
|
||||
class QuickToggleShortcut : Activity(), ShadowsocksConnection.Interface {
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
|
||||
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
|
||||
ShortcutInfoCompat.Builder(this, "toggle")
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.support.v7.widget.Toolbar
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.utils.resolveResourceId
|
||||
import com.google.zxing.Result
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
|
||||
@@ -54,7 +55,7 @@ class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
|
||||
setContentView(R.layout.layout_scanner)
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = title
|
||||
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
|
||||
toolbar.setNavigationIcon(theme.resolveResourceId(R.attr.homeAsUpIndicator))
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
scannerView = findViewById(R.id.scanner)
|
||||
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
|
||||
|
||||
@@ -41,6 +41,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
|
||||
val serviceCallback: IShadowsocksServiceCallback? get() = null
|
||||
val connection: ShadowsocksConnection
|
||||
get() = connections.getOrPut(this, { ShadowsocksConnection(this) })
|
||||
val listenForDeath get() = false
|
||||
|
||||
fun onServiceConnected(service: IShadowsocksService) { }
|
||||
/**
|
||||
@@ -66,7 +67,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||
this.binder = binder
|
||||
binder.linkToDeath(instance, 0)
|
||||
if (instance.listenForDeath) binder.linkToDeath(instance, 0)
|
||||
val service = IShadowsocksService.Stub.asInterface(binder)!!
|
||||
this.service = service
|
||||
if (instance.serviceCallback != null && !callbackRegistered) try {
|
||||
@@ -106,7 +107,7 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
|
||||
(instance as Context).unbindService(this)
|
||||
} catch (_: IllegalArgumentException) { } // ignore
|
||||
connectionActive = false
|
||||
binder?.unlinkToDeath(instance, 0)
|
||||
if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0)
|
||||
binder = null
|
||||
service = null
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -27,15 +28,13 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.utils.broadcastReceiver
|
||||
|
||||
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
|
||||
class VpnRequestActivity : Activity(), ShadowsocksConnection.Interface {
|
||||
companion object {
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
private const val REQUEST_CONNECT = 1
|
||||
@@ -43,8 +42,8 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (!BaseService.usingVpnMode) {
|
||||
finish()
|
||||
return
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.util.SortedList
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.Subnet
|
||||
import com.github.shadowsocks.utils.asIterable
|
||||
import com.j256.ormlite.field.DatabaseField
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
import java.net.URL
|
||||
|
||||
@@ -41,22 +42,29 @@ class Acl {
|
||||
const val GFWLIST = "gfwlist"
|
||||
const val CHINALIST = "china-list"
|
||||
const val CUSTOM_RULES = "custom-rules"
|
||||
const val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
|
||||
|
||||
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
|
||||
|
||||
fun getFile(id: String) = File(app.filesDir, id + ".acl")
|
||||
fun getFile(id: String, context: Context = app.deviceContext) = File(context.filesDir, id + ".acl")
|
||||
|
||||
val customRules: Acl get() {
|
||||
val acl = Acl()
|
||||
try {
|
||||
acl.fromId(CUSTOM_RULES)
|
||||
} catch (_: FileNotFoundException) { }
|
||||
acl.bypass = true
|
||||
acl.bypassHostnames.clear() // everything is bypassed
|
||||
return acl
|
||||
}
|
||||
fun save(id: String, acl: Acl): Unit = getFile(id).bufferedWriter().use { it.write(acl.toString()) }
|
||||
var customRules: Acl
|
||||
get() {
|
||||
val acl = Acl()
|
||||
val str = DataStore.publicStore.getString(CUSTOM_RULES)
|
||||
if (str != null) {
|
||||
acl.fromReader(str.reader())
|
||||
if (!acl.bypass) {
|
||||
acl.subnets.clear()
|
||||
acl.hostnames.clear()
|
||||
acl.bypass = true
|
||||
}
|
||||
} else acl.bypass = true
|
||||
return acl
|
||||
}
|
||||
set(value) = DataStore.publicStore.putString(CUSTOM_RULES, if ((!value.bypass ||
|
||||
value.subnets.size() == 0 && value.hostnames.size() == 0) && value.urls.size() == 0)
|
||||
null else value.toString())
|
||||
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
|
||||
}
|
||||
|
||||
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
|
||||
@@ -80,41 +88,39 @@ class Acl {
|
||||
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
|
||||
}
|
||||
|
||||
@DatabaseField(generatedId = true)
|
||||
var id = 0
|
||||
val bypassHostnames = SortedList(String::class.java, StringSorter)
|
||||
val proxyHostnames = SortedList(String::class.java, StringSorter)
|
||||
val hostnames = SortedList(String::class.java, StringSorter)
|
||||
val subnets = SortedList(Subnet::class.java, SubnetSorter)
|
||||
val urls = SortedList(URL::class.java, URLSorter)
|
||||
|
||||
@DatabaseField
|
||||
var bypass = false
|
||||
|
||||
fun fromAcl(other: Acl): Acl {
|
||||
bypassHostnames.clear()
|
||||
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
proxyHostnames.clear()
|
||||
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
fun clear(): Acl {
|
||||
hostnames.clear()
|
||||
subnets.clear()
|
||||
for (item in other.subnets.asIterable()) subnets.add(item)
|
||||
urls.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromAcl(other: Acl): Acl {
|
||||
clear()
|
||||
for (item in other.hostnames.asIterable()) hostnames.add(item)
|
||||
for (item in other.subnets.asIterable()) subnets.add(item)
|
||||
for (item in other.urls.asIterable()) urls.add(item)
|
||||
bypass = other.bypass
|
||||
return this
|
||||
}
|
||||
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
subnets.clear()
|
||||
urls.clear()
|
||||
clear()
|
||||
bypass = defaultBypass
|
||||
val proxyHostnames by lazy { SortedList(String::class.java, StringSorter) }
|
||||
val bypassHostnames by lazy { SortedList(String::class.java, StringSorter) }
|
||||
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) {
|
||||
val blocks = line.split('#', limit = 2)
|
||||
@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))
|
||||
val input = blocks[0].trim()
|
||||
@@ -133,31 +139,39 @@ class Acl {
|
||||
}
|
||||
"[reject_all]", "[bypass_all]" -> bypass = true
|
||||
"[accept_all]", "[proxy_all]" -> bypass = false
|
||||
else -> if (subnets != null && input.isNotEmpty()) try {
|
||||
subnets!!.add(Subnet.fromString(input))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
hostnames!!.add(input)
|
||||
else -> if (subnets != null && input.isNotEmpty()) {
|
||||
val subnet = Subnet.fromString(input)
|
||||
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (item in (if (bypass) proxyHostnames else bypassHostnames).asIterable()) this.hostnames.add(item)
|
||||
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromId(id: String): Acl = fromReader(Acl.getFile(id).bufferedReader())
|
||||
fun fromId(id: String): Acl = try {
|
||||
fromReader(Acl.getFile(id).bufferedReader())
|
||||
} catch (_: IOException) { this }
|
||||
|
||||
fun flatten(depth: Int): Acl {
|
||||
if (depth > 0) for (url in urls.asIterable()) {
|
||||
val child = Acl().fromReader(url.openStream().bufferedReader(), bypass).flatten(depth - 1)
|
||||
val child = Acl()
|
||||
try {
|
||||
child.fromReader(url.openStream().bufferedReader(), bypass).flatten(depth - 1)
|
||||
} catch (_: IOException) {
|
||||
continue
|
||||
}
|
||||
if (bypass != child.bypass) {
|
||||
Log.w(TAG, "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
|
||||
// rules for the different mode are discarded
|
||||
child.hostnames.clear()
|
||||
child.subnets.clear()
|
||||
child.bypass = bypass
|
||||
}
|
||||
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
|
||||
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
|
||||
for (item in child.hostnames.asIterable()) hostnames.add(item)
|
||||
for (item in child.subnets.asIterable()) subnets.add(item)
|
||||
}
|
||||
urls.clear()
|
||||
@@ -166,21 +180,11 @@ class Acl {
|
||||
|
||||
override fun toString(): String {
|
||||
val result = StringBuilder()
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
val bypassList = (if (bypass) bypassHostnames.asIterable().asSequence() else
|
||||
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()).toList()
|
||||
val proxyList = (if (bypass) subnets.asIterable().asSequence().map(Subnet::toString) +
|
||||
proxyHostnames.asIterable().asSequence() else bypassHostnames.asIterable().asSequence()).toList()
|
||||
if (bypassList.isNotEmpty()) {
|
||||
result.append("[bypass_list]\n")
|
||||
result.append(bypassList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
if (proxyList.isNotEmpty()) {
|
||||
result.append("[proxy_list]\n")
|
||||
result.append(proxyList.joinToString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
result.append(if (bypass) "[bypass_all]\n[proxy_list]\n" else "[proxy_all]\n[bypass_list]\n")
|
||||
result.append(subnets.asIterable().joinToString("\n"))
|
||||
result.append('\n')
|
||||
result.append(hostnames.asIterable().joinToString("\n"))
|
||||
result.append('\n')
|
||||
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
@@ -132,20 +132,20 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
override fun onBindViewHolder(holder: AclRuleViewHolder, i: Int) {
|
||||
val j = i - acl.subnets.size()
|
||||
if (j < 0) holder.bind(acl.subnets[i]) else {
|
||||
val k = j - acl.proxyHostnames.size()
|
||||
if (k < 0) holder.bind(acl.proxyHostnames[j]) else holder.bind(acl.urls[k])
|
||||
val k = j - acl.hostnames.size()
|
||||
if (k < 0) holder.bind(acl.hostnames[j]) else holder.bind(acl.urls[k])
|
||||
}
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AclRuleViewHolder(LayoutInflater
|
||||
.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false))
|
||||
override fun getItemCount(): Int = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.size()
|
||||
override fun getItemCount(): Int = acl.subnets.size() + acl.hostnames.size() + acl.urls.size()
|
||||
override fun getSectionTitle(i: Int): String {
|
||||
val j = i - acl.subnets.size()
|
||||
return try {
|
||||
(if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
|
||||
val k = j - acl.proxyHostnames.size()
|
||||
val k = j - acl.hostnames.size()
|
||||
if (k < 0) {
|
||||
val hostname = acl.proxyHostnames[j]
|
||||
val hostname = acl.hostnames[j]
|
||||
// don't convert IDN yet
|
||||
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
|
||||
} else acl.urls[k].host
|
||||
@@ -157,7 +157,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
if (!savePending) {
|
||||
savePending = true
|
||||
list.post {
|
||||
Acl.save(Acl.CUSTOM_RULES, acl)
|
||||
Acl.customRules = acl
|
||||
savePending = false
|
||||
}
|
||||
}
|
||||
@@ -179,9 +179,9 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
return index
|
||||
}
|
||||
fun addHostname(hostname: String): Int {
|
||||
val old = acl.proxyHostnames.size()
|
||||
val index = acl.subnets.size() + acl.proxyHostnames.add(hostname)
|
||||
if (old != acl.proxyHostnames.size()) {
|
||||
val old = acl.hostnames.size()
|
||||
val index = acl.subnets.size() + acl.hostnames.add(hostname)
|
||||
if (old != acl.hostnames.size()) {
|
||||
notifyItemInserted(index)
|
||||
apply()
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
fun addURL(url: URL): Int {
|
||||
val old = acl.urls.size()
|
||||
val index = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.add(url)
|
||||
val index = acl.subnets.size() + acl.hostnames.size() + acl.urls.add(url)
|
||||
if (old != acl.urls.size()) {
|
||||
notifyItemInserted(index)
|
||||
apply()
|
||||
@@ -201,7 +201,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
var result: Int? = null
|
||||
if (acl.bypass) acl.subnets.asIterable().asSequence().map { addSubnet(it) }
|
||||
.forEach { if (result == null) result = it }
|
||||
(acl.proxyHostnames.asIterable().asSequence().map { addHostname(it) } +
|
||||
(acl.hostnames.asIterable().asSequence().map { addHostname(it) } +
|
||||
acl.urls.asIterable().asSequence().map { addURL(it) })
|
||||
.forEach { if (result == null) result = it }
|
||||
return result
|
||||
@@ -227,10 +227,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
undoManager.remove(Pair(i, acl.subnets[i]))
|
||||
acl.subnets.removeItemAt(i)
|
||||
} else {
|
||||
val k = j - acl.proxyHostnames.size()
|
||||
val k = j - acl.hostnames.size()
|
||||
if (k < 0) {
|
||||
undoManager.remove(Pair(j, acl.proxyHostnames[j]))
|
||||
acl.proxyHostnames.removeItemAt(j)
|
||||
undoManager.remove(Pair(j, acl.hostnames[j]))
|
||||
acl.hostnames.removeItemAt(j)
|
||||
} else {
|
||||
undoManager.remove(Pair(k, acl.urls[k]))
|
||||
acl.urls.removeItemAt(k)
|
||||
@@ -247,12 +247,12 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
apply()
|
||||
}
|
||||
is String -> {
|
||||
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.indexOf(item))
|
||||
acl.proxyHostnames.remove(item)
|
||||
notifyItemRemoved(acl.subnets.size() + acl.hostnames.indexOf(item))
|
||||
acl.hostnames.remove(item)
|
||||
apply()
|
||||
}
|
||||
is URL -> {
|
||||
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.indexOf(item))
|
||||
notifyItemRemoved(acl.subnets.size() + acl.hostnames.size() + acl.urls.indexOf(item))
|
||||
acl.urls.remove(item)
|
||||
apply()
|
||||
}
|
||||
@@ -271,7 +271,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
fun selectAll() {
|
||||
selectedItems.clear()
|
||||
selectedItems.addAll(acl.subnets.asIterable())
|
||||
selectedItems.addAll(acl.proxyHostnames.asIterable())
|
||||
selectedItems.addAll(acl.hostnames.asIterable())
|
||||
selectedItems.addAll(acl.urls.asIterable())
|
||||
onSelectedItemsUpdated()
|
||||
notifyDataSetChanged()
|
||||
@@ -333,8 +333,8 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS)?.map(Subnet.Companion::fromString)
|
||||
?: listOf())
|
||||
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS)
|
||||
?.mapNotNull(Subnet.Companion::fromString) ?: listOf())
|
||||
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_HOSTNAMES)
|
||||
?: arrayOf())
|
||||
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_URLS)?.map { URL(it) }
|
||||
@@ -386,7 +386,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
selectedItems.forEach {
|
||||
when (it) {
|
||||
is Subnet -> acl.subnets.add(it)
|
||||
is String -> acl.proxyHostnames.add(it)
|
||||
is String -> acl.hostnames.add(it)
|
||||
is URL -> acl.urls.add(it)
|
||||
}
|
||||
}
|
||||
@@ -432,7 +432,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
R.id.action_import_gfwlist -> {
|
||||
val acl = Acl().fromId(Acl.GFWLIST)
|
||||
if (!acl.bypass) acl.subnets.asIterable().forEach { adapter.addSubnet(it) }
|
||||
acl.proxyHostnames.asIterable().forEach { adapter.addHostname(it) }
|
||||
acl.hostnames.asIterable().forEach { adapter.addHostname(it) }
|
||||
acl.urls.asIterable().forEach { adapter.addURL(it) }
|
||||
true
|
||||
}
|
||||
|
||||
@@ -20,12 +20,15 @@
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.text.TextUtils
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteCallbackList
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
@@ -66,6 +69,8 @@ object BaseService {
|
||||
const val STOPPING = 3
|
||||
const val STOPPED = 4
|
||||
|
||||
const val CONFIG_FILE = "shadowsocks.conf"
|
||||
|
||||
class Data internal constructor(private val service: Interface) {
|
||||
@Volatile var profile: Profile? = null
|
||||
@Volatile var state = STOPPED
|
||||
@@ -91,10 +96,8 @@ object BaseService {
|
||||
}
|
||||
var closeReceiverRegistered = false
|
||||
|
||||
private fun state() = state
|
||||
|
||||
val binder = object : IShadowsocksService.Stub() {
|
||||
override fun getState(): Int = state()
|
||||
override fun getState(): Int = this@Data.state
|
||||
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
|
||||
|
||||
override fun registerCallback(cb: IShadowsocksServiceCallback) {
|
||||
@@ -165,7 +168,8 @@ object BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildShadowsocksConfig() {
|
||||
internal var shadowsocksConfigFile: File? = null
|
||||
internal fun buildShadowsocksConfig(): File {
|
||||
val profile = profile!!
|
||||
val config = JSONObject()
|
||||
.put("server", profile.host)
|
||||
@@ -180,7 +184,30 @@ object BaseService {
|
||||
.put("plugin", Commandline.toString(service.buildAdditionalArguments(pluginCmd)))
|
||||
.put("plugin_opts", plugin.toString())
|
||||
}
|
||||
File(app.filesDir, "shadowsocks.json").bufferedWriter().use { it.write(config.toString()) }
|
||||
// sensitive Shadowsocks config is stored in
|
||||
val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
|
||||
app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state
|
||||
}), CONFIG_FILE)
|
||||
shadowsocksConfigFile = file
|
||||
file.writeText(config.toString())
|
||||
return file
|
||||
}
|
||||
|
||||
val aclFile: File? get() {
|
||||
val route = profile!!.route
|
||||
return if (route == Acl.ALL) null else Acl.getFile(route)
|
||||
}
|
||||
|
||||
fun changeState(s: Int, msg: String? = null) {
|
||||
if (state == s && msg == null) return
|
||||
if (callbacks.registeredCallbackCount > 0) app.handler.post {
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i in 0 until n) try {
|
||||
callbacks.getBroadcastItem(i).stateChanged(s, binder.profileName, msg)
|
||||
} catch (_: Exception) { } // ignore
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
state = s
|
||||
}
|
||||
}
|
||||
interface Interface {
|
||||
@@ -189,7 +216,7 @@ object BaseService {
|
||||
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun checkProfile(profile: Profile): Boolean =
|
||||
if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
||||
if (profile.host.isEmpty() || profile.password.isEmpty()) {
|
||||
stopRunner(true, (this as Context).getString(R.string.proxy_empty))
|
||||
false
|
||||
} else true
|
||||
@@ -212,16 +239,15 @@ object BaseService {
|
||||
|
||||
fun startNativeProcesses() {
|
||||
val data = data
|
||||
data.buildShadowsocksConfig()
|
||||
val cmd = buildAdditionalArguments(arrayListOf(
|
||||
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-u",
|
||||
"-b", "127.0.0.1",
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-c", "shadowsocks.json"))
|
||||
"-c", data.buildShadowsocksConfig().absolutePath))
|
||||
|
||||
val acl = getAclFile()
|
||||
val acl = data.aclFile
|
||||
if (acl != null) {
|
||||
cmd += "--acl"
|
||||
cmd += acl.absolutePath
|
||||
@@ -232,29 +258,7 @@ object BaseService {
|
||||
data.sslocalProcess = GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
fun getAclFile(): File? {
|
||||
val route = data.profile!!.route
|
||||
return if (route == Acl.ALL) null else
|
||||
Acl.getFile(if (route == Acl.CUSTOM_RULES) Acl.CUSTOM_RULES_FLATTENED else route)
|
||||
}
|
||||
|
||||
fun getListFile(): File? {
|
||||
val aclFile = getAclFile() ?: return null
|
||||
val listFile = File(aclFile.absolutePath + ".lst")
|
||||
val list = StringBuilder()
|
||||
aclFile.bufferedReader().useLines {
|
||||
for (line in it) {
|
||||
if (line.startsWith("^(.*\\.)?")) {
|
||||
list.append(line.substring(8).replace("\\","").replace("$",""))
|
||||
list.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
listFile.bufferedWriter().use { it.write(list.toString()) }
|
||||
return listFile
|
||||
}
|
||||
|
||||
fun createNotification(): ServiceNotification
|
||||
fun createNotification(profileName: String): ServiceNotification
|
||||
|
||||
fun startRunner() {
|
||||
this as Context
|
||||
@@ -270,20 +274,23 @@ object BaseService {
|
||||
|
||||
fun stopRunner(stopService: Boolean, msg: String? = null) {
|
||||
// channge the state
|
||||
changeState(STOPPING)
|
||||
val data = data
|
||||
data.changeState(STOPPING)
|
||||
|
||||
app.track(tag, "stop")
|
||||
|
||||
killProcesses()
|
||||
|
||||
// clean up recevier
|
||||
val data = data
|
||||
this as Service
|
||||
if (data.closeReceiverRegistered) {
|
||||
unregisterReceiver(data.closeReceiver)
|
||||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
data.shadowsocksConfigFile?.delete() // remove old config possibly in device storage
|
||||
data.shadowsocksConfigFile = null
|
||||
|
||||
data.notification?.destroy()
|
||||
data.notification = null
|
||||
|
||||
@@ -295,7 +302,7 @@ object BaseService {
|
||||
data.trafficMonitorThread = null
|
||||
|
||||
// change the state
|
||||
changeState(STOPPED, msg)
|
||||
data.changeState(STOPPED, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (stopService) stopSelf()
|
||||
@@ -309,12 +316,13 @@ object BaseService {
|
||||
val data = data
|
||||
if (data.state != STOPPED) return Service.START_NOT_STICKY
|
||||
val profile = app.currentProfile
|
||||
this as Context
|
||||
if (profile == null) {
|
||||
stopRunner(true)
|
||||
data.notification = createNotification("") // gracefully shutdown: https://stackoverflow.com/questions/47337857/context-startforegroundservice-did-not-then-call-service-startforeground-eve
|
||||
stopRunner(true, getString(R.string.profile_empty))
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
data.profile = profile
|
||||
profile.name = profile.formattedName
|
||||
|
||||
TrafficMonitor.reset()
|
||||
val thread = TrafficMonitorThread()
|
||||
@@ -327,17 +335,16 @@ object BaseService {
|
||||
filter.addAction(Action.RELOAD)
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
(this as Context).registerReceiver(data.closeReceiver, filter)
|
||||
registerReceiver(data.closeReceiver, filter)
|
||||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
data.notification = createNotification()
|
||||
data.notification = createNotification(profile.formattedName)
|
||||
app.track(tag, "start")
|
||||
|
||||
changeState(CONNECTING)
|
||||
data.changeState(CONNECTING)
|
||||
|
||||
thread {
|
||||
this as Context
|
||||
try {
|
||||
if (profile.host == "198.199.101.152") {
|
||||
val client = OkHttpClient.Builder()
|
||||
@@ -370,7 +377,7 @@ object BaseService {
|
||||
}
|
||||
|
||||
if (profile.route == Acl.CUSTOM_RULES)
|
||||
Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
|
||||
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10))
|
||||
|
||||
data.plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||
data.pluginPath = PluginManager.init(data.plugin)
|
||||
@@ -385,7 +392,7 @@ object BaseService {
|
||||
|
||||
if (profile.route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncJob.schedule(profile.route)
|
||||
|
||||
changeState(CONNECTED)
|
||||
data.changeState(CONNECTED)
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(true, getString(R.string.invalid_server))
|
||||
} catch (_: VpnService.NullConnectionException) {
|
||||
@@ -397,19 +404,6 @@ object BaseService {
|
||||
}
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun changeState(s: Int, msg: String? = null) {
|
||||
val data = instances[this]!!
|
||||
if (data.state == s && msg == null) return
|
||||
if (data.callbacks.registeredCallbackCount > 0) app.handler.post {
|
||||
val n = data.callbacks.beginBroadcast()
|
||||
for (i in 0 until n) try {
|
||||
data.callbacks.getBroadcastItem(i).stateChanged(s, data.binder.profileName, msg)
|
||||
} catch (_: Exception) { } // ignore
|
||||
data.callbacks.finishBroadcast()
|
||||
}
|
||||
data.state = s
|
||||
}
|
||||
}
|
||||
|
||||
private val instances = WeakHashMap<Interface, Data>()
|
||||
|
||||
@@ -37,8 +37,7 @@ object Executable {
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(File(process, "cmdline").bufferedReader().use { it.readText() }
|
||||
.split(Character.MIN_VALUE, limit = 2).first())
|
||||
val exe = File(File(process, "cmdline").readText().split(Character.MIN_VALUE, limit = 2).first())
|
||||
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
|
||||
val errno = JniHelper.sigkill(process.name.toInt())
|
||||
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
|
||||
|
||||
@@ -62,7 +62,7 @@ class GuardedProcess(private val cmd: List<String>) {
|
||||
|
||||
process = ProcessBuilder(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.directory(app.filesDir)
|
||||
.directory(app.deviceContext.filesDir)
|
||||
.start()
|
||||
|
||||
streamLogger(process.inputStream, Log::i)
|
||||
|
||||
@@ -44,6 +44,7 @@ object LocalDnsService {
|
||||
|
||||
override fun startNativeProcesses() {
|
||||
super.startNativeProcesses()
|
||||
val data = data
|
||||
val profile = data.profile!!
|
||||
|
||||
fun makeDns(name: String, address: String, edns: Boolean = true): JSONObject {
|
||||
@@ -89,7 +90,7 @@ object LocalDnsService {
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
.put("IPNetworkFile", "china_ip_list.txt")
|
||||
.put("DomainFile", getListFile()!!.absolutePath)
|
||||
.put("DomainFile", data.aclFile!!.absolutePath)
|
||||
Acl.CHINALIST -> config
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
@@ -98,7 +99,7 @@ object LocalDnsService {
|
||||
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
|
||||
.put("OnlyPrimaryDNS", true)
|
||||
}
|
||||
File(filesDir, file).bufferedWriter().use { it.write(config.toString()) }
|
||||
File(app.deviceContext.filesDir, file).writeText(config.toString())
|
||||
return file
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.github.shadowsocks.bg
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.github.shadowsocks.database.Profile
|
||||
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
@@ -33,8 +34,8 @@ class ProxyService : Service(), BaseService.Interface {
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.formattedName, "service-proxy", true)
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
|
||||
@@ -69,7 +69,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, Intent(service, MainActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
|
||||
.setSmallIcon(R.drawable.ic_start_connected)
|
||||
.setSmallIcon(R.drawable.ic_service_active)
|
||||
private val style = NotificationCompat.BigTextStyle(builder)
|
||||
private var isVisible = true
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ import android.service.quicksettings.TileService as BaseTileService
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TileService : BaseTileService(), ShadowsocksConnection.Interface {
|
||||
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_start_idle).setTint(0x79ffffff) }
|
||||
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_start_busy) }
|
||||
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_start_connected) }
|
||||
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle).setTint(0x79ffffff) }
|
||||
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
|
||||
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
|
||||
|
||||
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
|
||||
override val socketFile = File(app.filesDir, "/stat_path")
|
||||
override val socketFile = File(app.deviceContext.filesDir, "stat_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.github.shadowsocks.bg
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
@@ -32,8 +33,8 @@ class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.formattedName, "service-transproxy", true)
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
@@ -47,14 +48,14 @@ class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
"-t", "10",
|
||||
"-b", "127.0.0.1",
|
||||
"-u",
|
||||
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
|
||||
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
|
||||
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
|
||||
"-c", "shadowsocks.json") // config is already built by BaseService.Interface
|
||||
"-c", data.shadowsocksConfigFile!!.absolutePath) // config is already built by BaseService.Interface
|
||||
sstunnelProcess = GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
val config = """base {
|
||||
File(app.deviceContext.filesDir, "redsocks.conf").writeText("""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
@@ -68,8 +69,7 @@ redsocks {
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
File(filesDir, "redsocks.conf").bufferedWriter().use { it.write(config) }
|
||||
""")
|
||||
redsocksProcess = GuardedProcess(arrayListOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c", "redsocks.conf")
|
||||
|
||||
@@ -52,7 +52,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
}
|
||||
|
||||
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
|
||||
override val socketFile: File = File(filesDir, "protect_path")
|
||||
override val socketFile: File = File(app.deviceContext.filesDir, "protect_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
try {
|
||||
@@ -76,8 +76,8 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksVpnService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.formattedName, "service-vpn")
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-vpn")
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
@@ -159,10 +159,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||
else -> {
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val subnet = Subnet.fromString(it)
|
||||
val subnet = Subnet.fromString(it)!!
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
profile.remoteDns.split(",").map { it.trim().parseNumericAddress() }
|
||||
profile.remoteDns.split(",").mapNotNull { it.trim().parseNumericAddress() }
|
||||
.forEach { builder.addRoute(it, it.address.size shl 3) }
|
||||
}
|
||||
}
|
||||
@@ -194,10 +194,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
|
||||
private fun sendFd(fd: Int): Boolean {
|
||||
if (fd != -1) {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000L * tries)
|
||||
if (JniHelper.sendFd(fd, File(filesDir, "sock_path").absolutePath) != -1) return true
|
||||
var tries = 0
|
||||
while (tries < 10) {
|
||||
Thread.sleep(30L shl tries)
|
||||
if (JniHelper.sendFd(fd, File(app.deviceContext.filesDir, "sock_path").absolutePath) != -1) return true
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class KeyValuePair() {
|
||||
get() = if (valueType == TYPE_LONG) ByteBuffer.wrap(value).long else null
|
||||
val string: String?
|
||||
get() = if (valueType == TYPE_STRING) String(value) else null
|
||||
val stringSet: MutableSet<String>?
|
||||
val stringSet: Set<String>?
|
||||
get() = if (valueType == TYPE_STRING_SET) {
|
||||
val buffer = ByteBuffer.wrap(value)
|
||||
val result = HashSet<String>()
|
||||
@@ -95,7 +95,7 @@ class KeyValuePair() {
|
||||
this.value = value.toByteArray()
|
||||
return this
|
||||
}
|
||||
fun put(value: MutableSet<String>): KeyValuePair {
|
||||
fun put(value: Set<String>): KeyValuePair {
|
||||
valueType = TYPE_STRING_SET
|
||||
val stream = ByteArrayOutputStream()
|
||||
for (v in value) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
import com.j256.ormlite.table.TableUtils
|
||||
|
||||
object DBHelper : OrmLiteSqliteOpenHelper(app, Key.PROFILE, null, 24) {
|
||||
object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val profileDao: Dao<Profile, Int> by lazy { getDao(Profile::class.java) as Dao<Profile, Int> }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -134,8 +134,12 @@ object DBHelper : OrmLiteSqliteOpenHelper(app, Key.PROFILE, null, 24) {
|
||||
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
val old = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
|
||||
kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
|
||||
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
|
||||
PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
|
||||
}
|
||||
|
||||
if (oldVersion < 25) {
|
||||
PublicDatabase.onUpgrade(database, 0, -1)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
app.track(ex)
|
||||
@@ -29,10 +29,11 @@ import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.parsePort
|
||||
import com.j256.ormlite.field.DataType
|
||||
import com.j256.ormlite.field.DatabaseField
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
|
||||
class Profile {
|
||||
class Profile : Serializable {
|
||||
companion object {
|
||||
private const val TAG = "ShadowParser"
|
||||
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex()
|
||||
@@ -153,34 +154,34 @@ class Profile {
|
||||
override fun toString() = toUri().toString()
|
||||
|
||||
fun serialize() {
|
||||
DataStore.putString(Key.name, name)
|
||||
DataStore.putString(Key.host, host)
|
||||
DataStore.putString(Key.remotePort, remotePort.toString())
|
||||
DataStore.putString(Key.password, password)
|
||||
DataStore.putString(Key.route, route)
|
||||
DataStore.putString(Key.remoteDns, remoteDns)
|
||||
DataStore.putString(Key.method, method)
|
||||
DataStore.privateStore.putString(Key.name, name)
|
||||
DataStore.privateStore.putString(Key.host, host)
|
||||
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
|
||||
DataStore.privateStore.putString(Key.password, password)
|
||||
DataStore.privateStore.putString(Key.route, route)
|
||||
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
|
||||
DataStore.privateStore.putString(Key.method, method)
|
||||
DataStore.proxyApps = proxyApps
|
||||
DataStore.bypass = bypass
|
||||
DataStore.putBoolean(Key.udpdns, udpdns)
|
||||
DataStore.putBoolean(Key.ipv6, ipv6)
|
||||
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
|
||||
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
|
||||
DataStore.individual = individual
|
||||
DataStore.plugin = plugin ?: ""
|
||||
DataStore.remove(Key.dirty)
|
||||
DataStore.privateStore.remove(Key.dirty)
|
||||
}
|
||||
fun deserialize() {
|
||||
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
||||
name = DataStore.getString(Key.name) ?: ""
|
||||
host = DataStore.getString(Key.host) ?: ""
|
||||
remotePort = parsePort(DataStore.getString(Key.remotePort), 8388, 1)
|
||||
password = DataStore.getString(Key.password) ?: ""
|
||||
method = DataStore.getString(Key.method) ?: ""
|
||||
route = DataStore.getString(Key.route) ?: ""
|
||||
remoteDns = DataStore.getString(Key.remoteDns) ?: ""
|
||||
name = DataStore.privateStore.getString(Key.name) ?: ""
|
||||
host = DataStore.privateStore.getString(Key.host) ?: ""
|
||||
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
|
||||
password = DataStore.privateStore.getString(Key.password) ?: ""
|
||||
method = DataStore.privateStore.getString(Key.method) ?: ""
|
||||
route = DataStore.privateStore.getString(Key.route) ?: ""
|
||||
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
|
||||
proxyApps = DataStore.proxyApps
|
||||
bypass = DataStore.bypass
|
||||
udpdns = DataStore.getBoolean(Key.udpdns, false)
|
||||
ipv6 = DataStore.getBoolean(Key.ipv6, false)
|
||||
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
|
||||
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
|
||||
individual = DataStore.individual
|
||||
plugin = DataStore.plugin
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ package com.github.shadowsocks.database
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.ProfilesFragment
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
|
||||
object ProfileManager {
|
||||
private const val TAG = "ProfileManager"
|
||||
@@ -41,10 +43,10 @@ object ProfileManager {
|
||||
profile.individual = oldProfile.individual
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
}
|
||||
val last = DBHelper.profileDao.queryRaw(DBHelper.profileDao.queryBuilder().selectRaw("MAX(userOrder)")
|
||||
.prepareStatementString()).firstResult
|
||||
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
|
||||
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
|
||||
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
|
||||
DBHelper.profileDao.createOrUpdate(profile)
|
||||
PrivateDatabase.profileDao.createOrUpdate(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.add(profile)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "addProfile", ex)
|
||||
@@ -53,8 +55,11 @@ object ProfileManager {
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
|
||||
*/
|
||||
fun updateProfile(profile: Profile): Boolean = try {
|
||||
DBHelper.profileDao.update(profile)
|
||||
PrivateDatabase.profileDao.update(profile)
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "updateProfile", ex)
|
||||
@@ -63,7 +68,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getProfile(id: Int): Profile? = try {
|
||||
DBHelper.profileDao.queryForId(id)
|
||||
PrivateDatabase.profileDao.queryForId(id)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getProfile", ex)
|
||||
app.track(ex)
|
||||
@@ -71,8 +76,9 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun delProfile(id: Int): Boolean = try {
|
||||
DBHelper.profileDao.deleteById(id)
|
||||
PrivateDatabase.profileDao.deleteById(id)
|
||||
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
|
||||
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "delProfile", ex)
|
||||
@@ -81,7 +87,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getFirstProfile(): Profile? = try {
|
||||
val result = DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().limit(1L).prepare())
|
||||
val result = PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare())
|
||||
if (result.size == 1) result[0] else null
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
@@ -90,7 +96,7 @@ object ProfileManager {
|
||||
}
|
||||
|
||||
fun getAllProfiles(): List<Profile>? = try {
|
||||
DBHelper.profileDao.query(DBHelper.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
|
||||
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
app.track(ex)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.database
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.j256.ormlite.android.AndroidDatabaseConnection
|
||||
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
import com.j256.ormlite.table.TableUtils
|
||||
|
||||
object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC, null, 2) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
|
||||
|
||||
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
|
||||
TableUtils.createTable(connectionSource, KeyValuePair::class.java)
|
||||
}
|
||||
|
||||
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
|
||||
oldVersion: Int, newVersion: Int) {
|
||||
if (oldVersion < 1) {
|
||||
PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key",
|
||||
Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query()
|
||||
.forEach { kvPairDao.createOrUpdate(it) }
|
||||
}
|
||||
|
||||
if (oldVersion < 2) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
|
||||
val connection = AndroidDatabaseConnection(db, true)
|
||||
connectionSource.saveSpecialConnection(connection)
|
||||
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
|
||||
onCreate(db, connectionSource)
|
||||
connectionSource.clearSpecialConnection(connection)
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val se
|
||||
})
|
||||
|
||||
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
|
||||
pluginsOptions.get(id) ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
|
||||
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
|
||||
val selectedOptions: PluginOptions get() = getOptions(selected)
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
@@ -157,7 +157,7 @@ object PluginManager {
|
||||
var initialized = false
|
||||
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
|
||||
val list = ArrayList<String>()
|
||||
val pluginDir = File(app.filesDir, "plugin")
|
||||
val pluginDir = File(app.deviceContext.filesDir, "plugin")
|
||||
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
|
||||
?: return null).use { cursor ->
|
||||
if (!cursor.moveToFirst()) entryNotFound()
|
||||
|
||||
@@ -21,115 +21,72 @@
|
||||
package com.github.shadowsocks.preference
|
||||
|
||||
import android.os.Binder
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.DBHelper
|
||||
import com.github.shadowsocks.database.KeyValuePair
|
||||
import com.github.shadowsocks.BootReceiver
|
||||
import com.github.shadowsocks.database.PrivateDatabase
|
||||
import com.github.shadowsocks.database.PublicDatabase
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.parsePort
|
||||
import java.util.*
|
||||
|
||||
@Suppress("MemberVisibilityCanPrivate", "unused")
|
||||
object DataStore : PreferenceDataStore() {
|
||||
fun getBoolean(key: String?) = DBHelper.kvPairDao.queryForId(key)?.boolean
|
||||
fun getFloat(key: String?) = DBHelper.kvPairDao.queryForId(key)?.float
|
||||
fun getInt(key: String?) = DBHelper.kvPairDao.queryForId(key)?.int
|
||||
fun getLong(key: String?) = DBHelper.kvPairDao.queryForId(key)?.long
|
||||
fun getString(key: String?) = DBHelper.kvPairDao.queryForId(key)?.string
|
||||
fun getStringSet(key: String?) = DBHelper.kvPairDao.queryForId(key)?.stringSet
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
|
||||
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
|
||||
|
||||
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
|
||||
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
|
||||
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
|
||||
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||
DBHelper.kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String?) {
|
||||
DBHelper.kvPairDao.deleteById(key)
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||
object DataStore {
|
||||
val publicStore = OrmLitePreferenceDataStore(PublicDatabase.kvPairDao)
|
||||
// privateStore will only be used as temp storage for ProfileConfigFragment
|
||||
val privateStore = OrmLitePreferenceDataStore(PrivateDatabase.kvPairDao)
|
||||
|
||||
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
||||
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
||||
private fun getLocalPort(key: String, default: Int): Int {
|
||||
val value = getInt(key)
|
||||
val value = publicStore.getInt(key)
|
||||
return if (value != null) {
|
||||
putString(key, value.toString())
|
||||
publicStore.putString(key, value.toString())
|
||||
value
|
||||
} else parsePort(getString(key), default + userIndex)
|
||||
} else parsePort(publicStore.getString(key), default + userIndex)
|
||||
}
|
||||
|
||||
var profileId: Int
|
||||
get() = getInt(Key.id) ?: 0
|
||||
set(value) = putInt(Key.id, value)
|
||||
get() = publicStore.getInt(Key.id) ?: 0
|
||||
set(value) {
|
||||
publicStore.putInt(Key.id, value)
|
||||
if (DataStore.directBootAware) DirectBoot.update()
|
||||
}
|
||||
/**
|
||||
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
|
||||
*/
|
||||
val directBootAware: Boolean get() = BootReceiver.enabled && (publicStore.getBoolean(Key.directBootAware) ?: false)
|
||||
var serviceMode: String
|
||||
get() = getString(Key.serviceMode) ?: Key.modeVpn
|
||||
set(value) = putString(Key.serviceMode, value)
|
||||
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
|
||||
set(value) = publicStore.putString(Key.serviceMode, value)
|
||||
var portProxy: Int
|
||||
get() = getLocalPort(Key.portProxy, 1080)
|
||||
set(value) = putString(Key.portProxy, value.toString())
|
||||
set(value) = publicStore.putString(Key.portProxy, value.toString())
|
||||
var portLocalDns: Int
|
||||
get() = getLocalPort(Key.portLocalDns, 5450)
|
||||
set(value) = putString(Key.portLocalDns, value.toString())
|
||||
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
|
||||
var portTransproxy: Int
|
||||
get() = getLocalPort(Key.portTransproxy, 8200)
|
||||
set(value) = putString(Key.portTransproxy, value.toString())
|
||||
|
||||
var proxyApps: Boolean
|
||||
get() = getBoolean(Key.proxyApps) ?: false
|
||||
set(value) = putBoolean(Key.proxyApps, value)
|
||||
var bypass: Boolean
|
||||
get() = getBoolean(Key.bypass) ?: false
|
||||
set(value) = putBoolean(Key.bypass, value)
|
||||
var individual: String
|
||||
get() = getString(Key.individual) ?: ""
|
||||
set(value) = putString(Key.individual, value)
|
||||
var plugin: String
|
||||
get() = getString(Key.plugin) ?: ""
|
||||
set(value) = putString(Key.plugin, value)
|
||||
var dirty: Boolean
|
||||
get() = getBoolean(Key.dirty) ?: false
|
||||
set(value) = putBoolean(Key.dirty, value)
|
||||
set(value) = publicStore.putString(Key.portTransproxy, value.toString())
|
||||
|
||||
fun initGlobal() {
|
||||
// temporary workaround for support lib bug
|
||||
if (getString(Key.serviceMode) == null) serviceMode = serviceMode
|
||||
if (getString(Key.portProxy) == null) portProxy = portProxy
|
||||
if (getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||
if (getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
if (publicStore.getString(Key.serviceMode) == null) serviceMode = serviceMode
|
||||
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
|
||||
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
|
||||
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
|
||||
}
|
||||
|
||||
var proxyApps: Boolean
|
||||
get() = privateStore.getBoolean(Key.proxyApps) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.proxyApps, value)
|
||||
var bypass: Boolean
|
||||
get() = privateStore.getBoolean(Key.bypass) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.bypass, value)
|
||||
var individual: String
|
||||
get() = privateStore.getString(Key.individual) ?: ""
|
||||
set(value) = privateStore.putString(Key.individual, value)
|
||||
var plugin: String
|
||||
get() = privateStore.getString(Key.plugin) ?: ""
|
||||
set(value) = privateStore.putString(Key.plugin, value)
|
||||
var dirty: Boolean
|
||||
get() = privateStore.getBoolean(Key.dirty) ?: false
|
||||
set(value) = privateStore.putBoolean(Key.dirty, value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.preference
|
||||
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.KeyValuePair
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import java.util.HashSet
|
||||
|
||||
@Suppress("MemberVisibilityCanPrivate", "unused")
|
||||
open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() {
|
||||
fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean
|
||||
fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float
|
||||
fun getInt(key: String?) = kvPairDao.queryForId(key)?.int
|
||||
fun getLong(key: String?) = kvPairDao.queryForId(key)?.long
|
||||
fun getString(key: String?) = kvPairDao.queryForId(key)?.string
|
||||
fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet
|
||||
|
||||
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
|
||||
override fun getInt(key: String?, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String?, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String?, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String?, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
|
||||
|
||||
fun putBoolean(key: String?, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
|
||||
fun putFloat(key: String?, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
|
||||
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
|
||||
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String?, value: Boolean) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putFloat(key: String?, value: Float) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putInt(key: String?, value: Int) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putLong(key: String?, value: Long) {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
|
||||
kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String?) {
|
||||
kvPairDao.deleteById(key)
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String?) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import android.widget.Switch
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.utils.resolveResourceId
|
||||
|
||||
class ConfigActivity : AppCompatActivity() {
|
||||
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
@@ -44,9 +45,7 @@ class ConfigActivity : AppCompatActivity() {
|
||||
private val text = itemView.findViewById<CheckedTextView>(android.R.id.text1)
|
||||
|
||||
init {
|
||||
val typedArray = obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
|
||||
view.setBackgroundResource(typedArray.getResourceId(0, 0))
|
||||
typedArray.recycle()
|
||||
view.setBackgroundResource(theme.resolveResourceId(android.R.attr.selectableItemBackground))
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.twofortyfouram.locale.api.Intent as ApiIntent
|
||||
|
||||
class Settings(bundle: Bundle) {
|
||||
class Settings(bundle: Bundle?) {
|
||||
companion object {
|
||||
private val KEY_SWITCH_ON = "switch_on"
|
||||
private val KEY_PROFILE_ID = "profile_id"
|
||||
@@ -35,8 +35,8 @@ class Settings(bundle: Bundle) {
|
||||
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
|
||||
}
|
||||
|
||||
var switchOn: Boolean = bundle.getBoolean(KEY_SWITCH_ON, true)
|
||||
var profileId: Int = bundle.getInt(KEY_PROFILE_ID, -1)
|
||||
var switchOn: Boolean = bundle?.getBoolean(KEY_SWITCH_ON, true) ?: true
|
||||
var profileId: Int = bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1
|
||||
|
||||
fun toIntent(context: Context): Intent {
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
object Key {
|
||||
const val PROFILE = "profile.db"
|
||||
/**
|
||||
* Public config that doesn't need to be kept secret.
|
||||
*/
|
||||
const val DB_PUBLIC = "config.db"
|
||||
const val DB_PROFILE = "profile.db"
|
||||
|
||||
const val id = "profileId"
|
||||
const val name = "profileName"
|
||||
@@ -39,6 +43,7 @@ object Key {
|
||||
const val route = "route"
|
||||
|
||||
const val isAutoConnect = "isAutoConnect"
|
||||
const val directBootAware = "directBootAware"
|
||||
|
||||
const val proxyApps = "isProxyApps"
|
||||
const val bypass = "isBypassApps"
|
||||
|
||||
@@ -0,0 +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 com.github.shadowsocks.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
|
||||
@TargetApi(24)
|
||||
class DeviceContext(context: Context) : ContextWrapper(context.createDeviceProtectedStorageContext()) {
|
||||
/**
|
||||
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
|
||||
* protected storage.
|
||||
*/
|
||||
override fun getApplicationContext(): Context = this
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
|
||||
@TargetApi(24)
|
||||
object DirectBoot {
|
||||
private val file = File(app.deviceContext.noBackupFilesDir, "directBootProfile")
|
||||
|
||||
fun getDeviceProfile(): Profile? = try {
|
||||
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
|
||||
} catch (_: FileNotFoundException) { null }
|
||||
|
||||
fun clean() {
|
||||
file.delete()
|
||||
File(app.deviceContext.noBackupFilesDir, BaseService.CONFIG_FILE).delete()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
val profile = ProfileManager.getProfile(DataStore.profileId) // app.currentProfile will call this
|
||||
if (profile == null) clean() else ObjectOutputStream(file.outputStream()).use { it.writeObject(profile) }
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,16 @@ import java.util.*
|
||||
|
||||
class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet> {
|
||||
companion object {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun fromString(value: String): Subnet {
|
||||
val parts = value.split('/')
|
||||
val addr = parts[0].parseNumericAddress()
|
||||
return when (parts.size) {
|
||||
1 -> Subnet(addr, addr.address.size shl 3)
|
||||
2 -> Subnet(addr, parts[1].toInt())
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
fun fromString(value: String): Subnet? {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
val parts = (value as java.lang.String).split("/", 2)
|
||||
val addr = parts[0].parseNumericAddress() ?: return null
|
||||
return if (parts.size == 2) try {
|
||||
val prefixSize = parts[1].toInt()
|
||||
if (prefixSize < 0 || prefixSize > addr.address.size shl 3) null else Subnet(addr, prefixSize)
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
} else Subnet(addr, addr.address.size shl 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ object TcpFastOpen {
|
||||
|
||||
val sendEnabled: Boolean get() {
|
||||
val file = File("/proc/sys/net/ipv4/tcp_fastopen")
|
||||
// File.readText doesn't work since this special file will return length 0
|
||||
return file.canRead() && file.bufferedReader().use { it.readText() }.trim().toInt() and 1 > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -3,31 +3,28 @@ package com.github.shadowsocks.utils
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.support.annotation.AttrRes
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.util.SortedList
|
||||
import android.util.TypedValue
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import java.net.InetAddress
|
||||
import java.net.URLConnection
|
||||
|
||||
private val isNumericMethod by lazy { InetAddress::class.java.getMethod("isNumeric", String::class.java) }
|
||||
private val parseNumericAddressMethod by lazy {
|
||||
InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
|
||||
}
|
||||
private val fieldChildFragmentManager by lazy {
|
||||
val field = Fragment::class.java.getDeclaredField("mChildFragmentManager")
|
||||
field.isAccessible = true
|
||||
field
|
||||
}
|
||||
|
||||
fun String.isNumericAddress(): Boolean = isNumericMethod.invoke(null, this) as Boolean
|
||||
fun String.parseNumericAddress(): InetAddress = try {
|
||||
parseNumericAddressMethod.invoke(null, this) as InetAddress
|
||||
} catch (exc: InvocationTargetException) {
|
||||
throw exc.cause ?: exc
|
||||
fun String.isNumericAddress() = JniHelper.parseNumericAddress(this) != null
|
||||
fun String.parseNumericAddress(): InetAddress? {
|
||||
val addr = JniHelper.parseNumericAddress(this)
|
||||
return if (addr == null) null else InetAddress.getByAddress(this, addr)
|
||||
}
|
||||
|
||||
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||
@@ -60,6 +57,15 @@ var Fragment.childFragManager: FragmentManager?
|
||||
get() = childFragmentManager
|
||||
set(value) = fieldChildFragmentManager.set(this, value)
|
||||
|
||||
/**
|
||||
* Based on: https://stackoverflow.com/a/26348729/2245107
|
||||
*/
|
||||
fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
if (!resolveAttribute(resId, typedValue, true)) throw Resources.NotFoundException()
|
||||
return typedValue.resourceId
|
||||
}
|
||||
|
||||
private class SortedListIterable<out T>(private val list: SortedList<T>) : Iterable<T> {
|
||||
override fun iterator(): Iterator<T> = SortedListIterator(list)
|
||||
}
|
||||
|
||||
@@ -28,12 +28,13 @@ import com.github.shadowsocks.R
|
||||
/**
|
||||
* Based on: http://stackoverflow.com/a/6212120/2245107
|
||||
*/
|
||||
class BoundedGridLayout(context: Context, attrs: AttributeSet) : GridLayout(context, attrs) {
|
||||
class BoundedGridLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0) : GridLayout(context, attrs, defStyleAttr) {
|
||||
private val boundedWidth: Int
|
||||
private val boundedHeight: Int
|
||||
|
||||
init {
|
||||
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedGridLayout)
|
||||
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedGridLayout, defStyleAttr, defStyleRes)
|
||||
boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_width, 0)
|
||||
boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_height, 0)
|
||||
arr.recycle()
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* 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.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.support.annotation.DrawableRes
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.graphics.drawable.Animatable2Compat
|
||||
import android.support.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import android.support.v7.widget.TooltipCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import java.util.*
|
||||
|
||||
class ServiceButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||
FloatingActionButton(context, attrs, defStyleAttr) {
|
||||
private val callback = object : Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable) {
|
||||
super.onAnimationEnd(drawable)
|
||||
var next = animationQueue.peek() ?: return
|
||||
if (next.current == drawable) {
|
||||
animationQueue.pop()
|
||||
next = animationQueue.peek() ?: return
|
||||
}
|
||||
setImageDrawable(next)
|
||||
next.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIcon(@DrawableRes resId: Int): AnimatedVectorDrawableCompat {
|
||||
val result = AnimatedVectorDrawableCompat.create(context, resId)!!
|
||||
result.registerAnimationCallback(callback)
|
||||
return result
|
||||
}
|
||||
|
||||
private val iconStopped by lazy { createIcon(R.drawable.ic_service_stopped) }
|
||||
private val iconConnecting by lazy { createIcon(R.drawable.ic_service_connecting) }
|
||||
private val iconConnected by lazy { createIcon(R.drawable.ic_service_connected) }
|
||||
private val iconStopping by lazy { createIcon(R.drawable.ic_service_stopping) }
|
||||
private val animationQueue = ArrayDeque<AnimatedVectorDrawableCompat>()
|
||||
|
||||
private var checked = false
|
||||
|
||||
override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
if (checked) View.mergeDrawableStates(drawableState, intArrayOf(android.R.attr.state_checked))
|
||||
return drawableState
|
||||
}
|
||||
|
||||
fun changeState(state: Int, animate: Boolean) {
|
||||
when (state) {
|
||||
BaseService.CONNECTING -> changeState(iconConnecting, animate)
|
||||
BaseService.CONNECTED -> changeState(iconConnected, animate)
|
||||
BaseService.STOPPING -> changeState(iconStopping, animate)
|
||||
else -> changeState(iconStopped, animate)
|
||||
}
|
||||
if (state == BaseService.CONNECTED) {
|
||||
checked = true
|
||||
TooltipCompat.setTooltipText(this, context.getString(R.string.stop))
|
||||
} else {
|
||||
checked = false
|
||||
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
|
||||
}
|
||||
refreshDrawableState()
|
||||
isEnabled = false
|
||||
if (state == BaseService.CONNECTED || state == BaseService.STOPPED) app.handler.postDelayed(
|
||||
{ isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED }, 1000)
|
||||
}
|
||||
|
||||
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
|
||||
a == iconStopped && b == iconConnecting ||
|
||||
a == iconConnecting && b == iconStopped ||
|
||||
a == iconConnected && b == iconStopping ||
|
||||
a == iconStopping && b == iconConnected
|
||||
|
||||
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
|
||||
if (animate) {
|
||||
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
|
||||
animationQueue.add(icon)
|
||||
if (animationQueue.size == 1) {
|
||||
setImageDrawable(icon)
|
||||
icon.start()
|
||||
}
|
||||
} else animationQueue.removeLast()
|
||||
} else {
|
||||
animationQueue.peekFirst()?.stop()
|
||||
animationQueue.clear()
|
||||
setImageDrawable(icon)
|
||||
icon.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/material_green_700" android:state_checked="true"/>
|
||||
<item android:color="@color/material_primary_500"/>
|
||||
</selector>
|
||||
@@ -5,9 +5,7 @@
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
|
||||
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 221 B |
|
After Width: | Height: | Size: 198 B |
|
After Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 175 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 298 B |
|
Before Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 257 B |
|
After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 284 B |
|
After Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 110 KiB |
@@ -5,9 +5,7 @@
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_busy"
|
||||
tools:ignore="NewApi">
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||
android:valueTo="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_idle"
|
||||
tools:ignore="NewApi">
|
||||
<target android:name="strike_thru_path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||
android:valueTo="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="strike_thru_mask">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
|
||||
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="strike_thru_path"
|
||||
android:pathData="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||
android:fillColor="#fff"
|
||||
android:strokeWidth="1"/>
|
||||
<clip-path
|
||||
android:name="strike_thru_mask"
|
||||
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"/>
|
||||
<path
|
||||
android:name="holey_icon"
|
||||
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_idle"
|
||||
tools:ignore="NewApi">
|
||||
<target android:name="strike_thru_path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
|
||||
android:valueTo="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="strike_thru_mask">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
|
||||
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_busy"
|
||||
tools:ignore="NewApi">
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="pathData"
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:valueFrom="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||
android:valueTo="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
|
||||
android:valueType="pathType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -1,24 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||
<path
|
||||
android:strokeColor="#fff"
|
||||
android:strokeWidth="2"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M0.71,2.12L21.88,23.29Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 13 10.17 L 16.58 7.16 L 13.57 10.75 L 14.99 12.16 L 17.68 8.96 L 16.59 13.76 L 18.26 15.43 L 21.25 2.28 L 10.45 7.62 L 13 10.17 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 15.93 15.93 L 12.71 14.89 L 13.7 13.7 L 12.28 12.28 L 9.26 15.89 L 17.55 18.55 L 17.74 17.74 L 15.93 15.93 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M8.56,8.56L0,12.8l6.83,2.57,4.64-3.9ZM6.44,13.08L5,12.55l5-2.47Z" />
|
||||
</vector>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:fontProviderAuthority="com.google.android.gms.fonts"
|
||||
app:fontProviderPackage="com.google.android.gms"
|
||||
app:fontProviderQuery="Iceland"
|
||||
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
|
||||
</font-family>
|
||||
@@ -6,6 +6,7 @@
|
||||
<Spinner android:id="@+id/template_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:labelFor="@+id/content"
|
||||
android:entries="@array/acl_rule_templates"/>
|
||||
<EditText android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/background_header"/>
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/drawer_title"
|
||||
<TextView android:id="@+id/drawer_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
android:ellipsize="marquee"
|
||||
android:paddingEnd="72dip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="RtlSymmetry"/>
|
||||
<com.github.shadowsocks.widget.BoundedGridLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:bounded_width="288dp"
|
||||
@@ -66,23 +67,25 @@
|
||||
android:layout_gravity="fill_horizontal" android:gravity="end"/>
|
||||
</com.github.shadowsocks.widget.BoundedGridLayout>
|
||||
</LinearLayout>
|
||||
<com.github.jorgecastilloprz.FABProgressCircle android:id="@+id/fabProgressCircle"
|
||||
app:reusable="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="6dp"
|
||||
android:layout_alignTop="@+id/stat"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="@dimen/fab_margin_top">
|
||||
<android.support.design.widget.FloatingActionButton android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="@color/material_primary_500"
|
||||
app:srcCompat="@drawable/ic_start_idle"
|
||||
app:pressedTranslationZ="6dp"
|
||||
app:borderWidth="0dp"/>
|
||||
</com.github.jorgecastilloprz.FABProgressCircle>
|
||||
|
||||
<!-- workaround for 0 elevation in disabled state -->
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignTop="@+id/stat"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="@dimen/fab_margin_top"
|
||||
android:elevation="6dp">
|
||||
<com.github.shadowsocks.widget.ServiceButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="@color/background_service"
|
||||
app:fabSize="normal"
|
||||
app:pressedTranslationZ="6dp"
|
||||
app:srcCompat="@drawable/ic_service_idle"/>
|
||||
</FrameLayout>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/snackbar"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/selection"
|
||||
android:title="@string/action_selection"
|
||||
android:icon="@drawable/abc_ic_menu_selectall_mtrl_alpha"
|
||||
android:alphabeticShortcut="s"
|
||||
android:numericShortcut="0"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<item
|
||||
android:id="@+id/selection"
|
||||
android:alphabeticShortcut="s"
|
||||
android:icon="?attr/actionModeSelectAllDrawable"
|
||||
android:numericShortcut="0"
|
||||
android:title="@string/action_selection"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item android:id="@+id/action_select_all"
|
||||
android:title="@android:string/selectAll"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_cut"
|
||||
android:title="@android:string/cut"
|
||||
android:alphabeticShortcut="t"
|
||||
android:numericShortcut="2"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_copy"
|
||||
android:title="@android:string/copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:numericShortcut="3"
|
||||
app:showAsAction="always"/>
|
||||
<item android:id="@+id/action_delete"
|
||||
android:title="@string/delete"
|
||||
android:alphabeticShortcut="d"
|
||||
android:numericShortcut="4"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_cut"
|
||||
android:alphabeticShortcut="t"
|
||||
android:numericShortcut="2"
|
||||
android:title="@android:string/cut"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:numericShortcut="3"
|
||||
android:title="@android:string/copy"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:alphabeticShortcut="d"
|
||||
android:numericShortcut="4"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/action_add_rule"
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
<dimen name="profile_item_max_width">480dp</dimen>
|
||||
<dimen name="qr_code_size">250dp</dimen>
|
||||
<dimen name="profile_padding">8dp</dimen>
|
||||
<!-- TODO: Remove when this issue is fixed: https://github.com/JorgeCastilloPrz/FABProgressCircle/issues/19 -->
|
||||
<dimen name="fab_margin_top">-44dp</dimen>
|
||||
</resources>
|
||||
|
||||