Compare commits

...
79 Commits
Author SHA1 Message Date
Max Lv 5fa8e966e5 Bump version 2016-03-18 10:12:17 +08:00
Max Lv 17e84e58d8 Refine AdView 2016-03-18 10:03:51 +08:00
Max Lv 10940f2b1d Merge pull request #611 from AnyOfYou/master
NFC state check
2016-03-17 07:09:52 +08:00
Lucas Liu dd4fad1a41 ParserActivity bug fixes. 2016-03-17 00:25:13 +08:00
Lucas Liu 49a3cfdc34 NFC state check 2016-03-16 23:30:52 +08:00
Max Lv 82325ecdba Merge pull request #610 from Mygod/master
Refine #609
2016-03-16 22:41:05 +08:00
Mygod 555f4f6854 Refine #609 2016-03-16 22:31:07 +08:00
Max Lv e99a0eaafc Merge pull request #609 from AnyOfYou/master
Fix parser bug, add share profile by NFC.
2016-03-16 13:46:20 +08:00
Lucas Liu 7f2f67f27c Fix crash when cancel qrcode scan. 2016-03-16 13:22:53 +08:00
Lucas Liu 488d825637 Add share profile by NFC. 2016-03-16 12:47:10 +08:00
Lucas Liu f792c8ebfb Fix parser bug 2016-03-16 01:15:17 +08:00
Max Lv 78e57de833 Refine #602 further 2016-03-13 13:33:36 +08:00
Max Lv 017dd425a0 Refine #602 2016-03-13 13:13:37 +08:00
Max Lv 04ec29c9ea Merge pull request #603 from Mygod/master
Import/export one/all ss urls
2016-03-12 15:42:06 +08:00
Mygod 16e39f8b6c Hide quick switch if there's nothing to switch 2016-03-12 15:28:23 +08:00
Mygod 6dede5c067 Import/export one/all ss urls 2016-03-12 15:11:11 +08:00
Mygod fb3f7429b7 Refine parser 2016-03-12 14:39:00 +08:00
Mygod 0e0e65c45d Move profileTip to Key 2016-03-12 14:20:18 +08:00
Max Lv 86da4f535d Merge pull request #602 from Mygod/restart
Automatic service restart (with zero latency) after switching profile
2016-03-12 14:10:56 +08:00
Mygod ee48c0773c Automatic service restart 2016-03-12 13:40:13 +08:00
Max Lv 6ade064599 Merge pull request #600 from Mygod/master
Refine layout for #598
2016-03-12 12:45:19 +08:00
Mygod 8fd8514cc2 Refine layout for #598 2016-03-12 11:18:21 +08:00
Mygod 0aad8f13ed Merge branch 'master' of https://github.com/shadowsocks/shadowsocks-android 2016-03-12 11:17:53 +08:00
Max Lv 14a5324852 Merge pull request #598 from AnyOfYou/master
Add quick switch from notification.
2016-03-12 08:00:42 +08:00
Lucas Liu 7c5efdaa82 Add quick switch from notification. 2016-03-12 01:13:03 +08:00
Mygod 1c37c449d5 Update dependencies 2016-03-11 23:05:31 +08:00
Mygod 0f2ae6521b Oops fix a bug 2016-03-11 22:55:47 +08:00
Max Lv 3ae15514a5 Merge pull request #597 from Mygod/master
Refine #574, #583, #585, #594
2016-03-11 22:50:29 +08:00
Max Lv cee74f2563 Update setup scripts 2016-03-11 22:51:18 +08:00
Mygod 4c1a817c1b Fix a crash on Android N preview 2016-03-11 22:17:06 +08:00
Mygod 390d79c9aa Add missing volatile 2016-03-11 22:05:13 +08:00
Mygod 216cc9efe2 Refine #574 2016-03-11 22:02:52 +08:00
Mygod 3bb746254e Refine #594, again 2016-03-11 21:53:18 +08:00
Mygod 8fa2e60cf3 Remove redundant bindToService
Please do not create methods casually.
2016-03-11 20:41:46 +08:00
Mygod 8657ef87f4 Crash recovery for NAT mode
Bugfix: when NAT process is killed, Internet access is gone. Reproduce with:

    adb shell killall com.github.shadowsocks:nat

