Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e1a42146 | ||
|
|
76108ec450 | ||
|
|
5b9a0d0659 | ||
|
|
6558f275fe | ||
|
|
c13cf6f5d2 | ||
|
|
cacde5cff1 | ||
|
|
bb2bd82d73 | ||
|
|
0a46eedba1 | ||
|
|
63bc60e513 | ||
|
|
d9eb0e5163 | ||
|
|
fc3d26d626 | ||
|
|
1bb3a6bf5d | ||
|
|
f6cab9b99c | ||
|
|
a24d29196f | ||
|
|
2eb12ee2ae | ||
|
|
ef7701982d | ||
|
|
bd942c688f | ||
|
|
3f08a66aab | ||
|
|
ae0801b71b | ||
|
|
241b84976f | ||
|
|
b76d089fb2 | ||
|
|
2f4cdbce25 | ||
|
|
8e2fc2b228 | ||
|
|
d794632616 | ||
|
|
529b1a32d3 | ||
|
|
396b49dab6 | ||
|
|
06deb4bbcb | ||
|
|
8127b9f8e2 | ||
|
|
3556eebfeb | ||
|
|
10e3b211e3 | ||
|
|
05981fb117 | ||
|
|
4258e95f1e | ||
|
|
cd5c2ef8ef | ||
|
|
5a84854881 | ||
|
|
0cf1585712 | ||
|
|
b8d97dde03 | ||
|
|
fbdf2dbf04 | ||
|
|
c7dd9aeeca | ||
|
|
eaac098027 | ||
|
|
65635c6b93 | ||
|
|
187cb28d15 | ||
|
|
2840a75c92 | ||
|
|
95e214ef11 | ||
|
|
217ed797d9 | ||
|
|
d337f0e2e4 | ||
|
|
713ff53030 | ||
|
|
35f17f5338 | ||
|
|
172c0b240f | ||
|
|
e9406080e2 | ||
|
|
7f937a8ee6 | ||
|
|
3facb73cf1 | ||
|
|
8f10e95329 | ||
|
|
b620a2e666 | ||
|
|
9cf8c4c483 | ||
|
|
bfbbb90ac3 | ||
|
|
dc840fd0e9 | ||
|
|
79d1ccba23 | ||
|
|
19df6caa93 | ||
|
|
5f268caa0a | ||
|
|
04c86a537a | ||
|
|
f9667d767e | ||
|
|
5ddd8a1686 | ||
|
|
7d44e4fc9f | ||
|
|
92b9c52944 | ||
|
|
4f3ebf9395 | ||
|
|
bf135107d8 | ||
|
|
5a3d2c07bc | ||
|
|
d8ab3733dc | ||
|
|
b6fc013a06 | ||
|
|
7d01ca81c8 | ||
|
|
9feb5bd96a | ||
|
|
2641de436d | ||
|
|
4d7bf8d707 | ||
|
|
0018a7ca9e | ||
|
|
51ceb3dac5 | ||
|
|
179aab8094 | ||
|
|
6bff4f1a44 | ||
|
|
8dcfd5a8c5 | ||
|
|
ef162cd47e | ||
|
|
569f056a22 | ||
|
|
971ea09077 | ||
|
|
fd01712b6c | ||
|
|
6a0955c082 | ||
|
|
92c8e0856c | ||
|
|
a5ef0cd6f3 | ||
|
|
0612a00474 | ||
|
|
0282975e6c | ||
|
|
c4583bea22 | ||
|
|
0d582f2392 | ||
|
|
8cf37954d7 | ||
|
|
eadb95ca2d | ||
|
|
0f9b686e55 | ||
|
|
c93a0348ad | ||
|
|
0cf509833a | ||
|
|
aed6308841 | ||
|
|
9fb0639f86 | ||
|
|
164becfed0 | ||
|
|
eed7ec6eba | ||
|
|
7e5b44d36f |
@@ -2,9 +2,7 @@
|
||||
|
||||
A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
||||
|
||||
Help to translate shadowsocks: http://crowdin.net/project/shadowsocks/invite
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=com.github.shadowsocks)
|
||||
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
|
||||
|
||||
### CI STATUS
|
||||
|
||||
@@ -70,7 +68,7 @@ Help to translate shadowsocks: http://crowdin.net/project/shadowsocks/invite
|
||||
|
||||
### LICENSE
|
||||
|
||||
Copyright (C) 2015 Max Lv <max.c.lv@gmail.com>
|
||||
Copyright (C) 2016 Max Lv <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
|
||||
|
||||
@@ -29,14 +29,15 @@ resolvers += "JRAF" at "http://JRAF.org/static/maven/2"
|
||||
libraryDependencies ++= Seq(
|
||||
"dnsjava" % "dnsjava" % "2.1.7",
|
||||
"com.github.kevinsawicki" % "http-request" % "6.0",
|
||||
"commons-net" % "commons-net" % "3.3",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201510071325",
|
||||
"commons-net" % "commons-net" % "3.4",
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201602011018",
|
||||
"com.google.zxing" % "android-integration" % "3.2.1",
|
||||
"net.glxn.qrgen" % "android" % "2.0",
|
||||
"com.google.android.gms" % "play-services-base" % "8.3.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "8.3.0",
|
||||
"com.google.android.gms" % "play-services-analytics" % "8.3.0",
|
||||
"com.google.android.gms" % "play-services-base" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-ads" % "8.4.0",
|
||||
"com.google.android.gms" % "play-services-analytics" % "8.4.0",
|
||||
"com.android.support" % "design" % "23.1.1",
|
||||
"com.android.support" % "gridlayout-v7" % "23.1.1",
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
|
||||
"com.j256.ormlite" % "ormlite-core" % "4.48",
|
||||
"com.j256.ormlite" % "ormlite-android" % "4.48",
|
||||
|
||||
@@ -13,43 +13,8 @@ def main():
|
||||
for line in sys.stdin:
|
||||
china_list_set.add(IPy.IP(line))
|
||||
|
||||
# 添加内网地址
|
||||
internal_list = IPy.IPSet(map(IPy.IP, [
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"112.124.47.0/24",
|
||||
"114.114.114.0/24",
|
||||
"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/4",
|
||||
"240.0.0.0/4",
|
||||
]))
|
||||
china_list_set += internal_list
|
||||
|
||||
all = china_list_set
|
||||
|
||||
# 取反
|
||||
# all = IPy.IPSet([IPy.IP("0.0.0.0/0")])
|
||||
# 剔除所有孤立的C段
|
||||
# for ip in china_list_set:
|
||||
# all.discard(ip)
|
||||
|
||||
# filter = itertools.ifilter(lambda x: len(x) <= 65536, all)
|
||||
# for ip in filter:
|
||||
# all.discard(ip)
|
||||
# all.add(IPy.IP(ip.strNormal(0)).make_net('255.255.0.0'))
|
||||
|
||||
# 输出结果
|
||||
for ip in all:
|
||||
for ip in china_list_set:
|
||||
print '<item>' + str(ip) + '</item>'
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
resolvers += Resolver.url("scalasbt releases", new URL("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-snapshots"))(Resolver.ivyStylePatterns)
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.11")
|
||||
|
||||
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.18")
|
||||
|
||||
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
|
||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.7.0-SNAPSHOT")
|
||||
|
||||
resolvers += Resolver.url("rtimush/sbt-plugin-snapshots", new URL("https://dl.bintray.com/rtimush/sbt-plugin-snapshots/"))(Resolver.ivyStylePatterns)
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.9-6-g5a7705c")
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.shadowsocks"
|
||||
android:versionCode="126"
|
||||
android:versionName="2.9.4">
|
||||
android:versionCode="133"
|
||||
android:versionName="2.9.11">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
@@ -109,14 +108,6 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".AppManagerReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
<data android:scheme="package"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".ShadowsocksReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
@@ -2,6 +2,5 @@ package com.github.shadowsocks.aidl;
|
||||
|
||||
interface IShadowsocksServiceCallback {
|
||||
oneway void stateChanged(int state, String msg);
|
||||
oneway void trafficUpdated(String txRate, String rxRate,
|
||||
String txTotal, String rxTotal);
|
||||
oneway void trafficUpdated(long txRate, long rxRate, long txTotal, long rxTotal);
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ img {
|
||||
</head>
|
||||
<body>
|
||||
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Scala.</p>
|
||||
<p>Copyright (C) 2014 Max Lv <a href="mailto:max.c.lv@gmail.com">max.c.lv@gmail.com</a></p>
|
||||
<p>Copyright (C) 2016 Max Lv <a href="mailto:max.c.lv@gmail.com">max.c.lv@gmail.com</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
|
||||
@@ -289,11 +289,11 @@ along with this program. If not, see <a href="http://www.gnu.org/licenses/">http
|
||||
<h3>Open Source Licenses</h3>
|
||||
<ul>
|
||||
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li>
|
||||
<li>tun2socks: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/badvpn/COPYING">BSD</a></li>
|
||||
<li>redsocks: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/redsocks/README">APL 2.0</a></li>
|
||||
<li>OpenSSL: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/openssl/NOTICE">OpenSSL</a></li>
|
||||
<li>tun2socks: <a href="https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING">BSD</a></li>
|
||||
<li>redsocks: <a href="https://github.com/shadowsocks/redsocks/blob/shadowsocks-android/README">APL 2.0</a></li>
|
||||
<li>OpenSSL: <a href="https://github.com/shadowsocks/openssl-android/blob/master/NOTICE">OpenSSL</a></li>
|
||||
<li>pdnsd: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/pdnsd/COPYING">GPLv3</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libev/LICENSE">GPLv2</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/libevent/blob/shadowsocks-android/LICENSE">GPLv2</a></li>
|
||||
<li>libevent: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libevent/LICENSE">BSD</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -45,6 +45,6 @@ public class System {
|
||||
|
||||
public static native int exec(String cmd);
|
||||
public static native String getABI();
|
||||
public static native int sendfd(int fd);
|
||||
public static native int sendfd(int fd, String path);
|
||||
public static native void jniclose(int fd);
|
||||
}
|
||||
|
||||
@@ -64,9 +64,10 @@ void Java_com_github_shadowsocks_system_jniclose(JNIEnv *env, jobject thiz, jint
|
||||
close(fd);
|
||||
}
|
||||
|
||||
jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint tun_fd) {
|
||||
jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint tun_fd, jstring path) {
|
||||
int fd;
|
||||
struct sockaddr_un addr;
|
||||
const char *sock_str = env->GetStringUTFChars(path, 0);
|
||||
|
||||
if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
||||
LOGE("socket() failed: %s (socket fd = %d)\n", strerror(errno), fd);
|
||||
@@ -75,7 +76,7 @@ jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint t
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, "/data/data/com.github.shadowsocks/sock_path", sizeof(addr.sun_path)-1);
|
||||
strncpy(addr.sun_path, sock_str, sizeof(addr.sun_path)-1);
|
||||
|
||||
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
LOGE("connect() failed: %s (fd = %d)\n", strerror(errno), fd);
|
||||
@@ -90,6 +91,7 @@ jint Java_com_github_shadowsocks_system_sendfd(JNIEnv *env, jobject thiz, jint t
|
||||
}
|
||||
|
||||
close(fd);
|
||||
env->ReleaseStringUTFChars(path, sock_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ static const char *classPathName = "com/github/shadowsocks/System";
|
||||
static JNINativeMethod method_table[] = {
|
||||
{ "jniclose", "(I)V",
|
||||
(void*) Java_com_github_shadowsocks_system_jniclose },
|
||||
{ "sendfd", "(I)I",
|
||||
{ "sendfd", "(ILjava/lang/String;)I",
|
||||
(void*) Java_com_github_shadowsocks_system_sendfd },
|
||||
{ "exec", "(Ljava/lang/String;)I",
|
||||
(void*) Java_com_github_shadowsocks_system_exec },
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h0.71C7.37,7.69 9.48,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3s-1.34,3 -3,3z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
|
||||
<path
|
||||
android:strokeColor="#fff"
|
||||
android:strokeWidth="2"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M0.71,2.12L21.88,23.29Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 13 10.17 L 16.58 7.16 L 13.57 10.75 L 14.99 12.16 L 17.68 8.96 L 16.59 13.76 L 18.26 15.43 L 21.25 2.28 L 10.45 7.62 L 13 10.17 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 15.93 15.93 L 12.71 14.89 L 13.7 13.7 L 12.28 12.28 L 9.26 15.89 L 17.55 18.55 L 17.74 17.74 L 15.93 15.93 Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M8.56,8.56L0,12.8l6.83,2.57,4.64-3.9ZM6.44,13.08L5,12.55l5-2.47Z" />
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 297 B |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 425 B |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 209 B |
|
Before Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 343 B |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 354 B |
|
Before Width: | Height: | Size: 593 B |
|
Before Width: | Height: | Size: 537 B |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 518 B |
|
After Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 517 B |
|
Before Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 785 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 660 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -19,7 +19,7 @@
|
||||
<android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="@color/material_blue_grey_700"
|
||||
android:src="@drawable/ic_cloud_off"
|
||||
android:src="@drawable/ic_start_idle"
|
||||
app:pressedTranslationZ="6dp" app:borderWidth="0dp" />
|
||||
</com.github.jorgecastilloprz.FABProgressCircle>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/widget_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="15dip"
|
||||
android:paddingRight="15dip"
|
||||
android:paddingTop="6dip"
|
||||
android:paddingBottom="6dip"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end" />
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.v7.widget.GridLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:columnCount="4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
app:layout_column="0"
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/sent" />
|
||||
<TextView
|
||||
app:layout_column="1"
|
||||
android:id="@+id/tx"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_horizontal" />
|
||||
<TextView
|
||||
app:layout_column="2"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="▲" />
|
||||
<TextView
|
||||
app:layout_column="3"
|
||||
android:id="@+id/txRate"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_horizontal"
|
||||
android:gravity="end" />
|
||||
|
||||
<TextView
|
||||
app:layout_row="1"
|
||||
app:layout_column="0"
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/received"/>
|
||||
<TextView
|
||||
app:layout_row="1"
|
||||
app:layout_column="1"
|
||||
android:id="@+id/rx"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_horizontal" />
|
||||
<TextView
|
||||
app:layout_row="1"
|
||||
app:layout_column="2"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="▼" />
|
||||
<TextView
|
||||
app:layout_row="1"
|
||||
app:layout_column="3"
|
||||
android:id="@+id/rxRate"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_gravity="fill_horizontal"
|
||||
android:gravity="end" />
|
||||
|
||||
</android.support.v7.widget.GridLayout>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="proxied_apps_summary">Set proxy for selected apps</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="proxied_apps_summary">为应用程序分别设置代理</string>
|
||||
</resources>
|
||||
@@ -26,7 +26,7 @@
|
||||
<string name="auto_set_gfwlist_summary">绕过所有位于中国的网站</string>
|
||||
<string name="route_list">路由</string>
|
||||
<string name="proxied_apps">分应用代理</string>
|
||||
<string name="proxied_apps_summary">为应用程序分别设置代理</string>
|
||||
<string name="proxied_apps_summary">为应用程序分别设置代理,在 4.x 下需要启用 NAT 模式</string>
|
||||
<string name="on">开</string>
|
||||
<string name="bypass_apps">绕行模式</string>
|
||||
<string name="bypass_apps_summary">启用该选项,以使所选应用程序的流量不经过代理</string>
|
||||
@@ -34,6 +34,7 @@
|
||||
<string name="auto_connect_summary">随系统启动后台服务</string>
|
||||
<string name="forward_success">后台服务已开始运行。</string>
|
||||
<string name="service_failed">无法连接远程服务器</string>
|
||||
<string name="nat_deprecated">警告:在 Android 5.0 及更高版本中不建议使用 NAT 模式</string>
|
||||
<string name="nat_no_root">NAT 模式需要 ROOT 权限</string>
|
||||
<string name="switch_to_vpn">切换到 VPN 模式</string>
|
||||
<string name="stop">停止服务</string>
|
||||
@@ -41,7 +42,7 @@
|
||||
<string name="vpn_error">后台服务启动失败:%s</string>
|
||||
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
|
||||
<string name="close">关闭</string>
|
||||
<string name="proxy_empty">代理服务器地址不能为空</string>
|
||||
<string name="proxy_empty">代理服务器地址及密码不能为空</string>
|
||||
<string name="connect">连接</string>
|
||||
<string name="initializing">正在初始化……</string>
|
||||
<string name="recovering">重置中……</string>
|
||||
@@ -75,16 +76,19 @@
|
||||
|
||||
<string name="stat">网络流量</string>
|
||||
<string name="stat_summary">发送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="other">字节</item>
|
||||
</plurals>
|
||||
<string name="connection_test_pending">点击检查是否接入互联网。</string>
|
||||
|
||||
<string name="connection_test_pending">检查网络连接</string>
|
||||
<string name="connection_test_testing">测试中……</string>
|
||||
<string name="connection_test_available">已接入互联网。(延时:%d 毫秒)</string>
|
||||
<string name="connection_test_available">连接有效: 延时 %d 毫秒</string>
|
||||
<string name="connection_test_fail">无互联网连接</string>
|
||||
<string name="connection_test_error">失败:%s</string>
|
||||
<string name="connection_test_error_status_code">状态码无效。(#%d)</string>
|
||||
<string name="connection_test_error_status_code">状态码无效(#%d)</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_manager_dialog">配置文件</string>
|
||||
<string name="profile_manager_dialog_content">• 点击选择配置文件;\n• 滑动删除;\n•
|
||||
长按拖动改变顺序。</string>
|
||||
<string name="gotcha">好的</string>
|
||||
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
|
||||
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
|
||||
<string name="add_profile_methods_manual_settings">手动设置</string>
|
||||
@@ -92,6 +96,7 @@
|
||||
<string-array name="route_entry">
|
||||
<item>全局</item>
|
||||
<item>绕过局域网地址</item>
|
||||
<item>绕过中国大陆地址</item>
|
||||
<item>绕过局域网及中国大陆地址</item>
|
||||
</string-array>
|
||||
|
||||
@@ -102,7 +107,10 @@
|
||||
<string name="action_import_msg">导入成功</string>
|
||||
<string name="action_import_err">导入失败</string>
|
||||
|
||||
<string name="removed">已删除</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">已删除</item>
|
||||
<item quantity="other">已删除 %d 项</item>
|
||||
</plurals>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="toggle_service_state">启动服务</string>
|
||||
<string name="start_service_default">连接到当前服务器</string>
|
||||
@@ -110,4 +118,8 @@
|
||||
<string name="stop_service">切换到 %s</string>
|
||||
<string name="profile_default">使用当前配置</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">发送:</string>
|
||||
<string name="received">接收:</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<string-array name="route_entry">
|
||||
<item>All</item>
|
||||
<item>Bypass LAN</item>
|
||||
<item>Bypass China</item>
|
||||
<item>Bypass LAN & China</item>
|
||||
</string-array>
|
||||
|
||||
@@ -52,6 +53,7 @@
|
||||
<item>all</item>
|
||||
<item>bypass-lan</item>
|
||||
<item>bypass-china</item>
|
||||
<item>bypass-lan-china</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="private_route">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="chn_route_full">
|
||||
<item>0.0.0.0/8</item>
|
||||
<string-array name="chn_route">
|
||||
<item>1.0.1.0/24</item>
|
||||
<item>1.0.2.0/23</item>
|
||||
<item>1.0.8.0/21</item>
|
||||
@@ -49,7 +48,6 @@
|
||||
<item>1.192.0.0/13</item>
|
||||
<item>1.202.0.0/15</item>
|
||||
<item>1.204.0.0/14</item>
|
||||
<item>10.0.0.0/8</item>
|
||||
<item>14.0.0.0/21</item>
|
||||
<item>14.0.12.0/22</item>
|
||||
<item>14.1.0.0/22</item>
|
||||
@@ -771,7 +769,6 @@
|
||||
<item>61.236.0.0/15</item>
|
||||
<item>61.240.0.0/14</item>
|
||||
<item>91.234.36.0/24</item>
|
||||
<item>100.64.0.0/10</item>
|
||||
<item>101.0.0.0/22</item>
|
||||
<item>101.1.0.0/22</item>
|
||||
<item>101.2.172.0/22</item>
|
||||
@@ -2270,7 +2267,6 @@
|
||||
<item>125.215.0.0/18</item>
|
||||
<item>125.216.0.0/13</item>
|
||||
<item>125.254.128.0/17</item>
|
||||
<item>127.0.0.0/8</item>
|
||||
<item>137.59.88.0/22</item>
|
||||
<item>139.9.0.0/16</item>
|
||||
<item>139.129.0.0/16</item>
|
||||
@@ -2386,7 +2382,6 @@
|
||||
<item>167.189.0.0/16</item>
|
||||
<item>167.220.244.0/22</item>
|
||||
<item>168.160.0.0/16</item>
|
||||
<item>169.254.0.0/16</item>
|
||||
<item>171.8.0.0/13</item>
|
||||
<item>171.34.0.0/15</item>
|
||||
<item>171.36.0.0/14</item>
|
||||
@@ -2395,7 +2390,6 @@
|
||||
<item>171.104.0.0/13</item>
|
||||
<item>171.112.0.0/12</item>
|
||||
<item>171.208.0.0/12</item>
|
||||
<item>172.16.0.0/12</item>
|
||||
<item>175.0.0.0/12</item>
|
||||
<item>175.16.0.0/13</item>
|
||||
<item>175.24.0.0/14</item>
|
||||
@@ -2507,14 +2501,8 @@
|
||||
<item>183.182.0.0/19</item>
|
||||
<item>183.184.0.0/13</item>
|
||||
<item>183.192.0.0/10</item>
|
||||
<item>192.0.0.0/29</item>
|
||||
<item>192.0.2.0/24</item>
|
||||
<item>192.88.99.0/24</item>
|
||||
<item>192.124.154.0/24</item>
|
||||
<item>192.168.0.0/16</item>
|
||||
<item>192.188.170.0/24</item>
|
||||
<item>198.18.0.0/15</item>
|
||||
<item>198.51.100.0/24</item>
|
||||
<item>202.0.100.0/23</item>
|
||||
<item>202.0.122.0/23</item>
|
||||
<item>202.0.176.0/22</item>
|
||||
@@ -2910,7 +2898,6 @@
|
||||
<item>203.0.90.0/23</item>
|
||||
<item>203.0.96.0/23</item>
|
||||
<item>203.0.104.0/21</item>
|
||||
<item>203.0.113.0/24</item>
|
||||
<item>203.0.114.0/23</item>
|
||||
<item>203.0.122.0/24</item>
|
||||
<item>203.0.128.0/24</item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<color name="material_green_a700">#64DD17</color>
|
||||
<color name="material_green_700">#388E3C</color>
|
||||
<color name="material_blue_grey_300">#90A4AE</color>
|
||||
<color name="material_blue_grey_500">#607D8B</color>
|
||||
<color name="material_blue_grey_700">#455A64</color>
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
<item quantity="one">Byte</item>
|
||||
<item quantity="other">Bytes</item>
|
||||
</plurals>
|
||||
<string name="connection_test_pending">Tap to check Internet connection availability.</string>
|
||||
<string name="connection_test_pending">Check Connectivity</string>
|
||||
<string name="connection_test_testing">Testing…</string>
|
||||
<string name="connection_test_available">Internet connection available. (latency: %dms)</string>
|
||||
<string name="connection_test_error">Fail: %s</string>
|
||||
<string name="connection_test_error_status_code">Invalid status code. (#%d)</string>
|
||||
<string name="connection_test_available">Success: %dms latency</string>
|
||||
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
||||
<string name="connection_test_fail">Internet Unavailable</string>
|
||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
||||
|
||||
<!-- proxy category -->
|
||||
<string name="proxy_cat">Server Settings</string>
|
||||
@@ -53,7 +54,7 @@
|
||||
</string>
|
||||
<string name="route_list">Route</string>
|
||||
<string name="proxied_apps">Per-App Proxy</string>
|
||||
<string name="proxied_apps_summary">Set proxy for selected apps</string>
|
||||
<string name="proxied_apps_summary">Set proxy for selected apps, requires NAT mode under Android 4.x</string>
|
||||
<string name="on">On</string>
|
||||
<string name="bypass_apps">Bypass Mode</string>
|
||||
<string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
|
||||
@@ -65,6 +66,7 @@
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">Shadowsocks started.</string>
|
||||
<string name="service_failed">Failed to connect the remote server</string>
|
||||
<string name="nat_deprecated">WARNING: NAT mode has been deprecated since Android 5.0</string>
|
||||
<string name="nat_no_root">NAT mode requires ROOT permission</string>
|
||||
<string name="switch_to_vpn">Switch to VPN mode</string>
|
||||
<string name="stop">Stop the service</string>
|
||||
@@ -74,7 +76,7 @@
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">Close</string>
|
||||
<string name="proxy_empty">Proxy should not be empty</string>
|
||||
<string name="proxy_empty">Proxy/Password should not be empty</string>
|
||||
<string name="connect">Connect</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="recovering">Resetting…</string>
|
||||
@@ -103,10 +105,17 @@
|
||||
<string name="action_import_err">Failed to import.</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_manager_dialog">Profiles</string>
|
||||
<string name="profile_manager_dialog_content">• Tap to select a profile;\n• Swipe to remove;\n•
|
||||
Long press to reorder profiles.</string>
|
||||
<string name="gotcha">Gotcha!</string>
|
||||
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
|
||||
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
|
||||
<string name="add_profile_methods_manual_settings">Manual Settings</string>
|
||||
<string name="removed">Removed</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">Removed</item>
|
||||
<item quantity="other">%d items removed</item>
|
||||
</plurals>
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
<!-- tasker -->
|
||||
@@ -115,4 +124,8 @@
|
||||
<string name="start_service">Connect to %s</string>
|
||||
<string name="stop_service">Switch to %s</string>
|
||||
<string name="profile_default">Use the current profile</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">Sent:</string>
|
||||
<string name="received">Received:</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
<intent android:action="com.github.shadowsocks.ProfileManagerActivity"/>
|
||||
</Preference>
|
||||
|
||||
<Preference android:key="stat" android:title="@string/stat"/>
|
||||
<com.github.shadowsocks.preferences.StatusPreference
|
||||
android:key="stat"
|
||||
android:summary="@string/connection_test_pending"
|
||||
android:title="@string/stat"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
@@ -39,8 +39,11 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.content._
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.{ClipData, ClipboardManager, Context}
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.{Bundle, Handler}
|
||||
@@ -54,7 +57,6 @@ import android.view._
|
||||
import android.widget.AbsListView.OnScrollListener
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget._
|
||||
import android.Manifest.permission
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
@@ -65,13 +67,31 @@ object AppManager {
|
||||
case class ProxiedApp(name: String, packageName: String, icon: Drawable)
|
||||
private case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
|
||||
|
||||
var cachedApps: Array[ProxiedApp] = _
|
||||
private var instance: AppManager = _
|
||||
|
||||
private var receiverRegistered: Boolean = _
|
||||
private var cachedApps: Array[ProxiedApp] = _
|
||||
private def getApps(pm: PackageManager) = {
|
||||
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
|
||||
.map(p => new ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
|
||||
p.applicationInfo.loadIcon(pm))).toArray
|
||||
cachedApps
|
||||
if (!receiverRegistered) {
|
||||
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
||||
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
filter.addDataScheme("package")
|
||||
ShadowsocksApplication.instance.registerReceiver((context: Context, intent: Intent) =>
|
||||
if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
|
||||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
|
||||
synchronized(cachedApps = null)
|
||||
val instance = AppManager.instance
|
||||
if (instance != null) instance.reloadApps()
|
||||
}, filter)
|
||||
receiverRegistered = true
|
||||
}
|
||||
synchronized {
|
||||
if (cachedApps == null) cachedApps = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||
.filter(p => p.requestedPermissions != null && p.requestedPermissions.contains(permission.INTERNET))
|
||||
.map(p => new ProxiedApp(pm.getApplicationLabel(p.applicationInfo).toString, p.packageName,
|
||||
p.applicationInfo.loadIcon(pm))).toArray
|
||||
cachedApps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,43 +106,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
private var loadingView: View = _
|
||||
private var overlay: TextView = _
|
||||
private var adapter: ListAdapter = _
|
||||
@volatile private var appsLoading: Boolean = _
|
||||
|
||||
def loadApps() {
|
||||
appsLoading = true
|
||||
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
|
||||
apps = getApps(getPackageManager).sortWith((a, b) => {
|
||||
val aProxied = proxiedApps.contains(a.packageName)
|
||||
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
|
||||
})
|
||||
adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) {
|
||||
override def getView(position: Int, view: View, parent: ViewGroup): View = {
|
||||
var convertView = view
|
||||
var entry: ListEntry = null
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false)
|
||||
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
|
||||
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
|
||||
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
|
||||
convertView.setOnClickListener(AppManager.this)
|
||||
convertView.setTag(entry)
|
||||
entry.switch.setOnCheckedChangeListener(AppManager.this)
|
||||
} else {
|
||||
entry = convertView.getTag.asInstanceOf[ListEntry]
|
||||
}
|
||||
|
||||
val app: ProxiedApp = apps(position)
|
||||
|
||||
entry.text.setText(app.name)
|
||||
entry.icon.setImageDrawable(app.icon)
|
||||
val switch = entry.switch
|
||||
switch.setTag(app)
|
||||
switch.setChecked(proxiedApps.contains(app.packageName))
|
||||
entry.text.setTag(switch)
|
||||
convertView
|
||||
}
|
||||
}
|
||||
}
|
||||
private val appsLoading = new AtomicBoolean
|
||||
|
||||
private def setProxied(pn: String, proxied: Boolean) = if (proxied) proxiedApps.add(pn) else proxiedApps.remove(pn)
|
||||
|
||||
@@ -145,6 +129,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
|
||||
override def onDestroy() {
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
if (handler != null) {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
@@ -186,7 +171,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
// Restart activity
|
||||
appListView.setVisibility(View.GONE)
|
||||
loadingView.setVisibility(View.VISIBLE)
|
||||
if (appsLoading) appsLoading = false else loadAppsAsync()
|
||||
reloadApps()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -252,13 +237,49 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
}
|
||||
})
|
||||
instance = this
|
||||
loadAppsAsync()
|
||||
}
|
||||
|
||||
def reloadApps() = if (!appsLoading.compareAndSet(true, false)) loadAppsAsync()
|
||||
def loadAppsAsync() {
|
||||
if (!appsLoading.compareAndSet(false, true)) return
|
||||
ThrowableFuture {
|
||||
while (!appsLoading) loadApps()
|
||||
appsLoading = false
|
||||
do {
|
||||
proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet]
|
||||
appsLoading.set(true)
|
||||
apps = getApps(getPackageManager).sortWith((a, b) => {
|
||||
val aProxied = proxiedApps.contains(a.packageName)
|
||||
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
|
||||
})
|
||||
adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) {
|
||||
override def getView(position: Int, view: View, parent: ViewGroup): View = {
|
||||
var convertView = view
|
||||
var entry: ListEntry = null
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false)
|
||||
entry = new ListEntry(convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch],
|
||||
convertView.findViewById(R.id.itemtext).asInstanceOf[TextView],
|
||||
convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView])
|
||||
convertView.setOnClickListener(AppManager.this)
|
||||
convertView.setTag(entry)
|
||||
entry.switch.setOnCheckedChangeListener(AppManager.this)
|
||||
} else {
|
||||
entry = convertView.getTag.asInstanceOf[ListEntry]
|
||||
}
|
||||
|
||||
val app: ProxiedApp = apps(position)
|
||||
|
||||
entry.text.setText(app.name)
|
||||
entry.icon.setImageDrawable(app.icon)
|
||||
val switch = entry.switch
|
||||
switch.setTag(app)
|
||||
switch.setChecked(proxiedApps.contains(app.packageName))
|
||||
entry.text.setTag(switch)
|
||||
convertView
|
||||
}
|
||||
}
|
||||
} while (!appsLoading.compareAndSet(true, false))
|
||||
handler.post(() => {
|
||||
appListView.setAdapter(adapter)
|
||||
Utils.crossFade(AppManager.this, loadingView, appListView)
|
||||
@@ -267,7 +288,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
|
||||
}
|
||||
|
||||
def saveAppSettings(context: Context) {
|
||||
if (!appsLoading) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
|
||||
if (!appsLoading.get) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply
|
||||
}
|
||||
|
||||
var handler: Handler = null
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{Intent, Context, BroadcastReceiver}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class AppManagerReceiver extends BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent) = if (intent.getAction != Intent.ACTION_PACKAGE_REMOVED ||
|
||||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) AppManager.synchronized(AppManager.cachedApps = null)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ package com.github.shadowsocks
|
||||
import java.util.{Timer, TimerTask}
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.{Intent, Context}
|
||||
import android.os.{Handler, RemoteCallbackList}
|
||||
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
|
||||
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
|
||||
@@ -50,13 +50,13 @@ import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread
|
||||
trait BaseService extends Service {
|
||||
|
||||
@volatile private var state = State.STOPPED
|
||||
@volatile private var callbackCount = 0
|
||||
@volatile protected var config: Config = null
|
||||
|
||||
var timer: Timer = null
|
||||
var trafficMonitorThread: TrafficMonitorThread = null
|
||||
|
||||
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
|
||||
var callbacksCount: Int = _
|
||||
|
||||
val binder = new IShadowsocksService.Stub {
|
||||
override def getMode: Int = {
|
||||
@@ -68,34 +68,29 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
override def unregisterCallback(cb: IShadowsocksServiceCallback) {
|
||||
if (cb != null ) {
|
||||
callbacks.unregister(cb)
|
||||
callbackCount -= 1
|
||||
}
|
||||
if (callbackCount == 0 && timer != null) {
|
||||
timer.cancel()
|
||||
timer = null
|
||||
}
|
||||
if (callbackCount == 0 && state == State.STOPPED) {
|
||||
stopBackgroundService()
|
||||
if (cb != null && callbacks.unregister(cb)) {
|
||||
callbacksCount -= 1
|
||||
if (callbacksCount == 0 && timer != null) {
|
||||
timer.cancel()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def registerCallback(cb: IShadowsocksServiceCallback) {
|
||||
if (cb != null) {
|
||||
if (callbackCount == 0 && timer == null) {
|
||||
if (cb != null && callbacks.register(cb)) {
|
||||
callbacksCount += 1
|
||||
if (callbacksCount != 0 && timer == null) {
|
||||
val task = new TimerTask {
|
||||
def run {
|
||||
TrafficMonitor.updateRate()
|
||||
updateTrafficRate(TrafficMonitor.getTxRate, TrafficMonitor.getRxRate,
|
||||
TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal)
|
||||
if (TrafficMonitor.updateRate()) updateTrafficRate()
|
||||
}
|
||||
}
|
||||
timer = new Timer(true)
|
||||
timer.schedule(task, 1000, 1000)
|
||||
}
|
||||
callbacks.register(cb)
|
||||
callbackCount += 1
|
||||
TrafficMonitor.updateRate()
|
||||
cb.trafficUpdated(TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,45 +110,46 @@ trait BaseService extends Service {
|
||||
def startRunner(config: Config) {
|
||||
this.config = config
|
||||
|
||||
startService(new Intent(getContext, getClass))
|
||||
TrafficMonitor.reset()
|
||||
trafficMonitorThread = new TrafficMonitorThread()
|
||||
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
|
||||
trafficMonitorThread.start()
|
||||
}
|
||||
|
||||
def stopRunner() {
|
||||
// Make sure update total traffic when stopping the runner
|
||||
updateTrafficTotal(TrafficMonitor.getDeltaTx, TrafficMonitor.getDeltaRx)
|
||||
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
|
||||
TrafficMonitor.reset()
|
||||
if (trafficMonitorThread != null) {
|
||||
trafficMonitorThread.stopThread()
|
||||
trafficMonitorThread = null
|
||||
}
|
||||
|
||||
// change the state
|
||||
changeState(State.STOPPED)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
def updateTrafficTotal(tx: Long, rx: Long) {
|
||||
val handler = new Handler(getContext.getMainLooper)
|
||||
handler.post(() => {
|
||||
if (config != null) {
|
||||
ShadowsocksApplication.profileManager.getProfile(config.profileId) match {
|
||||
case Some(profile) =>
|
||||
profile.tx += tx
|
||||
profile.rx += rx
|
||||
ShadowsocksApplication.profileManager.updateProfile(profile)
|
||||
case None => // Ignore
|
||||
}
|
||||
val config = this.config // avoid race conditions without locking
|
||||
if (config != null) {
|
||||
ShadowsocksApplication.profileManager.getProfile(config.profileId) match {
|
||||
case Some(profile) =>
|
||||
profile.tx += tx
|
||||
profile.rx += rx
|
||||
ShadowsocksApplication.profileManager.updateProfile(profile)
|
||||
case None => // Ignore
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def stopBackgroundService()
|
||||
def getServiceMode: Int
|
||||
def getTag: String
|
||||
def getContext: Context
|
||||
|
||||
def getCallbackCount: Int = {
|
||||
callbackCount
|
||||
}
|
||||
def getState: Int = {
|
||||
state
|
||||
}
|
||||
@@ -161,10 +157,14 @@ trait BaseService extends Service {
|
||||
changeState(s, null)
|
||||
}
|
||||
|
||||
def updateTrafficRate(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
def updateTrafficRate() {
|
||||
val handler = new Handler(getContext.getMainLooper)
|
||||
handler.post(() => {
|
||||
if (callbackCount > 0) {
|
||||
if (callbacksCount > 0) {
|
||||
val txRate = TrafficMonitor.txRate
|
||||
val rxRate = TrafficMonitor.rxRate
|
||||
val txTotal = TrafficMonitor.txTotal
|
||||
val rxTotal = TrafficMonitor.rxTotal
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i <- 0 until n) {
|
||||
try {
|
||||
@@ -178,10 +178,13 @@ trait BaseService extends Service {
|
||||
})
|
||||
}
|
||||
|
||||
// Service of shadowsocks should always be started explicitly
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
|
||||
|
||||
protected def changeState(s: Int, msg: String) {
|
||||
val handler = new Handler(getContext.getMainLooper)
|
||||
handler.post(() => if (state != s) {
|
||||
if (callbackCount > 0) {
|
||||
if (callbacksCount > 0) {
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i <- 0 until n) {
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.{Handler, Bundle}
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.support.v7.app.{AlertDialog, AppCompatActivity}
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
@@ -11,12 +10,12 @@ import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
|
||||
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
|
||||
import android.text.style.TextAppearanceSpan
|
||||
import android.text.{SpannableStringBuilder, Spanned}
|
||||
import android.view.View.{OnAttachStateChangeListener, OnClickListener}
|
||||
import android.view.{LayoutInflater, MenuItem, View, ViewGroup}
|
||||
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils.{Parser, TrafficMonitor, Utils}
|
||||
import com.github.shadowsocks.widget.UndoSnackbarManager
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import net.glxn.qrgen.android.QRCode
|
||||
|
||||
@@ -25,9 +24,15 @@ import scala.collection.mutable.ArrayBuffer
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object ProfileManagerActivity {
|
||||
private final val profileTip = "profileTip"
|
||||
}
|
||||
|
||||
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext {
|
||||
import ProfileManagerActivity._
|
||||
|
||||
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
private var item: Profile = _
|
||||
var item: Profile = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
@@ -55,17 +60,15 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
})
|
||||
}
|
||||
|
||||
def updateText(refetch: Boolean = false) {
|
||||
def updateText(txTotal: Long = 0, rxTotal: Long = 0) {
|
||||
val builder = new SpannableStringBuilder
|
||||
val item = if (refetch) ShadowsocksApplication.profileManager.getProfile(this.item.id) match {
|
||||
case Some(profile) => profile
|
||||
case None => return
|
||||
} else this.item
|
||||
val tx = item.tx + txTotal
|
||||
val rx = item.rx + rxTotal
|
||||
builder.append(item.name)
|
||||
if (item.tx != 0 || item.rx != 0) {
|
||||
if (tx != 0 || rx != 0) {
|
||||
val start = builder.length
|
||||
builder.append(getString(R.string.stat_profiles,
|
||||
TrafficMonitor.formatTraffic(item.tx), TrafficMonitor.formatTraffic(item.rx)))
|
||||
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
|
||||
builder.setSpan(new TextAppearanceSpan(ProfileManagerActivity.this, android.R.style.TextAppearance_Small),
|
||||
start + 1, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
@@ -91,8 +94,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
}
|
||||
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
private val recycleBin = new ArrayBuffer[(Int, Profile)]
|
||||
private var profiles = new ArrayBuffer[Profile]
|
||||
var profiles = new ArrayBuffer[Profile]
|
||||
profiles ++= ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
|
||||
def getItemCount = profiles.length
|
||||
@@ -103,31 +105,42 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
|
||||
|
||||
def add(item: Profile) {
|
||||
removedSnackbar.dismiss
|
||||
commitRemoves
|
||||
undoManager.flush
|
||||
val pos = getItemCount
|
||||
profiles += item
|
||||
notifyItemInserted(pos)
|
||||
}
|
||||
|
||||
def move(from: Int, to: Int) {
|
||||
undoManager.flush
|
||||
val step = if (from < to) 1 else -1
|
||||
val first = profiles(from)
|
||||
var previousOrder = profiles(from).userOrder
|
||||
for (i <- from until to by step) {
|
||||
val next = profiles(i + step)
|
||||
val order = next.userOrder
|
||||
next.userOrder = previousOrder
|
||||
previousOrder = order
|
||||
profiles(i) = next
|
||||
ShadowsocksApplication.profileManager.updateProfile(next)
|
||||
}
|
||||
first.userOrder = previousOrder
|
||||
profiles(to) = first
|
||||
ShadowsocksApplication.profileManager.updateProfile(first)
|
||||
notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
def remove(pos: Int) {
|
||||
recycleBin.append((pos, profiles(pos)))
|
||||
profiles.remove(pos)
|
||||
notifyItemRemoved(pos)
|
||||
}
|
||||
def undoRemoves {
|
||||
for ((index, item) <- recycleBin.reverseIterator) {
|
||||
profiles.insert(index, item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
recycleBin.clear
|
||||
def undo(actions: Iterator[(Int, Profile)]) = for ((index, item) <- actions) {
|
||||
profiles.insert(index, item)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
def commitRemoves {
|
||||
for ((index, item) <- recycleBin) {
|
||||
ShadowsocksApplication.profileManager.delProfile(item.id)
|
||||
if (item.id == ShadowsocksApplication.profileId) ShadowsocksApplication.profileId(-1)
|
||||
}
|
||||
recycleBin.clear
|
||||
def commit(actions: Iterator[(Int, Profile)]) = for ((index, item) <- actions) {
|
||||
ShadowsocksApplication.profileManager.delProfile(item.id)
|
||||
if (item.id == ShadowsocksApplication.profileId) ShadowsocksApplication.profileId(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +148,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
private val handler = new Handler
|
||||
|
||||
private lazy val profilesAdapter = new ProfilesAdapter
|
||||
private var removedSnackbar: Snackbar = _
|
||||
private var undoManager: UndoSnackbarManager[Profile] = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -153,35 +166,54 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
|
||||
|
||||
ShadowsocksApplication.profileManager.setProfileAddedListener(profilesAdapter.add)
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
profilesList.setLayoutManager(new LinearLayoutManager(this))
|
||||
val layoutManager = new LinearLayoutManager(this)
|
||||
profilesList.setLayoutManager(layoutManager)
|
||||
profilesList.setItemAnimator(new DefaultItemAnimator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
removedSnackbar = Snackbar.make(profilesList, R.string.removed, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo, ((v: View) => profilesAdapter.undoRemoves): OnClickListener)
|
||||
removedSnackbar.getView.addOnAttachStateChangeListener(new OnAttachStateChangeListener {
|
||||
def onViewDetachedFromWindow(v: View) = profilesAdapter.commitRemoves
|
||||
def onViewAttachedToWindow(v: View) = ()
|
||||
})
|
||||
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
||||
case (profile, i) if profile.id == ShadowsocksApplication.profileId => i
|
||||
}.getOrElse(-1))
|
||||
undoManager = new UndoSnackbarManager[Profile](profilesList, profilesAdapter.undo, profilesAdapter.commit)
|
||||
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||
def onSwiped(viewHolder: ViewHolder, direction: Int) = {
|
||||
profilesAdapter.remove(viewHolder.getAdapterPosition)
|
||||
removedSnackbar.show
|
||||
val index = viewHolder.getAdapterPosition
|
||||
profilesAdapter.remove(index)
|
||||
undoManager.remove(index, viewHolder.asInstanceOf[ProfileViewHolder].item)
|
||||
}
|
||||
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = {
|
||||
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
|
||||
true
|
||||
}
|
||||
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = false // TODO?
|
||||
}).attachToRecyclerView(profilesList)
|
||||
|
||||
attachService(new IShadowsocksServiceCallback.Stub {
|
||||
def stateChanged(state: Int, msg: String) = () // ignore
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) =
|
||||
if (selectedItem != null) selectedItem.updateText(true)
|
||||
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) =
|
||||
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
|
||||
})
|
||||
|
||||
if (ShadowsocksApplication.settings.getBoolean(profileTip, true)) {
|
||||
ShadowsocksApplication.settings.edit.putBoolean(profileTip, false).commit
|
||||
new AlertDialog.Builder(this).setTitle(R.string.profile_manager_dialog)
|
||||
.setMessage(R.string.profile_manager_dialog_content).setPositiveButton(R.string.gotcha, null).create.show
|
||||
}
|
||||
}
|
||||
|
||||
override def onStart() {
|
||||
super.onStart()
|
||||
registerCallback
|
||||
}
|
||||
override def onStop() {
|
||||
super.onStop()
|
||||
unregisterCallback
|
||||
}
|
||||
|
||||
override def onDestroy {
|
||||
deattachService()
|
||||
super.onDestroy
|
||||
undoManager.flush
|
||||
ShadowsocksApplication.profileManager.setProfileAddedListener(null)
|
||||
profilesAdapter.commitRemoves
|
||||
super.onDestroy
|
||||
}
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||
|
||||
@@ -8,25 +8,17 @@ import com.github.shadowsocks.utils.Action
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
trait ServiceBoundContext extends Context {
|
||||
|
||||
trait ServiceBoundContext extends Context { outer =>
|
||||
class ShadowsocksServiceConnection extends ServiceConnection {
|
||||
override def onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
bgService = IShadowsocksService.Stub.asInterface(service)
|
||||
if (callback != null) try {
|
||||
bgService.registerCallback(callback)
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
bgService.asBinder().linkToDeath(new ShadowsocksDeathRecipient(outer), 0)
|
||||
registerCallback
|
||||
ServiceBoundContext.this.onServiceConnected()
|
||||
}
|
||||
override def onServiceDisconnected(name: ComponentName) {
|
||||
if (callback != null) {
|
||||
try {
|
||||
if (bgService != null) bgService.unregisterCallback(callback)
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
unregisterCallback
|
||||
callback = null
|
||||
}
|
||||
ServiceBoundContext.this.onServiceDisconnected()
|
||||
@@ -34,11 +26,26 @@ trait ServiceBoundContext extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
def registerCallback = if (bgService != null && callback != null && !callbackRegistered) try {
|
||||
bgService.registerCallback(callback)
|
||||
callbackRegistered = true
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
|
||||
def unregisterCallback = if (bgService != null && callback != null && callbackRegistered) try {
|
||||
bgService.unregisterCallback(callback)
|
||||
callbackRegistered = false
|
||||
} catch {
|
||||
case ignored: RemoteException => callbackRegistered = false
|
||||
}
|
||||
|
||||
def onServiceConnected() = ()
|
||||
def onServiceDisconnected() = ()
|
||||
|
||||
private var callback: IShadowsocksServiceCallback.Stub = _
|
||||
private var connection: ShadowsocksServiceConnection = _
|
||||
private var callbackRegistered: Boolean = _
|
||||
|
||||
// Variables
|
||||
var bgService: IShadowsocksService = _
|
||||
@@ -54,19 +61,14 @@ trait ServiceBoundContext extends Context {
|
||||
|
||||
connection = new ShadowsocksServiceConnection()
|
||||
bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
|
||||
startService(new Intent(this, s))
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
def deattachService() {
|
||||
if (bgService != null) {
|
||||
if (callback != null) {
|
||||
try {
|
||||
bgService.unregisterCallback(callback)
|
||||
} catch {
|
||||
case ignored: RemoteException => // Nothing
|
||||
}
|
||||
unregisterCallback
|
||||
callback = null
|
||||
}
|
||||
if (connection != null) {
|
||||
|
||||
@@ -49,7 +49,6 @@ import android.content.res.AssetManager
|
||||
import android.graphics.Typeface
|
||||
import android.net.VpnService
|
||||
import android.os._
|
||||
import android.preference.{Preference, SwitchPreference}
|
||||
import android.support.design.widget.{FloatingActionButton, Snackbar}
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
@@ -60,7 +59,6 @@ import android.widget._
|
||||
import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database._
|
||||
import com.github.shadowsocks.preferences.{DropDownPreference, NumberPickerPreference, PasswordEditTextPreference, SummaryEditTextPreference}
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
|
||||
|
||||
@@ -91,52 +89,9 @@ object Shadowsocks {
|
||||
// Constants
|
||||
val TAG = "Shadowsocks"
|
||||
val REQUEST_CONNECT = 1
|
||||
|
||||
val PREFS_NAME = "Shadowsocks"
|
||||
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey, Key.encMethod,
|
||||
Key.isAuth)
|
||||
val FEATURE_PREFS = Array(Key.route, Key.isProxyApps, Key.isUdpDns, Key.isIpv6)
|
||||
val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL,
|
||||
Executable.TUN2SOCKS)
|
||||
|
||||
// Helper functions
|
||||
def updateDropDownPreference(pref: Preference, value: String) {
|
||||
pref.asInstanceOf[DropDownPreference].setValue(value)
|
||||
}
|
||||
|
||||
def updatePasswordEditTextPreference(pref: Preference, value: String) {
|
||||
pref.setSummary(value)
|
||||
pref.asInstanceOf[PasswordEditTextPreference].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.profileName => updateSummaryEditTextPreference(pref, profile.name)
|
||||
case Key.proxy => updateSummaryEditTextPreference(pref, profile.host)
|
||||
case Key.remotePort => updateNumberPickerPreference(pref, profile.remotePort)
|
||||
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
|
||||
case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password)
|
||||
case Key.encMethod => updateDropDownPreference(pref, profile.method)
|
||||
case Key.route => updateDropDownPreference(pref, profile.route)
|
||||
case Key.isProxyApps => updateSwitchPreference(pref, profile.proxyApps)
|
||||
case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns)
|
||||
case Key.isAuth => updateSwitchPreference(pref, profile.auth)
|
||||
case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Shadowsocks
|
||||
@@ -149,11 +104,8 @@ class Shadowsocks
|
||||
var progressDialog: ProgressDialog = _
|
||||
var progressTag = -1
|
||||
var state = State.STOPPED
|
||||
var prepared = false
|
||||
var currentProfile = new Profile
|
||||
var vpnEnabled = -1
|
||||
var trafficCache: String = _
|
||||
var connectionTestResult: String = _
|
||||
|
||||
// Services
|
||||
var currentServiceName = classOf[ShadowsocksNatService].getName
|
||||
@@ -163,23 +115,23 @@ class Shadowsocks
|
||||
s match {
|
||||
case State.CONNECTING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
fab.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
setPreferenceEnabled(enabled = false)
|
||||
preferences.setEnabled(false)
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
if (state == State.CONNECTING) {
|
||||
fabProgressCircle.beginFinalAnimation()
|
||||
} else {
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fabProgressCircle.postDelayed(hideCircle, 1000)
|
||||
}
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = true)
|
||||
setPreferenceEnabled(enabled = false)
|
||||
preferences.setEnabled(false)
|
||||
case State.STOPPED =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
|
||||
fabProgressCircle.postDelayed(hideCircle, 1000)
|
||||
fab.setEnabled(true)
|
||||
changeSwitch(checked = false)
|
||||
if (m != null) {
|
||||
@@ -188,40 +140,39 @@ class Shadowsocks
|
||||
if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn,
|
||||
(_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
|
||||
snackbar.show
|
||||
Log.e(Shadowsocks.TAG, "Error to start VPN service: " + m)
|
||||
}
|
||||
setPreferenceEnabled(enabled = true)
|
||||
preferences.setEnabled(true)
|
||||
case State.STOPPING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
fab.setEnabled(false)
|
||||
if (state == State.CONNECTED) fabProgressCircle.show() // ignore for stopped
|
||||
setPreferenceEnabled(enabled = false)
|
||||
preferences.setEnabled(false)
|
||||
}
|
||||
state = s
|
||||
})
|
||||
}
|
||||
def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
trafficCache = getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal)
|
||||
handler.post(updateTraffic)
|
||||
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
|
||||
}
|
||||
}
|
||||
|
||||
def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(TrafficMonitor.getTxRate,
|
||||
TrafficMonitor.getRxRate, TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal) else {
|
||||
if (connectionTestResult == null) connectionTestResult = getString(R.string.connection_test_pending)
|
||||
if (preferences.natSwitch.isChecked) {
|
||||
preferences.stat.setSummary(trafficCache)
|
||||
} else {
|
||||
preferences.stat.setSummary(connectionTestResult + "\n" + trafficCache)
|
||||
}
|
||||
}
|
||||
def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) = preferences.stat
|
||||
.setRate(TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal),
|
||||
TrafficMonitor.formatTraffic(txRate) + "/s", TrafficMonitor.formatTraffic(rxRate) + "/s")
|
||||
|
||||
def attachService: Unit = attachService(callback)
|
||||
|
||||
override def onServiceConnected() {
|
||||
// Update the UI
|
||||
if (fab != null) fab.setEnabled(true)
|
||||
stateUpdate()
|
||||
updateState()
|
||||
if (Build.VERSION.SDK_INT >= 21 && !ShadowsocksApplication.isVpnEnabled) {
|
||||
val snackbar = Snackbar.make(findViewById(android.R.id.content), R.string.nat_deprecated, Snackbar.LENGTH_LONG)
|
||||
snackbar.setAction(R.string.switch_to_vpn, (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
|
||||
snackbar.show
|
||||
}
|
||||
|
||||
if (!ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)) {
|
||||
ShadowsocksApplication.settings.edit.putBoolean(ShadowsocksApplication.getVersionName, true).apply()
|
||||
@@ -255,13 +206,13 @@ class Shadowsocks
|
||||
getFragmentManager.findFragmentById(android.R.id.content).asInstanceOf[ShadowsocksSettings]
|
||||
private var adView: AdView = _
|
||||
private lazy val greyTint = ContextCompat.getColorStateList(this, R.color.material_blue_grey_700)
|
||||
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_a700)
|
||||
private lazy val greenTint = ContextCompat.getColorStateList(this, R.color.material_green_700)
|
||||
|
||||
var handler = new Handler()
|
||||
|
||||
private def changeSwitch(checked: Boolean) {
|
||||
serviceStarted = checked
|
||||
fab.setImageResource(if (checked) R.drawable.ic_cloud else R.drawable.ic_cloud_off)
|
||||
fab.setImageResource(if (checked) R.drawable.ic_start_connected else R.drawable.ic_start_idle)
|
||||
if (fab.isEnabled) {
|
||||
fab.setEnabled(false)
|
||||
handler.postDelayed(() => fab.setEnabled(true), 1000)
|
||||
@@ -298,7 +249,7 @@ class Shadowsocks
|
||||
} else {
|
||||
in = assetManager.open(file)
|
||||
}
|
||||
out = new FileOutputStream(Path.BASE + file)
|
||||
out = new FileOutputStream(getApplicationInfo.dataDir + "/" + file)
|
||||
copyFile(in, out)
|
||||
in.close()
|
||||
in = null
|
||||
@@ -328,8 +279,8 @@ class Shadowsocks
|
||||
val cmd = new ArrayBuffer[String]()
|
||||
|
||||
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
|
||||
cmd.append("chmod 666 %s%s-nat.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("chmod 666 %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("chmod 666 %s/%s-nat.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
cmd.append("chmod 666 %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
}
|
||||
|
||||
if (ShadowsocksApplication.isVpnEnabled) {
|
||||
@@ -342,19 +293,19 @@ class Shadowsocks
|
||||
|
||||
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
|
||||
try {
|
||||
val pid_nat = scala.io.Source.fromFile(Path.BASE + task + "-nat.pid").mkString.trim.toInt
|
||||
val pid_vpn = scala.io.Source.fromFile(Path.BASE + task + "-vpn.pid").mkString.trim.toInt
|
||||
val pid_nat = scala.io.Source.fromFile(getApplicationInfo.dataDir + "/" + task + "-nat.pid").mkString.trim.toInt
|
||||
val pid_vpn = scala.io.Source.fromFile(getApplicationInfo.dataDir + "/" + task + "-vpn.pid").mkString.trim.toInt
|
||||
cmd.append("kill -9 %d".formatLocal(Locale.ENGLISH, pid_nat))
|
||||
cmd.append("kill -9 %d".formatLocal(Locale.ENGLISH, pid_vpn))
|
||||
Process.killProcess(pid_nat)
|
||||
Process.killProcess(pid_vpn)
|
||||
} catch {
|
||||
case e: Throwable => Log.e(Shadowsocks.TAG, "unable to kill " + task)
|
||||
case e: Throwable => // Ignore
|
||||
}
|
||||
cmd.append("rm -f %s%s-nat.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("rm -f %s%s-nat.conf".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("rm -f %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("rm -f %s%s-vpn.conf".formatLocal(Locale.ENGLISH, Path.BASE, task))
|
||||
cmd.append("rm -f %s/%s-nat.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
cmd.append("rm -f %s/%s-nat.conf".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
cmd.append("rm -f %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
cmd.append("rm -f %s/%s-vpn.conf".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
|
||||
}
|
||||
if (ShadowsocksApplication.isVpnEnabled) Console.runCommand(cmd.toArray) else {
|
||||
Console.runRootCommand(cmd.toArray)
|
||||
@@ -367,12 +318,7 @@ class Shadowsocks
|
||||
changeSwitch(checked = false)
|
||||
}
|
||||
|
||||
def isReady: Boolean = {
|
||||
if (!checkText(Key.proxy)) return false
|
||||
if (!checkText(Key.sitekey)) return false
|
||||
if (bgService == null) return false
|
||||
true
|
||||
}
|
||||
def isReady: Boolean = checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null
|
||||
|
||||
def prepareStartService() {
|
||||
ThrowableFuture {
|
||||
@@ -429,8 +375,13 @@ class Shadowsocks
|
||||
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show
|
||||
true
|
||||
})
|
||||
updateTraffic(0, 0, 0, 0)
|
||||
|
||||
// Bind to the service
|
||||
bindToService()
|
||||
}
|
||||
|
||||
// Bind to the service
|
||||
def bindToService() {
|
||||
handler.post(() => attachService)
|
||||
}
|
||||
|
||||
@@ -438,9 +389,10 @@ class Shadowsocks
|
||||
currentProfile = ShadowsocksApplication.currentProfile match {
|
||||
case Some(profile) => profile // updated
|
||||
case None => // removed
|
||||
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List[Profile]())
|
||||
if (profiles.isEmpty) ShadowsocksApplication.profileManager.createDefault()
|
||||
else ShadowsocksApplication.switchProfile(profiles.head.id)
|
||||
ShadowsocksApplication.profileManager.getFirstProfile match {
|
||||
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
|
||||
case None => ShadowsocksApplication.profileManager.createDefault()
|
||||
}
|
||||
}
|
||||
|
||||
updatePreferenceScreen()
|
||||
@@ -451,36 +403,43 @@ class Shadowsocks
|
||||
protected override def onPause() {
|
||||
super.onPause()
|
||||
ShadowsocksApplication.profileManager.save
|
||||
prepared = false
|
||||
}
|
||||
|
||||
private def stateUpdate() {
|
||||
private def hideCircle() {
|
||||
try {
|
||||
fabProgressCircle.hide()
|
||||
} catch {
|
||||
case _: java.lang.NullPointerException => // Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private def updateState() {
|
||||
if (bgService != null) {
|
||||
bgService.getState match {
|
||||
case State.CONNECTING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
setPreferenceEnabled(false)
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
preferences.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
case State.CONNECTED =>
|
||||
fab.setBackgroundTintList(greenTint)
|
||||
serviceStarted = true
|
||||
fab.setImageResource(R.drawable.ic_cloud)
|
||||
setPreferenceEnabled(false)
|
||||
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
|
||||
fab.setImageResource(R.drawable.ic_start_connected)
|
||||
preferences.setEnabled(false)
|
||||
fabProgressCircle.postDelayed(hideCircle, 100)
|
||||
case State.STOPPING =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_queue)
|
||||
setPreferenceEnabled(false)
|
||||
fab.setImageResource(R.drawable.ic_start_busy)
|
||||
preferences.setEnabled(false)
|
||||
fabProgressCircle.show()
|
||||
case _ =>
|
||||
fab.setBackgroundTintList(greyTint)
|
||||
serviceStarted = false
|
||||
fab.setImageResource(R.drawable.ic_cloud_off)
|
||||
setPreferenceEnabled(true)
|
||||
fabProgressCircle.postDelayed(fabProgressCircle.hide, 100)
|
||||
fab.setImageResource(R.drawable.ic_start_idle)
|
||||
preferences.setEnabled(true)
|
||||
fabProgressCircle.postDelayed(hideCircle, 100)
|
||||
}
|
||||
state = bgService.getState
|
||||
}
|
||||
@@ -488,62 +447,42 @@ class Shadowsocks
|
||||
|
||||
protected override def onResume() {
|
||||
super.onResume()
|
||||
stateUpdate()
|
||||
ConfigUtils.refresh(this)
|
||||
|
||||
// Check if current profile changed
|
||||
if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile()
|
||||
|
||||
updateTraffic()
|
||||
}
|
||||
|
||||
private def setPreferenceEnabled(enabled: Boolean) {
|
||||
preferences.findPreference(Key.isNAT).setEnabled(enabled)
|
||||
for (name <- Shadowsocks.PROXY_PREFS) {
|
||||
val pref = preferences.findPreference(name)
|
||||
if (pref != null) {
|
||||
pref.setEnabled(enabled)
|
||||
}
|
||||
}
|
||||
for (name <- Shadowsocks.FEATURE_PREFS) {
|
||||
val pref = preferences.findPreference(name)
|
||||
if (pref != null) {
|
||||
if (name == Key.isProxyApps) {
|
||||
pref.setEnabled(enabled && (Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
|
||||
} else {
|
||||
pref.setEnabled(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateState()
|
||||
}
|
||||
|
||||
private def updatePreferenceScreen() {
|
||||
val profile = currentProfile
|
||||
if (profile.host == "198.199.101.152" && adView == null) {
|
||||
if (profile.host == "198.199.101.152") if (adView == null) {
|
||||
adView = new AdView(this)
|
||||
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
|
||||
adView.setAdSize(AdSize.SMART_BANNER)
|
||||
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 0)
|
||||
adView.loadAd(new AdRequest.Builder().build())
|
||||
}
|
||||
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
|
||||
|
||||
for (name <- Shadowsocks.PROXY_PREFS) {
|
||||
val pref = preferences.findPreference(name)
|
||||
Shadowsocks.updatePreference(pref, name, profile)
|
||||
}
|
||||
for (name <- Shadowsocks.FEATURE_PREFS) {
|
||||
val pref = preferences.findPreference(name)
|
||||
Shadowsocks.updatePreference(pref, name, profile)
|
||||
}
|
||||
preferences.update(profile)
|
||||
}
|
||||
|
||||
override def onStart() {
|
||||
super.onStart()
|
||||
registerCallback
|
||||
}
|
||||
override def onStop() {
|
||||
super.onStop()
|
||||
unregisterCallback
|
||||
clearDialog()
|
||||
}
|
||||
|
||||
private var _isDestroyed: Boolean = _
|
||||
override def isDestroyed = if (Build.VERSION.SDK_INT >= 17) super.isDestroyed else _isDestroyed
|
||||
override def onDestroy() {
|
||||
super.onDestroy()
|
||||
_isDestroyed = true
|
||||
deattachService()
|
||||
new BackupManager(this).dataChanged()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
@@ -554,7 +493,7 @@ class Shadowsocks
|
||||
|
||||
val ab = new ArrayBuffer[String]
|
||||
for (executable <- Shadowsocks.EXECUTABLES) {
|
||||
ab.append("chmod 755 " + Path.BASE + executable)
|
||||
ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable)
|
||||
}
|
||||
Console.runCommand(ab.toArray)
|
||||
}
|
||||
@@ -585,7 +524,6 @@ class Shadowsocks
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
|
||||
case Activity.RESULT_OK =>
|
||||
prepared = true
|
||||
serviceStart()
|
||||
case _ =>
|
||||
cancelStart()
|
||||
@@ -613,8 +551,8 @@ class Shadowsocks
|
||||
}
|
||||
|
||||
def clearDialog() {
|
||||
if (progressDialog != null) {
|
||||
progressDialog.dismiss()
|
||||
if (progressDialog != null && progressDialog.isShowing) {
|
||||
if (!isDestroyed) progressDialog.dismiss()
|
||||
progressDialog = null
|
||||
progressTag = -1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.utils.ProcessUtils._
|
||||
|
||||
/**
|
||||
* @author chentaov5@gmail.com
|
||||
*/
|
||||
class ShadowsocksDeathRecipient(val mContext: ServiceBoundContext)
|
||||
extends IBinder.DeathRecipient {
|
||||
|
||||
val TAG = "ShadowsocksDeathRecipient"
|
||||
|
||||
override def binderDied(): Unit = {
|
||||
Log.d(TAG, "[ShadowsocksDeathRecipient] binder died.")
|
||||
|
||||
mContext match {
|
||||
case ss: Shadowsocks =>
|
||||
inShadowsocks(mContext) {
|
||||
ss.unregisterCallback
|
||||
ss.bindToService()
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -163,33 +163,36 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
def startShadowsocksDaemon() {
|
||||
if (config.route != Route.ALL) {
|
||||
val acl: Array[String] = config.route match {
|
||||
case Route.BYPASS_LAN => getResources.getStringArray(R.array.private_route)
|
||||
case Route.BYPASS_CHN => getResources.getStringArray(R.array.chn_route_full)
|
||||
val acl: Array[Array[String]] = config.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))
|
||||
}
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "acl.list"))(p => {
|
||||
acl.foreach(item => p.println(item))
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
|
||||
acl.flatten.foreach(p.println)
|
||||
})
|
||||
}
|
||||
|
||||
val conf = ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, config.localPort,
|
||||
config.sitekey, config.encMethod, 600)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "ss-local-nat.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-nat.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
|
||||
val cmd = new ArrayBuffer[String]
|
||||
cmd += (Path.BASE + "ss-local"
|
||||
cmd += (getApplicationInfo.dataDir + "/ss-local"
|
||||
, "-b" , "127.0.0.1"
|
||||
, "-t" , "600"
|
||||
, "-c" , Path.BASE + "ss-local-nat.conf")
|
||||
, "-P", getApplicationInfo.dataDir
|
||||
, "-c" , getApplicationInfo.dataDir + "/ss-local-nat.conf")
|
||||
|
||||
if (config.isAuth) cmd += "-A"
|
||||
|
||||
if (config.route != Route.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += (Path.BASE + "acl.list")
|
||||
cmd += (getApplicationInfo.dataDir + "/acl.list")
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -204,16 +207,17 @@ class ShadowsocksNatService extends BaseService {
|
||||
val conf = ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, 8153,
|
||||
config.sitekey, config.encMethod, 10)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "ss-tunnel-nat.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
val cmd = new ArrayBuffer[String]
|
||||
cmd += (Path.BASE + "ss-tunnel"
|
||||
cmd += (getApplicationInfo.dataDir + "/ss-tunnel"
|
||||
, "-u"
|
||||
, "-t" , "10"
|
||||
, "-b" , "127.0.0.1"
|
||||
, "-L" , "8.8.8.8:53"
|
||||
, "-c" , Path.BASE + "ss-tunnel-nat.conf")
|
||||
, "-P" , getApplicationInfo.dataDir
|
||||
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
|
||||
|
||||
cmd += ("-l" , "8153")
|
||||
|
||||
@@ -230,17 +234,18 @@ class ShadowsocksNatService extends BaseService {
|
||||
val conf = ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, 8163,
|
||||
config.sitekey, config.encMethod, 10)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "ss-tunnel-nat.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
val cmdBuf = new ArrayBuffer[String]
|
||||
cmdBuf += (Path.BASE + "ss-tunnel"
|
||||
cmdBuf += (getApplicationInfo.dataDir + "/ss-tunnel"
|
||||
, "-u"
|
||||
, "-t" , "10"
|
||||
, "-b" , "127.0.0.1"
|
||||
, "-l" , "8163"
|
||||
, "-L" , "8.8.8.8:53"
|
||||
, "-c" , Path.BASE + "ss-tunnel-nat.conf")
|
||||
, "-P", getApplicationInfo.dataDir
|
||||
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
|
||||
|
||||
if (config.isAuth) cmdBuf += "-A"
|
||||
|
||||
@@ -255,20 +260,20 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
def startDnsDaemon() {
|
||||
|
||||
val conf = if (config.route == Route.BYPASS_CHN) {
|
||||
val conf = if (config.route == Route.BYPASS_CHN || config.route == Route.BYPASS_LAN_CHN) {
|
||||
val reject = ConfigUtils.getRejectList(getContext)
|
||||
val blackList = ConfigUtils.getBlackList(getContext)
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "127.0.0.1", 8153,
|
||||
reject, blackList, 8163, "")
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"127.0.0.1", 8153, reject, blackList, 8163, "")
|
||||
} else {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "127.0.0.1", 8153,
|
||||
8163, "")
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"127.0.0.1", 8153, 8163, "")
|
||||
}
|
||||
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "pdnsd-nat.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-nat.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
val cmd = Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd-nat.conf"
|
||||
val cmd = getApplicationInfo.dataDir + "/pdnsd -c " + getApplicationInfo.dataDir + "/pdnsd-nat.conf"
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
|
||||
@@ -292,9 +297,9 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
def startRedsocksDaemon() {
|
||||
val conf = ConfigUtils.REDSOCKS.formatLocal(Locale.ENGLISH, config.localPort)
|
||||
val cmd = Path.BASE + "redsocks -c %sredsocks-nat.conf"
|
||||
.formatLocal(Locale.ENGLISH, Path.BASE, Path.BASE)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "redsocks-nat.conf"))(p => {
|
||||
val cmd = "%s/redsocks -c %s/redsocks-nat.conf"
|
||||
.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, getApplicationInfo.dataDir)
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/redsocks-nat.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
|
||||
@@ -390,13 +395,10 @@ class ShadowsocksNatService extends BaseService {
|
||||
}
|
||||
|
||||
override def startRunner(config: Config) {
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
if (!Console.isRoot) {
|
||||
changeState(State.STOPPED, getString(R.string.nat_no_root))
|
||||
return
|
||||
}
|
||||
|
||||
super.startRunner(config)
|
||||
|
||||
// register close receiver
|
||||
@@ -410,6 +412,8 @@ class ShadowsocksNatService extends BaseService {
|
||||
registerReceiver(closeReceiver, filter)
|
||||
|
||||
ShadowsocksApplication.track(TAG, "start")
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
|
||||
ThrowableFuture {
|
||||
if (config.proxy == "198.199.101.152") {
|
||||
@@ -458,8 +462,6 @@ class ShadowsocksNatService extends BaseService {
|
||||
|
||||
override def stopRunner() {
|
||||
|
||||
super.stopRunner()
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPING)
|
||||
|
||||
@@ -476,17 +478,7 @@ class ShadowsocksNatService extends BaseService {
|
||||
// reset NAT
|
||||
killProcesses()
|
||||
|
||||
// stop the service if no callback registered
|
||||
if (getCallbackCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
// change the state
|
||||
changeState(State.STOPPED)
|
||||
}
|
||||
|
||||
override def stopBackgroundService() {
|
||||
stopSelf()
|
||||
super.stopRunner()
|
||||
}
|
||||
|
||||
override def getTag = TAG
|
||||
|
||||
@@ -2,26 +2,29 @@ package com.github.shadowsocks
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.app.{KeyguardManager, PendingIntent}
|
||||
import android.app.{KeyguardManager, NotificationManager, PendingIntent}
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
||||
import android.support.v4.content.ContextCompat
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
|
||||
import com.github.shadowsocks.utils.{Action, State, Utils}
|
||||
import com.github.shadowsocks.utils.{TrafficMonitor, Action, State, Utils}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
|
||||
private lazy val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||
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 trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
|
||||
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txRate, rxRate))
|
||||
style.bigText(service.getString(R.string.stat_summary)
|
||||
.formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal))
|
||||
override def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
val txr = TrafficMonitor.formatTraffic(txRate)
|
||||
val rxr = TrafficMonitor.formatTraffic(rxRate)
|
||||
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txr, rxr))
|
||||
style.bigText(service.getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txr, rxr,
|
||||
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
|
||||
show()
|
||||
}
|
||||
}
|
||||
@@ -73,8 +76,9 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
show()
|
||||
} else if (forceShow) show()
|
||||
|
||||
def notification = builder.build()
|
||||
def show() = service.startForeground(1, notification)
|
||||
def show() = {
|
||||
service.startForeground(1, builder.build)
|
||||
}
|
||||
|
||||
def destroy() {
|
||||
if (lockReceiver != null) {
|
||||
@@ -83,5 +87,6 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
}
|
||||
unregisterCallback
|
||||
service.stopForeground(true)
|
||||
nm.cancel(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ import android.app.{Activity, KeyguardManager}
|
||||
import android.content.{Intent, IntentFilter, Context, BroadcastReceiver}
|
||||
import android.net.VpnService
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.utils.ConfigUtils
|
||||
|
||||
class ShadowsocksRunnerActivity extends AppCompatActivity with ServiceBoundContext {
|
||||
class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
val handler = new Handler()
|
||||
|
||||
// Variables
|
||||
|
||||
@@ -9,16 +9,67 @@ import android.content.{DialogInterface, Intent, SharedPreferences}
|
||||
import android.net.Uri
|
||||
import android.os.{Build, Bundle}
|
||||
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.webkit.{WebView, WebViewClient}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.preferences._
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.{Key, Utils}
|
||||
|
||||
object ShadowsocksSettings {
|
||||
// Constants
|
||||
val PREFS_NAME = "Shadowsocks"
|
||||
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey, Key.encMethod,
|
||||
Key.isAuth)
|
||||
val FEATURE_PREFS = Array(Key.route, Key.isProxyApps, Key.isUdpDns, Key.isIpv6)
|
||||
|
||||
// Helper functions
|
||||
def updateDropDownPreference(pref: Preference, value: String) {
|
||||
pref.asInstanceOf[DropDownPreference].setValue(value)
|
||||
}
|
||||
|
||||
def updatePasswordEditTextPreference(pref: Preference, value: String) {
|
||||
pref.setSummary(value)
|
||||
pref.asInstanceOf[PasswordEditTextPreference].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.profileName => updateSummaryEditTextPreference(pref, profile.name)
|
||||
case Key.proxy => updateSummaryEditTextPreference(pref, profile.host)
|
||||
case Key.remotePort => updateNumberPickerPreference(pref, profile.remotePort)
|
||||
case Key.localPort => updateNumberPickerPreference(pref, profile.localPort)
|
||||
case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password)
|
||||
case Key.encMethod => updateDropDownPreference(pref, profile.method)
|
||||
case Key.route => updateDropDownPreference(pref, profile.route)
|
||||
case Key.isProxyApps => updateSwitchPreference(pref, profile.proxyApps)
|
||||
case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns)
|
||||
case Key.isAuth => updateSwitchPreference(pref, profile.auth)
|
||||
case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move related logic here
|
||||
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
|
||||
import ShadowsocksSettings._
|
||||
|
||||
private def activity = getActivity.asInstanceOf[Shadowsocks]
|
||||
lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
|
||||
var stat: Preference = _
|
||||
var stat: StatusPreference = _
|
||||
|
||||
private var isProxyApps: SwitchPreference = _
|
||||
private var testCount: Int = _
|
||||
@@ -28,12 +79,11 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
addPreferencesFromResource(R.xml.pref_all)
|
||||
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
stat = findPreference(Key.stat)
|
||||
stat = findPreference(Key.stat).asInstanceOf[StatusPreference]
|
||||
stat.setOnPreferenceClickListener(_ => {
|
||||
val id = synchronized {
|
||||
testCount += 1
|
||||
activity.connectionTestResult = getString(R.string.connection_test_testing)
|
||||
activity.updateTraffic()
|
||||
activity.handler.post(() => stat.setSummary(R.string.connection_test_testing))
|
||||
testCount
|
||||
}
|
||||
ThrowableFuture {
|
||||
@@ -46,6 +96,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
conn.setUseCaches(false)
|
||||
if (testCount == id) {
|
||||
var result: String = null
|
||||
var success = true
|
||||
try {
|
||||
val start = currentTimeMillis
|
||||
conn.getInputStream
|
||||
@@ -55,17 +106,24 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
result = getString(R.string.connection_test_available, elapsed: java.lang.Long)
|
||||
else throw new Exception(getString(R.string.connection_test_error_status_code, code: Integer))
|
||||
} catch {
|
||||
case e: Exception => result = getString(R.string.connection_test_error, e.getMessage)
|
||||
case e: Exception =>
|
||||
success = false
|
||||
result = getString(R.string.connection_test_error, e.getMessage)
|
||||
}
|
||||
synchronized(if (testCount == id) {
|
||||
activity.connectionTestResult = result
|
||||
activity.handler.post(activity.updateTraffic)
|
||||
if (ShadowsocksApplication.isVpnEnabled) activity.handler.post(() => {
|
||||
if (success) stat.setSummary(result) else {
|
||||
stat.setSummary(R.string.connection_test_fail)
|
||||
Snackbar.make(activity.findViewById(android.R.id.content), result, Snackbar.LENGTH_LONG).show
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
stat.setSummary(if (ShadowsocksApplication.isVpnEnabled) getString(R.string.connection_test_pending) else null)
|
||||
|
||||
isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference]
|
||||
isProxyApps.setOnPreferenceClickListener((preference: Preference) => {
|
||||
@@ -94,7 +152,11 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
web.loadUrl("file:///android_asset/pages/about.html")
|
||||
web.setWebViewClient(new WebViewClient() {
|
||||
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
} catch {
|
||||
case _: android.content.ActivityNotFoundException => // Ignore
|
||||
}
|
||||
true
|
||||
}
|
||||
})
|
||||
@@ -116,16 +178,44 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
|
||||
isProxyApps.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isProxyApps, false)) // update
|
||||
}
|
||||
|
||||
override def onPause {
|
||||
super.onPause()
|
||||
override def onDestroy {
|
||||
super.onDestroy()
|
||||
ShadowsocksApplication.settings.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = key match {
|
||||
case Key.isNAT => activity.handler.post(() => {
|
||||
activity.deattachService
|
||||
activity.attachService
|
||||
})
|
||||
case Key.isNAT =>
|
||||
activity.handler.post(() => {
|
||||
activity.deattachService
|
||||
activity.attachService
|
||||
})
|
||||
stat.setSummary(if (ShadowsocksApplication.isVpnEnabled) getString(R.string.connection_test_pending) else null)
|
||||
case _ =>
|
||||
}
|
||||
|
||||
private var enabled = true
|
||||
def setEnabled(enabled: Boolean) {
|
||||
this.enabled = enabled
|
||||
findPreference(Key.isNAT).setEnabled(enabled)
|
||||
for (name <- PROXY_PREFS) {
|
||||
val pref = findPreference(name)
|
||||
if (pref != null) pref.setEnabled(enabled)
|
||||
}
|
||||
for (name <- FEATURE_PREFS) {
|
||||
val pref = findPreference(name)
|
||||
if (pref != null) pref.setEnabled(enabled &&
|
||||
(name != Key.isProxyApps || Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
|
||||
}
|
||||
}
|
||||
|
||||
def update(profile: Profile) {
|
||||
for (name <- PROXY_PREFS) {
|
||||
val pref = findPreference(name)
|
||||
updatePreference(pref, name, profile)
|
||||
}
|
||||
for (name <- FEATURE_PREFS) {
|
||||
val pref = findPreference(name)
|
||||
updatePreference(pref, name, profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ import android.net.VpnService
|
||||
import android.os._
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
|
||||
import com.github.shadowsocks.aidl.Config
|
||||
import com.github.shadowsocks.utils._
|
||||
import org.apache.commons.net.util.SubnetUtils
|
||||
@@ -113,8 +115,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
override def stopRunner() {
|
||||
|
||||
super.stopRunner()
|
||||
|
||||
if (vpnThread != null) {
|
||||
vpnThread.stopThread()
|
||||
vpnThread = null
|
||||
@@ -136,19 +136,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
conn = null
|
||||
}
|
||||
|
||||
// stop the service if no callback registered
|
||||
if (getCallbackCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
// clean up recevier
|
||||
if (closeReceiver != null) {
|
||||
unregisterReceiver(closeReceiver)
|
||||
closeReceiver = null
|
||||
}
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPED)
|
||||
super.stopRunner()
|
||||
}
|
||||
|
||||
def getVersionName: String = {
|
||||
@@ -267,7 +261,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000 * tries)
|
||||
if (System.sendfd(fd) != -1) {
|
||||
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
|
||||
return true
|
||||
}
|
||||
tries += 1
|
||||
@@ -279,33 +273,36 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
def startShadowsocksDaemon() {
|
||||
|
||||
if (config.route != Route.ALL) {
|
||||
val acl: Array[String] = config.route match {
|
||||
case Route.BYPASS_LAN => getResources.getStringArray(R.array.private_route)
|
||||
case Route.BYPASS_CHN => getResources.getStringArray(R.array.chn_route_full)
|
||||
val acl: Array[Array[String]] = config.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))
|
||||
}
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "acl.list"))(p => {
|
||||
acl.foreach(item => p.println(item))
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
|
||||
acl.flatten.foreach(p.println)
|
||||
})
|
||||
}
|
||||
|
||||
val conf = ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, config.localPort,
|
||||
config.sitekey, config.encMethod, 600)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "ss-local-vpn.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
|
||||
val cmd = new ArrayBuffer[String]
|
||||
cmd += (Path.BASE + "ss-local", "-V", "-u"
|
||||
cmd += (getApplicationInfo.dataDir + "/ss-local", "-V", "-u"
|
||||
, "-b", "127.0.0.1"
|
||||
, "-t", "600"
|
||||
, "-c", Path.BASE + "ss-local-vpn.conf")
|
||||
, "-P", getApplicationInfo.dataDir
|
||||
, "-c", getApplicationInfo.dataDir + "/ss-local-vpn.conf")
|
||||
|
||||
if (config.isAuth) cmd += "-A"
|
||||
|
||||
if (config.route != Route.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += (Path.BASE + "acl.list")
|
||||
cmd += (getApplicationInfo.dataDir + "/acl.list")
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
|
||||
@@ -320,18 +317,19 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
val conf = ConfigUtils
|
||||
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, 8163,
|
||||
config.sitekey, config.encMethod, 10)
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "ss-tunnel-vpn.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
val cmd = new ArrayBuffer[String]
|
||||
cmd += (Path.BASE + "ss-tunnel"
|
||||
cmd += (getApplicationInfo.dataDir + "/ss-tunnel"
|
||||
, "-V"
|
||||
, "-u"
|
||||
, "-t", "10"
|
||||
, "-b", "127.0.0.1"
|
||||
, "-l", "8163"
|
||||
, "-L", "8.8.8.8:53"
|
||||
, "-c", Path.BASE + "ss-tunnel-vpn.conf")
|
||||
, "-P", getApplicationInfo.dataDir
|
||||
, "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf")
|
||||
|
||||
if (config.isAuth) cmd += "-A"
|
||||
|
||||
@@ -346,20 +344,20 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
def startDnsDaemon() {
|
||||
val ipv6 = if (config.isIpv6) "" else "reject = ::/0;"
|
||||
val conf = {
|
||||
if (config.route == Route.BYPASS_CHN) {
|
||||
if (config.route == Route.BYPASS_CHN || config.route == Route.BYPASS_LAN_CHN) {
|
||||
val reject = ConfigUtils.getRejectList(getContext)
|
||||
val blackList = ConfigUtils.getBlackList(getContext)
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "0.0.0.0", 8153,
|
||||
reject, blackList, 8163, ipv6)
|
||||
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", 8153, reject, blackList, 8163, ipv6)
|
||||
} else {
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, "0.0.0.0", 8153,
|
||||
8163, ipv6)
|
||||
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
|
||||
"0.0.0.0", 8153, 8163, ipv6)
|
||||
}
|
||||
}
|
||||
ConfigUtils.printToFile(new File(Path.BASE + "pdnsd-vpn.conf"))(p => {
|
||||
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => {
|
||||
p.println(conf)
|
||||
})
|
||||
val cmd = Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd-vpn.conf"
|
||||
val cmd = getApplicationInfo.dataDir + "/pdnsd -c " + getApplicationInfo.dataDir + "/pdnsd-vpn.conf"
|
||||
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
|
||||
|
||||
@@ -389,16 +387,21 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (config.isProxyApps) {
|
||||
for (pkg <- config.proxiedAppString.split('\n')) {
|
||||
if (!config.isBypassApps) {
|
||||
builder.addAllowedApplication(pkg)
|
||||
} else {
|
||||
builder.addDisallowedApplication(pkg)
|
||||
try {
|
||||
if (!config.isBypassApps) {
|
||||
builder.addAllowedApplication(pkg)
|
||||
} else {
|
||||
builder.addDisallowedApplication(pkg)
|
||||
}
|
||||
} catch {
|
||||
case ex: NameNotFoundException =>
|
||||
Log.e(TAG, "Invalid package name", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.route == Route.ALL) {
|
||||
if (config.route == Route.ALL || config.route == Route.BYPASS_CHN) {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
} else {
|
||||
val privateList = getResources.getStringArray(R.array.bypass_private_route)
|
||||
@@ -429,16 +432,17 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
val fd = conn.getFd
|
||||
|
||||
var cmd = (Path.BASE +
|
||||
"tun2socks --netif-ipaddr %s "
|
||||
var cmd = (getApplicationInfo.dataDir +
|
||||
"/tun2socks --netif-ipaddr %s "
|
||||
+ "--netif-netmask 255.255.255.0 "
|
||||
+ "--socks-server-addr 127.0.0.1:%d "
|
||||
+ "--tunfd %d "
|
||||
+ "--tunmtu %d "
|
||||
+ "--sock-path %s "
|
||||
+ "--loglevel 3")
|
||||
.formatLocal(Locale.ENGLISH,
|
||||
PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"),
|
||||
config.localPort, fd, VPN_MTU)
|
||||
config.localPort, fd, VPN_MTU, getApplicationInfo.dataDir + "/sock_path")
|
||||
|
||||
if (config.isIpv6)
|
||||
cmd += " --netif-ip6addr " + PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2")
|
||||
@@ -458,10 +462,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
fd
|
||||
}
|
||||
|
||||
override def stopBackgroundService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override def getTag = TAG
|
||||
|
||||
override def getServiceMode = Mode.VPN
|
||||
|
||||
@@ -45,10 +45,15 @@ import java.util.concurrent.Executors
|
||||
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
|
||||
import android.util.Log
|
||||
|
||||
object ShadowsocksVpnThread {
|
||||
val getInt = classOf[FileDescriptor].getDeclaredMethod("getInt$")
|
||||
}
|
||||
|
||||
class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
|
||||
import ShadowsocksVpnThread._
|
||||
|
||||
val TAG = "ShadowsocksVpnService"
|
||||
val PATH = "/data/data/com.github.shadowsocks/protect_path"
|
||||
lazy val PATH = vpnService.getApplicationInfo.dataDir + "/protect_path"
|
||||
|
||||
@volatile var isRunning: Boolean = true
|
||||
@volatile var serverSocket: LocalServerSocket = null
|
||||
@@ -103,11 +108,8 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
|
||||
val fds = socket.getAncillaryFileDescriptors
|
||||
|
||||
if (fds.nonEmpty) {
|
||||
var ret = false
|
||||
|
||||
val getInt = classOf[FileDescriptor].getDeclaredMethod("getInt$")
|
||||
val fd = getInt.invoke(fds(0)).asInstanceOf[Int]
|
||||
ret = vpnService.protect(fd)
|
||||
val ret = vpnService.protect(fd)
|
||||
|
||||
// Trick to close file decriptor
|
||||
System.jniclose(fd)
|
||||
|
||||
@@ -82,7 +82,7 @@ class TaskerActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
|
||||
private val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
|
||||
def getItemCount = 1 + profiles.length
|
||||
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = i match {
|
||||
case 0 => vh.bindDefault
|
||||
@@ -110,9 +110,13 @@ class TaskerActivity extends AppCompatActivity {
|
||||
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
|
||||
switch.setChecked(taskerOption.switchOn)
|
||||
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
|
||||
profilesList.setLayoutManager(new LinearLayoutManager(this))
|
||||
val lm = new LinearLayoutManager(this)
|
||||
profilesList.setLayoutManager(lm)
|
||||
profilesList.setItemAnimator(new DefaultItemAnimator)
|
||||
profilesList.setAdapter(profilesAdapter)
|
||||
if (taskerOption.profileId >= 0) lm.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
||||
case (profile, i) if profile.id == taskerOption.profileId => i + 1
|
||||
}.getOrElse(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import android.os.Handler
|
||||
import com.github.shadowsocks.helper.TaskerSettings
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
@@ -55,6 +56,10 @@ class TaskerReceiver extends BroadcastReceiver {
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
if (settings.switchOn) Utils.startSsService(context) else if (!switched) Utils.stopSsService(context)
|
||||
val handler = new Handler(context.getMainLooper)
|
||||
handler.postDelayed(() => {
|
||||
if (settings.switchOn) Utils.startSsService(context)
|
||||
else if (!switched) Utils.stopSsService(context)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ object DBHelper {
|
||||
}
|
||||
|
||||
class DBHelper(val context: Context)
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 14) {
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 16) {
|
||||
import DBHelper._
|
||||
|
||||
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
|
||||
@@ -110,12 +110,21 @@ class DBHelper(val context: Context)
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
|
||||
}
|
||||
if (oldVersion < 14) {
|
||||
|
||||
if (oldVersion < 15) {
|
||||
if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
|
||||
var i = 0
|
||||
for (profile <- profileDao.queryForAll.asScala) {
|
||||
profile.individual = updateProxiedApps(context, profile.individual)
|
||||
if (oldVersion < 14) profile.individual = updateProxiedApps(context, profile.individual)
|
||||
profile.userOrder = i
|
||||
profileDao.update(profile)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 16) {
|
||||
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +55,13 @@ class Profile {
|
||||
var localPort: Int = 1080
|
||||
|
||||
@DatabaseField
|
||||
var remotePort: Int = 8338
|
||||
var remotePort: Int = 8388
|
||||
|
||||
@DatabaseField
|
||||
var password: String = ""
|
||||
|
||||
@DatabaseField
|
||||
var method: String = "rc4"
|
||||
var method: String = "aes-256-cfb"
|
||||
|
||||
@DatabaseField
|
||||
var route: String = "all"
|
||||
@@ -92,4 +92,7 @@ class Profile {
|
||||
|
||||
@DatabaseField
|
||||
val date: java.util.Date = new java.util.Date()
|
||||
|
||||
@DatabaseField
|
||||
var userOrder: Long = _
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
case _ =>
|
||||
}
|
||||
val last = dbHelper.profileDao.queryRaw(dbHelper.profileDao.queryBuilder.selectRaw("MAX(userOrder)")
|
||||
.prepareStatementString).getFirstResult
|
||||
if (last != null && last.length == 1 && last(0) != null) profile.userOrder = last(0).toInt + 1
|
||||
dbHelper.profileDao.createOrUpdate(profile)
|
||||
if (profileAddedListener != null) profileAddedListener(profile)
|
||||
profile
|
||||
@@ -109,10 +112,21 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
|
||||
}
|
||||
}
|
||||
|
||||
def getFirstProfile = {
|
||||
try {
|
||||
val result = dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.limit(1L).prepare)
|
||||
if (result.size == 1) Option(result.get(0)) else None
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
Log.e(Shadowsocks.TAG, "getAllProfiles", ex)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def getAllProfiles: Option[List[Profile]] = {
|
||||
try {
|
||||
import scala.collection.JavaConversions._
|
||||
Option(dbHelper.profileDao.queryForAll().toList)
|
||||
Option(dbHelper.profileDao.query(dbHelper.profileDao.queryBuilder.orderBy("userOrder", true).prepare).toList)
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
Log.e(Shadowsocks.TAG, "getAllProfiles", ex)
|
||||
|
||||
@@ -2,12 +2,17 @@ package com.github
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Success, Failure, Try}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
package object shadowsocks {
|
||||
def ThrowableFuture[T](f: => T) = Future(f) onFailure {
|
||||
case e: Throwable => e.printStackTrace()
|
||||
|
||||
val handleFailure: PartialFunction[Try[_], Unit] = {
|
||||
case Success(_) =>
|
||||
case Failure(e) => e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
def ThrowableFuture[T](f: => T) = Future(f) onComplete handleFailure
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.github.shadowsocks.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.Preference
|
||||
import android.util.AttributeSet
|
||||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import android.widget.TextView
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
/**
|
||||
* @author madeye
|
||||
*/
|
||||
final class StatusPreference(context: Context, attrs: AttributeSet = null)
|
||||
extends Preference(context, attrs) {
|
||||
var tx: String = _
|
||||
var rx: String = _
|
||||
var txRate: String = _
|
||||
var rxRate: String = _
|
||||
var txView: TextView = _
|
||||
var rxView: TextView = _
|
||||
var txRateView: TextView = _
|
||||
var rxRateView: TextView = _
|
||||
|
||||
override def onCreateView(parent: ViewGroup): View = {
|
||||
val view = getContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
|
||||
.inflate(R.layout.status_pref, parent, false)
|
||||
|
||||
txView = view.findViewById(R.id.tx).asInstanceOf[TextView]
|
||||
rxView = view.findViewById(R.id.rx).asInstanceOf[TextView]
|
||||
txRateView = view.findViewById(R.id.txRate).asInstanceOf[TextView]
|
||||
rxRateView = view.findViewById(R.id.rxRate).asInstanceOf[TextView]
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
override def onBindView(view: View) = {
|
||||
super.onBindView(view)
|
||||
txView.setText(tx)
|
||||
rxView.setText(rx)
|
||||
txRateView.setText(txRate)
|
||||
rxRateView.setText(rxRate)
|
||||
}
|
||||
|
||||
def setRate(tx: String, rx: String, txRate: String, rxRate: String) {
|
||||
this.tx = tx
|
||||
if (txView != null) {
|
||||
txView.setText(tx)
|
||||
}
|
||||
this.rx = rx
|
||||
if (rxView != null) {
|
||||
rxView.setText(rx)
|
||||
}
|
||||
this.txRate = txRate
|
||||
if (txRateView != null) {
|
||||
txRateView.setText(txRate)
|
||||
}
|
||||
this.rxRate = rxRate
|
||||
if (rxRateView != null) {
|
||||
rxRateView.setText(rxRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ object ConfigUtils {
|
||||
"""
|
||||
|global {
|
||||
| perm_cache = 2048;
|
||||
| cache_dir = "/data/data/com.github.shadowsocks";
|
||||
| cache_dir = "%s";
|
||||
| server_ip = %s;
|
||||
| server_port = %d;
|
||||
| query_method = tcp_only;
|
||||
@@ -100,7 +100,7 @@ object ConfigUtils {
|
||||
"""
|
||||
|global {
|
||||
| perm_cache = 2048;
|
||||
| cache_dir = "/data/data/com.github.shadowsocks";
|
||||
| cache_dir = "%s";
|
||||
| server_ip = %s;
|
||||
| server_port = %d;
|
||||
| query_method = tcp_only;
|
||||
@@ -143,7 +143,7 @@ object ConfigUtils {
|
||||
"""
|
||||
|global {
|
||||
| perm_cache = 2048;
|
||||
| cache_dir = "/data/data/com.github.shadowsocks";
|
||||
| cache_dir = "%s";
|
||||
| server_ip = %s;
|
||||
| server_port = %d;
|
||||
| query_method = tcp_only;
|
||||
|
||||
@@ -55,10 +55,6 @@ object Msg {
|
||||
val VPN_ERROR = 6
|
||||
}
|
||||
|
||||
object Path {
|
||||
val BASE = "/data/data/com.github.shadowsocks/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
val profileId = "profileId"
|
||||
val profileName = "profileName"
|
||||
@@ -114,4 +110,5 @@ object Route {
|
||||
val ALL = "all"
|
||||
val BYPASS_LAN = "bypass-lan"
|
||||
val BYPASS_CHN = "bypass-china"
|
||||
val BYPASS_LAN_CHN = "bypass-lan-china"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* @author chentaov5@gmail.com
|
||||
*/
|
||||
object IOUtils {
|
||||
|
||||
val TAG = "IOUtils"
|
||||
|
||||
def autoClose[R <: {def close() : Unit}, T](in: R)(fun: => T): T = {
|
||||
try {
|
||||
fun
|
||||
} finally if (in ne null)
|
||||
try {
|
||||
in.close()
|
||||
} catch {
|
||||
case e: Exception => Log.d(TAG, e.getMessage, e)
|
||||
}
|
||||
}
|
||||
|
||||
def inSafe[T](fun: => T): Option[T] = {
|
||||
var res: Option[T] = None
|
||||
try {
|
||||
res = Some(fun)
|
||||
} catch {
|
||||
case e: Exception => Log.d(TAG, e.getMessage, e)
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import java.io.{BufferedReader, FileInputStream, InputStreamReader}
|
||||
|
||||
import android.content.Context
|
||||
import com.github.shadowsocks.utils.IOUtils._
|
||||
|
||||
/**
|
||||
* @author chentaov5@gmail.com
|
||||
*/
|
||||
object ProcessUtils {
|
||||
|
||||
def getCurrentProcessName: String = {
|
||||
inSafe {
|
||||
val inputStream = new FileInputStream("/proc/self/cmdline")
|
||||
val reader = new BufferedReader(new InputStreamReader(inputStream))
|
||||
autoClose(reader) {
|
||||
val sb = new StringBuilder
|
||||
var c: Int = 0
|
||||
while({c = reader.read(); c > 0})
|
||||
sb.append(c.asInstanceOf[Char])
|
||||
sb.toString()
|
||||
}
|
||||
} match {
|
||||
case Some(name: String) => name
|
||||
case None => ""
|
||||
}
|
||||
}
|
||||
|
||||
def inShadowsocks[A](context: Context)(fun: => A): Unit = {
|
||||
if (context != null && getCurrentProcessName.equals(context.getPackageName)) fun
|
||||
}
|
||||
|
||||
|
||||
def inVpn[A](context: Context)(fun: => A): Unit = {
|
||||
if (context != null && getCurrentProcessName.equals(context.getPackageName + ":vpn")) fun
|
||||
}
|
||||
|
||||
|
||||
def inNat[A](context: Context)(fun: => A): Unit = {
|
||||
if (context != null && getCurrentProcessName.equals(context.getPackageName + ":nat")) fun
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,26 +5,20 @@ import java.text.DecimalFormat
|
||||
|
||||
import com.github.shadowsocks.{R, ShadowsocksApplication}
|
||||
|
||||
case class Traffic(tx: Long, rx: Long, timestamp: Long)
|
||||
|
||||
object TrafficMonitor {
|
||||
var last: Traffic = getTraffic(0, 0)
|
||||
|
||||
// Bytes per second
|
||||
var txRate: Long = 0
|
||||
var rxRate: Long = 0
|
||||
var txRate: Long = _
|
||||
var rxRate: Long = _
|
||||
|
||||
// Bytes for the current session
|
||||
var txTotal: Long = 0
|
||||
var rxTotal: Long = 0
|
||||
var txTotal: Long = _
|
||||
var rxTotal: Long = _
|
||||
|
||||
// Bytes for the last query
|
||||
var txLast: Long = 0
|
||||
var rxLast: Long = 0
|
||||
|
||||
def getTraffic(tx: Long, rx: Long): Traffic = {
|
||||
new Traffic(tx, rx, System.currentTimeMillis())
|
||||
}
|
||||
var txLast: Long = _
|
||||
var rxLast: Long = _
|
||||
var timestampLast: Long = _
|
||||
@volatile var dirty = true
|
||||
|
||||
private val units = Array("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
|
||||
private val numberFormat = new DecimalFormat("@@@")
|
||||
@@ -39,21 +33,42 @@ object TrafficMonitor {
|
||||
else numberFormat.format(n) + ' ' + units(i)
|
||||
}
|
||||
|
||||
def updateRate() {
|
||||
val now = getTraffic(txTotal, rxTotal)
|
||||
val delta = now.timestamp - last.timestamp
|
||||
val deltaTx = now.tx - last.tx
|
||||
val deltaRx = now.rx - last.rx
|
||||
def updateRate() = {
|
||||
val now = System.currentTimeMillis()
|
||||
val delta = now - timestampLast
|
||||
var updated = false
|
||||
if (delta != 0) {
|
||||
txRate = deltaTx * 1000 / delta
|
||||
rxRate = deltaRx * 1000 / delta
|
||||
if (dirty) {
|
||||
txRate = (txTotal - txLast) * 1000 / delta
|
||||
rxRate = (rxTotal - rxLast) * 1000 / delta
|
||||
txLast = txTotal
|
||||
rxLast = rxTotal
|
||||
dirty = false
|
||||
updated = true
|
||||
} else {
|
||||
if (txRate != 0) {
|
||||
txRate = 0
|
||||
updated = true
|
||||
}
|
||||
if (rxRate != 0) {
|
||||
rxRate = 0
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
timestampLast = now
|
||||
}
|
||||
last = now
|
||||
updated
|
||||
}
|
||||
|
||||
def update(tx: Long, rx: Long) {
|
||||
txTotal = tx
|
||||
rxTotal = rx
|
||||
if (txTotal != tx) {
|
||||
txTotal = tx
|
||||
dirty = true
|
||||
}
|
||||
if (rxTotal != rx) {
|
||||
rxTotal = rx
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
def reset() {
|
||||
@@ -63,43 +78,7 @@ object TrafficMonitor {
|
||||
rxTotal = 0
|
||||
txLast = 0
|
||||
rxLast = 0
|
||||
last = getTraffic(0, 0)
|
||||
}
|
||||
|
||||
def getTxTotal(): String = {
|
||||
formatTraffic(txTotal)
|
||||
}
|
||||
|
||||
def getRxTotal(): String = {
|
||||
formatTraffic(rxTotal)
|
||||
}
|
||||
|
||||
def getTotal(): String = {
|
||||
formatTraffic(txTotal + rxTotal)
|
||||
}
|
||||
|
||||
def getTxRate(): String = {
|
||||
formatTraffic(txRate)
|
||||
}
|
||||
|
||||
def getRxRate(): String = {
|
||||
formatTraffic(rxRate)
|
||||
}
|
||||
|
||||
def getRate(): String = {
|
||||
formatTraffic(txRate + rxRate)
|
||||
}
|
||||
|
||||
def getDeltaTx(): Long = {
|
||||
val last = txLast
|
||||
txLast = txTotal
|
||||
txTotal - last
|
||||
}
|
||||
|
||||
def getDeltaRx(): Long = {
|
||||
val last = rxLast
|
||||
rxLast = rxTotal
|
||||
rxTotal - last
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,17 @@
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import java.io.{File, IOException}
|
||||
import java.nio.{ByteBuffer, ByteOrder}
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import android.content.Context
|
||||
import android.net.{LocalServerSocket, LocalSocket, LocalSocketAddress}
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.BaseService
|
||||
|
||||
class TrafficMonitorThread() extends Thread {
|
||||
class TrafficMonitorThread(context: Context) extends Thread {
|
||||
|
||||
val TAG = "TrafficMonitorThread"
|
||||
val PATH = "/data/data/com.github.shadowsocks/stat_path"
|
||||
lazy val PATH = context.getApplicationInfo.dataDir + "/stat_path"
|
||||
|
||||
@volatile var serverSocket: LocalServerSocket = null
|
||||
@volatile var isRunning: Boolean = true
|
||||
@@ -99,16 +100,10 @@ class TrafficMonitorThread() extends Thread {
|
||||
val input = socket.getInputStream
|
||||
val output = socket.getOutputStream
|
||||
|
||||
val buffer = new Array[Byte](256)
|
||||
val size = input.read(buffer)
|
||||
|
||||
val array = new Array[Byte](size)
|
||||
Array.copy(buffer, 0, array, 0, size)
|
||||
|
||||
val stat = new String(array, "UTF-8").split("\\|")
|
||||
if (stat.length == 2) {
|
||||
TrafficMonitor.update(stat(0).toLong, stat(1).toLong)
|
||||
}
|
||||
val buffer = new Array[Byte](16)
|
||||
if (input.read(buffer) != 16) throw new IOException("Unexpected traffic stat length")
|
||||
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
|
||||
|
||||
output.write(0)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ import android.util.{DisplayMetrics, Base64, Log}
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.{Gravity, View, Window}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.{ShadowsocksRunnerActivity, ShadowsocksRunnerService, ShadowsocksApplication, BuildConfig}
|
||||
import com.github.shadowsocks.{ShadowsocksRunnerService, ShadowsocksApplication, BuildConfig}
|
||||
import org.xbill.DNS._
|
||||
|
||||
|
||||
@@ -405,15 +405,8 @@ object Utils {
|
||||
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
|
||||
if (!isInstalled) return
|
||||
|
||||
if (Utils.isLollipopOrAbove) {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
} else {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerActivity])
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
def stopSsService(context: Context): Unit = {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.github.shadowsocks.widget
|
||||
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.view.View
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
* @param view The view to find a parent from.
|
||||
* @param undo Callback for undoing removals.
|
||||
* @param commit Callback for committing removals.
|
||||
* @tparam T Item type.
|
||||
*/
|
||||
class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
|
||||
commit: Iterator[(Int, T)] => Unit = null) {
|
||||
private val recycleBin = new ArrayBuffer[(Int, T)]
|
||||
private val removedCallback = new Snackbar.Callback {
|
||||
override def onDismissed(snackbar: Snackbar, event: Int) = {
|
||||
event match {
|
||||
case Snackbar.Callback.DISMISS_EVENT_MANUAL | Snackbar.Callback.DISMISS_EVENT_TIMEOUT =>
|
||||
if (commit != null) commit(recycleBin.iterator)
|
||||
recycleBin.clear
|
||||
case _ =>
|
||||
}
|
||||
last = null
|
||||
}
|
||||
}
|
||||
private var last: Snackbar = _
|
||||
|
||||
def remove(index: Int, item: T) = {
|
||||
recycleBin.append((index, item))
|
||||
val count = recycleBin.length
|
||||
last = Snackbar
|
||||
.make(view, view.getResources.getQuantityString(R.plurals.removed, count, count: Integer), Snackbar.LENGTH_LONG)
|
||||
.setCallback(removedCallback).setAction(R.string.undo, (_ => {
|
||||
undo(recycleBin.reverseIterator)
|
||||
recycleBin.clear
|
||||
}): View.OnClickListener)
|
||||
last.show
|
||||
}
|
||||
|
||||
def flush = if (last != null) last.dismiss
|
||||
}
|
||||