Compare commits

...
60 Commits
Author SHA1 Message Date
Max Lv 42bb5be390 Refine overture 2017-12-24 13:27:01 +08:00
Mygod 37c051c30a Fix #1515 2017-12-23 09:32:13 +08:00
Mygod 582a6e441b Revert "Donwload fonts through support library. #1515"
This reverts commit 6c89207eff.
2017-12-23 09:20:19 +08:00
Max Lv 6c89207eff Donwload fonts through support library. #1515 2017-12-22 00:29:31 -08:00
Mygod 0b2c9fbcfd Fix issues on Android 4.4 2017-12-20 10:24:35 -08:00
Mygod a742512892 Fix customRules not saved 2017-12-19 22:50:01 -08:00
Mygod 8b0c8168f8 Use JvmOverloads for BoundedGridLayout 2017-12-19 17:40:46 -08:00
Mygod 020a2a5d43 Refine "Catch IOExpections when reading ACLs"
This commit refines 37856ab4d2.
2017-12-19 17:36:12 -08:00
Mygod 7624f4ac52 Fix lint errors 2017-12-19 17:33:19 -08:00
Mygod ef519a8581 Clean up ServiceButton related code 2017-12-19 17:31:15 -08:00
Mygod 540b1d3710 Add some sweet animation to replace old circular progress 2017-12-19 17:01:11 -08:00
Mygod 6d6682d4a7 Merge paths in ic_start_* 2017-12-19 15:30:40 -08:00
Mygod 157a750e53 Remove the unmaintained fab progress circle 2017-12-19 15:24:39 -08:00
Mygod 6408450b31 Disable unavailable configurations for proxy mode
This prevents user confusion.
2017-12-19 13:45:30 -08:00
Mygod 723097fc8c Fix release build failing 2017-12-19 13:28:50 -08:00
Mygod 37ff7fddee Revert "Fix issues with proguard"
This reverts commit 2202041e80.
2017-12-19 13:28:44 -08:00
Mygod 536a1b4bbe Refrain from using private APIs for parseNumericAddress 2017-12-19 13:10:15 -08:00
Max Lv 2202041e80 Fix issues with proguard 2017-12-19 00:48:11 -08:00
Max Lv 37856ab4d2 Catch IOExpections when reading ACLs 2017-12-18 22:32:42 -08:00
Max Lv 3f30fc7b0c Bump version 2017-12-18 22:05:57 -08:00
Mygod f381ad6300 Fix crash when no profile is selected 2017-12-17 05:53:35 -08:00
Mygod b25e9a0905 Fix unit tests 2017-12-17 05:35:33 -08:00
Mygod 922eb8b202 Faster VPN connection and less wait
I just saved everyone ~0.9s every VPN connection.
2017-12-17 05:04:40 -08:00
Mygod 1010edc561 Speed up Acl.fromReader
It should run 9x faster than before now.

Past mistakes:

* InetAddress.parseNumericAddress throws TWO exceptions underthehood if the string passed is invalid. This caused a huge overhead;
* CharSequence.split in Kotlin is EXTREMELY SLOW.
2017-12-17 04:43:19 -08:00
Mygod bc22b0c481 Speed up AppManager
Also solves a cache problem.
2017-12-17 02:16:04 -08:00
Mygod 1bea09c5c6 Remove unnecessary call to linkToDeath 2017-12-17 01:36:20 -08:00
Mygod 5c252a2895 Fix incorrect back button color 2017-12-17 01:20:38 -08:00
Mygod 8ee9331222 Improve performance of reordering profiles 2017-12-17 00:21:31 -08:00
Mygod 870fd9e652 Move custom-rules.acl to PublicDatabase
Less files to backup. Less constants to keep track of.
2017-12-16 23:50:54 -08:00
Mygod 4e71722a75 Parse ACL files properly in overture 2017-12-16 23:42:52 -08:00
Mygod e5cd145b24 Remove unnecessary bypassHostnames in Acl
Fix #1505
2017-12-16 22:38:47 -08:00
Max Lv f89088ef34 Update GFWList 2017-12-17 13:36:10 +08:00
Max Lv 9b87d6a796 Refine ACL handlingn in overture 2017-12-17 13:12:38 +08:00
Max Lv 0ac113e7ef Refine regexp in GFWList 2017-12-17 10:12:54 +08:00
Mygod 11a3bade54 Add unit test for PluginOptions 2017-12-16 16:20:21 -08:00
Mygod 11dd7112c3 Implement plugin entirely in Kotlin 2017-12-16 16:07:01 -08:00
Max Lv 612a34a690 Merge pull request #1508 from Mygod/direct-boot
Implement direct boot awareness
2017-12-16 15:41:56 +08:00
Mygod f1934f876d Fix lint errors 2017-12-15 15:59:45 -08:00
Mygod 6eec78a343 Fix #1507 2017-12-15 15:43:39 -08:00
Mygod 32d0789c94 Update Kotlin 2017-12-15 15:43:00 -08:00
Mygod d7a11eaa3b Implement direct boot awareness
To do direct boot, important files need to be moved to device storage (i.e. not as safe storage). The only thing sensitive is user credentials (server settings in each profile) and the config file passed to libss-*.so. In order to implement direct boot, at least the active profile has to be moved/copied to device storage.