One circumstance it cannot recover from is when updating to a new version, where both processes are killed at the same time. But a reset will be executed when the new version is launched.
2016-03-11 20:39:01 +08:00
Mygod da1b404175 Remove redundant IOUtils 2016-03-11 20:19:45 +08:00
Mygod fcdeb5144a Remove ProcessUtils
It's safe to say that Shadowsocks is only instantialized in the main process, so boom, problem solved.
2016-03-11 20:18:22 +08:00
Mygod 270d436017 Refine ShadowsocksDeathRecipient and ServiceBoundContext 2016-03-11 20:17:23 +08:00
Mygod a4e186b759 Generate images with android-sdk-plugin 2016-03-11 19:27:50 +08:00
Max Lv 9965a71c93 Refine #594 2016-03-07 17:59:15 +08:00
Max Lv 9e09e0329b Merge pull request #594 from ayanamist/miui
Recover unexpected process exit on MIUI
2016-03-07 17:04:45 +08:00
ayanamist c19eb4e9e5 Convert GuardedProcess to Scala 2016-03-07 16:46:54 +08:00
ayanamist cad6d7b62a Recover unexpected process exit on MIUI 2016-03-07 15:48:37 +08:00
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
88 changed files with 904 additions and 571 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
+5 -3
View File
@@ -6,7 +6,7 @@ platformTarget in Android := "android-23"
name := "shadowsocks"
scalaVersion := "2.11.7"
scalaVersion := "2.11.8"
compileOrder in Compile := CompileOrder.JavaThenScala
@@ -30,13 +30,15 @@ libraryDependencies ++= Seq(
"dnsjava" % "dnsjava" % "2.1.7",
"com.github.kevinsawicki" % "http-request" % "6.0",
"commons-net" % "commons-net" % "3.4",
"eu.chainfire" % "libsuperuser" % "1.0.0.201602011018",
"eu.chainfire" % "libsuperuser" % "1.0.0.201602271131",
"com.google.zxing" % "android-integration" % "3.2.1",
"net.glxn.qrgen" % "android" % "2.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" % "design" % "23.2.1",
"com.android.support" % "gridlayout-v7" % "23.2.1",
"com.android.support" % "cardview-v7" % "23.2.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>'
+1 -1
View File
@@ -1,6 +1,6 @@
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.16")
addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.19")
resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
+23 -2
View File
@@ -1,16 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.shadowsocks"
android:versionCode="129"
android:versionName="2.9.7">
android:versionCode="134"
android:versionName="2.10.1">
<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.NFC" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="23"/>
@@ -46,6 +49,8 @@
<activity
android:name=".ParserActivity"
android:theme="@style/PopupTheme"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@@ -53,6 +58,11 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="ss"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="ss" />
</intent-filter>
</activity>
<activity
@@ -86,6 +96,17 @@
</intent-filter>
</activity>
<activity android:name=".ShadowsocksQuickSwitchActivity"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:taskAffinity=""
android:theme="@style/PopupTheme.QuickSwitch">
<intent-filter>
<action android:name="com.github.shadowsocks.QUICK_SWITCH" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<service
android:name=".ShadowsocksRunnerService"
android:exported="false">
@@ -10,6 +10,5 @@ interface IShadowsocksService {
oneway void registerCallback(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void start(in Config config);
oneway void stop();
oneway void use(in Config config);
}
+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>
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 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.

Before

Width:  |  Height:  |  Size: 1.0 KiB

+3 -2
View File
@@ -23,11 +23,12 @@
android:drawablePadding="20dp"
android:maxLines="4"
android:scrollHorizontally="false" />
<ImageView android:id="@+id/qrcode"
<ImageView android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/qrcode"/>
android:contentDescription="@string/share"/>
</LinearLayout>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_margin="8dp"
android:layout_gravity="center_vertical"
app:cardElevation="5dp"
app:cardBackgroundColor="#EEEEEE">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/profilesList"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</android.support.v7.widget.CardView>
+67 -51
View File
@@ -4,33 +4,23 @@
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingEnd="?android:attr/scrollbarSize"
android:paddingLeft="15dip"
android:paddingRight="15dip"
android:paddingTop="6dip"
android:paddingBottom="6dip"
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"
<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="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_column="3"
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"
@@ -38,71 +28,97 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end" />
</TableRow>
</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">
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_column="0"
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
android:layout_column="1"
app:layout_column="1"
android:id="@+id/tx"
style="@style/TextAppearance.AppCompat.Caption"
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:gravity="end"
android:text="0 Byte" />
android:text="▲" />
<TextView
android:layout_column="3"
app:layout_column="3"
android:id="@+id/txRate"
style="@style/TextAppearance.AppCompat.Caption"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:text="▲ 0 B/s" />
</TableRow>
app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal"
android:gravity="end" />
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_column="0"
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
android:layout_column="1"
app:layout_row="1"
app:layout_column="1"
android:id="@+id/rx"
style="@style/TextAppearance.AppCompat.Caption"
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:gravity="end"
android:text="0 Byte" />
android:text="▼" />
<TextView
android:layout_column="3"
app:layout_row="1"
app:layout_column="3"
android:id="@+id/rxRate"
style="@style/TextAppearance.AppCompat.Caption"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="marquee"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:text="▼ 0 B/s" />
</TableRow>
app:layout_columnWeight="1"
app:layout_gravity="fill_horizontal"
android:gravity="end" />
</TableLayout>
</android.support.v7.widget.GridLayout>
</LinearLayout>
+9 -5
View File
@@ -1,8 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:showAsAction="never" />
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:showAsAction="never" />
android:title="@string/action_import"
android:showAsAction="never"
android:alphabeticShortcut="i"
android:numericShortcut="1"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:showAsAction="never"
android:alphabeticShortcut="e"
android:numericShortcut="2"/>
</menu>
@@ -15,6 +15,14 @@
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="2"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="3"/>
<item android:id="@+id/action_export"
android:title="@string/action_export"
android:alphabeticShortcut="e"
android:numericShortcut="4"/>
</menu>
</item>
</menu>
+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>
+14 -4
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,9 +34,11 @@
<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>
<string name="quick_switch">快速切换</string>
<string name="stopping">正在关闭……</string>
<string name="vpn_error">后台服务启动失败:%s</string>
<string name="reboot_required">VPN 服务启动失败。你可能需要重启设备。</string>
@@ -58,7 +60,11 @@
<string name="flush_dnscache_summary_root">只需在 NAT 模式下使用,需要 ROOT 权限</string>
<string name="flush_dnscache_no_root">在没有 ROOT 权限的情况下无法刷新 DNS 缓存</string>
<string name="flushing">刷新中……</string>
<string name="qrcode">二维码</string>
<string name="share">分享</string>
<string name="share_message">扫描二维码或者使用 NFC(Android Beam)</string>
<string name="share_message_without_nfc">扫描二维码</string>
<string name="share_message_nfc_disabled">扫描二维码,NFC(Android Beam)被禁用</string>
<string name="turn_on_nfc">打开 NFC &amp; Android Beam</string>
<string name="add_profile">添加配置文件</string>
<string name="nat">NAT 模式 (仅限调试)</string>
@@ -88,6 +94,7 @@
<string name="profile_manager_dialog_content">&#8226; 点击选择配置文件;\n&#8226; 滑动删除;\n&#8226;
长按拖动改变顺序。</string>
<string name="gotcha">好的</string>
<string name="copy_url">复制 URL</string>
<string name="add_profile_dialog">为影梭添加此配置文件?</string>
<string name="add_profile_methods_scan_qr_code">扫描二维码</string>
<string name="add_profile_methods_manual_settings">手动设置</string>
@@ -95,6 +102,7 @@
<string-array name="route_entry">
<item>全局</item>
<item>绕过局域网地址</item>
<item>绕过中国大陆地址</item>
<item>绕过局域网及中国大陆地址</item>
</string-array>
@@ -105,7 +113,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>
@@ -116,6 +127,5 @@
<!-- status -->
<string name="sent">发送:</string>
<string name="received">接收:</string>
<string name="check_connectivity">检查网络连接</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>
+13 -4
View File
@@ -54,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>
@@ -66,12 +66,14 @@
<!-- 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>
<string name="stopping">Shutting down…</string>
<string name="vpn_error">%s</string>
<string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string>
<string name="quick_switch">Quick Switch</string>
<!-- alert category -->
<string name="close">Close</string>
@@ -89,7 +91,11 @@
<string name="recovery_summary">Reset all the background services</string>
<string name="about">About</string>
<string name="about_title">Shadowsocks %s</string>
<string name="qrcode">QR Code</string>
<string name="share">Share</string>
<string name="share_message">Scan QRCode or Use NFC (Android Beam)</string>
<string name="share_message_without_nfc">Scan QRCode</string>
<string name="share_message_nfc_disabled">Scan QRCode, NFC (Android Beam) is disabled</string>
<string name="turn_on_nfc">Turn on NFC &amp; Android Beam</string>
<string name="flush_dnscache">Flush DNS cache</string>
<string name="flush_dnscache_summary">Only useful in NAT mode</string>
<string name="flush_dnscache_summary_root">Only useful in NAT mode. Requires ROOT permission.</string>
@@ -111,7 +117,11 @@
<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>
<string name="copy_url">Copy URL</string>
<plurals name="removed">
<item quantity="one">Removed</item>
<item quantity="other">%d items removed</item>
</plurals>
<string name="undo">Undo</string>
<!-- tasker -->
@@ -124,5 +134,4 @@
<!-- status -->
<string name="sent">Sent:</string>
<string name="received">Received:</string>
<string name="check_connectivity">Check Connectivity</string>
</resources>
+11
View File
@@ -5,6 +5,7 @@
<style name="Toolbar.Logo" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">24sp</item>
</style>
<style name="Theme.Material" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:alertDialogTheme">@style/Theme.Material.Dialog.Alert</item>
<item name="android:dialogTheme">@style/Theme.Material.Dialog</item>
@@ -14,19 +15,23 @@
<item name="colorPrimary">@color/material_accent_500</item>
<item name="colorPrimaryDark">@color/material_accent_700</item>
</style>
<style name="Theme.Material.DialogBase" parent="Theme.AppCompat.Light.Dialog">
<item name="colorAccent">@color/material_accent_500</item>
<item name="colorPrimary">@color/material_accent_500</item>
<item name="colorPrimaryDark">@color/material_accent_700</item>
</style>
<style name="Theme.Material.Dialog.AlertBase" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="colorAccent">@color/material_accent_500</item>
<item name="colorPrimary">@color/material_accent_500</item>
<item name="colorPrimaryDark">@color/material_accent_700</item>
</style>
<style name="Theme.Material.Dialog" parent="Theme.Material.DialogBase">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Theme.Material.Dialog.Alert" parent="Theme.Material.Dialog.AlertBase">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
@@ -39,4 +44,10 @@
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="PopupTheme.QuickSwitch" parent="PopupTheme">
<item name="windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
</style>
</resources>
+1 -1
View File
@@ -10,7 +10,7 @@
<com.github.shadowsocks.preferences.StatusPreference
android:key="stat"
android:summary="@string/check_connectivity"
android:summary="@string/connection_test_pending"
android:title="@string/stat"/>
</PreferenceCategory>
@@ -154,26 +154,22 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
return true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
val clipdata = clipboard.getPrimaryClip
val label = clipdata.getDescription.getLabel
if (label == Key.proxied) {
val proxiedAppSequence = clipdata.getItemAt(0).getText
if (proxiedAppSequence != null) {
val proxiedAppString = proxiedAppSequence.toString
if (!proxiedAppString.isEmpty) {
val editor = prefs.edit
val i = proxiedAppString.indexOf('\n')
if (i < 0)
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
// Restart activity
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
reloadApps()
return true
}
val proxiedAppSequence = clipboard.getPrimaryClip.getItemAt(0).getText
if (proxiedAppSequence != null) {
val proxiedAppString = proxiedAppSequence.toString
if (!proxiedAppString.isEmpty) {
val editor = prefs.edit
val i = proxiedAppString.indexOf('\n')
if (i < 0)
editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply()
else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean)
.putString(Key.proxied, proxiedAppString.substring(i + 1)).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
// Restart activity
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
reloadApps()
return true
}
}
}
@@ -42,10 +42,12 @@ package com.github.shadowsocks
import java.util.{Timer, TimerTask}
import android.app.Service
import android.content.{Intent, Context}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.{Handler, RemoteCallbackList}
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
import com.github.shadowsocks.utils._
trait BaseService extends Service {
@@ -57,6 +59,13 @@ trait BaseService extends Service {
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
var callbacksCount: Int = _
lazy val handler = new Handler(getContext.getMainLooper)
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(true)
}
var closeReceiverRegistered: Boolean = _
val binder = new IShadowsocksService.Stub {
override def getMode: Int = {
@@ -94,17 +103,15 @@ trait BaseService extends Service {
}
}
override def stop() {
if (state == State.CONNECTED) {
stopRunner()
}
}
override def start(config: Config) {
if (state == State.STOPPED) {
startRunner(config)
}
}
override def use(config: Config) = synchronized(state match {
case State.STOPPED => if (config != null) startRunner(config)
case State.CONNECTED =>
if (config == null) stopRunner(true) else if (config.profileId != BaseService.this.config.profileId) {
stopRunner(false)
startRunner(config)
}
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
})
}
def startRunner(config: Config) {
@@ -114,9 +121,24 @@ trait BaseService extends Service {
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
}
def stopRunner() {
def stopRunner(stopService: Boolean) {
// clean up recevier
if (closeReceiverRegistered) {
unregisterReceiver(closeReceiver)
closeReceiverRegistered = false
}
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
@@ -130,7 +152,7 @@ trait BaseService extends Service {
changeState(State.STOPPED)
// stop the service if nothing has bound to it
stopSelf()
if (stopService) stopSelf()
}
def updateTrafficTotal(tx: Long, rx: Long) {
@@ -158,7 +180,6 @@ trait BaseService extends Service {
}
def updateTrafficRate() {
val handler = new Handler(getContext.getMainLooper)
handler.post(() => {
if (callbacksCount > 0) {
val txRate = TrafficMonitor.txRate
@@ -0,0 +1,121 @@
/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2016 <max.c.lv@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks
import java.io.{IOException, InputStream, OutputStream}
import java.lang.System.currentTimeMillis
import java.util.concurrent.Semaphore
import android.util.Log
import scala.collection.JavaConversions._
/**
* @author ayanamist@gmail.com
*/
class GuardedProcess(cmd: Seq[String]) extends Process {
private val TAG = classOf[GuardedProcess].getSimpleName
@volatile private var guardThread: Thread = _
@volatile private var isDestroyed: Boolean = _
@volatile private var process: Process = _
def start(onRestartCallback: () => Unit = null): GuardedProcess = {
val semaphore = new Semaphore(1)
semaphore.acquire
@volatile var ioException: IOException = null
guardThread = new Thread(() => {
try {
var callback: () => Unit = null
while (!isDestroyed) {
Log.i(TAG, "start process: " + cmd)
val startTime = currentTimeMillis
process = new ProcessBuilder(cmd).redirectErrorStream(true).start
if (callback == null) callback = onRestartCallback else callback()
semaphore.release
process.waitFor
if (currentTimeMillis - startTime < 1000) {
Log.w(TAG, "process exit too fast, stop guard: " + cmd)
isDestroyed = true
}
}
} catch {
case ignored: InterruptedException =>
Log.i(TAG, "thread interrupt, destroy process: " + cmd)
process.destroy()
case e: IOException => ioException = e
} finally semaphore.release
}, "GuardThread-" + cmd)
guardThread.start()
semaphore.acquire
if (ioException != null) {
throw ioException
}
this
}
def destroy() {
isDestroyed = true
guardThread.interrupt()
process.destroy()
try guardThread.join() catch {
case ignored: InterruptedException =>
}
}
def exitValue: Int = throw new UnsupportedOperationException
def getErrorStream: InputStream = throw new UnsupportedOperationException
def getInputStream: InputStream = throw new UnsupportedOperationException
def getOutputStream: OutputStream = throw new UnsupportedOperationException
@throws(classOf[InterruptedException])
def waitFor = {
guardThread.join()
0
}
}
@@ -39,20 +39,43 @@
package com.github.shadowsocks
import android.content.DialogInterface
import android.content.{DialogInterface, Intent}
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.nfc.{NdefMessage, NfcAdapter}
import android.os.Bundle
import android.support.v7.app.{AppCompatActivity, AlertDialog}
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.text.TextUtils
import android.view.WindowManager
import com.github.shadowsocks.utils.Parser
class ParserActivity extends AppCompatActivity {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val data = getIntent.getData.toString
val profile = Parser.parse(data)
if (profile.isEmpty) { // ignore
handledSharedIntent()
}
override def onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handledSharedIntent()
}
def handledSharedIntent() {
val intent = getIntent()
val sharedStr = intent.getAction match {
case Intent.ACTION_VIEW => intent.getData.toString
case NfcAdapter.ACTION_NDEF_DISCOVERED =>
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null && rawMsgs.nonEmpty)
new String(rawMsgs(0).asInstanceOf[NdefMessage].getRecords()(0).getPayload)
else null
case _ => null
}
if (TextUtils.isEmpty(sharedStr)) return
val profiles = Parser.findAll(sharedStr).toList
if (profiles.isEmpty) {
finish()
return
}
@@ -60,13 +83,12 @@ class ParserActivity extends AppCompatActivity {
val dialog = new AlertDialog.Builder(this)
.setTitle(R.string.add_profile_dialog)
.setCancelable(false)
.setPositiveButton(android.R.string.yes, ((dialog: DialogInterface, id: Int) => {
ShadowsocksApplication.profileManager.createProfile(profile.get)
}): DialogInterface.OnClickListener)
.setPositiveButton(android.R.string.yes, ((_, _) =>
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)): DialogInterface.OnClickListener)
.setNegativeButton(android.R.string.no, null)
.setMessage(data)
.setMessage(profiles.mkString("\n"))
.create()
dialog.setOnDismissListener((dialog: DialogInterface) => finish()) // builder method was added in API 17
dialog.setOnDismissListener((_: DialogInterface) => finish()) // builder method was added in API 17
dialog.show()
}
@@ -1,8 +1,12 @@
package com.github.shadowsocks
import android.content.Intent
import android.os.{Bundle, Handler}
import android.support.design.widget.Snackbar
import java.nio.charset.Charset
import android.content._
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.os.{Build, Bundle, Handler}
import android.provider.Settings
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
@@ -10,52 +14,70 @@ import android.support.v7.widget.helper.ItemTouchHelper
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.text.{SpannableStringBuilder, Spanned, TextUtils}
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.utils.{Key, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.zxing.integration.android.IntentIntegrator
import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.ArrayBuffer
/**
* @author Mygod
*/
object ProfileManagerActivity {
private final val profileTip = "profileTip"
}
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext {
import ProfileManagerActivity._
class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListener with ServiceBoundContext
with CreateNdefMessageCallback {
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)
{
val qrcode = itemView.findViewById(R.id.qrcode)
qrcode.setOnClickListener((v: View) => {
val shareBtn = itemView.findViewById(R.id.share)
shareBtn.setOnClickListener(_ => {
val url = item.toString
if (nfcBeamEnable) {
nfcAdapter.setNdefPushMessageCallback(ProfileManagerActivity.this,ProfileManagerActivity.this)
nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
}
val image = new ImageView(ProfileManagerActivity.this)
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(Parser.generate(item))
val qrcode = QRCode.from(url)
.withSize(Utils.dpToPx(ProfileManagerActivity.this, 250), Utils.dpToPx(ProfileManagerActivity.this, 250))
.asInstanceOf[QRCode].bitmap()
image.setImageBitmap(qrcode)
new AlertDialog.Builder(ProfileManagerActivity.this)
val dialog = new AlertDialog.Builder(ProfileManagerActivity.this)
.setCancelable(true)
.setNegativeButton(R.string.close, null)
.setPositiveButton(R.string.close, null)
.setNegativeButton(R.string.copy_url, ((_, _) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))): DialogInterface.OnClickListener)
.setView(image)
.setTitle(R.string.share)
.create()
.show()
if (!nfcAvailable){
dialog.setMessage(getString(R.string.share_message_without_nfc))
} else {
if (!nfcBeamEnable) {
dialog.setMessage(getString(R.string.share_message_nfc_disabled))
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, getString(R.string.turn_on_nfc), ((_, _) =>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
} else {
startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
): DialogInterface.OnClickListener)
} else {
dialog.setMessage(getString(R.string.share_message))
dialog.setOnDismissListener(_ =>
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this))
}
}
dialog.show()
})
qrcode.setOnLongClickListener((v: View) => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.qrcode, Toast.LENGTH_SHORT), qrcode,
shareBtn.setOnLongClickListener(_ => {
Utils.positionToast(Toast.makeText(ProfileManagerActivity.this, R.string.share, Toast.LENGTH_SHORT), shareBtn,
getWindow, 0, Utils.dpToPx(ProfileManagerActivity.this, 8)).show
true
})
@@ -76,12 +98,11 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
handler.post(() => text.setText(builder))
}
def bind(item: Profile, i: Int) {
def bind(item: Profile) {
this.item = item
updateText()
if (item.id == ShadowsocksApplication.profileId) {
text.setChecked(true)
selectedIndex = i
selectedItem = this
} else {
text.setChecked(false)
@@ -96,26 +117,25 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
private val recycleBin = new ArrayBuffer[(Int, 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), i)
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = vh.bind(profiles(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
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
@@ -134,32 +154,32 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
}
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)
}
}
private var selectedIndex = -1
private var selectedItem: ProfileViewHolder = _
private val handler = new Handler
private lazy val profilesAdapter = new ProfilesAdapter
private var removedSnackbar: Snackbar = _
private var undoManager: UndoSnackbarManager[Profile] = _
private lazy val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
private var nfcAdapter : NfcAdapter = _
private var nfcShareItem: Array[Byte] = _
private var nfcAvailable = false
private var nfcEnable = false
private var nfcBeamEnable = false
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
@@ -181,21 +201,16 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
profilesList.setLayoutManager(layoutManager)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
if (selectedIndex < 0) selectedIndex = profilesAdapter.profiles.zipWithIndex.collectFirst {
layoutManager.scrollToPosition(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) = ()
})
}.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)
@@ -209,13 +224,38 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
if (selectedItem != null) selectedItem.updateText(txTotal, rxTotal)
})
if (ShadowsocksApplication.settings.getBoolean(profileTip, true)) {
ShadowsocksApplication.settings.edit.putBoolean(profileTip, false).commit
if (ShadowsocksApplication.settings.getBoolean(Key.profileTip, true)) {
ShadowsocksApplication.settings.edit.putBoolean(Key.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 onResume() {
super.onResume()
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter != null){
nfcAvailable = true
if (nfcAdapter.isEnabled) {
nfcEnable = true
if (nfcAdapter.isNdefPushEnabled) {
nfcBeamEnable = true
nfcAdapter.setNdefPushMessageCallback(null, ProfileManagerActivity.this)
} else {
nfcBeamEnable = false
}
} else{
nfcEnable = false
nfcBeamEnable = false
}
} else {
nfcAvailable = false
nfcEnable = false
nfcBeamEnable = false
}
}
override def onStart() {
super.onStart()
registerCallback
@@ -227,20 +267,24 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
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) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) Parser.parse(scanResult.getContents) match {
case Some(profile) => ShadowsocksApplication.profileManager.createProfile(profile)
case _ => // ignore
if (scanResult != null) {
val contents = scanResult.getContents
if (!TextUtils.isEmpty(contents))
Parser.findAll(contents).foreach(ShadowsocksApplication.profileManager.createProfile)
}
}
def onMenuItemClick(item: MenuItem) = item.getItemId match {
def createNdefMessage(nfcEvent: NfcEvent) =
new NdefMessage(Array(new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, nfcShareItem, Array[Byte](), nfcShareItem)))
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.scan_qr_code =>
val integrator = new IntentIntegrator(this)
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
@@ -253,6 +297,25 @@ class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClickListe
ShadowsocksApplication.switchProfile(ShadowsocksApplication.profileManager.save.id)
finish
true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
val profiles = Parser.findAll(clipboard.getPrimaryClip.getItemAt(0).getText)
if (profiles.nonEmpty) {
profiles.foreach(ShadowsocksApplication.profileManager.createProfile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show
return true
}
}
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show
true
case R.id.action_export =>
ShadowsocksApplication.profileManager.getAllProfiles match {
case Some(profiles) =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, profiles.mkString("\n")))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show
}
true
case _ => false
}
}
@@ -4,15 +4,14 @@ 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
*/
trait ServiceBoundContext extends Context {
class ShadowsocksServiceConnection extends ServiceConnection {
override def onServiceConnected(name: ComponentName, service: IBinder) {
service.linkToDeath(new ShadowsocksDeathRecipient(ServiceBoundContext.this), 0)
bgService = IShadowsocksService.Stub.asInterface(service)
registerCallback
ServiceBoundContext.this.onServiceConnected()
@@ -27,17 +26,18 @@ trait ServiceBoundContext extends Context {
}
}
def registerCallback = if (bgService != null && callback != null && !callbackRegistered) try {
protected 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)
protected def unregisterCallback = {
if (bgService != null && callback != null && callbackRegistered) try bgService.unregisterCallback(callback) catch {
case ignored: RemoteException =>
}
callbackRegistered = false
} catch {
case ignored: RemoteException => // Nothing
}
def onServiceConnected() = ()
@@ -66,16 +66,12 @@ trait ServiceBoundContext extends Context {
}
def deattachService() {
if (bgService != null) {
if (callback != null) {
unregisterCallback
callback = null
}
if (connection != null) {
unbindService(connection)
connection = null
}
bgService = null
unregisterCallback
callback = null
if (connection != null) {
unbindService(connection)
connection = null
}
bgService = null
}
}
@@ -58,7 +58,6 @@ 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.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
@@ -107,9 +106,6 @@ class Shadowsocks
var state = State.STOPPED
var currentProfile = new Profile
var vpnEnabled = -1
var trafficCache: Array[String] = _
var connectionTestResult: String = _
var connectionTestSuccess: Boolean = true
// Services
var currentServiceName = classOf[ShadowsocksNatService].getName
@@ -158,28 +154,13 @@ class Shadowsocks
})
}
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)
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
}
}
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("")
} else {
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 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)
@@ -187,6 +168,11 @@ class Shadowsocks
// Update the UI
if (fab != null) fab.setEnabled(true)
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()
@@ -263,7 +249,7 @@ class Shadowsocks
} else {
in = assetManager.open(file)
}
out = new FileOutputStream(getApplicationInfo().dataDir + "/" + file)
out = new FileOutputStream(getApplicationInfo.dataDir + "/" + file)
copyFile(in, out)
in.close()
in = null
@@ -289,12 +275,12 @@ class Shadowsocks
}
}
private def crashRecovery() {
def crashRecovery() {
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, getApplicationInfo().dataDir, task))
cmd.append("chmod 666 %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo().dataDir, 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) {
@@ -307,8 +293,8 @@ class Shadowsocks
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
try {
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
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)
@@ -316,10 +302,10 @@ class Shadowsocks
} catch {
case e: Throwable => // Ignore
}
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))
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)
@@ -332,8 +318,6 @@ class Shadowsocks
changeSwitch(checked = false)
}
def isReady: Boolean = checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null
def prepareStartService() {
ThrowableFuture {
if (ShadowsocksApplication.isVpnEnabled) {
@@ -344,7 +328,7 @@ class Shadowsocks
handler.post(() => onActivityResult(Shadowsocks.REQUEST_CONNECT, Activity.RESULT_OK, null))
}
} else {
serviceStart()
serviceLoad()
}
}
}
@@ -372,43 +356,19 @@ class Shadowsocks
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener((v: View) => {
serviceStarted = !serviceStarted
serviceStarted match {
case true =>
if (isReady)
prepareStartService()
else
changeSwitch(checked = false)
case false =>
serviceStop()
}
})
fab.setOnClickListener((v: View) => if (serviceStarted) serviceStop()
else if (checkText(Key.proxy) && checkText(Key.sitekey) && bgService != null) prepareStartService()
else changeSwitch(checked = false))
fab.setOnLongClickListener((v: View) => {
Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect,
Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8)).show
true
})
updateTraffic(0, 0, 0, 0)
// Bind to the service
handler.post(() => attachService)
}
def reloadProfile() {
currentProfile = ShadowsocksApplication.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
ShadowsocksApplication.profileManager.getFirstProfile match {
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
case None => ShadowsocksApplication.profileManager.createDefault()
}
}
updatePreferenceScreen()
serviceStop()
}
protected override def onPause() {
super.onPause()
ShadowsocksApplication.profileManager.save
@@ -459,9 +419,21 @@ class Shadowsocks
ConfigUtils.refresh(this)
// Check if current profile changed
if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile()
if (ShadowsocksApplication.profileId != currentProfile.id) {
currentProfile = ShadowsocksApplication.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
ShadowsocksApplication.profileManager.getFirstProfile match {
case Some(first) => ShadowsocksApplication.switchProfile(first.id)
case None => ShadowsocksApplication.profileManager.createDefault()
}
}
updatePreferenceScreen()
if (serviceStarted) serviceLoad()
}
updateTraffic()
updateState()
}
@@ -471,7 +443,7 @@ class Shadowsocks
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 0)
preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1)
adView.loadAd(new AdRequest.Builder().build())
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
@@ -498,24 +470,20 @@ class Shadowsocks
handler.removeCallbacksAndMessages(null)
}
def install() {
def reset() {
crashRecovery()
copyAssets(System.getABI)
val ab = new ArrayBuffer[String]
for (executable <- Shadowsocks.EXECUTABLES) {
ab.append("chmod 755 " + getApplicationInfo().dataDir + "/" + executable)
ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable)
}
Console.runCommand(ab.toArray)
}
def reset() {
crashRecovery()
install()
}
def recovery() {
serviceStop()
if (serviceStarted) serviceStop()
val h = showProgress(R.string.recovering)
ThrowableFuture {
reset()
@@ -534,14 +502,14 @@ class Shadowsocks
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) = resultCode match {
case Activity.RESULT_OK =>
serviceStart()
serviceLoad()
case _ =>
cancelStart()
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
}
def serviceStop() {
if (bgService != null) bgService.stop()
if (bgService != null) bgService.use(null)
}
def checkText(key: String): Boolean = {
@@ -552,8 +520,8 @@ class Shadowsocks
}
/** Called when connect button is clicked. */
def serviceStart() {
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
def serviceLoad() {
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
if (ShadowsocksApplication.isVpnEnabled) {
changeSwitch(checked = false)
@@ -0,0 +1,24 @@
package com.github.shadowsocks
import android.os.IBinder
import android.util.Log
/**
* @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 =>
ss.deattachService
ss.crashRecovery
ss.attachService
case _ =>
}
}
}
@@ -44,13 +44,11 @@ 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}
import android.os._
import android.util.{Log, SparseArray}
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
import com.github.shadowsocks.utils._
@@ -66,7 +64,6 @@ class ShadowsocksNatService extends BaseService {
"-j DNAT --to-destination 127.0.0.1:8123"
private var notification: ShadowsocksNotification = _
var closeReceiver: BroadcastReceiver = _
var connReceiver: BroadcastReceiver = _
val myUid = android.os.Process.myUid()
@@ -164,12 +161,14 @@ 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(getApplicationInfo.dataDir + "/acl.list"))(p => {
acl.foreach(item => p.println(item))
acl.flatten.foreach(p.println)
})
}
@@ -195,10 +194,7 @@ class ShadowsocksNatService extends BaseService {
}
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sslocalProcess = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
sslocalProcess = new GuardedProcess(cmd).start()
}
def startTunnel() {
@@ -224,10 +220,7 @@ class ShadowsocksNatService extends BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sstunnelProcess = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
sstunnelProcess = new GuardedProcess(cmd).start()
} else {
val conf = ConfigUtils
@@ -250,16 +243,13 @@ class ShadowsocksNatService extends BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmdBuf.mkString(" "))
sstunnelProcess = new ProcessBuilder()
.command(cmdBuf)
.redirectErrorStream(true)
.start()
sstunnelProcess = new GuardedProcess(cmdBuf).start()
}
}
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, getApplicationInfo.dataDir,
@@ -276,10 +266,7 @@ class ShadowsocksNatService extends BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
pdnsdProcess = new ProcessBuilder()
.command(cmd.split(" ").toSeq)
.redirectErrorStream(true)
.start()
pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
}
def getVersionName: String = {
@@ -303,10 +290,7 @@ class ShadowsocksNatService extends BaseService {
})
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
redsocksProcess = new ProcessBuilder()
.command(cmd.split(" ").toSeq)
.redirectErrorStream(true)
.start()
redsocksProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
}
/** Called when the activity is first created. */
@@ -400,16 +384,6 @@ class ShadowsocksNatService extends BaseService {
}
super.startRunner(config)
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
closeReceiver = (context: Context, intent: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner()
}
registerReceiver(closeReceiver, filter)
ShadowsocksApplication.track(TAG, "start")
changeState(State.CONNECTING)
@@ -422,7 +396,7 @@ class ShadowsocksNatService extends BaseService {
} catch {
case ex: Exception =>
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
stopRunner(true)
this.config = null
}
}
@@ -453,23 +427,17 @@ class ShadowsocksNatService extends BaseService {
notification = new ShadowsocksNotification(this, config.profileName, true)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
stopRunner(true)
}
}
}
}
override def stopRunner() {
override def stopRunner(stopService: Boolean) {
// channge the state
changeState(State.STOPPING)
// clean up recevier
if (closeReceiver != null) {
unregisterReceiver(closeReceiver)
closeReceiver = null
}
if (notification != null) notification.destroy()
ShadowsocksApplication.track(TAG, "stop")
@@ -477,7 +445,7 @@ class ShadowsocksNatService extends BaseService {
// reset NAT
killProcesses()
super.stopRunner()
super.stopRunner(stopService)
}
override def getTag = TAG
@@ -41,6 +41,12 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, service.getString(R.string.stop),
PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
ShadowsocksApplication.profileManager.getAllProfiles match {
case Some(profiles) => if (profiles.length > 1)
builder.addAction(android.R.drawable.ic_menu_manage, service.getString(R.string.quick_switch),
PendingIntent.getActivity(service, 0, new Intent(Action.QUICK_SWITCH), 0))
case _ =>
}
private lazy val style = new BigTextStyle(builder)
private val showOnUnlock = visible && Utils.isLollipopOrAbove
private var isVisible = true
@@ -0,0 +1,73 @@
package com.github.shadowsocks
import android.content.res.Resources
import android.os.{Build, Bundle}
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.CheckedTextView
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.Utils
/**
* Created by Lucas on 3/10/16.
*/
class ShadowsocksQuickSwitchActivity extends AppCompatActivity {
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
{
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
typedArray.recycle
}
private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
itemView.setOnClickListener(this)
def bind(item: Profile) {
this.item = item
text.setText(item.name)
text.setChecked(item.id == ShadowsocksApplication.profileId)
}
def onClick(v: View) {
ShadowsocksApplication.switchProfile(item.id)
Utils.startSsService(ShadowsocksQuickSwitchActivity.this)
finish
}
}
private class ProfilesAdapter extends RecyclerView.Adapter[ProfileViewHolder] {
val profiles = ShadowsocksApplication.profileManager.getAllProfiles.getOrElse(List.empty[Profile])
def getItemCount = profiles.length
def onBindViewHolder(vh: ProfileViewHolder, i: Int) = i match {
case _ => vh.bind(profiles(i))
}
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
def onCreateViewHolder(vg: ViewGroup, i: Int) = new ProfileViewHolder(LayoutInflater.from(vg.getContext)
.inflate(Resources.getSystem.getIdentifier(name, "layout", "android"), vg, false))
}
private val profilesAdapter = new ProfilesAdapter
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_quick_switch)
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(R.string.quick_switch)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val lm = new LinearLayoutManager(this)
profilesList.setLayoutManager(lm)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
if (ShadowsocksApplication.profileId >= 0) lm.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == ShadowsocksApplication.profileId => i + 1
}.getOrElse(0))
}
}
@@ -65,7 +65,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
onActivityResult(Shadowsocks.REQUEST_CONNECT, Activity.RESULT_OK, null)
}
} else {
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
finish()
}
}
@@ -101,7 +101,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
resultCode match {
case Activity.RESULT_OK =>
if (bgService != null) {
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
}
case _ =>
Log.e(Shadowsocks.TAG, "Failed to start VpnService")
@@ -61,11 +61,11 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
val intent = VpnService.prepare(ShadowsocksRunnerService.this)
if (intent == null) {
if (bgService != null) {
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
}
}
} else {
bgService.start(ConfigUtils.load(ShadowsocksApplication.settings))
bgService.use(ConfigUtils.load(ShadowsocksApplication.settings))
}
stopSelf()
}
@@ -9,10 +9,11 @@ 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.{DropDownPreference, NumberPickerPreference, PasswordEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.preferences._
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.{Key, Utils}
@@ -68,7 +69,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
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 = _
@@ -78,13 +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.connectionTestSuccess = true
activity.updateTraffic()
activity.handler.post(() => stat.setSummary(R.string.connection_test_testing))
testCount
}
ThrowableFuture {
@@ -97,7 +96,7 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
conn.setUseCaches(false)
if (testCount == id) {
var result: String = null
var success: Boolean = true
var success = true
try {
val start = currentTimeMillis
conn.getInputStream
@@ -107,21 +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 => {
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)
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) => {
@@ -182,22 +184,19 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
}
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) {
for (name <- Key.isNAT #:: PROXY_PREFS.toStream #::: FEATURE_PREFS.toStream) {
val pref = findPreference(name)
if (pref != null) pref.setEnabled(enabled &&
(name != Key.isProxyApps || Utils.isLollipopOrAbove || !ShadowsocksApplication.isVpnEnabled))
@@ -44,18 +44,15 @@ import java.lang.Process
import java.util.Locale
import android.content._
import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.{PackageInfo, PackageManager}
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
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
class ShadowsocksVpnService extends VpnService with BaseService {
@@ -66,7 +63,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
var conn: ParcelFileDescriptor = _
var vpnThread: ShadowsocksVpnThread = _
private var notification: ShadowsocksNotification = _
var closeReceiver: BroadcastReceiver = _
var sslocalProcess: Process = _
var sstunnelProcess: Process = _
@@ -110,10 +106,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
override def onRevoke() {
stopRunner()
stopRunner(true)
}
override def stopRunner() {
override def stopRunner(stopService: Boolean) {
if (vpnThread != null) {
vpnThread.stopThread()
@@ -136,13 +132,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
conn = null
}
// clean up recevier
if (closeReceiver != null) {
unregisterReceiver(closeReceiver)
closeReceiver = null
}
super.stopRunner()
super.stopRunner(stopService)
}
def getVersionName: String = {
@@ -183,16 +173,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = new ShadowsocksVpnThread(this)
vpnThread.start()
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
closeReceiver = (context: Context, intent: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner()
}
registerReceiver(closeReceiver, filter)
// ensure the VPNService is prepared
if (VpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
@@ -213,7 +193,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
} catch {
case ex: Exception =>
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
stopRunner(true)
this.config = null
}
}
@@ -241,7 +221,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
notification = new ShadowsocksNotification(this, config.profileName)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
stopRunner(true)
}
}
}
@@ -256,29 +236,20 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
val fd = startVpn()
if (fd != -1) {
var tries = 1
while (tries < 5) {
Thread.sleep(1000 * tries)
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
return true
}
tries += 1
}
}
false
sendFd(fd)
}
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(getApplicationInfo.dataDir + "/acl.list"))(p => {
acl.foreach(item => p.println(item))
acl.flatten.foreach(p.println)
})
}
@@ -305,10 +276,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sslocalProcess = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
sslocalProcess = new GuardedProcess(cmd).start()
}
def startDnsTunnel() = {
@@ -333,16 +301,13 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
sstunnelProcess = new ProcessBuilder()
.command(cmd)
.redirectErrorStream(true)
.start()
sstunnelProcess = new GuardedProcess(cmd).start()
}
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, getApplicationInfo.dataDir,
@@ -359,10 +324,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
pdnsdProcess = new ProcessBuilder()
.command(cmd.split(" ").toSeq)
.redirectErrorStream(true)
.start()
pdnsdProcess = new GuardedProcess(cmd.split(" ").toSeq).start()
}
override def getContext = getBaseContext
@@ -399,7 +361,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
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)
@@ -424,7 +386,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
if (conn == null) {
stopRunner()
stopRunner(true)
return -1
}
@@ -452,14 +414,25 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
tun2socksProcess = new ProcessBuilder()
.command(cmd.split(" ").toSeq)
.redirectErrorStream(true)
.start()
tun2socksProcess = new GuardedProcess(cmd.split(" ").toSeq).start(() => sendFd(fd))
fd
}
def sendFd(fd: Int): Boolean = {
if (fd != -1) {
var tries = 1
while (tries < 5) {
Thread.sleep(1000 * tries)
if (System.sendfd(fd, getApplicationInfo.dataDir + "/sock_path") != -1) {
return true
}
tries += 1
}
}
false
}
override def getTag = TAG
override def getServiceMode = Mode.VPN
@@ -66,7 +66,7 @@ class ShadowsocksVpnThread(vpnService: ShadowsocksVpnService) extends Thread {
case _: Exception => // ignore
}
serverSocket = null
}
}
}
def stopThread() {
@@ -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,7 +39,6 @@
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
@@ -51,15 +50,10 @@ class TaskerReceiver extends BroadcastReceiver {
val settings = TaskerSettings.fromIntent(intent)
val switched = ShadowsocksApplication.profileManager.getProfile(settings.profileId) match {
case Some(p) =>
Utils.stopSsService(context)
ShadowsocksApplication.switchProfile(settings.profileId)
true
case _ => false
}
val handler = new Handler(context.getMainLooper)
handler.postDelayed(() => {
if (settings.switchOn) Utils.startSsService(context)
else if (!switched) Utils.stopSsService(context)
}, 3000)
if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context)
}
}
@@ -64,7 +64,7 @@ object DBHelper {
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 15) {
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 16) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
@@ -121,6 +121,10 @@ class DBHelper(val context: Context)
i += 1
}
}
if (oldVersion < 16) {
profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
}
}
}
}
@@ -39,6 +39,9 @@
package com.github.shadowsocks.database
import java.util.Locale
import android.util.Base64
import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile {
@@ -95,4 +98,7 @@ class Profile {
@DatabaseField
var userOrder: Long = _
override def toString = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
method, password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP)
}
@@ -2,12 +2,16 @@ package com.github
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Try}
/**
* @author Mygod
*/
package object shadowsocks {
def ThrowableFuture[T](f: => T) = Future(f) onFailure {
case e: Throwable => e.printStackTrace()
private val handleFailure: Try[_] => Unit = {
case Failure(e) => e.printStackTrace()
case _ =>
}
def ThrowableFuture[T](f: => T) = Future(f) onComplete handleFailure
}
@@ -2,8 +2,8 @@ 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.view.{LayoutInflater, View, ViewGroup}
import android.widget.TextView
import com.github.shadowsocks.R
@@ -12,14 +12,18 @@ import com.github.shadowsocks.R
*/
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
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 li = getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
val view = li.inflate(R.layout.status_pref, parent, false)
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]
@@ -29,16 +33,28 @@ final class StatusPreference(context: Context, attrs: AttributeSet = null)
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)
}
@@ -4,11 +4,14 @@ package com.github.shadowsocks.utils
* @author Mygod
*/
object CloseUtils {
type Closeable = {
def close()
}
type Disconnectable = {
def disconnect()
}
def autoClose[A <: AutoCloseable, B](x: => A)(block: A => B): B = {
def autoClose[A <: Closeable, B](x: => A)(block: A => B): B = {
var a: Option[A] = None
try {
a = Some(x)
@@ -72,14 +72,16 @@ object Key {
val isProxyApps = "isProxyApps"
val isBypassApps = "isBypassApps"
val isUdpDns = "isUdpDns"
val isAuth= "isAuth"
val isIpv6= "isIpv6"
val isAuth = "isAuth"
val isIpv6 = "isIpv6"
val proxy = "proxy"
val sitekey = "sitekey"
val encMethod = "encMethod"
val remotePort = "remotePortNum"
val localPort = "localPortNum"
val profileTip = "profileTip"
}
object Scheme {
@@ -104,10 +106,12 @@ object State {
object Action {
val SERVICE = "com.github.shadowsocks.SERVICE"
val CLOSE = "com.github.shadowsocks.CLOSE"
val QUICK_SWITCH = "com.github.shadowsocks.QUICK_SWITCH"
}
object Route {
val ALL = "all"
val BYPASS_LAN = "bypass-lan"
val BYPASS_CHN = "bypass-china"
val BYPASS_LAN_CHN = "bypass-lan-china"
}
@@ -39,44 +39,30 @@
package com.github.shadowsocks.utils
import android.util.{Base64, Log}
import com.github.shadowsocks.database.Profile
import java.util.Locale
import android.net.Uri
import android.util.{Log, Base64}
object Parser {
val TAG = "ShadowParser"
private val pattern = "(?i)ss://([A-Za-z0-9+-/=_]+)".r
def generate (profile: Profile): String = "ss://" + Base64.encodeToString("%s:%s@%s:%d".formatLocal(Locale.ENGLISH,
profile.method, profile.password, profile.host, profile.remotePort).getBytes, Base64.NO_PADDING)
def findAll(data: CharSequence) = pattern.findAllMatchIn(data).map(m => try {
val content = new String(Base64.decode(m.group(1), Base64.NO_PADDING), "UTF-8")
val methodIdx = content.indexOf(':')
val passwordIdx = content.lastIndexOf('@')
val hostIdx = content.lastIndexOf(':')
val host = content.substring(passwordIdx + 1, hostIdx)
def parse (data: String): Option[Profile] = {
try {
Log.d(TAG, data)
val uri = Uri.parse(data.trim)
if (uri.getScheme == Scheme.SS) {
val encoded = data.replace(Scheme.SS + "://", "")
val content = new String(Base64.decode(encoded, Base64.NO_PADDING), "UTF-8")
val methodIdx = content.indexOf(':')
val passwordIdx = content.lastIndexOf('@')
val hostIdx = content.lastIndexOf(':')
val method = content.substring(0, methodIdx)
val password = content.substring(methodIdx + 1, passwordIdx)
val host = content.substring(passwordIdx + 1, hostIdx)
val port = content.substring(hostIdx + 1)
val profile = new Profile
profile.name = host
profile.host = host
profile.remotePort = port.toInt
profile.localPort = 1080
profile.method = method.toLowerCase
profile.password = password
return Some(profile)
}
} catch {
case ex : Exception => Log.e(TAG, "parser error", ex)// Ignore
}
None
}
val profile = new Profile
profile.name = host
profile.host = host
profile.remotePort = content.substring(hostIdx + 1).toInt
profile.method = content.substring(0, methodIdx).toLowerCase
profile.password = content.substring(methodIdx + 1, passwordIdx)
profile
} catch {
case ex: Exception =>
Log.e(TAG, "parser error: " + m.source, ex)// Ignore
null
}).filter(_ != null)
}
@@ -1,6 +1,5 @@
package com.github.shadowsocks.utils
import java.lang.System
import java.text.DecimalFormat
import com.github.shadowsocks.{R, ShadowsocksApplication}
@@ -81,4 +80,3 @@ object TrafficMonitor {
dirty = true
}
}
@@ -42,7 +42,7 @@ import java.io._
import java.net._
import java.security.MessageDigest
import android.animation.{AnimatorListenerAdapter, Animator}
import android.animation.{Animator, AnimatorListenerAdapter}
import android.app.ActivityManager
import android.content.pm.{ApplicationInfo, PackageManager}
import android.content.{Context, Intent}
@@ -51,11 +51,11 @@ import android.graphics.drawable.{BitmapDrawable, Drawable}
import android.os.Build
import android.provider.Settings
import android.support.v4.content.ContextCompat
import android.util.{DisplayMetrics, Base64, Log}
import android.util.{Base64, DisplayMetrics, Log}
import android.view.View.MeasureSpec
import android.view.{Gravity, View, Window}
import android.widget.Toast
import com.github.shadowsocks.{ShadowsocksRunnerService, ShadowsocksApplication, BuildConfig}
import com.github.shadowsocks.{BuildConfig, ShadowsocksApplication, ShadowsocksRunnerService}
import org.xbill.DNS._
@@ -401,7 +401,7 @@ object Utils {
}
}
def startSsService(context: Context): Unit = {
def startSsService(context: Context) {
val isInstalled: Boolean = ShadowsocksApplication.settings.getBoolean(ShadowsocksApplication.getVersionName, false)
if (!isInstalled) return
@@ -409,8 +409,9 @@ object Utils {
context.startService(intent)
}
def stopSsService(context: Context): Unit = {
context.sendBroadcast(new Intent(Action.CLOSE))
def stopSsService(context: Context) {
val intent = new Intent(Action.CLOSE)
context.sendBroadcast(intent)
}
}
@@ -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
}
+2 -2
View File
@@ -22,5 +22,5 @@ if [ ! -d "$ANDROID_NDK_HOME" ]; then
popd
fi
echo "y" | android update sdk --filter tools,platform-tools,build-tools-23.0.2,android-23,extra-google-m2repository --no-ui -a
echo "y" | android update sdk --filter extra-android-m2repository --no-ui -a
( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter tools,platform-tools,build-tools-23.0.2,android-23,extra-google-m2repository --no-ui -a
( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter extra-android-m2repository --no-ui -a