Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
158afb1d9f | ||
|
|
085c6dbebf | ||
|
|
c401d7eb3a | ||
|
|
118cea9f7a | ||
|
|
5b1ae13faa | ||
|
|
b5f655beb5 | ||
|
|
9a3ed94fe0 | ||
|
|
0191d1d0fc | ||
|
|
6a56704f52 | ||
|
|
a95308d283 | ||
|
|
6acbae0c7e | ||
|
|
97d31cb927 | ||
|
|
7474269f6b | ||
|
|
40447ce1d2 | ||
|
|
4162057a22 | ||
|
|
0f24d7c0bc | ||
|
|
bbf614484d | ||
|
|
396d31a0a7 | ||
|
|
812dc6769a | ||
|
|
8feb587ebe | ||
|
|
89f073cc49 | ||
|
|
07ac8fb839 | ||
|
|
57b75ad971 | ||
|
|
1d6a0b0f2b | ||
|
|
abcbc98ce3 | ||
|
|
2722b1aa78 | ||
|
|
4bfc5ee070 | ||
|
|
c842f7fd41 | ||
|
|
c2f02994f8 | ||
|
|
54247d6b2b | ||
|
|
e7fbfd668d | ||
|
|
b6d0af1ec3 | ||
|
|
f58301b80b | ||
|
|
792870b289 | ||
|
|
3b6eff6d9d | ||
|
|
528fea136d | ||
|
|
5d7e88310b | ||
|
|
8c4508843b | ||
|
|
79acf14069 | ||
|
|
5f43500b9f | ||
|
|
8b4a8a92f2 | ||
|
|
7887d18f6f | ||
|
|
fb960d0664 | ||
|
|
9ed320d33b | ||
|
|
2599e88a81 | ||
|
|
ac0a8c7600 | ||
|
|
63fc69f495 | ||
|
|
5761962754 | ||
|
|
20506eeda8 | ||
|
|
dd63bda2cd | ||
|
|
e6d7a0855d | ||
|
|
4da89cd18e | ||
|
|
66250eb66b | ||
|
|
005c3443ae | ||
|
|
6b60f276b2 | ||
|
|
0edbe01237 | ||
|
|
c87882e5d4 | ||
|
|
8f80fbfd4a | ||
|
|
6ec8117e1c | ||
|
|
3c68a26794 | ||
|
|
fda89933d0 | ||
|
|
d8f721db3b | ||
|
|
28339ce7c1 | ||
|
|
edda3179dd | ||
|
|
1d79990656 | ||
|
|
2a47362195 | ||
|
|
02d5951f8b | ||
|
|
b126c62365 | ||
|
|
bbaf1779ea | ||
|
|
65ddd6f5d8 | ||
|
|
772bc18eb8 | ||
|
|
d503818169 | ||
|
|
b209e73386 | ||
|
|
6c381b0ecc | ||
|
|
e3f0fa6756 | ||
|
|
35cb273592 | ||
|
|
b76f410c34 | ||
|
|
d3678531b7 | ||
|
|
122ca599de | ||
|
|
96fcec788c | ||
|
|
1c67b39a3e | ||
|
|
30fea4a705 | ||
|
|
6df227d231 |
@@ -0,0 +1,37 @@
|
||||
Please read FAQ in wiki then answer these questions before submitting your issue. Thanks!
|
||||
|
||||
### Environment
|
||||
|
||||
* Android version: *e.g. "7.0.0_r14", more detailed description is preferred*
|
||||
* Device: *e.g. Google Pixel XL*
|
||||
* Shadowsocks version: *version code "v3.0.0" or commit ID "a073f85"*
|
||||
|
||||
### Configuration
|
||||
|
||||
_Put an `x` inside the [ ] that applies._
|
||||
|
||||
* [ ] IPv4 server address
|
||||
* [ ] IPv6 server address
|
||||
* [ ] Client IPv4 availability
|
||||
* [ ] Client IPv6 availability
|
||||
* Local port: 1080
|
||||
* Encrypt method:
|
||||
* [ ] One-time authentication
|
||||
* Route:
|
||||
* [ ] IPv6 route
|
||||
* [ ] Per-App Proxy
|
||||
* [ ] Bypass mode
|
||||
* [ ] UDP Forwarding
|
||||
* KCP Parameters: (leave blank if not enabled)
|
||||
* [ ] Auto Connect
|
||||
* [ ] TCP Fast Open
|
||||
* [ ] NAT mode
|
||||
|
||||
### What did you do?
|
||||
|
||||
|
||||
### What did you expect to see?
|
||||
|
||||
|
||||
### What did you see instead?
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
### Type of changes
|
||||
|
||||
_Put an `x` inside the [ ] that applies._
|
||||
|
||||
* [ ] Bugfix
|
||||
* [ ] New feature
|
||||
* [ ] Translation
|
||||
* [ ] General refinement
|
||||
* Other:
|
||||
|
||||
### Details
|
||||
|
||||
Please provide more details about changes if necessary.
|
||||
|
||||
@@ -15,6 +15,7 @@ gen_*
|
||||
*.o
|
||||
local.sbt
|
||||
.deps
|
||||
/proguard-sbt.txt
|
||||
|
||||
#Intellij IDEA
|
||||
*.iml
|
||||
|
||||
@@ -30,3 +30,6 @@
|
||||
[submodule "src/main/jni/mbedtls"]
|
||||
path = src/main/jni/mbedtls
|
||||
url = https://github.com/ARMmbed/mbedtls
|
||||
[submodule "src/main/jni/pcre"]
|
||||
path = src/main/jni/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
|
||||
@@ -49,7 +49,7 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
||||
### LICENSE
|
||||
|
||||
Copyright (C) 2016 by Max Lv <<max.c.lv@gmail.com>>
|
||||
Copyright (C) 2016 by Mygod Studio <<mygodstudio@gmail.com>>
|
||||
Copyright (C) 2016 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
|
||||
|
||||
@@ -22,7 +22,7 @@ shrinkResources in Android := true
|
||||
|
||||
typedResources in Android := false
|
||||
|
||||
resConfigs in Android := Seq("ru", "zh", "zh-rCN")
|
||||
resConfigs in Android := Seq("ru", "zh-rCN", "zh-rTW")
|
||||
|
||||
dexMaxHeap in Android := "4g"
|
||||
|
||||
@@ -31,22 +31,25 @@ resolvers += Resolver.jcenterRepo
|
||||
useSupportVectors
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"com.android.support" % "design" % "24.2.0",
|
||||
"com.android.support" % "gridlayout-v7" % "24.2.0",
|
||||
"com.android.support" % "design" % "24.2.1",
|
||||
"com.android.support" % "gridlayout-v7" % "24.2.1",
|
||||
"com.android.support" % "preference-v14" % "24.2.1",
|
||||
"com.evernote" % "android-job" % "1.1.0",
|
||||
"com.github.clans" % "fab" % "1.6.4",
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
|
||||
"com.github.kevinsawicki" % "http-request" % "6.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "9.4.0",
|
||||
"com.google.android.gms" % "play-services-analytics" % "9.4.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "9.6.1",
|
||||
"com.google.android.gms" % "play-services-analytics" % "9.6.1",
|
||||
"com.google.android.gms" % "play-services-gcm" % "9.6.1",
|
||||
"com.j256.ormlite" % "ormlite-android" % "5.0",
|
||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2",
|
||||
"dnsjava" % "dnsjava" % "2.1.7",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201607041850",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809",
|
||||
"me.dm7.barcodescanner" % "zxing" % "1.9",
|
||||
"net.glxn.qrgen" % "android" % "2.0"
|
||||
)
|
||||
|
||||
proguardVersion in Android := "5.2.1"
|
||||
proguardVersion in Android := "5.3"
|
||||
|
||||
proguardCache in Android := Seq()
|
||||
|
||||
|
||||
+1
-1
Submodule kcptun/kcptun updated: 91b2c650d1...ced94f7820
@@ -52,6 +52,7 @@ pushd kcptun/client
|
||||
|
||||
echo "Get dependences for kcptun"
|
||||
go get -u github.com/xtaci/kcp-go
|
||||
go get -u github.com/xtaci/smux
|
||||
go get
|
||||
|
||||
echo "Cross compile kcptun for arm"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.15")
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.18")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks"
|
||||
android:versionCode="150"
|
||||
android:versionName="3.0.8">
|
||||
android:versionCode="165"
|
||||
android:versionName="3.2.5">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
@@ -158,6 +158,18 @@
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- android-job components -->
|
||||
<service android:name="com.evernote.android.job.v21.PlatformJobService"
|
||||
android:process=":bg"/>
|
||||
<service android:name="com.evernote.android.job.v14.PlatformAlarmService"
|
||||
android:process=":bg"/>
|
||||
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
|
||||
android:process=":bg"/>
|
||||
<receiver android:name="com.evernote.android.job.v14.PlatformAlarmReceiver"
|
||||
android:process=":bg"/>
|
||||
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
||||
android:process=":bg"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.shadowsocks.aidl.IShadowsocksServiceCallback;
|
||||
|
||||
interface IShadowsocksService {
|
||||
int getState();
|
||||
String getProfileName();
|
||||
|
||||
oneway void registerCallback(IShadowsocksServiceCallback cb);
|
||||
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.github.shadowsocks.aidl;
|
||||
|
||||
interface IShadowsocksServiceCallback {
|
||||
oneway void stateChanged(int state, String msg);
|
||||
oneway void stateChanged(int state, String profileName, String msg);
|
||||
oneway void trafficUpdated(long txRate, long rxRate, long txTotal, long rxTotal);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/29
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.18.0.0/15
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/3
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -275,7 +275,7 @@ img {
|
||||
<body>
|
||||
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Scala.</p>
|
||||
<p>Copyright (C) 2016 by Max Lv <a href="mailto:max.c.lv@gmail.com">max.c.lv@gmail.com</a></p>
|
||||
<p>Copyright (C) 2016 by Mygod Studio <a href="mygodstudio@gmail.com">mygodstudio@gmail.com</a></p>
|
||||
<p>Copyright (C) 2016 by Mygod Studio <a href="contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
|
||||
<p>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
|
||||
|
||||
+54
-8
@@ -371,7 +371,9 @@ PDNSD_SOURCES := $(wildcard $(LOCAL_PATH)/pdnsd/src/*.c)
|
||||
LOCAL_MODULE := pdnsd
|
||||
LOCAL_SRC_FILES := $(PDNSD_SOURCES:$(LOCAL_PATH)/%=%)
|
||||
LOCAL_CFLAGS := -DANDROID -Wall -O2 -I$(LOCAL_PATH)/pdnsd \
|
||||
-I$(LOCAL_PATH)/include/pdnsd
|
||||
-I$(LOCAL_PATH)/include/pdnsd -I$(LOCAL_PATH)/libancillary
|
||||
LOCAL_STATIC_LIBRARIES := libancillary
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
include $(BUILD_EXECUTABLE)
|
||||
|
||||
@@ -381,25 +383,29 @@ include $(BUILD_EXECUTABLE)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
SHADOWSOCKS_SOURCES := local.c cache.c udprelay.c encrypt.c utils.c netutils.c json.c jconf.c acl.c android.c
|
||||
SHADOWSOCKS_SOURCES := local.c cache.c udprelay.c encrypt.c \
|
||||
utils.c netutils.c json.c jconf.c acl.c http.c tls.c rule.c \
|
||||
android.c
|
||||
|
||||
LOCAL_MODULE := ss-local
|
||||
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_LOCAL \
|
||||
-DUSE_CRYPTO_MBEDTLS -DANDROID -DHAVE_CONFIG_H \
|
||||
-DCONNECT_IN_PROGRESS=EINPROGRESS \
|
||||
-I$(LOCAL_PATH)/include/shadowsocks-libev \
|
||||
-I$(LOCAL_PATH)/include \
|
||||
-I$(LOCAL_PATH)/libancillary \
|
||||
-I$(LOCAL_PATH)/mbedtls/include \
|
||||
-I$(LOCAL_PATH)/pcre \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libudns \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libsodium/src/libsodium/include/sodium \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libipset/include \
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libev \
|
||||
-I$(LOCAL_PATH)/include/shadowsocks-libev
|
||||
-I$(LOCAL_PATH)/shadowsocks-libev/libev
|
||||
|
||||
LOCAL_STATIC_LIBRARIES := libev libmbedtls libipset libcork libudns libsodium libancillary
|
||||
LOCAL_STATIC_LIBRARIES := libev libmbedtls libipset libcork libudns \
|
||||
libsodium libancillary libpcre
|
||||
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
@@ -551,15 +557,55 @@ include $(BUILD_EXECUTABLE)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE:= mbedtls
|
||||
LOCAL_MODULE := mbedtls
|
||||
|
||||
LOCAL_C_INCLUDES:= $(LOCAL_PATH)/mbedtls/include
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/mbedtls/include
|
||||
|
||||
MBEDTLS_SOURCES := $(wildcard $(LOCAL_PATH)/mbedtls/library/*.c)
|
||||
MBEDTLS_SOURCES := $(wildcard $(LOCAL_PATH)/mbedtls/library/*.c)
|
||||
|
||||
LOCAL_SRC_FILES := $(MBEDTLS_SOURCES:$(LOCAL_PATH)/%=%)
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
########################################################
|
||||
## pcre
|
||||
########################################################
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := pcre
|
||||
|
||||
LOCAL_CFLAGS += -DHAVE_CONFIG_H
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/pcre/dist $(LOCAL_PATH)/pcre
|
||||
|
||||
libpcre_src_files := \
|
||||
pcre_chartables.c \
|
||||
dist/pcre_byte_order.c \
|
||||
dist/pcre_compile.c \
|
||||
dist/pcre_config.c \
|
||||
dist/pcre_dfa_exec.c \
|
||||
dist/pcre_exec.c \
|
||||
dist/pcre_fullinfo.c \
|
||||
dist/pcre_get.c \
|
||||
dist/pcre_globals.c \
|
||||
dist/pcre_jit_compile.c \
|
||||
dist/pcre_maketables.c \
|
||||
dist/pcre_newline.c \
|
||||
dist/pcre_ord2utf8.c \
|
||||
dist/pcre_refcount.c \
|
||||
dist/pcre_string_utils.c \
|
||||
dist/pcre_study.c \
|
||||
dist/pcre_tables.c \
|
||||
dist/pcre_ucd.c \
|
||||
dist/pcre_valid_utf8.c \
|
||||
dist/pcre_version.c \
|
||||
dist/pcre_xclass.c
|
||||
|
||||
LOCAL_SRC_FILES := $(addprefix pcre/, $(libpcre_src_files))
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
# Import cpufeatures
|
||||
$(call import-module,android/cpufeatures)
|
||||
|
||||
|
||||
@@ -417,3 +417,9 @@
|
||||
|
||||
/* Define as `fork' if `vfork' does not work. */
|
||||
/* #undef vfork */
|
||||
|
||||
/* Define to 1 if you have the <pcre.h> header file. */
|
||||
#define HAVE_PCRE_H 1
|
||||
|
||||
/* Define to 1 if you have the <pcre/pcre.h> header file. */
|
||||
/* #undef HAVE_PCRE_PCRE_H */
|
||||
|
||||
Submodule
+1
Submodule src/main/jni/pcre added at 222bbf4b3f
+1
-1
Submodule src/main/jni/pdnsd updated: 7a76962795...f6b0a56c74
Submodule src/main/jni/shadowsocks-libev updated: d31210c217...248fdcb610
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
|
||||
<item>
|
||||
<color android:color="@color/material_accent_300"/>
|
||||
<color android:color="@color/material_primary_300"/>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<color xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/material_accent_300"/>
|
||||
<color xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/material_primary_300"/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
@@ -11,4 +12,4 @@
|
||||
<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" />
|
||||
</vector>
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
@@ -11,4 +12,4 @@
|
||||
<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" />
|
||||
</vector>
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--suppress NewApi -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
@@ -22,4 +23,4 @@
|
||||
<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>
|
||||
</vector>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:menu_colorNormal="@color/material_accent_500"
|
||||
app:menu_colorPressed="@color/material_accent_700"
|
||||
app:menu_colorNormal="@color/material_primary_500"
|
||||
app:menu_colorPressed="@color/material_primary_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:layout_behavior="com.github.shadowsocks.widget.FloatingActionMenuBehavior">
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
android:id="@+id/fab_manual_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_700"
|
||||
app:fab_colorNormal="@color/material_primary_500"
|
||||
app:fab_colorPressed="@color/material_primary_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:fab_size="mini"
|
||||
app:fab_label="@string/add_profile_methods_manual_settings"
|
||||
@@ -36,8 +36,8 @@
|
||||
android:id="@+id/fab_qrcode_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_700"
|
||||
app:fab_colorNormal="@color/material_primary_500"
|
||||
app:fab_colorPressed="@color/material_primary_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:fab_size="mini"
|
||||
app:fab_label="@string/add_profile_methods_scan_qr_code"
|
||||
@@ -46,8 +46,8 @@
|
||||
android:id="@+id/fab_nfc_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_700"
|
||||
app:fab_colorNormal="@color/material_primary_500"
|
||||
app:fab_colorPressed="@color/material_primary_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:fab_size="mini"
|
||||
app:fab_label="NFC"
|
||||
@@ -56,8 +56,8 @@
|
||||
android:id="@+id/fab_import_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fab_colorNormal="@color/material_accent_500"
|
||||
app:fab_colorPressed="@color/material_accent_700"
|
||||
app:fab_colorNormal="@color/material_primary_500"
|
||||
app:fab_colorPressed="@color/material_primary_700"
|
||||
app:fab_colorRipple="@android:color/white"
|
||||
app:fab_size="mini"
|
||||
app:fab_label="@string/action_import"
|
||||
|
||||
@@ -24,13 +24,9 @@
|
||||
<string name="proxy_cat">Настройки Сервера</string>
|
||||
<string name="profile_name">Имя Профиля</string>
|
||||
<string name="proxy">Сервер</string>
|
||||
<string name="proxy_summary">Имя или IP удалённого сервера</string>
|
||||
<string name="remote_port">Удалённый порт</string>
|
||||
<string name="remote_port_summary">%d (номер порта удалённого сервера)</string>
|
||||
<string name="port">Локальный порт</string>
|
||||
<string name="port_summary">%d (номер порта локального сервера)</string>
|
||||
<string name="sitekey">Пароль</string>
|
||||
<string name="sitekey_summary">Пароль удалённого сервера</string>
|
||||
<string name="enc_method">Метод Шифрования</string>
|
||||
|
||||
<!-- feature category -->
|
||||
@@ -46,6 +42,14 @@
|
||||
<string name="auto_set_gfwlist">CHN Маршрут</string>
|
||||
<string name="auto_set_gfwlist_summary">Игнорировать все сайты находящиеся в Китае</string>
|
||||
<string name="route_list">Маршрут</string>
|
||||
<string-array name="route_entry">
|
||||
<item>Все</item>
|
||||
<item>Обход LAN</item>
|
||||
<item>Обходной Китай</item>
|
||||
<item>Обход LAN и Китай</item>
|
||||
<item>Список GFW</item>
|
||||
<item>Список Китай</item>
|
||||
</string-array>
|
||||
<string name="proxied_apps">Прокси для выбранных приложений</string>
|
||||
<string name="proxied_apps_summary">Установить прокси для выбранных приложений, требует режим NAT под Android 4.x</string>
|
||||
<string name="on">Вкл</string>
|
||||
@@ -141,5 +145,4 @@
|
||||
<string name="kcp_port">KCP порт</string>
|
||||
<string name="kcp_port_summary">%d (KCPTUN омер порта удалённого сервера)</string>
|
||||
<string name="kcpcli">KCP параметры</string>
|
||||
<string name="kcpcli_summary">Командная строка KCPTUN</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Material" parent="Theme.MaterialBase"/>
|
||||
</resources>
|
||||
@@ -9,13 +9,9 @@
|
||||
<string name="profile">配置文件</string>
|
||||
<string name="profile_summary">新建或切换到其他配置文件</string>
|
||||
<string name="proxy">服务器</string>
|
||||
<string name="proxy_summary">远程服务器的地址</string>
|
||||
<string name="remote_port">远程端口</string>
|
||||
<string name="remote_port_summary">%d(远程服务器的端口号)</string>
|
||||
<string name="port">本地端口</string>
|
||||
<string name="port_summary">%d(本地客户端的端口号)</string>
|
||||
<string name="sitekey">密码</string>
|
||||
<string name="sitekey_summary">远程服务器的密码</string>
|
||||
<string name="enc_method">加密方法</string>
|
||||
<string name="feature_cat">功能设置</string>
|
||||
<string name="http_proxy">HTTP 代理服务器</string>
|
||||
@@ -110,6 +106,8 @@
|
||||
<item>绕过局域网地址</item>
|
||||
<item>绕过中国大陆地址</item>
|
||||
<item>绕过局域网及中国大陆地址</item>
|
||||
<item>仅代理中国大陆无法访问的地址</item>
|
||||
<item>仅代理中国大陆地址</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_apply_all">应用设置到所有配置文件</string>
|
||||
@@ -140,6 +138,5 @@
|
||||
<string name="kcp_port">KCP 端口</string>
|
||||
<string name="kcp_port_summary">%d(远端 KCPTUN 服务器的端口号)</string>
|
||||
<string name="kcpcli">KCP 参数</string>
|
||||
<string name="kcpcli_summary">KCPTUN 的命令行参数</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -10,13 +10,9 @@
|
||||
<string name="profile">設定檔</string>
|
||||
<string name="profile_summary">切換至其他設定檔或新增新設定檔</string>
|
||||
<string name="proxy">伺服器</string>
|
||||
<string name="proxy_summary">遠端伺服器的 IP 位址或主機名稱</string>
|
||||
<string name="remote_port">遠端連接埠</string>
|
||||
<string name="remote_port_summary">%d(遠端伺服器的連接埠號碼)</string>
|
||||
<string name="port">連接埠號碼</string>
|
||||
<string name="port_summary">%d(本機伺服器的連接埠號碼)</string>
|
||||
<string name="sitekey">密碼</string>
|
||||
<string name="sitekey_summary">遠端伺服器的密碼</string>
|
||||
<string name="enc_method">加密方法</string>
|
||||
<string name="feature_cat">功能設定</string>
|
||||
<string name="http_proxy">HTTP Proxy</string>
|
||||
@@ -112,6 +108,8 @@
|
||||
<item>略過區域網路位址</item>
|
||||
<item>略過中國大陸位址</item>
|
||||
<item>略過區域網路及中國大陸位址</item>
|
||||
<item>僅代理大陸屏蔽位址</item>
|
||||
<item>僅代理大陸位址</item>
|
||||
</string-array>
|
||||
|
||||
<string name="action_apply_all">應用設置到所有配置文件</string>
|
||||
@@ -142,6 +140,5 @@
|
||||
<string name="kcp_port">KCP 連接埠</string>
|
||||
<string name="kcp_port_summary">%d(遠端 KCPTUN 伺服器的連接埠號碼)</string>
|
||||
<string name="kcpcli">KCP 參數</string>
|
||||
<string name="kcpcli_summary">KCPTUN 的命令行參數</string>
|
||||
|
||||
</resources>
|
||||
@@ -38,18 +38,13 @@
|
||||
<item>chacha20-ietf</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="route_entry">
|
||||
<item>All</item>
|
||||
<item>Bypass LAN</item>
|
||||
<item>Bypass China</item>
|
||||
<item>Bypass LAN & China</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="route_value">
|
||||
<item>all</item>
|
||||
<item>bypass-lan</item>
|
||||
<item>bypass-china</item>
|
||||
<item>bypass-lan-china</item>
|
||||
<item>gfwlist</item>
|
||||
<item>china-list</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="private_route">
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="DropDownPreference">
|
||||
<attr name="android:entries" />
|
||||
<attr name="android:entryValues" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="NumberPickerPreference">
|
||||
<attr name="max" format="integer" />
|
||||
<attr name="min" format="integer" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,9 @@
|
||||
<color name="material_blue_grey_300">#90A4AE</color>
|
||||
<color name="material_blue_grey_500">#607D8B</color>
|
||||
<color name="material_blue_grey_700">#455A64</color>
|
||||
<color name="material_accent_300">@color/material_blue_grey_300</color>
|
||||
<color name="material_accent_500">@color/material_blue_grey_500</color>
|
||||
<color name="material_accent_700">@color/material_blue_grey_700</color>
|
||||
<color name="material_primary_300">@color/material_blue_grey_300</color>
|
||||
<color name="material_primary_500">@color/material_blue_grey_500</color>
|
||||
<color name="material_primary_700">@color/material_blue_grey_700</color>
|
||||
<color name="material_accent_200">@color/material_blue_grey_500</color>
|
||||
|
||||
</resources>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -29,13 +29,9 @@
|
||||
<string name="proxy_cat">Server Settings</string>
|
||||
<string name="profile_name">Profile Name</string>
|
||||
<string name="proxy">Server</string>
|
||||
<string name="proxy_summary">Hostname or IP of the remote server</string>
|
||||
<string name="remote_port">Remote Port</string>
|
||||
<string name="remote_port_summary">%d (port number of the remote server)</string>
|
||||
<string name="port">Local Port</string>
|
||||
<string name="port_summary">%d (port number of the local server)</string>
|
||||
<string name="sitekey">Password</string>
|
||||
<string name="sitekey_summary">Password of the remote server</string>
|
||||
<string name="enc_method">Encrypt Method</string>
|
||||
|
||||
<!-- feature category -->
|
||||
@@ -52,6 +48,14 @@
|
||||
<string name="auto_set_gfwlist_summary">Bypass all sites located in China
|
||||
</string>
|
||||
<string name="route_list">Route</string>
|
||||
<string-array name="route_entry">
|
||||
<item>All</item>
|
||||
<item>Bypass LAN</item>
|
||||
<item>Bypass China</item>
|
||||
<item>Bypass LAN & China</item>
|
||||
<item>GFW List</item>
|
||||
<item>China List</item>
|
||||
</string-array>
|
||||
<string name="proxied_apps">Per-App Proxy</string>
|
||||
<string name="proxied_apps_summary">Set proxy for selected apps, requires NAT mode under Android 4.x</string>
|
||||
<string name="on">On</string>
|
||||
@@ -147,5 +151,4 @@
|
||||
<string name="kcp_port">KCP Port</string>
|
||||
<string name="kcp_port_summary">%d (port number of the remote KCPTUN server)</string>
|
||||
<string name="kcpcli">KCP Parameters (CLI)</string>
|
||||
<string name="kcpcli_summary">The command line of KCPTUN</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,19 +6,46 @@
|
||||
<item name="android:textSize">24sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:navigationBarColor">@color/material_accent_700</item>
|
||||
<item name="colorAccent">@color/material_accent_500</item>
|
||||
<item name="colorButtonNormal">@color/material_accent_500</item>
|
||||
<item name="colorPrimary">@color/material_accent_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_accent_700</item>
|
||||
<style name="Theme.MaterialBase" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!--suppress NewApi -->
|
||||
<item name="android:navigationBarColor">@color/material_primary_700</item>
|
||||
<item name="colorAccent">@color/material_primary_500</item>
|
||||
<item name="colorButtonNormal">@color/material_primary_500</item>
|
||||
<item name="colorPrimary">@color/material_primary_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_primary_700</item>
|
||||
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Material.Dialog" parent="Theme.AppCompat.Light.Dialog">
|
||||
<item name="colorAccent">@color/material_accent_500</item>
|
||||
<item name="colorPrimary">@color/material_accent_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_accent_700</item>
|
||||
<item name="colorAccent">@color/material_primary_500</item>
|
||||
<item name="colorButtonNormal">@color/material_accent_200</item>
|
||||
<item name="colorPrimary">@color/material_primary_500</item>
|
||||
<item name="colorPrimaryDark">@color/material_primary_700</item>
|
||||
<item name="preferenceTheme">@style/Theme.Material.PreferenceThemeOverlay</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Fix for wrong padding on v17-v20 based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/blob/a6082cb/preference-v7/src/main/res/values-v17/styles.xml -->
|
||||
<style name="Theme.Material" parent="Theme.MaterialBase">
|
||||
<item name="android:listPreferredItemPaddingStart">16dp</item>
|
||||
<item name="android:listPreferredItemPaddingEnd">16dp</item>
|
||||
</style>
|
||||
<!-- This is needed because the default PreferenceCategory appearance doesn't use bold text style -->
|
||||
<style name="Preference_TextAppearanceMaterialBody2">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
<style name="Theme.Material.PreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay.v14.Material">
|
||||
<item name="preferenceFragmentListStyle">@style/Theme.Material.PreferenceFragmentList</item>
|
||||
</style>
|
||||
<style name="Theme.Material.PreferenceFragmentList" parent="@style/PreferenceFragmentList.Material">
|
||||
<item name="android:fadeScrollbars">true</item>
|
||||
<item name="android:scrollbars">vertical</item>
|
||||
</style>
|
||||
<style name="NumberPickerStyle">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -2,39 +2,40 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res/com.github.shadowsocks">
|
||||
|
||||
<PreferenceCategory
|
||||
<be.mygod.preference.PreferenceCategory
|
||||
android:title="@string/proxy_cat">
|
||||
|
||||
<com.github.shadowsocks.preferences.SummaryEditTextPreference
|
||||
<be.mygod.preference.EditTextPreference
|
||||
android:key="profileName"
|
||||
android:persistent="false"
|
||||
android:summary="%s"
|
||||
android:title="@string/profile_name"/>
|
||||
<com.github.shadowsocks.preferences.SummaryEditTextPreference
|
||||
<be.mygod.preference.EditTextPreference
|
||||
android:key="proxy"
|
||||
android:persistent="false"
|
||||
android:summary="@string/proxy_summary"
|
||||
android:summary="%s"
|
||||
android:title="@string/proxy"/>
|
||||
<com.github.shadowsocks.preferences.NumberPickerPreference
|
||||
<be.mygod.preference.NumberPickerPreference
|
||||
app:min="1"
|
||||
app:max="65535"
|
||||
android:key="remotePortNum"
|
||||
android:persistent="false"
|
||||
android:summary="@string/remote_port_summary"
|
||||
android:summary="%d"
|
||||
android:title="@string/remote_port"/>
|
||||
<com.github.shadowsocks.preferences.NumberPickerPreference
|
||||
<be.mygod.preference.NumberPickerPreference
|
||||
app:min="1025"
|
||||
app:max="65535"
|
||||
android:key="localPortNum"
|
||||
android:persistent="false"
|
||||
android:summary="@string/port_summary"
|
||||
android:summary="%d"
|
||||
android:title="@string/port"/>
|
||||
<com.github.shadowsocks.preferences.PasswordEditTextPreference
|
||||
<be.mygod.preference.EditTextPreference
|
||||
android:inputType="textPassword"
|
||||
android:key="sitekey"
|
||||
android:persistent="false"
|
||||
android:summary="@string/sitekey_summary"
|
||||
android:summary="%s"
|
||||
android:title="@string/sitekey"/>
|
||||
<com.github.shadowsocks.preferences.DropDownPreference
|
||||
<DropDownPreference
|
||||
android:key="encMethod"
|
||||
android:persistent="false"
|
||||
android:entries="@array/enc_method_entry"
|
||||
@@ -47,12 +48,12 @@
|
||||
android:summary="@string/onetime_auth_summary"
|
||||
android:title="@string/onetime_auth"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
</be.mygod.preference.PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
<be.mygod.preference.PreferenceCategory
|
||||
android:title="@string/feature_cat">
|
||||
|
||||
<com.github.shadowsocks.preferences.DropDownPreference
|
||||
<DropDownPreference
|
||||
android:key="route"
|
||||
android:persistent="false"
|
||||
android:entries="@array/route_entry"
|
||||
@@ -75,9 +76,9 @@
|
||||
android:summary="@string/udp_dns_summary"
|
||||
android:title="@string/udp_dns"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
</be.mygod.preference.PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
<be.mygod.preference.PreferenceCategory
|
||||
android:title="@string/kcp">
|
||||
|
||||
<SwitchPreference
|
||||
@@ -86,7 +87,7 @@
|
||||
android:summary="@string/kcp_summary"
|
||||
android:title="@string/kcp"/>
|
||||
|
||||
<com.github.shadowsocks.preferences.NumberPickerPreference
|
||||
<be.mygod.preference.NumberPickerPreference
|
||||
app:min="1"
|
||||
app:max="65535"
|
||||
android:key="kcpPort"
|
||||
@@ -94,15 +95,15 @@
|
||||
android:summary="@string/kcp_port_summary"
|
||||
android:title="@string/kcp_port"/>
|
||||
|
||||
<com.github.shadowsocks.preferences.KcpCliPreference
|
||||
<be.mygod.preference.EditTextPreference
|
||||
android:key="kcpcli"
|
||||
android:persistent="false"
|
||||
android:summary="@string/kcpcli_summary"
|
||||
android:summary="%s"
|
||||
android:title="@string/kcpcli"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
</be.mygod.preference.PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/misc_cat">
|
||||
<be.mygod.preference.PreferenceCategory android:title="@string/misc_cat">
|
||||
|
||||
<SwitchPreference
|
||||
android:key="isAutoConnect"
|
||||
@@ -120,12 +121,9 @@
|
||||
android:summary="@string/nat_summary"/>
|
||||
<Preference android:key="recovery" android:title="@string/recovery"
|
||||
android:summary="@string/recovery_summary"/>
|
||||
<Preference android:key="flush_dnscache"
|
||||
android:title="@string/flush_dnscache"
|
||||
android:summary="@string/flush_dnscache_summary"/>
|
||||
<Preference android:key="about" android:title="@string/about"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
</be.mygod.preference.PreferenceCategory>
|
||||
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.{EditTextPreference => Parent}
|
||||
import android.support.v7.widget.AppCompatEditText
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
|
||||
/**
|
||||
* Fixed EditTextPreference + SummaryPreference with password support!
|
||||
* Based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/tree/master/app/src/main/java/android/support/v7/preference
|
||||
*/
|
||||
class EditTextPreference(context: Context, attrs: AttributeSet = null) extends Parent(context, attrs)
|
||||
with SummaryPreference {
|
||||
val editText = new AppCompatEditText(context, attrs)
|
||||
editText.setId(android.R.id.edit)
|
||||
|
||||
override protected def getSummaryValue = {
|
||||
var text = getText
|
||||
if (text == null) text = ""
|
||||
val inputType = editText.getInputType
|
||||
if (inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) ||
|
||||
inputType == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) ||
|
||||
inputType == (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD))
|
||||
"\u2022" * text.length else text
|
||||
}
|
||||
|
||||
override def setText(text: String) = {
|
||||
super.setText(text)
|
||||
notifyChanged
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v14.preference.PreferenceDialogFragment
|
||||
import android.view.{View, ViewGroup}
|
||||
|
||||
class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
|
||||
private lazy val preference = getPreference.asInstanceOf[EditTextPreference]
|
||||
private lazy val editText = preference.editText
|
||||
|
||||
override protected def onCreateDialogView(context: Context) = {
|
||||
val parent = editText.getParent.asInstanceOf[ViewGroup]
|
||||
if (parent != null) parent.removeView(editText)
|
||||
editText
|
||||
}
|
||||
|
||||
override protected def onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
editText.setText(preference.getText)
|
||||
val text = editText.getText
|
||||
if (text != null) editText.setSelection(0, text.length)
|
||||
}
|
||||
|
||||
override protected def needInputMethod = true
|
||||
|
||||
def onDialogClosed(positiveResult: Boolean) = if (positiveResult) {
|
||||
val value = editText.getText.toString
|
||||
if (preference.callChangeListener(value)) preference.setText(value)
|
||||
}
|
||||
}
|
||||
+8
-32
@@ -1,20 +1,16 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
package be.mygod.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.os.Bundle
|
||||
import android.preference.DialogPreference
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import android.view.{ViewGroup, WindowManager}
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.widget.NumberPicker
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
|
||||
class NumberPickerPreference(private val context: Context, attrs: AttributeSet = null)
|
||||
extends DialogPreference(context, attrs) with SummaryPreference {
|
||||
private val picker = new NumberPicker(context)
|
||||
private[preference] val picker = new NumberPicker(new ContextThemeWrapper(context, R.style.NumberPickerStyle))
|
||||
private var value: Int = _
|
||||
|
||||
{
|
||||
@@ -28,7 +24,7 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
|
||||
def getMin = if (picker == null) 0 else picker.getMinValue
|
||||
def getMax = picker.getMaxValue
|
||||
def setValue(i: Int) {
|
||||
if (i == getValue) return
|
||||
if (i == value) return
|
||||
picker.setValue(i)
|
||||
value = picker.getValue
|
||||
persistInt(value)
|
||||
@@ -37,28 +33,8 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
|
||||
def setMin(value: Int) = picker.setMinValue(value)
|
||||
def setMax(value: Int) = picker.setMaxValue(value)
|
||||
|
||||
override protected def showDialog(state: Bundle) {
|
||||
super.showDialog(state)
|
||||
getDialog.getWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
override protected def onCreateDialogView = {
|
||||
val parent = picker.getParent.asInstanceOf[ViewGroup]
|
||||
if (parent != null) parent.removeView(picker)
|
||||
picker
|
||||
}
|
||||
override protected def onDialogClosed(positiveResult: Boolean) {
|
||||
picker.clearFocus // commit changes
|
||||
super.onDialogClosed(positiveResult) // forward compatibility
|
||||
if (positiveResult) {
|
||||
val value = picker.getValue
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value)
|
||||
return
|
||||
}
|
||||
}
|
||||
picker.setValue(value)
|
||||
}
|
||||
override protected def onGetDefaultValue(a: TypedArray, index: Int) = a.getInt(index, getMin).asInstanceOf[AnyRef]
|
||||
override protected def onGetDefaultValue(a: TypedArray, index: Int): AnyRef =
|
||||
a.getInt(index, getMin).asInstanceOf[AnyRef]
|
||||
override protected def onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any) {
|
||||
val default = defaultValue.asInstanceOf[Int]
|
||||
setValue(if (restorePersistedValue) getPersistedInt(default) else default)
|
||||
@@ -0,0 +1,36 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v14.preference.PreferenceDialogFragment
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.{View, ViewGroup}
|
||||
|
||||
class NumberPickerPreferenceDialogFragment extends PreferenceDialogFragment {
|
||||
private lazy val preference = getPreference.asInstanceOf[NumberPickerPreference]
|
||||
private lazy val picker = preference.picker
|
||||
private lazy val inputMethodManager =
|
||||
getActivity.getSystemService(Context.INPUT_METHOD_SERVICE).asInstanceOf[InputMethodManager]
|
||||
|
||||
override protected def onCreateDialogView(context: Context) = {
|
||||
val parent = picker.getParent.asInstanceOf[ViewGroup]
|
||||
if (parent != null) parent.removeView(picker)
|
||||
picker
|
||||
}
|
||||
|
||||
override protected def onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
picker.setValue(preference.getValue)
|
||||
}
|
||||
|
||||
override protected def needInputMethod = true
|
||||
|
||||
def onDialogClosed(positiveResult: Boolean) {
|
||||
picker.clearFocus // commit changes
|
||||
if (positiveResult) {
|
||||
val value = picker.getValue
|
||||
if (preference.callChangeListener(value)) preference.setValue(value)
|
||||
}
|
||||
inputMethodManager.hideSoftInputFromWindow(getActivity.getCurrentFocus.getWindowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.{PreferenceViewHolder, PreferenceCategory => Base}
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
|
||||
/**
|
||||
* Based on: https://github.com/Gericop/Android-Support-Preference-V7-Fix/blob/4c2bb2896895dbedb37b659b36bd2a96b33c1605/preference-v7/src/main/java/com/takisoft/fix/support/v7/preference/PreferenceCategory.java
|
||||
*
|
||||
* @author Mygod
|
||||
*/
|
||||
class PreferenceCategory(context: Context, attrs: AttributeSet = null) extends Base(context, attrs) {
|
||||
override def onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
holder.findViewById(android.R.id.title).getLayoutParams.asInstanceOf[MarginLayoutParams].bottomMargin = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.app.DialogFragment
|
||||
import android.os.Bundle
|
||||
import android.support.v14.preference.{PreferenceFragment => Base}
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import android.view.{LayoutInflater, ViewGroup}
|
||||
|
||||
abstract class PreferenceFragment extends Base {
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle) =
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
protected final def displayPreferenceDialog(key: String, fragment: DialogFragment) {
|
||||
val bundle = new Bundle(1)
|
||||
bundle.putString("key", key)
|
||||
fragment.setArguments(bundle)
|
||||
fragment.setTargetFragment(this, 0)
|
||||
fragment.show(getFragmentManager, "android.support.v14.preference.PreferenceFragment.DIALOG")
|
||||
}
|
||||
|
||||
override protected def onCreateAdapter(screen: PreferenceScreen) = new PreferenceGroupAdapter(screen)
|
||||
|
||||
override def onResume {
|
||||
super.onResume
|
||||
getListView.scrollBy(0, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.util.List
|
||||
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v7.preference.{PreferenceGroup, PreferenceViewHolder, PreferenceGroupAdapter => Old}
|
||||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* Fix by: https://github.com/Gericop/Android-Support-Preference-V7-Fix/commit/7de016b007e28a264001a8bb353f110a7f64bb69
|
||||
*
|
||||
* @author Mygod
|
||||
*/
|
||||
object PreferenceGroupAdapter {
|
||||
private var preferenceLayoutsField: Field = _
|
||||
private var fieldResId: Field = _
|
||||
private var fieldWidgetResId: Field = _
|
||||
private val preferenceViewHolderConstructor = classOf[PreferenceViewHolder].getDeclaredConstructor(classOf[View])
|
||||
|
||||
{
|
||||
val oldClass = classOf[Old]
|
||||
preferenceLayoutsField = oldClass.getDeclaredField("mPreferenceLayouts")
|
||||
preferenceLayoutsField.setAccessible(true)
|
||||
val c = oldClass.getDeclaredClasses.filter(c => c.getSimpleName == "PreferenceLayout").head
|
||||
fieldResId = c.getDeclaredField("resId")
|
||||
fieldResId.setAccessible(true)
|
||||
fieldWidgetResId = c.getDeclaredField("widgetResId")
|
||||
fieldWidgetResId.setAccessible(true)
|
||||
preferenceViewHolderConstructor.setAccessible(true)
|
||||
}
|
||||
}
|
||||
|
||||
class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
|
||||
import PreferenceGroupAdapter._
|
||||
|
||||
protected lazy val preferenceLayouts = preferenceLayoutsField.get(this).asInstanceOf[List[AnyRef]]
|
||||
|
||||
override def onCreateViewHolder(parent: ViewGroup, viewType: Int) = if (Build.VERSION.SDK_INT < 21) {
|
||||
val context = parent.getContext
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val pl = preferenceLayouts.get(viewType)
|
||||
val view = inflater.inflate(fieldResId.get(pl).asInstanceOf[Int], parent, false)
|
||||
if (view.getBackground == null) {
|
||||
val array = context.obtainStyledAttributes(null, R.styleable.BackgroundStyle)
|
||||
var background = array.getDrawable(R.styleable.BackgroundStyle_android_selectableItemBackground)
|
||||
if (background == null)
|
||||
background = ContextCompat.getDrawable(context, android.R.drawable.list_selector_background)
|
||||
array.recycle
|
||||
val (s, t, e, b) = (ViewCompat.getPaddingStart(view), view.getPaddingTop,
|
||||
ViewCompat.getPaddingEnd(view), view.getPaddingBottom)
|
||||
view.setBackground(background)
|
||||
ViewCompat.setPaddingRelative(view, s, t, e, b)
|
||||
}
|
||||
val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup]
|
||||
if (widgetFrame != null) {
|
||||
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
|
||||
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
|
||||
}
|
||||
preferenceViewHolderConstructor.newInstance(view)
|
||||
} else super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package be.mygod.preference
|
||||
|
||||
import android.support.v7.preference.Preference
|
||||
|
||||
/**
|
||||
* Make your preference support %s in summary. Override getSummaryValue to customize what to put in.
|
||||
* Based on:
|
||||
* https://github.com/android/platform_frameworks_base/blob/master/core/java/android/preference/ListPreference.java
|
||||
*
|
||||
* @author Mygod
|
||||
*/
|
||||
trait SummaryPreference extends Preference {
|
||||
protected def getSummaryValue: AnyRef
|
||||
|
||||
/**
|
||||
* Returns the summary of this SummaryPreference. If the summary has a String formatting marker in it
|
||||
* (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place.
|
||||
*
|
||||
* @return the summary with appropriate string substitution
|
||||
*/
|
||||
override def getSummary = {
|
||||
val summary = super.getSummary
|
||||
if (summary == null) null else String.format(summary.toString, getSummaryValue)
|
||||
}
|
||||
}
|
||||
@@ -46,14 +46,13 @@ import android.content._
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.view._
|
||||
import android.widget._
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.mutable
|
||||
@@ -143,7 +142,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
private var loadingView: View = _
|
||||
private val appsLoading = new AtomicBoolean
|
||||
private var handler: Handler = _
|
||||
private val profile = app.currentProfile.get
|
||||
private val profile = app.currentProfile.orNull
|
||||
|
||||
private def initProxiedApps(str: String = profile.individual) = proxiedApps = str.split('\n').to[mutable.HashSet]
|
||||
|
||||
@@ -213,6 +212,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
protected override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (profile == null) finish()
|
||||
|
||||
handler = new Handler()
|
||||
|
||||
this.setContentView(R.layout.layout_apps)
|
||||
|
||||
@@ -50,10 +50,10 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.kevinsawicki.http.HttpRequest
|
||||
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
trait BaseService extends Service {
|
||||
|
||||
@@ -62,6 +62,7 @@ trait BaseService extends Service {
|
||||
|
||||
case class NameNotResolvedException() extends IOException
|
||||
case class KcpcliParseException(cause: Throwable) extends Exception(cause)
|
||||
case class NullConnectionException() extends NullPointerException
|
||||
|
||||
var timer: Timer = _
|
||||
var trafficMonitorThread: TrafficMonitorThread = _
|
||||
@@ -69,6 +70,8 @@ trait BaseService extends Service {
|
||||
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
|
||||
var callbacksCount: Int = _
|
||||
lazy val handler = new Handler(getMainLooper)
|
||||
lazy val restartHanlder = new Handler(getMainLooper)
|
||||
lazy val protectPath = getApplicationInfo.dataDir + "/protect_path"
|
||||
|
||||
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => {
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
@@ -82,7 +85,10 @@ trait BaseService extends Service {
|
||||
val activeNetwork = cm.getActiveNetworkInfo()
|
||||
val isConnected = activeNetwork != null && activeNetwork.isConnected()
|
||||
|
||||
if (isConnected && profile.kcp && kcptunProcess != null) kcptunProcess.restart()
|
||||
if (isConnected && profile.kcp && kcptunProcess != null) {
|
||||
restartHanlder.removeCallbacks(null)
|
||||
restartHanlder.postDelayed(() => kcptunProcess.restart(), 2000)
|
||||
}
|
||||
}
|
||||
var networkReceiverRegistered: Boolean = _
|
||||
|
||||
@@ -91,6 +97,8 @@ trait BaseService extends Service {
|
||||
state
|
||||
}
|
||||
|
||||
override def getProfileName: String = if (profile == null) null else profile.name
|
||||
|
||||
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
|
||||
if (cb != null && callbacks.unregister(cb)) {
|
||||
callbacksCount -= 1
|
||||
@@ -134,8 +142,7 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
def checkProfile(profile: Profile) = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
||||
changeState(State.STOPPED, getString(R.string.proxy_empty))
|
||||
stopRunner(true)
|
||||
stopRunner(true, getString(R.string.proxy_empty))
|
||||
false
|
||||
} else true
|
||||
|
||||
@@ -193,6 +200,7 @@ trait BaseService extends Service {
|
||||
case _: NameNotResolvedException => stopRunner(true, getString(R.string.invalid_server))
|
||||
case exc: KcpcliParseException =>
|
||||
stopRunner(true, getString(R.string.service_failed) + ": " + exc.cause.getMessage)
|
||||
case _: NullConnectionException => stopRunner(true, getString(R.string.reboot_required))
|
||||
case exc: Throwable =>
|
||||
stopRunner(true, getString(R.string.service_failed) + ": " + exc.getMessage)
|
||||
exc.printStackTrace()
|
||||
@@ -270,6 +278,7 @@ trait BaseService extends Service {
|
||||
override def onCreate() {
|
||||
super.onCreate()
|
||||
app.refreshContainerHolder
|
||||
app.updateAssets()
|
||||
}
|
||||
|
||||
// Service of shadowsocks should always be started explicitly
|
||||
@@ -282,7 +291,7 @@ trait BaseService extends Service {
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i <- 0 until n) {
|
||||
try {
|
||||
callbacks.getBroadcastItem(i).stateChanged(s, msg)
|
||||
callbacks.getBroadcastItem(i).stateChanged(s, binder.getProfileName, msg)
|
||||
} catch {
|
||||
case _: Exception => // Ignore
|
||||
}
|
||||
@@ -297,10 +306,11 @@ trait BaseService extends Service {
|
||||
val default = getString(R.string.black_list)
|
||||
try {
|
||||
val container = app.containerHolder.getContainer
|
||||
val update = container.getString("black_list")
|
||||
if (update == null || update.isEmpty) default else update
|
||||
val update = container.getString("black_list_lite")
|
||||
val list = if (update == null || update.isEmpty) default else update
|
||||
"exclude = " + list + ";"
|
||||
} catch {
|
||||
case ex: Exception => default
|
||||
case ex: Exception => "exclude = " + default + ";"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,7 @@ object BootReceiver {
|
||||
}
|
||||
|
||||
class BootReceiver extends BroadcastReceiver {
|
||||
def onReceive(context: Context, intent: Intent) = Utils.startSsService(context)
|
||||
def onReceive(context: Context, intent: Intent) = {
|
||||
Utils.startSsService(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,14 +97,17 @@ class GuardedProcess(cmd: Seq[String]) {
|
||||
semaphore.release
|
||||
process.waitFor
|
||||
|
||||
if (isRestart) {
|
||||
isRestart = false
|
||||
} else {
|
||||
if (currentTimeMillis - startTime < 1000) {
|
||||
Log.w(TAG, "process exit too fast, stop guard: " + cmd)
|
||||
isDestroyed = true
|
||||
this.synchronized {
|
||||
if (isRestart) {
|
||||
isRestart = false
|
||||
} else {
|
||||
if (currentTimeMillis - startTime < 1000) {
|
||||
Log.w(TAG, "process exit too fast, stop guard: " + cmd)
|
||||
isDestroyed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch {
|
||||
case ignored: InterruptedException =>
|
||||
@@ -134,8 +137,10 @@ class GuardedProcess(cmd: Seq[String]) {
|
||||
}
|
||||
|
||||
def restart() {
|
||||
isRestart = true
|
||||
process.destroy()
|
||||
this.synchronized {
|
||||
isRestart = true
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@throws(classOf[InterruptedException])
|
||||
|
||||
@@ -235,7 +235,7 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
|
||||
}).attachToRecyclerView(profilesList)
|
||||
|
||||
attachService(new IShadowsocksServiceCallback.Stub {
|
||||
def stateChanged(state: Int, msg: String) = () // ignore
|
||||
def stateChanged(state: Int, profileName: String, msg: String) = () // ignore
|
||||
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) =
|
||||
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
|
||||
})
|
||||
@@ -276,7 +276,7 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
|
||||
updateNfcState()
|
||||
}
|
||||
|
||||
override def onNewIntent(intent: Intent): Unit ={
|
||||
override def onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleShareIntent(intent)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -48,13 +47,13 @@ class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHand
|
||||
override def onResume() {
|
||||
super.onResume()
|
||||
val permissionCheck = ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.CAMERA)
|
||||
android.Manifest.permission.CAMERA)
|
||||
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
|
||||
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
|
||||
scannerView.startCamera() // Start camera on resume
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
Array(Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
|
||||
Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,16 +38,14 @@
|
||||
*/
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.io.{FileOutputStream, IOException, InputStream, OutputStream}
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.net.{HttpURLConnection, URL}
|
||||
import java.util
|
||||
import java.util.Locale
|
||||
import java.util.{GregorianCalendar, Locale}
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.app.{Activity, ProgressDialog}
|
||||
import android.content._
|
||||
import android.content.res.AssetManager
|
||||
import android.graphics.Typeface
|
||||
import android.net.VpnService
|
||||
import android.os._
|
||||
@@ -59,15 +57,16 @@ import android.util.Log
|
||||
import android.view.{View, ViewGroup}
|
||||
import android.widget._
|
||||
import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database._
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.util.Random
|
||||
|
||||
object Typefaces {
|
||||
def get(c: Context, assetPath: String): Typeface = {
|
||||
@@ -94,8 +93,6 @@ object Shadowsocks {
|
||||
// Constants
|
||||
private final val TAG = "Shadowsocks"
|
||||
private final val REQUEST_CONNECT = 1
|
||||
private val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL,
|
||||
Executable.TUN2SOCKS, Executable.KCPTUN)
|
||||
}
|
||||
|
||||
class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
@@ -111,7 +108,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
|
||||
// Services
|
||||
private val callback = new IShadowsocksServiceCallback.Stub {
|
||||
def stateChanged(s: Int, m: String) {
|
||||
def stateChanged(s: Int, profileName: String, m: String) {
|
||||
handler.post(() => {
|
||||
s match {
|
||||
case State.CONNECTING =>
|
||||
@@ -185,14 +182,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
snackbar.setAction(R.string.switch_to_vpn, (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
|
||||
snackbar.show
|
||||
}
|
||||
|
||||
if (!app.settings.getBoolean(BuildConfig.VERSION_NAME, false)) {
|
||||
app.editor.putBoolean(BuildConfig.VERSION_NAME, true).apply()
|
||||
|
||||
recovery()
|
||||
|
||||
updateCurrentProfile()
|
||||
}
|
||||
}
|
||||
|
||||
override def onServiceDisconnected() {
|
||||
@@ -239,53 +228,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
}
|
||||
}
|
||||
|
||||
private def copyAssets(path: String) {
|
||||
val assetManager: AssetManager = getAssets
|
||||
var files: Array[String] = null
|
||||
try {
|
||||
files = assetManager.list(path)
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.e(TAG, e.getMessage)
|
||||
app.track(e)
|
||||
}
|
||||
if (files != null) {
|
||||
for (file <- files) {
|
||||
var in: InputStream = null
|
||||
var out: OutputStream = null
|
||||
try {
|
||||
if (path.length > 0) {
|
||||
in = assetManager.open(path + "/" + file)
|
||||
} else {
|
||||
in = assetManager.open(file)
|
||||
}
|
||||
out = new FileOutputStream(getApplicationInfo.dataDir + "/" + file)
|
||||
copyFile(in, out)
|
||||
in.close()
|
||||
in = null
|
||||
out.flush()
|
||||
out.close()
|
||||
out = null
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
Log.e(TAG, ex.getMessage)
|
||||
app.track(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def copyFile(in: InputStream, out: OutputStream) {
|
||||
val buffer: Array[Byte] = new Array[Byte](1024)
|
||||
var read: Int = 0
|
||||
while ( {
|
||||
read = in.read(buffer)
|
||||
read
|
||||
} != -1) {
|
||||
out.write(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
|
||||
def crashRecovery() {
|
||||
val cmd = new ArrayBuffer[String]()
|
||||
|
||||
@@ -487,7 +429,18 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
|
||||
adView.setAdSize(AdSize.SMART_BANNER)
|
||||
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1)
|
||||
adView.loadAd(new AdRequest.Builder().build())
|
||||
|
||||
// Demographics
|
||||
val random = new Random()
|
||||
val adBuilder = new AdRequest.Builder()
|
||||
adBuilder.setGender(AdRequest.GENDER_MALE)
|
||||
val year = 1975 + random.nextInt(40)
|
||||
val month = 1 + random.nextInt(12)
|
||||
val day = random.nextInt(28)
|
||||
adBuilder.setBirthday(new GregorianCalendar(year, month, day).getTime)
|
||||
|
||||
// Load Ad
|
||||
adView.loadAd(adBuilder.build())
|
||||
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
|
||||
|
||||
preferences.setProfile(profile)
|
||||
@@ -513,13 +466,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
def reset() {
|
||||
crashRecovery()
|
||||
|
||||
copyAssets(System.getABI)
|
||||
|
||||
val ab = new ArrayBuffer[String]
|
||||
for (executable <- EXECUTABLES) {
|
||||
ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable)
|
||||
}
|
||||
Shell.SH.run(ab.toArray)
|
||||
app.copyAssets()
|
||||
}
|
||||
|
||||
def recovery() {
|
||||
@@ -531,15 +478,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
|
||||
}
|
||||
}
|
||||
|
||||
def flushDnsCache() {
|
||||
val h = showProgress(R.string.flushing)
|
||||
Utils.ThrowableFuture {
|
||||
if (!Utils.toggleAirplaneMode(getBaseContext)) h.post(() => Snackbar.make(findViewById(android.R.id.content),
|
||||
R.string.flush_dnscache_no_root, Snackbar.LENGTH_LONG).show)
|
||||
h.sendEmptyMessage(0)
|
||||
}
|
||||
}
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
|
||||
case Activity.RESULT_OK =>
|
||||
serviceLoad()
|
||||
|
||||
@@ -39,21 +39,41 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.io.{FileOutputStream, IOException}
|
||||
import java.util
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import android.os.{Build, LocaleList}
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.github.shadowsocks.database.{DBHelper, ProfileManager}
|
||||
import com.github.shadowsocks.utils.{Key, Utils, TcpFastOpen}
|
||||
import com.github.shadowsocks.job.DonaldTrump
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser}
|
||||
import com.google.android.gms.common.api.ResultCallback
|
||||
import com.google.android.gms.tagmanager.{ContainerHolder, TagManager}
|
||||
import com.j256.ormlite.logger.LocalLog
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
|
||||
object ShadowsocksApplication {
|
||||
var app: ShadowsocksApplication = _
|
||||
|
||||
private final val TAG = "ShadowsocksApplication"
|
||||
private val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL,
|
||||
Executable.TUN2SOCKS, Executable.KCPTUN)
|
||||
|
||||
// The ones in Locale doesn't have script included
|
||||
private final lazy val SIMPLIFIED_CHINESE =
|
||||
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
|
||||
private final lazy val TRADITIONAL_CHINESE =
|
||||
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
|
||||
}
|
||||
|
||||
class ShadowsocksApplication extends Application {
|
||||
@@ -88,10 +108,62 @@ class ShadowsocksApplication extends Application {
|
||||
profileManager.getProfile(id) getOrElse profileManager.createProfile()
|
||||
}
|
||||
|
||||
private def checkChineseLocale(locale: Locale): Locale = if (locale.getLanguage == "zh") locale.getCountry match {
|
||||
case "CN" | "TW" => null // already supported
|
||||
case _ => locale.getScript match { // fallback to the corresponding script
|
||||
case "Hans" => SIMPLIFIED_CHINESE
|
||||
case "Hant" => TRADITIONAL_CHINESE
|
||||
case script =>
|
||||
Log.w(TAG, "Unknown zh locale script: %s. Falling back to trying countries...".format(script))
|
||||
locale.getCountry match {
|
||||
case "SG" => SIMPLIFIED_CHINESE
|
||||
case "HK" | "MO" => TRADITIONAL_CHINESE
|
||||
case _ =>
|
||||
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN...".format(locale.toLanguageTag))
|
||||
SIMPLIFIED_CHINESE
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
@SuppressLint(Array("NewApi"))
|
||||
private def checkChineseLocale(config: Configuration): Unit = if (Build.VERSION.SDK_INT >= 24) {
|
||||
val localeList = config.getLocales
|
||||
val newList = new Array[Locale](localeList.size())
|
||||
var changed = false
|
||||
for (i <- 0 until localeList.size()) {
|
||||
val locale = localeList.get(i)
|
||||
val newLocale = checkChineseLocale(locale)
|
||||
if (newLocale == null) newList(i) = locale else {
|
||||
newList(i) = newLocale
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
val newConfig = new Configuration(config)
|
||||
newConfig.setLocales(new LocaleList(newList.distinct: _*))
|
||||
val res = getResources
|
||||
res.updateConfiguration(newConfig, res.getDisplayMetrics)
|
||||
}
|
||||
} else {
|
||||
val newLocale = checkChineseLocale(config.locale)
|
||||
if (config.locale != newLocale) {
|
||||
val newConfig = new Configuration(config)
|
||||
newConfig.locale = newLocale
|
||||
val res = getResources
|
||||
res.updateConfiguration(newConfig, res.getDisplayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
override def onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
checkChineseLocale(newConfig)
|
||||
}
|
||||
|
||||
override def onCreate() {
|
||||
java.lang.System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
||||
app = this
|
||||
if (!BuildConfig.DEBUG) java.lang.System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
checkChineseLocale(getResources.getConfiguration)
|
||||
val tm = TagManager.getInstance(this)
|
||||
val pending = tm.loadContainerPreferNonDefault("GTM-NT8WS8", R.raw.gtm_default_container)
|
||||
val callback = new ResultCallback[ContainerHolder] {
|
||||
@@ -111,12 +183,36 @@ class ShadowsocksApplication extends Application {
|
||||
}
|
||||
}
|
||||
pending.setResultCallback(callback, 2, TimeUnit.SECONDS)
|
||||
JobManager.create(this).addJobCreator(DonaldTrump)
|
||||
|
||||
TcpFastOpen.enabled(settings.getBoolean(Key.tfo, false))
|
||||
TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
}
|
||||
|
||||
def refreshContainerHolder {
|
||||
val holder = app.containerHolder
|
||||
if (holder != null) holder.refresh()
|
||||
}
|
||||
|
||||
private def copyAssets(path: String) {
|
||||
val assetManager = getAssets
|
||||
var files: Array[String] = null
|
||||
try files = assetManager.list(path) catch {
|
||||
case e: IOException =>
|
||||
Log.e(TAG, e.getMessage)
|
||||
app.track(e)
|
||||
}
|
||||
if (files != null) for (file <- files)
|
||||
autoClose(assetManager.open(if (path.nonEmpty) path + '/' + file else file))(in =>
|
||||
autoClose(new FileOutputStream(getApplicationInfo.dataDir + '/' + file))(out =>
|
||||
IOUtils.copy(in, out)))
|
||||
}
|
||||
|
||||
def copyAssets() {
|
||||
copyAssets(System.getABI)
|
||||
copyAssets("acl")
|
||||
Shell.SH.run(EXECUTABLES.map("chmod 755 " + getApplicationInfo.dataDir + '/' + _))
|
||||
editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply()
|
||||
}
|
||||
|
||||
def updateAssets() = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import android.os._
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.job.AclSyncJob
|
||||
import com.github.shadowsocks.utils._
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
|
||||
@@ -71,18 +72,6 @@ class ShadowsocksNatService extends BaseService {
|
||||
var su: Shell.Interactive = _
|
||||
|
||||
def startShadowsocksDaemon() {
|
||||
if (profile.route != Route.ALL) {
|
||||
val acl: Array[Array[String]] = profile.route match {
|
||||
case Route.BYPASS_LAN => Array(getResources.getStringArray(R.array.private_route))
|
||||
case Route.BYPASS_CHN => Array(getResources.getStringArray(R.array.chn_route))
|
||||
case Route.BYPASS_LAN_CHN =>
|
||||
Array(getResources.getStringArray(R.array.private_route), getResources.getStringArray(R.array.chn_route))
|
||||
}
|
||||
Utils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
|
||||
acl.flatten.foreach(p.println)
|
||||
})
|
||||
}
|
||||
|
||||
val conf = if (profile.kcp) {
|
||||
ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort,
|
||||
@@ -108,7 +97,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
if (profile.route != Route.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += (getApplicationInfo.dataDir + "/acl.list")
|
||||
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -118,8 +107,14 @@ class ShadowsocksNatService extends BaseService {
|
||||
def startKcptunDaemon() {
|
||||
if (profile.kcpcli == null) profile.kcpcli = ""
|
||||
|
||||
val host = if (profile.host.contains(":")) {
|
||||
"[" + profile.host + "]"
|
||||
} else {
|
||||
profile.host
|
||||
}
|
||||
|
||||
val cmd = ArrayBuffer[String](getApplicationInfo.dataDir + "/kcptun"
|
||||
, "-r", profile.host + ":" + profile.kcpPort
|
||||
, "-r", host + ":" + profile.kcpPort
|
||||
, "-l", "127.0.0.1:" + (profile.localPort + 90))
|
||||
try cmd ++= Utils.translateCommandline(profile.kcpcli) catch {
|
||||
case exc: Exception => throw KcpcliParseException(exc)
|
||||
@@ -142,11 +137,16 @@ class ShadowsocksNatService extends BaseService {
|
||||
, "-u"
|
||||
, "-t" , "10"
|
||||
, "-b" , "127.0.0.1"
|
||||
, "-L" , "8.8.8.8:53"
|
||||
, "-l" , (profile.localPort + 53).toString
|
||||
, "-P" , getApplicationInfo.dataDir
|
||||
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
|
||||
|
||||
cmd += "-L"
|
||||
if (profile.route == Route.CHINALIST)
|
||||
cmd += "114.114.114.114:53"
|
||||
else
|
||||
cmd += "8.8.8.8:53"
|
||||
|
||||
if (profile.auth) cmd += "-A"
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -184,12 +184,23 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
def startDnsDaemon() {
|
||||
|
||||
val conf = if (profile.route == Route.BYPASS_CHN || profile.route == Route.BYPASS_LAN_CHN) {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, getBlackList, profile.localPort + 63, "")
|
||||
} else {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, profile.localPort + 63, "")
|
||||
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
|
||||
|
||||
val conf = profile.route match {
|
||||
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST => {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, "1.2.4.8, 114.114.114.114",
|
||||
getBlackList, reject, profile.localPort + 63, reject)
|
||||
}
|
||||
case Route.CHINALIST => {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, "8.8.8.8, 208.67.222.222",
|
||||
"", reject, profile.localPort + 63, reject)
|
||||
}
|
||||
case _ => {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
|
||||
"127.0.0.1", profile.localPort + 53, profile.localPort + 63, reject)
|
||||
}
|
||||
}
|
||||
|
||||
Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-nat.conf"))(p => {
|
||||
@@ -298,23 +309,15 @@ class ShadowsocksNatService extends BaseService {
|
||||
su.addCommand((init_sb ++ http_sb).toArray)
|
||||
}
|
||||
|
||||
override def startRunner(profile: Profile) = if (su == null) {
|
||||
override def startRunner(profile: Profile) = if (su == null)
|
||||
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
|
||||
if (exitCode == 0) super.startRunner(profile) else {
|
||||
if (su != null) {
|
||||
Log.wtf(TAG, "libsuperuser#55 has been fixed. Please remove the redundant code.")
|
||||
su.close()
|
||||
su = null
|
||||
}
|
||||
super.stopRunner(true, getString(R.string.nat_no_root))
|
||||
})
|
||||
su.waitForIdle()
|
||||
if (!su.isRunning) {
|
||||
su.close()
|
||||
su = null
|
||||
super.stopRunner(true, getString(R.string.nat_no_root))
|
||||
}
|
||||
}
|
||||
|
||||
override def connect() {
|
||||
super.connect()
|
||||
@@ -328,8 +331,9 @@ class ShadowsocksNatService extends BaseService {
|
||||
}
|
||||
|
||||
handleConnection()
|
||||
// Set DNS
|
||||
su.addCommand(Utils.FLUSH_DNS)
|
||||
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
changeState(State.CONNECTED)
|
||||
notification = new ShadowsocksNotification(this, profile.name, true)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
||||
private lazy val callback = new Stub {
|
||||
override def stateChanged(state: Int, msg: String) = () // ignore
|
||||
override def stateChanged(state: Int, profileName: String, msg: String) = () // ignore
|
||||
override def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
val txr = TrafficMonitor.formatTraffic(txRate)
|
||||
val rxr = TrafficMonitor.formatTraffic(rxRate)
|
||||
@@ -34,7 +34,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
|
||||
private val builder = new NotificationCompat.Builder(service)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_accent_500))
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks])
|
||||
|
||||
@@ -54,10 +54,12 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
|
||||
}
|
||||
|
||||
override def onServiceConnected() {
|
||||
handler.postDelayed(() => if (bgService != null) {
|
||||
if (app.isNatEnabled) startBackgroundService()
|
||||
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
|
||||
stopSelf()
|
||||
handler.postDelayed(() => {
|
||||
if (bgService != null) {
|
||||
if (app.isNatEnabled) startBackgroundService()
|
||||
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
|
||||
handler.postDelayed(() => stopSelf(), 10000)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,16 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.{Intent, SharedPreferences}
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.preference.{DropDownPreference, Preference}
|
||||
import android.webkit.{WebView, WebViewClient}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.preferences._
|
||||
import com.github.shadowsocks.preferences.KcpCliPreferenceDialogFragment
|
||||
import com.github.shadowsocks.utils.{Key, TcpFastOpen, Utils}
|
||||
import be.mygod.preference._
|
||||
|
||||
object ShadowsocksSettings {
|
||||
// Constants
|
||||
@@ -27,31 +29,25 @@ object ShadowsocksSettings {
|
||||
pref.asInstanceOf[DropDownPreference].setValue(value)
|
||||
}
|
||||
|
||||
def updatePasswordEditTextPreference(pref: Preference, value: String) {
|
||||
pref.setSummary(value)
|
||||
pref.asInstanceOf[PasswordEditTextPreference].setText(value)
|
||||
def updateEditTextPreference(pref: Preference, value: String) {
|
||||
pref.asInstanceOf[EditTextPreference].setText(value)
|
||||
}
|
||||
|
||||
def updateNumberPickerPreference(pref: Preference, value: Int) {
|
||||
pref.asInstanceOf[NumberPickerPreference].setValue(value)
|
||||
}
|
||||
|
||||
def updateSummaryEditTextPreference(pref: Preference, value: String) {
|
||||
pref.setSummary(value)
|
||||
pref.asInstanceOf[SummaryEditTextPreference].setText(value)
|
||||
}
|
||||
|
||||
def updateSwitchPreference(pref: Preference, value: Boolean) {
|
||||
pref.asInstanceOf[SwitchPreference].setChecked(value)
|
||||
}
|
||||
|
||||
def updatePreference(pref: Preference, name: String, profile: Profile) {
|
||||
name match {
|
||||
case Key.name => updateSummaryEditTextPreference(pref, profile.name)
|
||||
case Key.host => updateSummaryEditTextPreference(pref, profile.host)
|
||||
case Key.name => updateEditTextPreference(pref, profile.name)
|
||||
case Key.host => updateEditTextPreference(pref, profile.host)
|
||||
case Key.remotePort => updateNumberPickerPreference(pref, profile.remotePort)
|
||||
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
|
||||
case Key.password => updatePasswordEditTextPreference(pref, profile.password)
|
||||
case Key.password => updateEditTextPreference(pref, profile.password)
|
||||
case Key.method => updateDropDownPreference(pref, profile.method)
|
||||
case Key.route => updateDropDownPreference(pref, profile.route)
|
||||
case Key.proxyApps => updateSwitchPreference(pref, profile.proxyApps)
|
||||
@@ -60,7 +56,7 @@ object ShadowsocksSettings {
|
||||
case Key.ipv6 => updateSwitchPreference(pref, profile.ipv6)
|
||||
case Key.kcp => updateSwitchPreference(pref, profile.kcp)
|
||||
case Key.kcpPort => updateNumberPickerPreference(pref, profile.kcpPort)
|
||||
case Key.kcpcli => updateSummaryEditTextPreference(pref, profile.kcpcli)
|
||||
case Key.kcpcli => updateEditTextPreference(pref, profile.kcpcli)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,8 +69,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
|
||||
private var isProxyApps: SwitchPreference = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override def onCreatePreferences(bundle: Bundle, key: String) {
|
||||
addPreferencesFromResource(R.xml.pref_all)
|
||||
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
@@ -175,12 +170,6 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
true
|
||||
})
|
||||
|
||||
findPreference("flush_dnscache").setOnPreferenceClickListener(_ => {
|
||||
app.track(TAG, "flush_dnscache")
|
||||
activity.flushDnsCache()
|
||||
true
|
||||
})
|
||||
|
||||
findPreference("about").setOnPreferenceClickListener((preference: Preference) => {
|
||||
app.track(TAG, "about")
|
||||
val web = new WebView(activity)
|
||||
@@ -206,8 +195,29 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
})
|
||||
}
|
||||
|
||||
override def onDisplayPreferenceDialog(preference: Preference) = preference match {
|
||||
case _: EditTextPreference => displayPreferenceDialog(preference.getKey,
|
||||
if (preference.getKey == Key.kcpcli) new KcpCliPreferenceDialogFragment()
|
||||
else new EditTextPreferenceDialogFragment())
|
||||
case _: NumberPickerPreference =>
|
||||
displayPreferenceDialog(preference.getKey, new NumberPickerPreferenceDialogFragment())
|
||||
case _ => super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
|
||||
def refreshProfile() {
|
||||
profile = app.currentProfile.get
|
||||
profile = app.currentProfile match {
|
||||
case Some(p) => p
|
||||
case None =>
|
||||
app.profileManager.getFirstProfile match {
|
||||
case Some(p) =>
|
||||
app.profileId(p.id)
|
||||
p
|
||||
case None =>
|
||||
val default = app.profileManager.createDefault()
|
||||
app.profileId(default.id)
|
||||
default
|
||||
}
|
||||
}
|
||||
isProxyApps.setChecked(profile.proxyApps)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.graphics.drawable.Icon
|
||||
import android.service.quicksettings.{Tile, TileService}
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.utils.{State, Utils}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
@@ -23,7 +22,7 @@ final class ShadowsocksTileService extends TileService with ServiceBoundContext
|
||||
private lazy val iconConnected = Icon.createWithResource(this, R.drawable.ic_start_connected)
|
||||
private lazy val callback = new IShadowsocksServiceCallback.Stub {
|
||||
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) = ()
|
||||
def stateChanged(state: Int, msg: String) {
|
||||
def stateChanged(state: Int, profileName: String, msg: String) {
|
||||
val tile = getQsTile
|
||||
if (tile != null) {
|
||||
state match {
|
||||
@@ -33,10 +32,7 @@ final class ShadowsocksTileService extends TileService with ServiceBoundContext
|
||||
tile.setState(Tile.STATE_INACTIVE)
|
||||
case State.CONNECTED =>
|
||||
tile.setIcon(iconConnected)
|
||||
tile.setLabel(app.currentProfile match {
|
||||
case Some(profile) => profile.name
|
||||
case None => getString(R.string.app_name)
|
||||
})
|
||||
tile.setLabel(if (profileName == null) getString(R.string.app_name) else profileName)
|
||||
tile.setState(Tile.STATE_ACTIVE)
|
||||
case _ =>
|
||||
tile.setIcon(iconBusy)
|
||||
@@ -48,7 +44,7 @@ final class ShadowsocksTileService extends TileService with ServiceBoundContext
|
||||
}
|
||||
}
|
||||
|
||||
override def onServiceConnected() = callback.stateChanged(bgService.getState, null)
|
||||
override def onServiceConnected() = callback.stateChanged(bgService.getState, bgService.getProfileName, null)
|
||||
|
||||
override def onCreate {
|
||||
super.onCreate
|
||||
|
||||
@@ -42,6 +42,7 @@ package com.github.shadowsocks
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content._
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.net.VpnService
|
||||
@@ -49,6 +50,7 @@ import android.os._
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.job.AclSyncJob
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
@@ -161,6 +163,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
handleConnection()
|
||||
changeState(State.CONNECTED)
|
||||
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
notification = new ShadowsocksNotification(this, profile.name)
|
||||
}
|
||||
|
||||
@@ -189,10 +194,16 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
def startKcptunDaemon() {
|
||||
if (profile.kcpcli == null) profile.kcpcli = ""
|
||||
|
||||
val host = if (profile.host.contains(":")) {
|
||||
"[" + profile.host + "]"
|
||||
} else {
|
||||
profile.host
|
||||
}
|
||||
|
||||
val cmd = ArrayBuffer(getApplicationInfo.dataDir + "/kcptun"
|
||||
, "-r", profile.host + ":" + profile.kcpPort
|
||||
, "-r", host + ":" + profile.kcpPort
|
||||
, "-l", "127.0.0.1:" + (profile.localPort + 90)
|
||||
, "--path", getApplicationInfo.dataDir + "/protect_path")
|
||||
, "--path", protectPath)
|
||||
try cmd ++= Utils.translateCommandline(profile.kcpcli) catch {
|
||||
case exc: Exception => throw KcpcliParseException(exc)
|
||||
}
|
||||
@@ -225,18 +236,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
def startShadowsocksDaemon() {
|
||||
if (profile.route != Route.ALL) {
|
||||
val acl: Array[Array[String]] = profile.route match {
|
||||
case Route.BYPASS_LAN => Array(getResources.getStringArray(R.array.private_route))
|
||||
case Route.BYPASS_CHN => Array(getResources.getStringArray(R.array.chn_route))
|
||||
case Route.BYPASS_LAN_CHN =>
|
||||
Array(getResources.getStringArray(R.array.private_route), getResources.getStringArray(R.array.chn_route))
|
||||
}
|
||||
Utils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
|
||||
acl.flatten.foreach(p.println)
|
||||
})
|
||||
}
|
||||
|
||||
val conf = if (profile.kcp) {
|
||||
ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort,
|
||||
@@ -262,7 +261,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (profile.route != Route.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += (getApplicationInfo.dataDir + "/acl.list")
|
||||
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
|
||||
}
|
||||
|
||||
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
|
||||
@@ -289,10 +288,15 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
, "-V"
|
||||
, "-t", "10"
|
||||
, "-b", "127.0.0.1"
|
||||
, "-L", "8.8.8.8:53"
|
||||
, "-P", getApplicationInfo.dataDir
|
||||
, "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf")
|
||||
|
||||
cmd += "-L"
|
||||
if (profile.route == Route.CHINALIST)
|
||||
cmd += "114.114.114.114:53"
|
||||
else
|
||||
cmd += "8.8.8.8:53"
|
||||
|
||||
if (profile.auth) cmd += "-A"
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -301,14 +305,22 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
|
||||
def startDnsDaemon() {
|
||||
val ipv6 = if (profile.ipv6) "" else "reject = ::/0;"
|
||||
val conf = {
|
||||
if (profile.route == Route.BYPASS_CHN || profile.route == Route.BYPASS_LAN_CHN) {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, getBlackList, profile.localPort + 63, ipv6)
|
||||
} else {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, profile.localPort + 63, ipv6)
|
||||
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
|
||||
val protect = "protect = \"" + protectPath +"\";"
|
||||
val conf = profile.route match {
|
||||
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST => {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, "114.114.114.114, 119.29.29.29, 1.2.4.8",
|
||||
getBlackList, reject, profile.localPort + 63, reject)
|
||||
}
|
||||
case Route.CHINALIST => {
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
|
||||
"", reject, profile.localPort + 63, reject)
|
||||
}
|
||||
case _ => {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", profile.localPort + 53, profile.localPort + 63, reject)
|
||||
}
|
||||
}
|
||||
Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => {
|
||||
@@ -321,6 +333,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
pdnsdProcess = new GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
@SuppressLint(Array("NewApi"))
|
||||
def startVpn(): Int = {
|
||||
|
||||
val builder = new Builder()
|
||||
@@ -328,7 +341,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
.setSession(profile.name)
|
||||
.setMtu(VPN_MTU)
|
||||
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
|
||||
.addDnsServer("8.8.8.8")
|
||||
|
||||
builder.addDnsServer("8.8.8.8")
|
||||
|
||||
if (profile.ipv6) {
|
||||
builder.addAddress(PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "1"), 126)
|
||||
@@ -365,16 +379,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
builder.addRoute("8.8.0.0", 16)
|
||||
|
||||
try {
|
||||
conn = builder.establish()
|
||||
if (conn == null) changeState(State.STOPPED, getString(R.string.reboot_required))
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
ex.printStackTrace()
|
||||
app.track(ex)
|
||||
stopRunner(true, ex.getMessage)
|
||||
return -1
|
||||
}
|
||||
conn = builder.establish()
|
||||
if (conn == null) throw new NullConnectionException
|
||||
|
||||
val fd = conn.getFd
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ package com.github.shadowsocks.database
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.os.Binder
|
||||
import android.util.Base64
|
||||
import com.j256.ormlite.field.{DataType, DatabaseField}
|
||||
|
||||
@@ -54,8 +55,9 @@ class Profile {
|
||||
@DatabaseField
|
||||
var host: String = ""
|
||||
|
||||
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
||||
@DatabaseField
|
||||
var localPort: Int = 1080
|
||||
var localPort: Int = 1080 + Binder.getCallingUserHandle.hashCode
|
||||
|
||||
@DatabaseField
|
||||
var remotePort: Int = 8388
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.github.shadowsocks.job
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.evernote.android.job.Job.{Params, Result}
|
||||
import com.evernote.android.job.{Job, JobRequest}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils.IOUtils
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object AclSyncJob {
|
||||
final val TAG = "AclSyncJob"
|
||||
|
||||
def schedule(route: String) = new JobRequest.Builder(AclSyncJob.TAG + ':' + route)
|
||||
.setExecutionWindow(1, TimeUnit.DAYS.toMillis(28))
|
||||
.setRequirementsEnforced(true)
|
||||
.setRequiredNetworkType(JobRequest.NetworkType.UNMETERED)
|
||||
.setRequiresCharging(true)
|
||||
.setUpdateCurrent(true)
|
||||
.build().schedule()
|
||||
}
|
||||
|
||||
class AclSyncJob(route: String) extends Job {
|
||||
override def onRunJob(params: Params): Result = {
|
||||
val filename = route + ".acl"
|
||||
try {
|
||||
//noinspection JavaAccessorMethodCalledAsEmptyParen
|
||||
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename, autoClose(
|
||||
new URL("https://raw.githubusercontent.com/shadowsocks/shadowsocks-android/master/src/main/assets/acl/" +
|
||||
filename).openConnection().getInputStream())(IOUtils.readString))
|
||||
Result.SUCCESS
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
e.printStackTrace()
|
||||
Result.RESCHEDULE
|
||||
case e: Exception => // unknown failures, probably shouldn't retry
|
||||
e.printStackTrace()
|
||||
Result.FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.shadowsocks.job
|
||||
|
||||
import android.util.Log
|
||||
import com.evernote.android.job.JobCreator
|
||||
|
||||
/**
|
||||
* “I create jobs all day long.
|
||||
* - Donald Trump, 2015
|
||||
*
|
||||
* Source: http://www.cnn.com/2015/09/24/politics/donald-trump-marco-rubio-foreign-policy/
|
||||
*
|
||||
* @author !Mygod
|
||||
*/
|
||||
object DonaldTrump extends JobCreator {
|
||||
def create(tag: String) = {
|
||||
val parts = tag.split(":")
|
||||
parts(0) match {
|
||||
case AclSyncJob.TAG => new AclSyncJob(parts(1))
|
||||
case _ =>
|
||||
Log.w("DonaldTrump", "Unknown job tag: " + tag)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.preference.Preference
|
||||
import android.support.annotation.{ArrayRes, NonNull}
|
||||
import android.support.v7.widget.AppCompatSpinner
|
||||
import android.util.AttributeSet
|
||||
import android.view.{View, ViewGroup}
|
||||
import android.widget.AdapterView.OnItemSelectedListener
|
||||
import android.widget.{AdapterView, ArrayAdapter}
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
final class DropDownPreference(private val mContext: Context, attrs: AttributeSet = null)
|
||||
extends Preference(mContext, attrs) with SummaryPreference {
|
||||
|
||||
private val mAdapter = new ArrayAdapter[String](mContext, android.R.layout.simple_spinner_dropdown_item)
|
||||
private val mSpinner = new AppCompatSpinner(mContext)
|
||||
private var mEntries: Array[CharSequence] = _
|
||||
private var mEntryValues: Array[CharSequence] = _
|
||||
private var mSelectedIndex: Int = -1
|
||||
|
||||
mSpinner.setVisibility(View.INVISIBLE)
|
||||
mSpinner.setAdapter(mAdapter)
|
||||
mSpinner.setOnItemSelectedListener(new OnItemSelectedListener {
|
||||
def onNothingSelected(parent: AdapterView[_]) { }
|
||||
def onItemSelected(parent: AdapterView[_], view: View, position: Int, id: Long) = {
|
||||
val value = getValue(position)
|
||||
if (position != mSelectedIndex && callChangeListener(value)) setValue(position, value)
|
||||
}
|
||||
})
|
||||
setOnPreferenceClickListener((preference: Preference) => {
|
||||
// TODO: not working with scrolling
|
||||
// mSpinner.setDropDownVerticalOffset(Utils.dpToPx(getContext, -48 * mSelectedIndex).toInt)
|
||||
mSpinner.performClick
|
||||
true
|
||||
})
|
||||
val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference)
|
||||
setEntries(a.getTextArray(R.styleable.DropDownPreference_android_entries))
|
||||
setEntryValues(a.getTextArray(R.styleable.DropDownPreference_android_entryValues))
|
||||
a.recycle
|
||||
|
||||
protected override def getSummaryValue = getEntry
|
||||
|
||||
/**
|
||||
* Sets the human-readable entries to be shown in the list. This will be shown in subsequent dialogs.
|
||||
*
|
||||
* Each entry must have a corresponding index in [[setEntryValues(CharSequence[])]].
|
||||
*
|
||||
* @param entries The entries.
|
||||
* @see [[setEntryValues(CharSequence[])]]
|
||||
*/
|
||||
def setEntries(entries: Array[CharSequence]) {
|
||||
mEntries = entries
|
||||
mAdapter.clear
|
||||
if (entries != null) for (entry <- entries) mAdapter.add(entry.toString)
|
||||
}
|
||||
/**
|
||||
* @see #setEntries(CharSequence[])
|
||||
* @param entriesResId The entries array as a resource.
|
||||
*/
|
||||
def setEntries(@ArrayRes entriesResId: Int): Unit = setEntries(getContext.getResources.getTextArray(entriesResId))
|
||||
/**
|
||||
* The list of entries to be shown in the list in subsequent dialogs.
|
||||
*
|
||||
* @return The list as an array.
|
||||
*/
|
||||
def getEntries = mEntries
|
||||
|
||||
/**
|
||||
* The array to find the value to save for a preference when an entry from
|
||||
* entries is selected. If a user clicks on the second item in entries, the
|
||||
* second item in this array will be saved to the preference.
|
||||
*
|
||||
* @param entryValues The array to be used as values to save for the preference.
|
||||
*/
|
||||
def setEntryValues(entryValues: Array[CharSequence]) = mEntryValues = entryValues
|
||||
/**
|
||||
* @see #setEntryValues(CharSequence[])
|
||||
* @param entryValuesResId The entry values array as a resource.
|
||||
*/
|
||||
def setEntryValues(@ArrayRes entryValuesResId: Int): Unit =
|
||||
setEntryValues(getContext.getResources.getTextArray(entryValuesResId))
|
||||
/**
|
||||
* Returns the array of values to be saved for the preference.
|
||||
*
|
||||
* @return The array of values.
|
||||
*/
|
||||
def getEntryValues: Array[CharSequence] = mEntryValues
|
||||
|
||||
private def getValue(index: Int) = (if (mEntryValues == null) index else mEntryValues(index)).toString
|
||||
/**
|
||||
* Sets the value of the key. This should be one of the entries in [[getEntryValues]].
|
||||
*
|
||||
* @param value The value to set for the key.
|
||||
*/
|
||||
def setValue(value: String): Unit = setValue(findIndexOfValue(value), value)
|
||||
|
||||
/**
|
||||
* Sets the value to the given index from the entry values.
|
||||
*
|
||||
* @param index The index of the value to set.
|
||||
*/
|
||||
def setValueIndex(index: Int) = setValue(index, getValue(index))
|
||||
|
||||
private def setValue(index: Int, value: String) {
|
||||
persistString(value)
|
||||
mSelectedIndex = index
|
||||
mSpinner.setSelection(index)
|
||||
notifyChanged
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the key. This should be one of the entries in [[getEntryValues]].
|
||||
*
|
||||
* @return The value of the key.
|
||||
*/
|
||||
def getValue = if (mEntryValues == null || mSelectedIndex < 0) null else mEntryValues(mSelectedIndex).toString
|
||||
|
||||
/**
|
||||
* Returns the entry corresponding to the current value.
|
||||
*
|
||||
* @return The entry corresponding to the current value, or null.
|
||||
*/
|
||||
def getEntry = {
|
||||
val index = getValueIndex
|
||||
if (index >= 0 && mEntries != null) mEntries(index) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the given value (in the entry values array).
|
||||
*
|
||||
* @param value The value whose index should be returned.
|
||||
* @return The index of the value, or -1 if not found.
|
||||
*/
|
||||
def findIndexOfValue(value: String): Int = {
|
||||
if (value != null && mEntryValues != null) {
|
||||
var i = mEntryValues.length - 1
|
||||
while (i >= 0) {
|
||||
if (mEntryValues(i) == value) return i
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
def getValueIndex = mSelectedIndex
|
||||
|
||||
protected override def onGetDefaultValue(a: TypedArray, index: Int) = a.getString(index)
|
||||
protected override def onSetInitialValue (restoreValue: Boolean, defaultValue: AnyRef) =
|
||||
setValue(if (restoreValue) getPersistedString(getValue) else defaultValue.asInstanceOf[String])
|
||||
|
||||
def setDropDownWidth(dimenResId: Int) =
|
||||
mSpinner.setDropDownWidth(mContext.getResources.getDimensionPixelSize(dimenResId))
|
||||
|
||||
protected override def onBindView(@NonNull view: View) {
|
||||
super.onBindView(view)
|
||||
val parent = mSpinner.getParent.asInstanceOf[ViewGroup]
|
||||
if (view eq parent) return
|
||||
if (parent != null) parent.removeView(mSpinner)
|
||||
view.asInstanceOf[ViewGroup].addView(mSpinner, 0)
|
||||
val lp = mSpinner.getLayoutParams
|
||||
lp.width = 0
|
||||
mSpinner.setLayoutParams(lp)
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -1,21 +1,22 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.{Context, DialogInterface}
|
||||
import android.util.AttributeSet
|
||||
import android.content.DialogInterface
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
import be.mygod.preference.EditTextPreferenceDialogFragment
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class KcpCliPreference(context: Context, attrs: AttributeSet) extends SummaryEditTextPreference(context, attrs) {
|
||||
class KcpCliPreferenceDialogFragment extends EditTextPreferenceDialogFragment {
|
||||
override def onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setNeutralButton("?", ((_, _) => new AlertDialog.Builder(context)
|
||||
builder.setNeutralButton("?", ((_, _) => new AlertDialog.Builder(builder.getContext)
|
||||
.setTitle("?")
|
||||
.setMessage(asScalaBuffer(Shell.SH.run(context.getApplicationInfo.dataDir + "/kcptun --help"))
|
||||
.setMessage(Shell.SH.run(builder.getContext.getApplicationInfo.dataDir + "/kcptun --help")
|
||||
.asScala
|
||||
.dropWhile(line => line != "GLOBAL OPTIONS:")
|
||||
.drop(1)
|
||||
.takeWhile(line => line.length() > 3)
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2013 <max.c.lv@gmail.com>
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.EditTextPreference
|
||||
import android.util.AttributeSet
|
||||
|
||||
class PasswordEditTextPreference(context: Context, attrs: AttributeSet, defStyle: Int)
|
||||
extends EditTextPreference(context, attrs, defStyle) {
|
||||
|
||||
def this(context: Context, attrs: AttributeSet) = {
|
||||
this(context, attrs, android.R.attr.editTextPreferenceStyle)
|
||||
mDefaultSummary = getSummary
|
||||
}
|
||||
|
||||
override def setText(text: String) {
|
||||
super.setText(text)
|
||||
setSummary(text)
|
||||
}
|
||||
|
||||
override def setSummary(summary: CharSequence) {
|
||||
if (summary.toString.isEmpty) {
|
||||
super.setSummary(mDefaultSummary)
|
||||
} else {
|
||||
super.setSummary(summary.toString.map(c => "*").mkString)
|
||||
}
|
||||
}
|
||||
|
||||
private var mDefaultSummary: CharSequence = getSummary
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Shadowsocks - A shadowsocks client for Android
|
||||
* Copyright (C) 2013 <max.c.lv@gmail.com>
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.EditTextPreference
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
|
||||
class SummaryEditTextPreference(context: Context, attrs: AttributeSet, defStyle: Int)
|
||||
extends EditTextPreference(context, attrs, defStyle) {
|
||||
|
||||
def this(context: Context, attrs: AttributeSet) = {
|
||||
this(context, attrs, android.R.attr.editTextPreferenceStyle)
|
||||
mDefaultSummary = getSummary
|
||||
}
|
||||
|
||||
override def setText(text: String) {
|
||||
super.setText(text)
|
||||
setSummary(text)
|
||||
}
|
||||
|
||||
override def setSummary(summary: CharSequence) {
|
||||
if (TextUtils.isEmpty(summary)) {
|
||||
super.setSummary(mDefaultSummary)
|
||||
} else {
|
||||
super.setSummary(summary)
|
||||
}
|
||||
}
|
||||
|
||||
private var mDefaultSummary: CharSequence = getSummary
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.preference.Preference
|
||||
|
||||
/**
|
||||
* Make your preference support %s in summary. Override getSummaryValue to customize what to put in.
|
||||
* @author Mygod
|
||||
*/
|
||||
trait SummaryPreference extends Preference {
|
||||
protected def getSummaryValue: AnyRef
|
||||
|
||||
/**
|
||||
* Returns the summary of this SummaryPreference. If the summary has a String formatting marker in it
|
||||
* (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place.
|
||||
*
|
||||
* @return the summary with appropriate string substitution
|
||||
*/
|
||||
override def getSummary = super.getSummary.toString.formatLocal(Locale.ENGLISH, getSummaryValue)
|
||||
}
|
||||
@@ -69,11 +69,11 @@ object ConfigUtils {
|
||||
"""
|
||||
|global {
|
||||
| perm_cache = 2048;
|
||||
| %s
|
||||
| cache_dir = "%s";
|
||||
| server_ip = %s;
|
||||
| server_port = %d;
|
||||
| query_method = tcp_only;
|
||||
| run_ipv4 = on;
|
||||
| min_ttl = 15m;
|
||||
| max_ttl = 1w;
|
||||
| timeout = 10;
|
||||
@@ -84,10 +84,9 @@ object ConfigUtils {
|
||||
| label = "local";
|
||||
| ip = 127.0.0.1;
|
||||
| port = %d;
|
||||
| %s
|
||||
| reject = %s;
|
||||
| reject_policy = negate;
|
||||
| reject_recursively = on;
|
||||
| timeout = 5;
|
||||
|}
|
||||
|
|
||||
|rr {
|
||||
@@ -103,32 +102,36 @@ object ConfigUtils {
|
||||
"""
|
||||
|global {
|
||||
| perm_cache = 2048;
|
||||
| %s
|
||||
| cache_dir = "%s";
|
||||
| server_ip = %s;
|
||||
| server_port = %d;
|
||||
| query_method = tcp_only;
|
||||
| run_ipv4 = on;
|
||||
| query_method = udp_only;
|
||||
| min_ttl = 15m;
|
||||
| max_ttl = 1w;
|
||||
| timeout = 10;
|
||||
| daemon = off;
|
||||
| par_queries = 4;
|
||||
|}
|
||||
|
|
||||
|server {
|
||||
| label = "china-servers";
|
||||
| ip = 114.114.114.114, 112.124.47.27;
|
||||
| timeout = 4;
|
||||
| exclude = %s;
|
||||
| label = "remote-servers";
|
||||
| ip = %s;
|
||||
| timeout = 3;
|
||||
| query_method = udp_only;
|
||||
| %s
|
||||
| policy = included;
|
||||
| uptest = none;
|
||||
| preset = on;
|
||||
| reject = %s;
|
||||
| reject_policy = fail;
|
||||
| reject_recursively = on;
|
||||
|}
|
||||
|
|
||||
|server {
|
||||
| label = "local-server";
|
||||
| ip = 127.0.0.1;
|
||||
| query_method = tcp_only;
|
||||
| port = %d;
|
||||
| %s
|
||||
| reject = %s;
|
||||
| reject_policy = negate;
|
||||
| reject_recursively = on;
|
||||
|}
|
||||
@@ -173,6 +176,7 @@ object Key {
|
||||
val kcpcli = "kcpcli"
|
||||
|
||||
val tfo = "tcp_fastopen"
|
||||
val currentVersionCode = "currentVersionCode"
|
||||
}
|
||||
|
||||
object State {
|
||||
@@ -194,4 +198,6 @@ object Route {
|
||||
val BYPASS_LAN = "bypass-lan"
|
||||
val BYPASS_CHN = "bypass-china"
|
||||
val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
val GFWLIST = "gfwlist"
|
||||
val CHINALIST = "china-list"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import java.io.{FileWriter, InputStream, OutputStream}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object IOUtils {
|
||||
private final val BUFFER_SIZE = 32 * 1024
|
||||
|
||||
def copy(in: InputStream, out: OutputStream) {
|
||||
val buffer = new Array[Byte](BUFFER_SIZE)
|
||||
while (true) {
|
||||
val count = in.read(buffer)
|
||||
if (count >= 0) out.write(buffer, 0, count) else return
|
||||
}
|
||||
}
|
||||
|
||||
def readString(in: InputStream): String = {
|
||||
val builder = new StringBuilder()
|
||||
val buffer = new Array[Byte](BUFFER_SIZE)
|
||||
while (true) {
|
||||
val count = in.read(buffer)
|
||||
if (count >= 0) builder.append(new String(buffer, 0, count)) else return builder.toString()
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
def writeString(file: String, content: String) = autoClose(new FileWriter(file))(writer => writer.write(content))
|
||||
}
|
||||
@@ -54,7 +54,6 @@ import android.view.{Gravity, View, Window}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
import org.xbill.DNS._
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
@@ -77,8 +76,7 @@ object Utils {
|
||||
new String(Base64.encode(mdg.digest, 0))
|
||||
}
|
||||
|
||||
def dpToPx(context: Context, dp: Int): Int =
|
||||
Math.round(dp * (context.getResources.getDisplayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT))
|
||||
def dpToPx(context: Context, dp: Int): Int = Math.round(dp * context.getResources.getDisplayMetrics.density)
|
||||
|
||||
/*
|
||||
* round or floor depending on whether you are using offsets(floor) or
|
||||
@@ -170,19 +168,6 @@ object Utils {
|
||||
result
|
||||
}
|
||||
|
||||
// Because /sys/class/net/* isn't accessible since API level 24
|
||||
final val FLUSH_DNS = "for if in /sys/class/net/*; do " +
|
||||
"if [ \"down\" != $(cat $if/operstate) ]; then " + // up or unknown
|
||||
"ndc resolver flushif ${if##*/}; " +
|
||||
"fi " +
|
||||
"done; echo done"
|
||||
|
||||
// Blocked > 3 seconds
|
||||
def toggleAirplaneMode(context: Context) = {
|
||||
val result = Shell.SU.run(FLUSH_DNS)
|
||||
result != null && !result.isEmpty
|
||||
}
|
||||
|
||||
def resolve(host: String, addrType: Int): Option[String] = {
|
||||
try {
|
||||
val lookup = new Lookup(host, addrType)
|
||||
@@ -259,9 +244,6 @@ object Utils {
|
||||
}
|
||||
|
||||
def startSsService(context: Context) {
|
||||
val isInstalled: Boolean = app.settings.getBoolean(BuildConfig.VERSION_NAME, false)
|
||||
if (!isInstalled) return
|
||||
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user