Compare commits

...
99 Commits
Author SHA1 Message Date
Max Lv e4e1a42146 Bump version 2016-02-27 21:09:09 +08:00
Max Lv 76108ec450 Merge pull request #585 from JLLK/master
Fix a issue in ProcessUtils
2016-02-26 14:31:16 +08:00
chentao 5b9a0d0659 Refine #585 2016-02-26 11:48:05 +08:00
Max Lv 6558f275fe Refine #577 2016-02-26 08:54:53 +08:00
chentao c13cf6f5d2 Fix a issue in ProcessUtils 2016-02-26 08:45:43 +08:00
Max Lv cacde5cff1 Fix #584 2016-02-25 21:38:57 +08:00
Max Lv bb2bd82d73 Update README.md 2016-02-25 21:12:56 +08:00
Max Lv 0a46eedba1 Update README.md 2016-02-25 21:04:09 +08:00
Max Lv 63bc60e513 Update README.md 2016-02-25 21:03:04 +08:00
Max Lv d9eb0e5163 Merge pull request #583 from JLLK/master
Fix binder ipc issue when process died unexpected
2016-02-25 21:00:51 +08:00
chentao fc3d26d626 Fix binder ipc issue when process died unexpected 2016-02-25 00:04:57 +08:00
chentao 1bb3a6bf5d Add ShadowsocksDeathRecipient 2016-02-25 00:00:33 +08:00
chentao f6cab9b99c Add IOUtils 2016-02-24 23:58:57 +08:00
chentao a24d29196f Add ProcessUtils 2016-02-24 23:57:38 +08:00
Max Lv 2eb12ee2ae Bump version 2016-02-22 09:00:29 +08:00
Max Lv ef7701982d Fix #577 2016-02-22 08:59:57 +08:00
Max Lv bd942c688f Bump version 2016-02-18 17:36:19 +08:00
Max Lv 3f08a66aab Merge pull request #574 from JLLK/master
Replace Future.onFailure with Future.onComplete
2016-02-18 17:31:15 +08:00
Max Lv ae0801b71b Fix #575 2016-02-18 17:22:08 +08:00
Max Lv 241b84976f Bump version 2016-02-18 16:43:45 +08:00
chentao b76d089fb2 Avoid MatchError 2016-02-17 11:30:58 +08:00
chentao 2f4cdbce25 Replace Future.onFailure with Future.onComplete 2016-02-16 10:05:43 +08:00
Max Lv 8e2fc2b228 Merge pull request #572 from Mygod/master
Refine undo
2016-02-11 23:12:36 +08:00
Mygod d794632616 Refine undo 2016-02-11 21:09:50 +08:00
Max Lv 529b1a32d3 Merge pull request #571 from Mygod/master
Add option: bypass China only
2016-02-08 16:43:03 +08:00
Mygod 396b49dab6 Fix broken links 2016-02-08 16:06:51 +08:00
Mygod 06deb4bbcb Add option: bypass China only
Fix #569.

