Compare commits

..
63 Commits
Author SHA1 Message Date
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
69 changed files with 799 additions and 494 deletions
+5 -5
View File
@@ -29,13 +29,13 @@ 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.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
"com.j256.ormlite" % "ormlite-core" % "4.48",
+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.16")
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="129"
android:versionName="2.9.7">
<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);
}
@@ -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 },
+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>
+108
View File
@@ -0,0 +1,108 @@
<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:paddingEnd="?android:attr/scrollbarSize"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical" >
<TableLayout
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:stretchColumns="3"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_column="0"
android:layout_span="2"
android:id="@android:id/title"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_column="3"
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" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android: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:gravity="end"
android:text="@string/sent" />
<TextView
android:layout_column="1"
android:id="@+id/tx"
style="@style/TextAppearance.AppCompat.Caption"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:text="0 Byte" />
<TextView
android:layout_column="3"
android:id="@+id/txRate"
style="@style/TextAppearance.AppCompat.Caption"
android:ellipsize="marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="▲ 0 B/s" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android: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:gravity="end"
android:text="@string/received"/>
<TextView
android:layout_column="1"
android:id="@+id/rx"
style="@style/TextAppearance.AppCompat.Caption"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:text="0 Byte" />
<TextView
android:layout_column="3"
android:id="@+id/rxRate"
style="@style/TextAppearance.AppCompat.Caption"
android:ellipsize="marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="▼ 0 B/s" />
</TableRow>
</TableLayout>
</LinearLayout>
+15 -7
View File
@@ -41,7 +41,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 +75,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>
@@ -110,4 +113,9 @@
<string name="stop_service">切换到 %s</string>
<string name="profile_default">使用当前配置</string>
<!-- status -->
<string name="sent">发送:</string>
<string name="received">接收:</string>
<string name="check_connectivity">检查网络连接</string>
</resources>
+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>
+15 -5
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>
@@ -74,7 +75,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,6 +104,10 @@
<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>
@@ -115,4 +120,9 @@
<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>
<string name="check_connectivity">Check Connectivity</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/check_connectivity"
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,7 +1,7 @@
package com.github.shadowsocks
import android.content.Intent
import android.os.{Handler, Bundle}
import android.os.{Bundle, Handler}
import android.support.design.widget.Snackbar
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder
@@ -25,7 +25,13 @@ 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 = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
@@ -55,28 +61,27 @@ 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)
}
handler.post(() => text.setText(builder))
}
def bind(item: Profile) {
def bind(item: Profile, i: Int) {
this.item = item
updateText()
if (item.id == ShadowsocksApplication.profileId) {
text.setChecked(true)
selectedIndex = i
selectedItem = this
} else {
text.setChecked(false)
@@ -92,12 +97,12 @@ 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
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i))
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i), i)
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
@@ -110,6 +115,24 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
notifyItemInserted(pos)
}
def move(from: Int, to: Int) {
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)
@@ -131,6 +154,7 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
}
}
private var selectedIndex = -1
private var selectedItem: ProfileViewHolder = _
private val handler = new Handler
@@ -153,28 +177,52 @@ 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)
if (selectedIndex < 0) selectedIndex = profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == ShadowsocksApplication.profileId => i
}.getOrElse(-1)
layoutManager.scrollToPosition(selectedIndex)
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) {
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
def onSwiped(viewHolder: ViewHolder, direction: Int) = {
profilesAdapter.remove(viewHolder.getAdapterPosition)
removedSnackbar.show
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = false // TODO?
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = {
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
true
}
}).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 {
@@ -4,6 +4,7 @@ import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{RemoteException, IBinder}
import com.github.shadowsocks.aidl.{IShadowsocksServiceCallback, IShadowsocksService}
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.Utils
/**
* @author Mygod
@@ -13,20 +14,12 @@ trait ServiceBoundContext extends Context {
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
}
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 +27,25 @@ 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 => // Nothing
}
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
@@ -59,8 +58,8 @@ import android.view.{View, ViewGroup, ViewParent}
import android.widget._
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preferences._
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 +90,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 +105,11 @@ 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 trafficCache: Array[String] = _
var connectionTestResult: String = _
var connectionTestSuccess: Boolean = true
// Services
var currentServiceName = classOf[ShadowsocksNatService].getName
@@ -163,23 +119,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,32 +144,41 @@ 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)
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
trafficCache = Array(
"▲ " + TrafficMonitor.formatTraffic(txRate) + "/s",
"▼ " + TrafficMonitor.formatTraffic(rxRate) + "/s",
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal))
handler.post(updateTraffic)
}
}
def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(TrafficMonitor.getTxRate,
TrafficMonitor.getRxRate, TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal) else {
def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(0, 0, 0, 0) else {
if (connectionTestResult == null) connectionTestResult = getString(R.string.connection_test_pending)
if (preferences.natSwitch.isChecked) {
preferences.stat.setSummary(trafficCache)
preferences.stat.setSummary("")
} else {
preferences.stat.setSummary(connectionTestResult + "\n" + trafficCache)
if (connectionTestSuccess) {
preferences.stat.setSummary(connectionTestResult)
} else {
preferences.stat.setSummary(getString(R.string.connection_test_fail))
Snackbar.make(findViewById(android.R.id.content), connectionTestResult, Snackbar.LENGTH_LONG).show
}
}
preferences.stat.asInstanceOf[StatusPreference].setRate(trafficCache(2), trafficCache(3), trafficCache(0), trafficCache(1))
}
def attachService: Unit = attachService(callback)
@@ -221,7 +186,7 @@ class Shadowsocks
override def onServiceConnected() {
// Update the UI
if (fab != null) fab.setEnabled(true)
stateUpdate()
updateState()
if (!ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)) {
ShadowsocksApplication.settings.edit.putBoolean(ShadowsocksApplication.getVersionName, true).apply()
@@ -255,13 +220,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 +263,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 +293,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 +307,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 +332,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 {
@@ -438,9 +398,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 +412,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 +456,43 @@ 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 +503,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 +534,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 +561,8 @@ class Shadowsocks
}
def clearDialog() {
if (progressDialog != null) {
progressDialog.dismiss()
if (progressDialog != null && progressDialog.isShowing) {
if (!isDestroyed) progressDialog.dismiss()
progressDialog = null
progressTag = -1
}
@@ -44,6 +44,7 @@ import java.lang.Process
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.app.Service
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.net.{ConnectivityManager, Network}
@@ -167,7 +168,7 @@ class ShadowsocksNatService extends BaseService {
case Route.BYPASS_LAN => getResources.getStringArray(R.array.private_route)
case Route.BYPASS_CHN => getResources.getStringArray(R.array.chn_route_full)
}
ConfigUtils.printToFile(new File(Path.BASE + "acl.list"))(p => {
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
acl.foreach(item => p.println(item))
})
}
@@ -175,21 +176,22 @@ class ShadowsocksNatService extends BaseService {
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 +206,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 +233,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"
@@ -258,17 +262,17 @@ class ShadowsocksNatService extends BaseService {
val conf = if (config.route == Route.BYPASS_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 +296,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 +394,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 +411,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 +461,6 @@ class ShadowsocksNatService extends BaseService {
override def stopRunner() {
super.stopRunner()
// channge the state
changeState(State.STOPPING)
@@ -476,17 +477,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
@@ -11,11 +11,61 @@ import android.os.{Build, Bundle}
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
import android.support.v7.app.AlertDialog
import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences.{DropDownPreference, NumberPickerPreference, PasswordEditTextPreference, SummaryEditTextPreference}
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 = _
@@ -28,11 +78,12 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
addPreferencesFromResource(R.xml.pref_all)
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
stat = findPreference(Key.stat)
stat= findPreference(Key.stat)
stat.setOnPreferenceClickListener(_ => {
val id = synchronized {
testCount += 1
activity.connectionTestResult = getString(R.string.connection_test_testing)
activity.connectionTestSuccess = true
activity.updateTraffic()
testCount
}
@@ -46,6 +97,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
conn.setUseCaches(false)
if (testCount == id) {
var result: String = null
var success: Boolean = true
try {
val start = currentTimeMillis
conn.getInputStream
@@ -55,9 +107,13 @@ 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.connectionTestSuccess = success
activity.connectionTestResult = result
activity.handler.post(activity.updateTraffic)
})
@@ -94,7 +150,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,8 +176,8 @@ 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)
}
@@ -128,4 +188,30 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
})
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
@@ -283,7 +277,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
case Route.BYPASS_LAN => getResources.getStringArray(R.array.private_route)
case Route.BYPASS_CHN => getResources.getStringArray(R.array.chn_route_full)
}
ConfigUtils.printToFile(new File(Path.BASE + "acl.list"))(p => {
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
acl.foreach(item => p.println(item))
})
}
@@ -291,21 +285,22 @@ class ShadowsocksVpnService extends VpnService with BaseService {
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 +315,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"
@@ -349,17 +345,17 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (config.route == Route.BYPASS_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,10 +385,15 @@ 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);
}
}
}
@@ -429,16 +430,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 +460,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)
@@ -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, 15) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
@@ -110,10 +110,15 @@ 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
}
}
}
@@ -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)
@@ -0,0 +1,46 @@
package com.github.shadowsocks.preferences
import android.content.Context
import android.preference.Preference
import android.view.{View, ViewGroup, LayoutInflater}
import android.util.AttributeSet
import android.widget.TextView
import com.github.shadowsocks.R
/**
* @author madeye
*/
final class StatusPreference(context: Context, attrs: AttributeSet = null)
extends Preference(context, attrs) {
var txView: TextView = null
var rxView: TextView = null
var txRateView: TextView = null
var rxRateView: TextView = null
override def onCreateView(parent: ViewGroup): View = {
val li = getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
val view = li.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
}
def setRate(tx: String, rx: String, txRate: String, rxRate: String) {
if (txView != null) {
txView.setText(tx)
}
if (rxView != null) {
rxView.setText(rx)
}
if (txRateView != null) {
txRateView.setText(txRate)
}
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"
@@ -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 = {