This commit keeps profile information (and temporary data used by ProfileConfigFragment) and Shadowsocks config in credential storage and move everything else to device storage. In order to make direct boot work, active profile is also copied into device storage and Shadowsocks config file is created in device storage during direct boot. Cleanup of those information is also implemented.

Fixes #1457.

NB:

* `android:directBootAware` can't be used on application element. That seems to be used by system apps.
* Works fine with multi-user;
* Old GSM files are not migrated (see App.kt) to device storage.
2017-12-15 15:36:19 -08:00
Mygod 14ac54c2de Add support for Auto Backup for Apps 2017-12-15 12:21:30 -08:00
Max Lv 8c28fffbdb Bump version 2017-12-15 01:41:49 -08:00
Max Lv 96c86b2441 Make QR code dialog public #1500 2017-12-14 20:04:37 -08:00
Mygod 15373f8d44 Add missing lint ignores 2017-12-14 15:34:45 -08:00
Mygod 30b56c97ea Add more tests, fix lint errors and add check to travis 2017-12-14 14:55:00 -08:00
Mygod d081d06903 Replace png with lossless webp 2017-12-14 14:53:11 -08:00
Mygod 090851ed3b Refine Travis cache 2017-12-14 13:07:01 -08:00
Mygod 9352f2dfe7 Fix SubnetTest 2017-12-14 12:41:09 -08:00
Mygod 9a2643ec2c Add missing error message 2017-12-14 12:00:13 -08:00
Mygod 5c2d548dee Reduce the number of default methods 2017-12-14 12:00:13 -08:00
Mygod 2fa8bc7f41 Refine #1500 2017-12-14 12:00:13 -08:00
Mygod 94527ea460 Refine #1497 2017-12-14 12:00:13 -08:00
Mygod 9259789fb9 Refine #1481 2017-12-14 12:00:13 -08:00
Mygod 8db642e721 Refine build.gradle format 2017-12-14 12:00:13 -08:00
Max Lv fca0e87b6c Update GFW List 2017-12-14 00:48:54 -08:00
Max Lv b88f836616 Refine #1489 2017-12-13 23:35:10 +08:00
Max Lv c3bbd09ef3 Fix #1500 2017-12-13 22:32:37 +08:00
Max Lv ea7ede033d Fix #1497 2017-12-13 02:06:20 -08:00
Max Lv 956443ef28 Fix the shortcut #1481 2017-12-12 23:45:22 -08:00
113 changed files with 1393 additions and 853 deletions
+7 -2
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
buildscript {
ext {
kotlinVersion = '1.2.0'
kotlinVersion = '1.2.10'
minSdkVersion = 19
sdkVersion = 27
buildToolsVersion = '27.0.2'
+2 -2
View File
@@ -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);
}
+2
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
APP_ABI := armeabi-v7a arm64-v8a x86
APP_PLATFORM := android-19
APP_STL := c++_static
NDK_TOOLCHAIN_VERSION := clang
+50 -30
View File
@@ -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);
}
}
+7 -10
View File
@@ -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"
+9
View File
@@ -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"))
}
}
+31 -14
View File
@@ -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>
+4 -5
View File
@@ -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$
+4 -5
View File
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

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>
+18
View File
@@ -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>
Binary file not shown.
-7
View File
@@ -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"
+1 -2
View File
@@ -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"
+21 -18
View File
@@ -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"
+32 -27
View File
@@ -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"
-1
View File
@@ -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>

Some files were not shown because too many files have changed in this diff Show More