It could have worked without upgrading database by using an option called `bypass-china-only`. But I think we will get better readability this way.
2016-02-08 15:44:10 +08:00
Max Lv 8127b9f8e2 Merge pull request #570 from Mygod/master
General enhancements
2016-02-08 15:13:14 +08:00
Mygod 3556eebfeb Tasker: scroll to selected profile 2016-02-08 15:06:53 +08:00
Mygod 10e3b211e3 Ask user to refrain from further using NAT mode 2016-02-08 14:40:49 +08:00
Mygod 05981fb117 Add a prompt: per-app proxy requires NAT mode under Android 4.x 2016-02-08 14:25:14 +08:00
Max Lv 4258e95f1e Merge pull request #568 from Mygod/layout
Refine layout of StatusPreference
2016-02-08 13:53:15 +08:00
Mygod cd5c2ef8ef Fix rate display disappearing in StatusPreference
It turns out that `onCreateView` gets called every time `setSummary` is invoked, and thus restoring texts for `TextView` is mandatory.
2016-02-08 13:47:30 +08:00
Mygod 5a84854881 Refine layout using GridView 2016-02-08 12:53:21 +08:00
Mygod 0cf1585712 Refine traffic stat layout 2016-02-07 18:41:50 +08:00
Mygod b8d97dde03 Remove obsolete xml files 2016-02-07 16:18:53 +08:00
Max Lv fbdf2dbf04 Merge pull request #565 from Mygod/ic-start
Replace placeholder icons with brand new icons
2016-02-07 06:29:58 +08:00
Mygod c7dd9aeeca Replace placeholder with brand new icon 2016-02-07 02:00:15 +08:00
Mygod eaac098027 Update dependencies 2016-02-07 00:58:47 +08:00
Max Lv 65635c6b93 Avoid service restarted by system 2016-02-02 09:52:36 +08:00
Max Lv 187cb28d15 Fix an issue of NAT mode
If call notify() after startForeground() on the same notification id, the
service becomes not that "foreground" and would be killed by system if the
activity is removed from the recent list.
2016-01-30 14:01:46 +08:00
Max Lv 2840a75c92 Fix an issue that stat_path is null 2016-01-30 11:00:25 +08:00
Max Lv 95e214ef11 Fix a typo 2016-01-25 20:31:12 +08:00
Max Lv 217ed797d9 Output VPN error to logcat #556 2016-01-25 20:28:11 +08:00
Max Lv d337f0e2e4 Bump version 2016-01-24 17:21:03 +08:00
Max Lv 713ff53030 Refine Traffic Stat 2016-01-24 16:11:18 +08:00
Max Lv 35f17f5338 Fix #547 2016-01-21 12:23:02 +08:00
Max Lv 172c0b240f Fix #545 2016-01-20 16:49:08 +08:00
Max Lv e9406080e2 Merge pull request #535 from wyzegob/master
Change the state after all the checks and initialization
2016-01-13 13:14:51 +08:00
wyzegob 7f937a8ee6 修正正在连接时点击返回,重新打开app界面不刷新的问题。
修正正在连接时点击返回,重新打开app界面不刷新的问题。对比了一下ShadowsocksVpnService.scala中的代码。
2016-01-13 12:28:37 +08:00
Max Lv 3facb73cf1 Bump version 2016-01-13 09:55:32 +08:00
Max Lv 8f10e95329 Refine #530 2016-01-13 09:54:52 +08:00
Max Lv b620a2e666 Fix #530 2016-01-12 12:18:52 +08:00
Max Lv 9cf8c4c483 Drop the broken support of autostart below Lolipop
Close #525
2016-01-11 10:26:22 +08:00
Max Lv bfbbb90ac3 Fix #523 2016-01-08 19:56:29 +08:00
Max Lv dc840fd0e9 Fix #517 2016-01-06 15:28:44 +08:00
Max Lv 79d1ccba23 Workaround for the crash caused by ProgressCircle
It's safe to ignore the exception throwed by hide().
Because the init state of the ProgressCircle is hidden.
2016-01-05 17:28:38 +08:00
Max Lv 19df6caa93 Fix #509 2016-01-04 08:25:58 +08:00
Max Lv 5f268caa0a Update dependency 2015-12-31 21:45:41 +08:00
Max Lv 04c86a537a Merge pull request #502 from Mygod/master
Fix dynamic traffic stat in profile selector
2015-12-29 20:49:02 +08:00
Mygod f9667d767e Fix dynamic traffic stat in profile selector
Fix #495
2015-12-29 20:27:30 +08:00
Max Lv 5ddd8a1686 Bump version 2015-12-27 16:59:11 +08:00
Max Lv 7d44e4fc9f Catch NameNotFoundException 2015-12-27 16:48:46 +08:00
Max Lv 92b9c52944 Merge branch 'master' of github.com:shadowsocks/shadowsocks-android 2015-12-27 16:33:55 +08:00
Max Lv 4f3ebf9395 Merge pull request #497 from Mygod/master
Fix #496
2015-12-27 10:40:31 +08:00
Mygod bf135107d8 Fix individual and userOrder not updated when upgrading from <12 2015-12-27 10:26:00 +08:00
Mygod 5a3d2c07bc Merge branch 'master' of https://github.com/shadowsocks/shadowsocks-android 2015-12-27 10:22:57 +08:00
Mygod d8ab3733dc Fix getCallbackCount is not available on API 16 2015-12-27 10:22:43 +08:00
Mygod b6fc013a06 Unregister preference change callback in onDestroy instead of onPause
Fix #496
2015-12-27 10:17:07 +08:00
Max Lv 7d01ca81c8 Fix #496 2015-12-27 10:08:23 +08:00
Max Lv 9feb5bd96a Fix an issue when updating database from version < 12 2015-12-27 10:01:23 +08:00
Max Lv 2641de436d Merge pull request #494 from Mygod/master
Reduce battery usage
2015-12-27 08:53:51 +08:00
Mygod 4d7bf8d707 Reduce battery usage - Unregister callbacks onStop 2015-12-26 23:46:18 +08:00
Mygod 0018a7ca9e Fix NAT mode not working after task removed from recent 2015-12-26 23:27:21 +08:00
Mygod 51ceb3dac5 Refine callback handling 2015-12-26 23:10:54 +08:00
Mygod 179aab8094 Reduce battery usage - Reduce expensive reflections 2015-12-26 22:32:23 +08:00
Mygod 6bff4f1a44 Reduce battery usage - transmit traffic stat with binary 2015-12-26 20:41:23 +08:00
Mygod 8dcfd5a8c5 Reduce battery usage - prevent unnecessary notification updates 2015-12-26 20:00:12 +08:00
Mygod ef162cd47e Reload apps if a change is detected 2015-12-26 19:21:03 +08:00
Mygod 569f056a22 Reduce battery usage - register broadcast at runtime 2015-12-26 18:56:12 +08:00
Mygod 971ea09077 Reduce battery usage - optimize performance critical block 2015-12-26 17:29:26 +08:00
Mygod fd01712b6c Reduce battery usage - use notify instead of startForeground 2015-12-26 17:14:47 +08:00
Max Lv 6a0955c082 Merge pull request #491 from Mygod/master
Hide adview if default profile is no longer active
2015-12-26 07:52:18 +08:00
Mygod 92c8e0856c Hide adview if default profile is no longer active 2015-12-26 00:00:11 +08:00
Mygod a5ef0cd6f3 Fix upgrading database from 13 2015-12-25 23:27:38 +08:00
Mygod 0612a00474 Remove deprecated permission 2015-12-25 23:00:39 +08:00
Max Lv 0282975e6c Merge pull request #490 from Mygod/master
Enhancements for profile selector, and bugfixes
2015-12-25 22:48:29 +08:00
Mygod c4583bea22 Add a friendly dialog 2015-12-25 22:33:31 +08:00
Mygod 0d582f2392 Update profile default values 2015-12-25 22:03:15 +08:00
Mygod 8cf37954d7 Revert "Use brighter accent color for FAB"
This reverts commit edd5fb4a4c.
2015-12-25 19:08:38 +08:00
Mygod eadb95ca2d Fix creating profiles 2015-12-25 17:43:18 +08:00
Mygod 0f9b686e55 Fix per-app proxy not enabled when NAT is on 2015-12-25 17:29:00 +08:00
Mygod c93a0348ad Fix isDestroyed is missing on API 16 2015-12-25 16:37:30 +08:00
Mygod 0cf509833a Fix dismissing ProgressDialog after activity destroyed 2015-12-25 16:33:44 +08:00
Mygod aed6308841 Fix a race condition 2015-12-25 16:25:39 +08:00
Mygod 9fb0639f86 Move preference related logic to ShadowsocksSettings 2015-12-25 16:07:07 +08:00
Mygod 164becfed0 Implement dragging to move profiles 2015-12-25 15:51:18 +08:00
Mygod eed7ec6eba Update library dependencies 2015-12-25 14:24:37 +08:00
Mygod 7e5b44d36f Scroll to selected profile 2015-12-25 14:20:13 +08:00
85 changed files with 1071 additions and 648 deletions
+2 -4
View File
@@ -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
[![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](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
+6 -5
View File
@@ -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",
+1 -36
View File
@@ -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>'
+4 -1
View File
@@ -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")
+2 -11
View File
@@ -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);
}
+5 -5
View File
@@ -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="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</a></p>
<p>Copyright (C) 2016 Max Lv <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</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);
}
+5 -3
View File
@@ -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>
+14
View File
@@ -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>
+14
View File
@@ -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>
+25
View File
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -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>
+124
View File
@@ -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>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="proxied_apps_summary">Set proxy for selected apps</string>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="proxied_apps_summary">为应用程序分别设置代理</string>
</resources>
+21 -9
View File
@@ -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">&#8226; 点击选择配置文件;\n&#8226; 滑动删除;\n&#8226;
长按拖动改变顺序。</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>
+2
View File
@@ -45,6 +45,7 @@
<string-array name="route_entry">
<item>All</item>
<item>Bypass LAN</item>
<item>Bypass China</item>
<item>Bypass LAN &amp; 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 -14
View File
@@ -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>
+1 -1
View File
@@ -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>
+20 -7
View File
@@ -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">&#8226; Tap to select a profile;\n&#8226; Swipe to remove;\n&#8226;
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>
+4 -1
View File
@@ -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
}