Compare commits

...
36 Commits
Author SHA1 Message Date
Max Lv f4026e5a86 Bump version 2017-01-07 11:03:06 +08:00
Max Lv fb98b3e4a1 Fix fast scroller in per-app config 2017-01-07 09:49:22 +08:00
Max Lv f629c432b0 Update licenses, close #1059 2017-01-07 08:56:19 +08:00
Mygod 4c15d27a99 Update dependencies 2017-01-07 04:05:02 +08:00
Mygod 341c85049b Add fast scroller to custom rules and per-app proxy config 2017-01-07 03:54:01 +08:00
Mygod 35d7611524 Add state check before sending initial trafficUpdated event 2017-01-07 02:52:25 +08:00
Mygod d10acf52ec Fix remoteDns not used in NAT with UDP disabled 2017-01-06 17:13:31 +08:00
Max Lv 0234acbf5f Merge pull request #1056 from Mygod/custom-rules
Implement custom rules
2017-01-06 02:59:32 -06:00
Mygod 292c76b227 Fix remove snackbar 2017-01-06 16:46:54 +08:00
Mygod a67c29314e Fix deprecation warning in UndoSnackbarManager 2017-01-06 16:43:37 +08:00
Mygod 0fc9c6fb8a Remove unused copy_url 2017-01-06 16:38:40 +08:00
Mygod 82a3e429d1 Prevent syncing custom-rules 2017-01-06 16:30:41 +08:00
Mygod b4425e3b06 Fix NullReferenceException 2017-01-06 16:29:37 +08:00
Mygod ac2b668a4b Implement custom rules
Fix #1009 finally!
2017-01-06 16:20:26 +08:00
Mygod 8374a8cdcc Fix building 2017-01-06 13:00:50 +08:00
Mygod 7feb049f4b Update gfwlist 2017-01-06 12:29:42 +08:00
Mygod 5bc7c9f8cc Fix bandwidth display in ProfilesFragment when hot swapping profiles 2017-01-05 22:08:37 +08:00
Mygod 115fbcb2fe Add a fading when switching profiles 2017-01-05 21:25:59 +08:00
Mygod 69d2bc1ee5 Set card background using StateListDrawable 2017-01-05 21:21:51 +08:00
Mygod af9e8c786d Bugfix for layout gravity 2017-01-05 20:51:51 +08:00
Max Lv 5ca3baac95 Update licenses #1043 2016-12-31 16:10:02 +08:00
Mygod 878c53ee30 Refine profile bandwidth gravity 2016-12-31 00:14:01 +08:00
Mygod eb7e608d0b Use custom tabs in AboutFragment 2016-12-30 22:22:06 +08:00
Mygod ef6a9e7727 Remove obsolete tools:ignore 2016-12-30 22:10:47 +08:00
Mygod 2edc14a695 Refine database package 2016-12-30 22:10:47 +08:00
Max Lv bc94adef38 Use centerCrop 2016-12-30 20:43:15 +08:00
Max Lv dd8031519c Revert "Replace image with gradient color"
This reverts commit b0412b4f1f.
2016-12-30 20:42:04 +08:00
Max Lv b0412b4f1f Replace image with gradient color 2016-12-30 18:33:36 +08:00
Mygod 7e94e25d49 Refine #1034 2016-12-29 17:51:44 +08:00
Mygod 068906cd0f Remove obsolete items in FAQ 2016-12-29 16:21:53 +08:00
Mygod e24c2256af Hide circle onResume 2016-12-29 16:20:37 +08:00
Mygod d78e823ce2 Allow user to use any QR code scanner and remove unused resources 2016-12-29 16:16:56 +08:00
Mygod 8e46be5900 Allow user to use any QR code scanner 2016-12-29 16:01:32 +08:00
Mygod 2e73741825 Fix excess unsaved prompt
AppManager's sub-activity should maintain dirty flags by themselves. Fix #1032.
2016-12-29 15:34:54 +08:00
Mygod 022b6da9c9 Refine issue template 2016-12-29 01:29:36 -06:00
Mygod 622cef048c Add an FAQ section in drawer 2016-12-29 01:29:36 -06:00
94 changed files with 5083 additions and 4148 deletions
+48
View File
@@ -0,0 +1,48 @@
### Troubleshooting
Cannot connect to server:
1. Stop battery saver if it's active;
2. If an upgrade breaks anything, do a manual reset first by pressing Reset at the end of the list;
3. Check your config;
4. Wipe app data.
Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issues/new) with logcat attached, or submit a crash report to Google Play. Then, try wiping app data.
### UI tips
* Tap the number to enter the port you wish to use; (if the keyboard doesn't pop up automatically for some reason)
* Use Tasker integration to create a desktop widget.
### Why is NAT mode deprecated?
1. Requiring ROOT permission;
2. No IPv6 support;
3. No UDP relay support.
### How to remove the exclamation mark when using VPN mode?
The exclamation mark in the Wi-Fi/cellular icon appears because the system fails to connect to portal server (defaults to `clients3.google.com`) without VPN connection. To remove it, follow the instructions in [this article](https://www.noisyfox.cn/45.html). (in Simplified Chinese)
### Why are MIUI, EMUI and other AOSPs in China not officially supported?
1. Broken VPNService implementation, especially for IPv6;
2. Aggressive (or called broken) background service killing policy.
Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
### How to pause Shadowsocks service?
* For Android 7.0+: Use quick switch tile in Quick Settings;
* Use Tasker integration;
* Add a profile with per-app proxy enabled for Shadowsocks only, bypass mode off.
### Why does Shadowsocks consume so much battery on Android 5.0+?
As Shadowsocks takes over the whole device network, any battery used by network activities from other apps are also counted as those from Shadowsocks. So, the battery usage of Shadowsocks equals to the sum of all the network activities of your device. Shadowsocks itself is a totally I/O bound application on modern Android devices, which is expected not to consume any notable battery.
So if you notice a significant increase in battery usage after you use Shadowsocks, it's most likely caused by other apps. For example, Google Play services can consume more battery after being able to connecting to Google, etc.
### It works fine under Wi-Fi but can't connect through cellular data?
Allow this app to consume background data in app settings.
+1 -1
View File
@@ -1,4 +1,4 @@
Please read FAQ in wiki then answer these questions before submitting your issue. Thanks!
Please read FAQ then answer these questions before submitting your issue. Thanks!
### Environment
+13 -8
View File
@@ -36,14 +36,19 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
## OPEN SOURCE LICENSES
* shadowsocks-libev: [GPLv3](https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE)
* tun2socks: [BSD](https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING)
* redsocks: [APL 2.0](https://github.com/shadowsocks/redsocks/blob/master/README)
* OpenSSL: [OpenSSL](https://github.com/shadowsocks/openssl-android/blob/master/NOTICE)
* pdnsd: [GPLv3](https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/pdnsd/COPYING)
* libev: [GPLv2](https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libev/LICENSE)
* libevent: [BSD](https://github.com/shadowsocks/libevent/blob/master/LICENSE)
* kcptun: [MIT](https://github.com/xtaci/kcptun)
<ul>
<li>redsocks: <a href="https://github.com/shadowsocks/redsocks/blob/shadowsocks-android/README">APL 2.0</a></li>
<li>mbed TLS: <a href="https://github.com/ARMmbed/mbedtls/blob/development/LICENSE">APL 2.0</a></li>
<li>libevent: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libevent/LICENSE">BSD</a></li>
<li>tun2socks: <a href="https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a></li>
<li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</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-libev/blob/master/libev/LICENSE">GPLv2</a></li>
<li>kcptun: <a href="https://github.com/xtaci/kcptun/commits/master/LICENSE.md">MIT</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul>
### LICENSE
+5 -4
View File
@@ -5,8 +5,8 @@ enablePlugins(AndroidApp)
android.useSupportVectors
name := "shadowsocks"
version := "3.4.0"
versionCode := Some(172)
version := "3.4.1"
versionCode := Some(173)
platformTarget := "android-25"
@@ -33,10 +33,12 @@ resConfigs := Seq("ja", "ru", "zh-rCN", "zh-rTW")
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.android.support" % "cardview-v7" % "25.1.0" ::
"com.android.support" % "customtabs" % "25.1.0" ::
"com.android.support" % "design" % "25.1.0" ::
"com.android.support" % "gridlayout-v7" % "25.1.0" ::
"com.android.support" % "preference-v14" % "25.1.0" ::
"com.evernote" % "android-job" % "1.1.3" ::
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.1.4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % "10.0.1" ::
"com.google.android.gms" % "play-services-analytics" % "10.0.1" ::
@@ -51,7 +53,6 @@ libraryDependencies ++=
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.7" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201608240809" ::
"com.google.zxing" % "android-integration" % "3.2.1" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
-2
View File
@@ -8,8 +8,6 @@
<uses-feature android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.software.leanback"
@@ -2,5 +2,7 @@ package com.github.shadowsocks.aidl;
interface IShadowsocksServiceCallback {
oneway void stateChanged(int state, String profileName, String msg);
oneway void trafficUpdated(long txRate, long rxRate, long txTotal, long rxTotal);
oneway void trafficUpdated(int profileId, long txRate, long rxRate, long txTotal, long rxTotal);
// Traffic data has persisted to database, listener should refetch their data from database
oneway void trafficPersisted(int profileId);
}
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -274,8 +274,8 @@ img {
</head>
<body>
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Scala.</p>
<p>Copyright (C) 2016 by Max Lv <a href="&#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 by Mygod Studio <a href="contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
<p>Copyright (C) 2017 by 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) 2017 by Mygod Studio <a href="contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
<p>This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
@@ -289,14 +289,17 @@ 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/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/libevent/blob/shadowsocks-android/LICENSE">GPLv2</a></li>
<li>mbed TLS: <a href="https://github.com/ARMmbed/mbedtls/blob/development/LICENSE">APL 2.0</a></li>
<li>libevent: <a href="https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libevent/LICENSE">BSD</a></li>
<li>tun2socks: <a href="https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a></li>
<li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</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-libev/blob/master/libev/LICENSE">GPLv2</a></li>
<li>kcptun: <a href="https://github.com/xtaci/kcptun/commits/master/LICENSE.md">MIT</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul>
</body>
+2 -1
View File
@@ -385,6 +385,7 @@ include $(CLEAR_VARS)
SHADOWSOCKS_SOURCES := local.c cache.c udprelay.c encrypt.c \
utils.c netutils.c json.c jconf.c acl.c http.c tls.c rule.c \
obfs_http.c obfs_tls.c base64.c \
android.c
LOCAL_MODULE := ss-local
@@ -417,7 +418,7 @@ include $(BUILD_EXECUTABLE)
include $(CLEAR_VARS)
SHADOWSOCKS_SOURCES := tunnel.c cache.c udprelay.c encrypt.c utils.c netutils.c json.c jconf.c android.c
SHADOWSOCKS_SOURCES := tunnel.c cache.c udprelay.c encrypt.c utils.c netutils.c json.c jconf.c android.c obfs_http.c obfs_tls.c base64.c
LOCAL_MODULE := ss-tunnel
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
</item>
</selector>
</item>
</ripple>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/material_green_700" />
<corners android:radius="4dp"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
<corners android:radius="4dp"/>
</shape>
</item>
</selector>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
@@ -1,15 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_primary_300" />
<corners android:radius="4dp"/>
</shape>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/material_primary_100"/>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
</selector>
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/material_green_700" />
<corners android:radius="4dp"/>
</shape>
</item>
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp"/>
</shape>
</item>
</layer-list>
@@ -0,0 +1,9 @@
<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="#FF000000"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>
@@ -0,0 +1,9 @@
<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="#FF000000"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>
@@ -0,0 +1,9 @@
<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="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
</vector>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Spinner android:id="@+id/template_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/acl_rule_templates"/>
<EditText android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"/>
</LinearLayout>
+18 -7
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
@@ -19,24 +20,22 @@
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary"
android:paddingStart="60dp"
android:paddingEnd="8dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="RtlHardcoded"/>
android:layout_height="wrap_content"/>
<Switch android:checked="true"
android:id="@+id/bypassSwitch"
android:text="@string/bypass_apps"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary"
android:paddingStart="60dp"
android:paddingEnd="8dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="RtlHardcoded"/>
android:layout_height="wrap_content"/>
</LinearLayout>
<FrameLayout android:layout_width="fill_parent"
android:layout_height="0dp"
@@ -48,7 +47,19 @@
<android.support.v7.widget.RecyclerView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/applistview"
android:id="@+id/list"
android:visibility="gone"/>
<com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:fastscroll__handleColor="@color/material_primary_500"
app:fastscroll__bubbleColor="@color/material_primary_500"
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
</FrameLayout>
</LinearLayout>
+1 -1
View File
@@ -28,6 +28,6 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:clickable="false"
android:layout_marginEnd="8dp"/>
android:layout_marginEnd="16dp"/>
</LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/>
<FrameLayout android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="32dp"
android:clipToPadding="false"/>
<com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroller"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
app:fastscroll__handleColor="@color/material_primary_500"
app:fastscroll__bubbleColor="@color/material_primary_500"
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
</FrameLayout>
</LinearLayout>
+2 -2
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:scaleType="centerCrop"
android:src="@drawable/background_header"/>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
@@ -4,12 +4,12 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/profilesList"
<android.support.v7.widget.RecyclerView android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="32dp"
android:scrollbars="vertical"
android:clipChildren="false"
android:clipToPadding="false"/>
</LinearLayout>
@@ -6,7 +6,6 @@
android:layout_height="wrap_content">
<com.github.shadowsocks.widget.BoundedCardView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_selectable"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
@@ -63,7 +62,6 @@
<TextView android:id="@+id/address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="top"
android:layout_weight="1"
android:maxLines="2"
android:ellipsize="end"
@@ -72,7 +70,7 @@
<TextView android:id="@+id/traffic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="top"
android:layout_gravity="bottom"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<me.dm7.barcodescanner.zxing.ZXingScannerView android:id="@+id/scanner"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
+3 -2
View File
@@ -15,7 +15,8 @@
android:textSize="18sp"
android:padding="16dp"/>
</FrameLayout>
<android.support.v7.widget.RecyclerView android:id="@+id/profilesList"
<android.support.v7.widget.RecyclerView android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</LinearLayout>
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/selection"
android:title="@string/action_selection"
android:icon="@drawable/abc_ic_menu_selectall_mtrl_alpha"
android:alphabeticShortcut="s"
android:numericShortcut="0"
android:visible="false"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_select_all"
android:title="@android:string/selectAll"
android:alphabeticShortcut="a"
android:numericShortcut="1"
app:showAsAction="always"/>
<item android:id="@+id/action_cut"
android:title="@android:string/cut"
android:alphabeticShortcut="t"
android:numericShortcut="2"
app:showAsAction="always"/>
<item android:id="@+id/action_copy"
android:title="@android:string/copy"
android:alphabeticShortcut="c"
android:numericShortcut="3"
app:showAsAction="always"/>
<item android:id="@+id/action_delete"
android:title="@string/delete"
android:alphabeticShortcut="d"
android:numericShortcut="4"
app:showAsAction="always"/>
</menu>
</item>
<item android:title="@string/action_add_rule"
android:icon="@drawable/ic_av_playlist_add"
android:alphabeticShortcut="a"
android:numericShortcut="1"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="m"
android:numericShortcut="1"/>
<item android:id="@+id/action_import"
android:title="@string/action_import"
android:alphabeticShortcut="i"
android:numericShortcut="2"/>
<item android:id="@+id/action_import_gfwlist"
android:title="@string/route_entry_gfwlist"
android:alphabeticShortcut="g"
android:numericShortcut="3"/>
</menu>
</item>
</menu>
+3 -3
View File
@@ -83,19 +83,19 @@
<string name="connection_test_error_status_code">ステータスコード無効(#%d)</string>
<!-- profile -->
<string name="copy_url">URL をコピー</string>
<string name="add_profile_dialog">シャドーソックスにプロファイルを追加しますか</string>
<string name="add_profile_methods_scan_qr_code">QRコードを読み取る</string>
<string name="add_profile_methods_manual_settings">手動でセットアップ</string>
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
<string name="route_entry_gfwlist">中国本土からアクセス不可なアドレス以外を迂回する</string>
<string-array name="route_entry">
<item>すべて</item>
<item>LANアドレスを迂回する</item>
<item>中国本土のアドレスを迂回する</item>
<item>LAN及び中国本土のアドレスを迂回する</item>
<item>中国本土からアクセス不可なアドレス以外を迂回する</item>
<item>@string/route_entry_gfwlist</item>
<item>中国本土のアドレス以外を迂回する</item>
<item>@string/custom_rules</item>
</string-array>
<string name="action_apply_all">設定をすべてのプロファイルに適応する</string>
+3 -3
View File
@@ -41,13 +41,15 @@
<string name="auto_set_gfwlist">CHN Маршрут</string>
<string name="auto_set_gfwlist_summary">Игнорировать все сайты находящиеся в Китае</string>
<string name="route_list">Маршрут</string>
<string name="route_entry_gfwlist">Список GFW</string>
<string-array name="route_entry">
<item>Все</item>
<item>Обход LAN</item>
<item>Обходной Китай</item>
<item>Обход LAN и Китай</item>
<item>Список GFW</item>
<item>@string/route_entry_gfwlist</item>
<item>Список Китай</item>
<item>@string/custom_rules</item>
</string-array>
<string name="proxied_apps">Прокси для выбранных приложений</string>
<string name="proxied_apps_summary">Установить прокси для выбранных приложений, требует режим NAT под Android 4.x</string>
@@ -108,8 +110,6 @@
<string name="add_profile_dialog">Добавить этот Профиль Shadowsocks?</string>
<string name="add_profile_methods_scan_qr_code">Сканировать QR код</string>
<string name="add_profile_methods_manual_settings">Настройка вручную</string>
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
<string name="copy_url">Копировать URL</string>
<plurals name="removed">
<item quantity="one">Удалён</item>
<item quantity="other">%d элементы удалены</item>
+15 -3
View File
@@ -53,6 +53,7 @@
<string name="profiles">配置文件</string>
<string name="settings">设置选项</string>
<string name="remove_profile">删除此配置文件「%s」?</string>
<string name="faq">常见问题</string>
<string name="recovery">重置</string>
<string name="about">关于</string>
<string name="about_title">影梭(shadowsocks)%s</string>
@@ -93,19 +94,20 @@
<string name="delete">删除</string>
<string name="delete_confirm_prompt">你是否确定要删除这个配置文件?</string>
<string name="share_qr_nfc">二维码 / NFC</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>
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
<string name="add_profile_scanner_not_installed">请安装任意兼容 ZXing 的二维码扫描应用。</string>
<string name="route_entry_gfwlist">仅代理中国大陆无法访问的地址</string>
<string-array name="route_entry">
<item>全局</item>
<item>绕过局域网地址</item>
<item>绕过中国大陆地址</item>
<item>绕过局域网及中国大陆地址</item>
<item>仅代理中国大陆无法访问的地址</item>
<item>@string/route_entry_gfwlist</item>
<item>仅代理中国大陆地址</item>
<item>@string/custom_rules</item>
</string-array>
<string name="action_apply_all">应用设置到所有配置文件</string>
@@ -141,4 +143,14 @@
<string name="kcp_port_summary">%d(远端 KCPTUN 服务器的端口号)</string>
<string name="kcpcli">KCP 参数</string>
<!-- acl -->
<string name="custom_rules">自定义规则</string>
<string name="action_selection">选择…</string>
<string name="action_add_rule">添加规则…</string>
<string-array name="acl_rule_templates">
<item>子网/域名 PCRE 正则表达式</item>
<item>域名及其子域名</item>
</string-array>
<string name="edit_rule">编辑规则</string>
</resources>
+15 -3
View File
@@ -54,6 +54,7 @@
<string name="profiles">設定檔</string>
<string name="settings">設定</string>
<string name="remove_profile">移除此設定檔 "%s"?</string>
<string name="faq">常問問題</string>
<string name="recovery">重設</string>
<string name="about">關於</string>
<string name="about_title">Shadowsocks %s</string>
@@ -94,19 +95,20 @@
<string name="delete">刪除</string>
<string name="delete_confirm_prompt">您確定要移除這個設定檔嗎?</string>
<string name="share_qr_nfc">QR 碼 / NFC</string>
<string name="copy_url">複製 URL</string>
<string name="add_profile_dialog">為 Shadowsocks 新增此設定檔?</string>
<string name="add_profile_methods_scan_qr_code">掃描 QR 碼</string>
<string name="add_profile_methods_manual_settings">手動設定</string>
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
<string name="add_profile_scanner_not_installed">請安裝任何兼容 ZXing 的 QR 碼掃描應用程式。</string>
<string name="route_entry_gfwlist">GFW List</string>
<string-array name="route_entry">
<item>全域</item>
<item>略過區域網路位址</item>
<item>略過中國大陸位址</item>
<item>略過區域網路及中國大陸位址</item>
<item>僅代理大陸屏蔽位址</item>
<item>@string/route_entry_gfwlist</item>
<item>僅代理大陸位址</item>
<item>@string/custom_rules</item>
</string-array>
<string name="action_apply_all">應用設置到所有配置文件</string>
@@ -142,4 +144,14 @@
<string name="kcp_port_summary">%d(遠端 KCPTUN 伺服器的連接埠號碼)</string>
<string name="kcpcli">KCP 參數</string>
<!-- acl -->
<string name="custom_rules">自定義規則</string>
<string name="action_selection">選擇…</string>
<string name="action_add_rule">添加規則…</string>
<string-array name="acl_rule_templates">
<item>子網/主機名 PCRE 正則表達式</item>
<item>域名及其所有子域名</item>
</string-array>
<string name="edit_rule">編輯規則</string>
</resources>
+1
View File
@@ -45,6 +45,7 @@
<item>bypass-lan-china</item>
<item>gfwlist</item>
<item>china-list</item>
<item>custom-rules</item>
</string-array>
<string-array name="bypass_private_route">
+2
View File
@@ -4,9 +4,11 @@
<color name="material_green_700">#388E3C</color>
<color name="material_green_a700">#00C853</color>
<color name="material_blue_grey_100">#CFD8DC</color>
<color name="material_blue_grey_300">#90A4AE</color>
<color name="material_blue_grey_500">#607D8B</color>
<color name="material_blue_grey_700">#455A64</color>
<color name="material_primary_100">@color/material_blue_grey_100</color>
<color name="material_primary_300">@color/material_blue_grey_300</color>
<color name="material_primary_500">@color/material_blue_grey_500</color>
<color name="material_primary_700">@color/material_blue_grey_700</color>
+16 -3
View File
@@ -48,13 +48,15 @@
<string name="auto_set_gfwlist_summary">Bypass all sites located in China
</string>
<string name="route_list">Route</string>
<string name="route_entry_gfwlist">GFW List</string>
<string-array name="route_entry">
<item>All</item>
<item>Bypass LAN</item>
<item>Bypass China</item>
<item>Bypass LAN &amp; China</item>
<item>GFW List</item>
<item>@string/route_entry_gfwlist</item>
<item>China List</item>
<item>@string/custom_rules</item>
</string-array>
<string name="proxied_apps">Per-App Proxy</string>
<string name="proxied_apps_summary">Set proxy for selected apps, requires NAT mode under Android 4.x</string>
@@ -97,6 +99,8 @@
<!-- menu category -->
<string name="profiles">Profiles</string>
<string name="settings">Settings</string>
<string name="faq">FAQ</string>
<string name="faq_url">https://github.com/shadowsocks/shadowsocks-android/blob/master/.github/faq.md</string>
<string name="recovery">Reset</string>
<string name="about">About</string>
<string name="about_title">Shadowsocks %s</string>
@@ -125,8 +129,7 @@
<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="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
<string name="copy_url">Copy URL</string>
<string name="add_profile_scanner_not_installed">Please install any ZXing-compliant QR code scanning app.</string>
<plurals name="removed">
<item quantity="one">Removed</item>
<item quantity="other">%d items removed</item>
@@ -154,4 +157,14 @@
<string name="kcp_port">KCP Port</string>
<string name="kcp_port_summary">%d (port number of the remote KCPTUN server)</string>
<string name="kcpcli">KCP Parameters (CLI)</string>
<!-- acl -->
<string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</string>
<string name="action_add_rule">Add rule(s)…</string>
<string-array name="acl_rule_templates">
<item>Subnet/Hostname PCRE pattern</item>
<item>Domain name and all its subdomain names</item>
</string-array>
<string name="edit_rule">Edit rule</string>
</resources>
+5
View File
@@ -49,5 +49,10 @@
<style name="NumberPickerStyle">
<item name="android:textSize">14sp</item>
</style>
<style name="StyledScrollerTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -38,9 +38,7 @@ class AboutFragment extends ToolbarFragment {
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))) catch {
case _: android.content.ActivityNotFoundException => // Ignore
}
getActivity.asInstanceOf[MainActivity].launchUrl(url)
true
}
})
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -23,6 +23,7 @@ package com.github.shadowsocks
import java.util.concurrent.atomic.AtomicBoolean
import android.Manifest.permission
import android.animation.{Animator, AnimatorListenerAdapter}
import android.app.TaskStackBuilder
import android.content._
import android.content.pm.PackageManager
@@ -33,6 +34,7 @@ import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
import android.view._
import android.widget._
import com.futuremind.recyclerviewfastscroll.{FastScroller, SectionTitleProvider}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.{Key, Utils}
@@ -102,7 +104,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
}
}
private final class AppsAdapter extends RecyclerView.Adapter[AppViewHolder] {
private final class AppsAdapter extends RecyclerView.Adapter[AppViewHolder] with SectionTitleProvider {
private val apps = getApps(getPackageManager).sortWith((a, b) => {
val aProxied = proxiedApps.contains(a.packageName)
if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0
@@ -112,12 +114,14 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
def onBindViewHolder(vh: AppViewHolder, i: Int): Unit = vh.bind(apps(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new AppViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_apps_item, vg, false))
def getSectionTitle(i: Int): String = apps(i).name.substring(0, 1)
}
private var proxiedApps: mutable.HashSet[String] = _
private var toolbar: Toolbar = _
private var bypassSwitch: Switch = _
private var appListView: RecyclerView = _
private var fastScroller: FastScroller = _
private var loadingView: View = _
private val appsLoading = new AtomicBoolean
private var handler: Handler = _
@@ -167,9 +171,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
app.editor.putString(Key.individual, apps).apply()
app.editor.putString(Key.individual, apps).putBoolean(Key.dirty, true).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
fastScroller.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
initProxiedApps(apps)
reloadApps()
@@ -205,22 +210,25 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!app.settings.getBoolean(Key.proxyApps, false)) app.editor.putBoolean(Key.proxyApps, true).apply()
if (!app.settings.getBoolean(Key.proxyApps, false))
app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply()
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.proxyApps, checked).apply()
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply()
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) => app.editor.putBoolean(Key.bypass, checked).apply())
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply())
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.applistview).asInstanceOf[RecyclerView]
appListView.setLayoutManager(new LinearLayoutManager(this))
appListView = findViewById(R.id.list).asInstanceOf[RecyclerView]
appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false))
appListView.setItemAnimator(new DefaultItemAnimator)
fastScroller = findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
instance = this
loadAppsAsync()
@@ -237,7 +245,17 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
} while (!appsLoading.compareAndSet(true, false))
handler.post(() => {
appListView.setAdapter(adapter)
Utils.crossFade(AppManager.this, loadingView, appListView)
fastScroller.setRecyclerView(appListView)
val shortAnimTime = getResources.getInteger(android.R.integer.config_shortAnimTime)
appListView.setAlpha(0)
appListView.setVisibility(View.VISIBLE)
appListView.animate().alpha(1).setDuration(shortAnimTime)
fastScroller.setAlpha(0)
fastScroller.setVisibility(View.VISIBLE)
fastScroller.animate().alpha(1).setDuration(shortAnimTime)
loadingView.animate().alpha(0).setDuration(shortAnimTime).setListener(new AnimatorListenerAdapter {
override def onAnimationEnd(animation: Animator): Unit = loadingView.setVisibility(View.GONE)
})
})
}
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -34,6 +34,7 @@ import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
@@ -97,7 +98,8 @@ trait BaseService extends Service {
}, 1000, 1000)
}
TrafficMonitor.updateRate()
cb.trafficUpdated(TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
if (state == State.CONNECTED) cb.trafficUpdated(profile.id,
TrafficMonitor.txRate, TrafficMonitor.rxRate, TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
}
override def stopListeningForBandwidth(cb: IShadowsocksServiceCallback): Unit =
@@ -131,38 +133,39 @@ trait BaseService extends Service {
false
} else true
def connect(): Unit = if (profile.host == "198.199.101.152") {
val holder = app.containerHolder
val container = holder.getContainer
val url = container.getString("proxy_url")
val sig = Utils.getSignature(this)
def connect() {
if (profile.host == "198.199.101.152") {
val holder = app.containerHolder
val container = holder.getContainer
val url = container.getString("proxy_url")
val sig = Utils.getSignature(this)
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
case _ => Dns.SYSTEM.lookup(hostname)
})
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val requestBody = new FormBody.Builder()
.add("sig", sig)
.build()
val request = new Request.Builder()
.url(url)
.post(requestBody)
.build()
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
case _ => Dns.SYSTEM.lookup(hostname)
})
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val requestBody = new FormBody.Builder()
.add("sig", sig)
.build()
val request = new Request.Builder()
.url(url)
.post(requestBody)
.build()
val resposne = client.newCall(request).execute()
val list = resposne.body.string
val proxies = Random.shuffle(list.split('|').toSeq)
val proxy = proxies.head.split(':')
profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt
profile.password = proxy(2).trim
profile.method = proxy(3).trim
val proxies = Random.shuffle(client.newCall(request).execute().body.string.split('|').toSeq)
val proxy = proxies.head.split(':')
profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt
profile.password = proxy(2).trim
profile.method = proxy(3).trim
}
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
}
def startRunner(profile: Profile) {
@@ -246,6 +249,20 @@ trait BaseService extends Service {
p.tx += tx
p.rx += rx
app.profileManager.updateProfile(p)
handler.post(() => {
if (bandwidthListeners.nonEmpty) {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder)) item.trafficPersisted(p.id)
} catch {
case _: Exception => // Ignore
}
}
callbacks.finishBroadcast()
}
})
case None =>
}
}
@@ -266,7 +283,8 @@ trait BaseService extends Service {
for (i <- 0 until n) {
try {
val item = callbacks.getBroadcastItem(i)
if (bandwidthListeners.contains(item.asBinder)) item.trafficUpdated(txRate, rxRate, txTotal, rxTotal)
if (bandwidthListeners.contains(item.asBinder))
item.trafficUpdated(profile.id, txRate, rxRate, txTotal, rxTotal)
} catch {
case _: Exception => // Ignore
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -28,9 +28,10 @@ import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.net.VpnService
import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter}
import android.os.{Build, Bundle, Handler, Message}
import android.support.customtabs.CustomTabsIntent
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
@@ -42,6 +43,7 @@ import android.view.View
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
@@ -60,6 +62,8 @@ object MainActivity {
private final val DRAWER_GLOBAL_SETTINGS = 1L
private final val DRAWER_RECOVERY = 2L
private final val DRAWER_ABOUT = 3L
private final val DRAWER_FAQ = 4L
private final val DRAWER_CUSTOM_RULES = 5L
}
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
@@ -82,15 +86,24 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
private var currentFragment: ToolbarFragment = _
private lazy val profilesFragment = new ProfilesFragment()
private lazy val customRulesFragment = new CustomRulesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
private lazy val aboutFragment = new AboutFragment()
private lazy val customTabsIntent = new CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.material_primary_500))
.build()
def launchUrl(url: String): Unit = try customTabsIntent.launchUrl(this, Uri.parse(url)) catch {
case _: ActivityNotFoundException => // Ignore
}
// Services
var state: Int = _
private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, profileName: String, m: String): Unit = handler.post(() => changeState(s, profileName, m))
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
handler.post(() => updateTraffic(txRate, rxRate, txTotal, rxTotal))
def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
handler.post(() => updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal))
override def trafficPersisted(profileId: Int): Unit = handler.post(() => if (ProfilesFragment.instance != null)
ProfilesFragment.instance.onTrafficPersisted(profileId))
}
private lazy val greyTint = ContextCompat.getColorStateList(MainActivity.this, R.color.material_primary_500)
@@ -128,25 +141,22 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
state = s
if (state == State.CONNECTED) fab.setBackgroundTintList(greenTint) else {
fab.setBackgroundTintList(greyTint)
updateTraffic(0, 0, 0, 0)
updateTraffic(-1, 0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
if (ProfilesFragment.instance != null) {
val adapter = ProfilesFragment.instance.profilesAdapter
adapter.notifyDataSetChanged() // refresh button enabled state
if (state == State.STOPPED) adapter.refreshId(app.profileId) // refresh bandwidth statistics
}
if (ProfilesFragment.instance != null)
ProfilesFragment.instance.profilesAdapter.notifyDataSetChanged() // refresh button enabled state
fab.setEnabled(false)
if (state == State.CONNECTED || state == State.STOPPED)
handler.postDelayed(() => fab.setEnabled(state == State.CONNECTED || state == State.STOPPED), 1000)
}
def updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
def updateTraffic(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.setText(TrafficMonitor.formatTraffic(txTotal))
rxText.setText(TrafficMonitor.formatTraffic(rxTotal))
txRateText.setText(TrafficMonitor.formatTraffic(txRate) + "/s")
rxRateText.setText(TrafficMonitor.formatTraffic(rxRate) + "/s")
val child = getFragmentManager.findFragmentById(R.id.fragment_holder).asInstanceOf[ToolbarFragment]
if (state != State.STOPPING && child != null) child.onTrafficUpdated(txRate, rxRate, txTotal, rxTotal)
if (state != State.STOPPING && child != null) child.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
}
override def onServiceConnected() {
@@ -186,6 +196,11 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.withName(R.string.profiles)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
.withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_CUSTOM_RULES)
.withName(R.string.custom_rules)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_assignment))
.withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings)
@@ -193,6 +208,12 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.withIconTintingEnabled(true)
)
.addStickyDrawerItems(
new PrimaryDrawerItem()
.withIdentifier(DRAWER_FAQ)
.withName(R.string.faq)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_help_outline))
.withIconTintingEnabled(true)
.withSelectable(false),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_RECOVERY)
.withName(R.string.recovery)
@@ -363,6 +384,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
case DRAWER_ABOUT =>
app.track(TAG, "about")
displayFragment(aboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
case DRAWER_CUSTOM_RULES => displayFragment(customRulesFragment)
}
true // unexpected cases will throw exception
}
@@ -370,6 +393,10 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
protected override def onResume() {
super.onResume()
app.refreshContainerHolder()
state match {
case State.STOPPING | State.CONNECTING =>
case _ => hideCircle()
}
}
override def onStart() {
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -63,7 +63,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
@@ -101,7 +101,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
def saveAndExit() {
profile.deserialize(app.settings)
app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.refreshId(profile.id)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
getActivity.finish()
}
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -22,7 +22,9 @@ package com.github.shadowsocks
import java.util.GregorianCalendar
import android.app.Activity
import android.content._
import android.net.Uri
import android.os.Bundle
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget._
@@ -37,13 +39,14 @@ import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.{AdRequest, AdSize, NativeExpressAdView}
import com.google.zxing.integration.android.IntentIntegrator
import scala.collection.mutable.ArrayBuffer
import scala.util.Random
object ProfilesFragment {
var instance: ProfilesFragment = _ // used for callback from ProfileManager and stateChanged from MainActivity
private final val REQUEST_SCAN_QR_CODE = 0
}
final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
@@ -68,7 +71,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
case _ => false
}
final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
final class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
var item: Profile = _
@@ -76,11 +79,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
private val title = itemView.findViewById(R.id.title).asInstanceOf[TextView]
private val address = itemView.findViewById(R.id.address).asInstanceOf[TextView]
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
private val indicator = itemView.findViewById(R.id.indicator).asInstanceOf[ViewGroup]
private val edit = itemView.findViewById(R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this)
// it will not take effect unless set in code
itemView.findViewById(R.id.indicator).setBackgroundResource(R.drawable.background_profile)
private var adView: NativeExpressAdView = _
@@ -102,7 +106,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
edit.setAlpha(if (editable) 1 else 0.5F)
var tx = item.tx
var rx = item.rx
if (item.id == app.profileId) {
if (item.id == bandwidthProfile) {
tx += txTotal
rx += rxTotal
}
@@ -112,10 +116,10 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
if (item.id == app.profileId) {
indicator.setBackgroundResource(R.drawable.background_selected)
itemView.setSelected(true)
selectedItem = this
} else {
indicator.setBackgroundResource(R.drawable.background_selectable)
itemView.setSelected(false)
if (selectedItem eq this) selectedItem = null
}
@@ -151,7 +155,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
val old = app.profileId
app.switchProfile(item.id)
profilesAdapter.refreshId(old)
bind(item)
itemView.setSelected(true)
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
}
@@ -175,7 +179,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onBindViewHolder(vh: ProfileViewHolder, i: Int): Unit = vh.bind(profiles(i))
def onCreateViewHolder(vg: ViewGroup, i: Int) =
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profiles_item, vg, false))
new ProfileViewHolder(LayoutInflater.from(vg.getContext).inflate(R.layout.layout_profile, vg, false))
setHasStableIds(true) // Reason: http://stackoverflow.com/a/32488059/2245107
override def getItemId(position: Int): Long = profiles(position).id
@@ -218,6 +222,10 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
for ((_, item) <- actions) app.profileManager.delProfile(item.id)
def refreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) notifyItemChanged(index)
}
def deepRefreshId(id: Int) {
val index = profiles.indexWhere(_.id == id)
if (index >= 0) {
profiles(index) = app.profileManager.getProfile(id).get
@@ -238,6 +246,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
lazy val profilesAdapter = new ProfilesAdapter
private var undoManager: UndoSnackbarManager[Profile] = _
private var bandwidthProfile: Int = _
private var txTotal: Long = _
private var rxTotal: Long = _
@@ -247,7 +256,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
.putExtra(Action.EXTRA_PROFILE_ID, id))
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_profiles, container, false)
inflater.inflate(R.layout.layout_list, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
@@ -256,18 +265,19 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(getActivity)
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager)
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
}.getOrElse(-1))
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
profilesList.setItemAnimator(animator)
profilesList.setAdapter(profilesAdapter)
instance = this
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
}.getOrElse(-1))
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar), profilesAdapter.undo, profilesAdapter.commit)
undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar),
profilesAdapter.undo, profilesAdapter.commit)
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
@@ -279,7 +289,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onSwiped(viewHolder: ViewHolder, direction: Int) {
val index = viewHolder.getAdapterPosition
profilesAdapter.remove(index)
undoManager.remove(index, viewHolder.asInstanceOf[ProfileViewHolder].item)
undoManager.remove((index, viewHolder.asInstanceOf[ProfileViewHolder].item))
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = {
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
@@ -288,10 +298,24 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
}).attachToRecyclerView(profilesList)
}
override def onTrafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = {
this.txTotal = txTotal
this.rxTotal = rxTotal
profilesAdapter.refreshId(app.profileId)
override def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit =
if (profileId != -1) { // ignore resets from MainActivity
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
this.txTotal = txTotal
this.rxTotal = rxTotal
profilesAdapter.refreshId(profileId)
}
def onTrafficPersisted(profileId: Int) {
txTotal = 0
rxTotal = 0
if (bandwidthProfile != profileId) {
onTrafficPersisted(bandwidthProfile)
bandwidthProfile = profileId
}
profilesAdapter.deepRefreshId(profileId)
}
override def onDetach() {
@@ -304,23 +328,24 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
super.onDestroy()
}
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
super.onActivityResult(resultCode, resultCode, data)
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (scanResult != null) {
val contents = scanResult.getContents
if (!TextUtils.isEmpty(contents))
Parser.findAll(contents).foreach(app.profileManager.createProfile)
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
case REQUEST_SCAN_QR_CODE => if (resultCode == Activity.RESULT_OK) {
val contents = data.getStringExtra("SCAN_RESULT")
if (!TextUtils.isEmpty(contents)) Parser.findAll(contents).foreach(app.profileManager.createProfile)
}
case _ => super.onActivityResult(resultCode, resultCode, data)
}
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code =>
val integrator = new IntentIntegrator(this)
val list = new java.util.ArrayList(IntentIntegrator.TARGET_ALL_KNOWN)
list.add("tw.com.quickmark")
integrator.setTargetApplications(list)
integrator.initiateScan()
try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN")
.addCategory(Intent.CATEGORY_DEFAULT)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT),
REQUEST_SCAN_QR_CODE) catch {
case _: ActivityNotFoundException =>
Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark")))
}
true
case R.id.action_import =>
if (clipboard.hasPrimaryClip) {
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -30,7 +30,20 @@ import android.widget.{ImageView, LinearLayout}
import com.github.shadowsocks.utils.Utils
import net.glxn.qrgen.android.QRCode
final class QRCodeDialog(url: String) extends DialogFragment {
object QRCodeDialog {
private final val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
}
final class QRCodeDialog extends DialogFragment {
import QRCodeDialog._
def this(url: String) {
this()
val bundle = new Bundle()
bundle.putString(KEY_URL, url)
setArguments(bundle)
}
private def url = getArguments.getString(KEY_URL)
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
private var adapter: NfcAdapter = _
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -34,8 +34,8 @@ import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
import com.github.shadowsocks.job.DonaldTrump
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -28,8 +28,8 @@ import android.content._
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.job.AclSyncJob
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
@@ -76,9 +76,9 @@ class ShadowsocksNatService extends BaseService {
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
if (profile.route != Route.ALL) {
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
cmd += Acl.getPath(profile.route)
}
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
@@ -119,16 +119,10 @@ class ShadowsocksNatService extends BaseService {
, "-t" , "10"
, "-b" , "127.0.0.1"
, "-l" , (profile.localPort + 53).toString
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P" , getApplicationInfo.dataDir
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
cmd += "-L"
if (profile.remoteDns == null)
cmd += "8.8.8.8:53"
else
cmd += profile.remoteDns + ":53"
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
@@ -152,7 +146,7 @@ class ShadowsocksNatService extends BaseService {
, "-t" , "10"
, "-b" , "127.0.0.1"
, "-l" , (profile.localPort + 63).toString
, "-L" , "8.8.8.8:53"
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P", getApplicationInfo.dataDir
, "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf")
@@ -169,11 +163,11 @@ class ShadowsocksNatService extends BaseService {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val conf = profile.route match {
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
"127.0.0.1", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
case Route.CHINALIST =>
case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
"127.0.0.1", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
@@ -311,7 +305,7 @@ class ShadowsocksNatService extends BaseService {
handleConnection()
if (profile.route != Route.ALL)
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED)
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -39,7 +39,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub {
override def stateChanged(state: Int, profileName: String, msg: String): Unit = () // ignore
override def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
override def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
val txr = TrafficMonitor.formatTraffic(txRate)
val rxr = TrafficMonitor.formatTraffic(rxRate)
builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txr, rxr))
@@ -47,6 +47,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
show()
}
override def trafficPersisted(profileId: Int): Unit = ()
}
private var lockReceiver: BroadcastReceiver = _
private var callbackRegistered: Boolean = _
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -36,7 +36,7 @@ final class ShadowsocksTileService extends TileService with ServiceBoundContext
private lazy val iconBusy = Icon.createWithResource(this, R.drawable.ic_start_busy)
private lazy val iconConnected = Icon.createWithResource(this, R.drawable.ic_start_connected)
private lazy val callback = new IShadowsocksServiceCallback.Stub {
def trafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
def trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
def stateChanged(state: Int, profileName: String, msg: String) {
val tile = getQsTile
if (tile != null) {
@@ -57,6 +57,7 @@ final class ShadowsocksTileService extends TileService with ServiceBoundContext
tile.updateTile()
}
}
override def trafficPersisted(profileId: Int): Unit = ()
}
override def onServiceConnected(): Unit = callback.stateChanged(bgService.getState, bgService.getProfileName, null)
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -30,8 +30,8 @@ import android.net.VpnService
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.job.AclSyncJob
import com.github.shadowsocks.utils._
import scala.collection.mutable.ArrayBuffer
@@ -145,7 +145,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
handleConnection()
changeState(State.CONNECTED)
if (profile.route != Route.ALL)
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.getName)
@@ -241,9 +241,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.udpdns && !profile.kcp) cmd += "-u"
if (profile.route != Route.ALL) {
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
cmd += Acl.getPath(profile.route)
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
@@ -270,16 +270,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
, "-V"
, "-t", "10"
, "-b", "127.0.0.1"
, "-L" , if (profile.remoteDns == null) "8.8.8.8:53" else profile.remoteDns + ":53"
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf")
cmd += "-L"
if (profile.remoteDns == null)
cmd += "8.8.8.8:53"
else
cmd += profile.remoteDns + ":53"
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
@@ -291,11 +285,11 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val protect = "protect = \"" + protectPath +"\";"
val conf = profile.route match {
case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
"0.0.0.0", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
case Route.CHINALIST =>
case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
"0.0.0.0", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
@@ -347,7 +341,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
if (profile.route == Route.ALL || profile.route == Route.BYPASS_CHN) {
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_CHN) {
builder.addRoute("0.0.0.0", 0)
} else {
val privateList = getResources.getStringArray(R.array.bypass_private_route)
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -35,7 +35,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app
* @author CzBiX
*/
class TaskerActivity extends AppCompatActivity {
private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
{
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
@@ -92,8 +92,8 @@ class TaskerActivity extends AppCompatActivity {
taskerOption = TaskerSettings.fromIntent(getIntent)
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
switch.setChecked(taskerOption.switchOn)
val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
val lm = new LinearLayoutManager(this)
val profilesList = findViewById(R.id.list).asInstanceOf[RecyclerView]
val lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(lm)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -35,5 +35,5 @@ class ToolbarFragment extends Fragment {
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
def onTrafficUpdated(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
def onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long): Unit = ()
}
@@ -0,0 +1,143 @@
package com.github.shadowsocks.acl
import java.io.FileNotFoundException
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
import scala.collection.mutable
import scala.io.Source
/**
* ACL handler compliant with: src/main/jni/shadowsocks-libev/src/acl.c
*
* OrmLite integration is unused for now.
*
* @author Mygod
*/
class Acl {
@DatabaseField(generatedId = true)
var id: Int = _
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
@DatabaseField
var bypass: Boolean = _
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
}
def setProxyHostnamesString(value: String) {
proxyHostnames.clear()
proxyHostnames ++= value.split("\n")
}
def setSubnetsString(value: String) {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
bypassHostnames ++= other.bypassHostnames
proxyHostnames.clear()
proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
bypass = other.bypass
this
}
def fromSource(value: Source): Acl = {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
bypass = false
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
lazy val proxySubnets = new mutable.SortedList[Subnet]()
var hostnames: mutable.SortedList[String] = proxyHostnames
var subnets: mutable.SortedList[Subnet] = proxySubnets
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => line
case index => line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
subnets = null
case "[black_list]" | "[bypass_list]" =>
hostnames = bypassHostnames
subnets = bypassSubnets
case "[white_list]" | "[proxy_list]" =>
hostnames = proxyHostnames
subnets = proxySubnets
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
case _: IllegalArgumentException => hostnames += input
}
case _ =>
}
this.subnets ++= (if (bypass) bypassSubnets else proxySubnets)
this
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getPath(id)))
override def toString: String = {
val result = new StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
var bypassList = bypassHostnames.toStream
var proxyList = proxyHostnames.toStream
if (bypass) bypassList = subnets.toStream.map(_.toString) #::: bypassList
else proxyList = subnets.toStream.map(_.toString) #::: proxyList
if (bypassList.nonEmpty) {
result.append("[bypass_list]\n")
result.append(bypassList.mkString("\n"))
result.append('\n')
}
if (proxyList.nonEmpty) {
result.append("[proxy_list]\n")
result.append(proxyList.mkString("\n"))
result.append('\n')
}
result.toString
}
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
// Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val bypassHostnamesString: String = null
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val proxyHostnamesString: String = null
// noinspection ScalaUnusedSymbol
@DatabaseField(useGetSet = true)
private val subnetsString: String = null
}
object Acl {
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
final val BYPASS_LAN_CHN = "bypass-lan-china"
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
def getPath(id: String): String = app.getApplicationInfo.dataDir + '/' + id + ".acl"
def customRules: Acl = {
val acl = new Acl()
try acl.fromId(CUSTOM_RULES) catch {
case _: FileNotFoundException =>
}
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
acl
}
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getPath(id), acl.toString)
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -18,18 +18,18 @@
/* */
/*******************************************************************************/
package com.github.shadowsocks.job
package com.github.shadowsocks.acl
import java.io.IOException
import java.net.URL
import java.util.concurrent.TimeUnit
import com.evernote.android.job.Job.{Params, Result}
import com.evernote.android.job.{Job, JobRequest}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.IOUtils
import scala.io.Source
/**
* @author Mygod
*/
@@ -50,9 +50,8 @@ class AclSyncJob(route: String) extends Job {
val filename = route + ".acl"
try {
//noinspection JavaAccessorMethodCalledAsEmptyParen
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename, autoClose(
new URL("https://shadowsocks.org/acl/android/v1/" +
filename).openConnection().getInputStream())(IOUtils.readString))
IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename,
Source.fromURL("https://shadowsocks.org/acl/android/v1/" + filename).mkString)
Result.SUCCESS
} catch {
case e: IOException =>
@@ -0,0 +1,288 @@
package com.github.shadowsocks.acl
import java.net.IDN
import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
import android.os.{Bundle, Handler}
import android.support.v7.app.AlertDialog
import android.support.v7.widget.RecyclerView.ViewHolder
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.view._
import android.widget.{EditText, Spinner, TextView, Toast}
import com.futuremind.recyclerviewfastscroll.{FastScroller, SectionTitleProvider}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.State
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.github.shadowsocks.{MainActivity, R, ToolbarFragment}
import scala.collection.mutable
/**
* @author Mygod
*/
object CustomRulesFragment {
private final val GENERIC = 0
private final val DOMAIN = 1
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
private final val TEMPLATE_DOMAIN = "(^|\\.)%s$"
private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
}
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
import CustomRulesFragment._
private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
case State.CONNECTED => app.currentProfile.get.route != Acl.CUSTOM_RULES
case State.STOPPED => true
case _ => false
}
private def createAclRuleDialog(text: CharSequence = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) =>
templateSelector.setSelection(DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
templateSelector.setSelection(GENERIC)
editText.setText(text)
}
(templateSelector, editText, new AlertDialog.Builder(getActivity)
.setTitle(R.string.edit_rule)
.setNegativeButton(android.R.string.cancel, null)
.setView(view))
}
private val handler = new Handler()
private val selectedItems = new mutable.HashSet[AnyRef]
private def onSelectedItemsUpdated(): Unit = selectionItem.setVisible(selectedItems.nonEmpty)
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable)
def bind(hostname: String) {
item = hostname
text.setText(hostname)
itemView.setSelected(selectedItems.contains(hostname))
}
def bind(subnet: Subnet) {
item = subnet
text.setText(subnet.toString)
itemView.setSelected(selectedItems.contains(subnet))
}
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
dialog
.setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item)
undoManager.remove((-1, item))
}): DialogInterface.OnClickListener)
.setPositiveButton(android.R.string.ok, ((_, _) =>
if (adapter.addFromTemplate(templateSelector.getSelectedItemPosition, editText.getText)) adapter.remove(item))
: DialogInterface.OnClickListener)
.create().show()
}
def onLongClick(v: View): Boolean = {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated()
itemView.setSelected(!itemView.isSelected)
true
}
}
private final class AclRulesAdapter extends RecyclerView.Adapter[AclRuleViewHolder] with SectionTitleProvider {
private val acl = Acl.customRules
private var savePending: Boolean = _
private def apply() = if (!savePending) {
savePending = true
handler.post(() => {
Acl.save(Acl.CUSTOM_RULES, acl)
savePending = false
})
}
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
val j = i - acl.subnets.size
if (j < 0) vh.bind(acl.subnets(i)) else vh.bind(acl.proxyHostnames(j))
}
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false))
def addSubnet(subnet: Subnet): Boolean = if (acl.subnets.add(subnet)) {
notifyItemInserted(acl.subnets.indexOf(subnet))
apply()
true
} else false
def addHostname(hostname: String): Boolean = if (acl.proxyHostnames.add(hostname)) {
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
apply()
true
} else false
def addToProxy(input: String): Boolean = try addSubnet(Subnet.fromString(input)) catch {
case _: IllegalArgumentException => addHostname(input)
}
def addFromTemplate(template: Int, text: CharSequence): Boolean = template match {
case GENERIC => addToProxy(text.toString)
case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
case exc: IllegalArgumentException =>
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
false
}
case _ => false
}
def remove(i: Int) {
val j = i - acl.subnets.size
if (j < 0) {
undoManager.remove((i, acl.subnets(i)))
acl.subnets.remove(i)
} else {
undoManager.remove((j, acl.proxyHostnames(j)))
acl.proxyHostnames.remove(j)
}
notifyItemRemoved(i)
apply()
}
def remove(item: AnyRef): Unit = item match {
case subnet: Subnet =>
notifyItemRemoved(acl.subnets.indexOf(subnet))
acl.subnets.remove(subnet)
apply()
case hostname: String =>
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
acl.proxyHostnames.remove(hostname)
apply()
}
def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
selectedItems.foreach(remove)
selectedItems.clear()
onSelectedItemsUpdated()
}
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
apply()
}
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
notifyItemInserted(acl.subnets.indexOf(subnet))
apply()
}
}
def selectAll() {
selectedItems.clear()
selectedItems ++= acl.subnets
selectedItems ++= acl.proxyHostnames
onSelectedItemsUpdated()
notifyDataSetChanged()
}
def getSectionTitle(i: Int): String = {
val j = i - acl.subnets.size
(if (j < 0) acl.subnets(i).address.toString.charAt(0).toString else {
val hostname = acl.proxyHostnames(i)
PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
}).substring(0, 1)
}
}
private lazy val adapter = new AclRulesAdapter()
private var list: RecyclerView = _
private var selectionItem: MenuItem = _
private var undoManager: UndoSnackbarManager[AnyRef] = _
private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
inflater.inflate(R.layout.layout_custom_rules, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
savedInstanceState.getStringArray(SELECTED_SUBNETS) match {
case null =>
case arr => selectedItems ++= arr.map(Subnet.fromString)
}
savedInstanceState.getStringArray(SELECTED_HOSTNAMES) match {
case null =>
case arr => selectedItems ++= arr
}
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
toolbar.inflateMenu(R.menu.custom_rules_menu)
toolbar.setOnMenuItemClickListener(this)
selectionItem = toolbar.getMenu.findItem(R.id.selection)
app.updateAssets()
list = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
list.setItemAnimator(new DefaultItemAnimator)
list.setAdapter(adapter)
val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
fastScroller.setRecyclerView(list)
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
if (isEnabled) super.getSwipeDirs(recyclerView, viewHolder) else 0
def onSwiped(viewHolder: ViewHolder, direction: Int): Unit = adapter.remove(viewHolder.getAdapterPosition)
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = false
}).attachToRecyclerView(list)
}
override def onSaveInstanceState(outState: Bundle): Unit = {
super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
}
override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
case R.id.action_select_all =>
adapter.selectAll()
true
case R.id.action_cut =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
adapter.removeSelected()
true
case R.id.action_copy =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
true
case R.id.action_delete =>
adapter.removeSelected()
true
case R.id.action_manual_settings =>
val (templateSelector, editText, dialog) = createAclRuleDialog()
dialog
.setPositiveButton(android.R.string.ok, ((_, _) => adapter.addFromTemplate(
templateSelector.getSelectedItemPosition, editText.getText)): DialogInterface.OnClickListener)
.create().show()
true
case R.id.action_import =>
clipboard.getPrimaryClip.getItemAt(0).getText.toString.split("\n").foreach(adapter.addToProxy)
true
case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet)
acl.proxyHostnames.foreach(adapter.addHostname)
true
case _ => false
}
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -18,7 +18,7 @@
/* */
/*******************************************************************************/
package com.github.shadowsocks.job
package com.github.shadowsocks.acl
import android.util.Log
import com.evernote.android.job.JobCreator
@@ -0,0 +1,51 @@
package com.github.shadowsocks.acl
import java.net.{Inet4Address, Inet6Address, InetAddress}
import com.github.shadowsocks.utils.Utils
/**
* @author Mygod
*/
@throws[IllegalArgumentException]
class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] {
if (prefixSize < 0) throw new IllegalArgumentException
address match {
case _: Inet4Address => if (prefixSize > 32) throw new IllegalArgumentException
case _: Inet6Address => if (prefixSize > 128) throw new IllegalArgumentException
}
override def toString: String = if (address match {
case _: Inet4Address => prefixSize == 32
case _: Inet6Address => prefixSize == 128
}) address.toString else address.toString + '/' + prefixSize
override def compareTo(that: Subnet): Int = {
val addrThis = address.getAddress
val addrThat = that.address.getAddress
var result = addrThis lengthCompare addrThat.length // IPv4 address goes first
if (result != 0) return result
for ((x, y) <- addrThis zip addrThat) {
result = x compare y
if (result != 0) return result
}
prefixSize compare that.prefixSize
}
}
object Subnet {
@throws[IllegalArgumentException]
def fromString(value: String): Subnet = {
val parts = value.split("/")
if (!Utils.isNumeric(parts(0))) throw new IllegalArgumentException()
val addr = InetAddress.getByName(parts(0))
parts.length match {
case 1 => new Subnet(addr, addr match {
case _: Inet4Address => 32
case _: Inet6Address => 128
})
case 2 => new Subnet(addr, parts(1).toInt)
case _ => throw new IllegalArgumentException()
}
}
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -23,11 +23,11 @@ package com.github.shadowsocks.database
import android.content.Context
import android.content.pm.ApplicationInfo
import android.database.sqlite.SQLiteDatabase
import com.github.shadowsocks.ShadowsocksApplication.app
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
import com.github.shadowsocks.ShadowsocksApplication.app
import scala.collection.JavaConverters._
import scala.collection.mutable
@@ -55,12 +55,16 @@ class DBHelper(val context: Context)
TableUtils.createTable(connectionSource, classOf[Profile])
}
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.dropTable(connectionSource, classOf[Profile], true)
onCreate(database, connectionSource)
}
def onUpgrade(database: SQLiteDatabase, connectionSource: ConnectionSource, oldVersion: Int,
newVersion: Int) {
if (oldVersion != newVersion) {
if (oldVersion < 7) {
profileDao.executeRawNoArgs("DROP TABLE IF EXISTS 'profile';")
onCreate(database, connectionSource)
recreate(database, connectionSource)
return
}
@@ -132,8 +136,7 @@ class DBHelper(val context: Context)
} catch {
case ex: Exception =>
app.track(ex)
profileDao.executeRawNoArgs("DROP TABLE IF EXISTS 'profile';")
onCreate(database, connectionSource)
recreate(database, connectionSource)
return
}
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -31,7 +31,7 @@ import com.j256.ormlite.field.{DataType, DatabaseField}
class Profile {
@DatabaseField(generatedId = true)
var id: Int = 0
var id: Int = _
@DatabaseField
var name: String = ""
@@ -59,28 +59,28 @@ class Profile {
var remoteDns: String = "8.8.8.8"
@DatabaseField
var proxyApps: Boolean = false
var proxyApps: Boolean = _
@DatabaseField
var bypass: Boolean = false
var bypass: Boolean = _
@DatabaseField
var udpdns: Boolean = false
var udpdns: Boolean = _
@DatabaseField
var auth: Boolean = false
var auth: Boolean = _
@DatabaseField
var ipv6: Boolean = false
var ipv6: Boolean = _
@DatabaseField(dataType = DataType.LONG_STRING)
var individual: String = ""
@DatabaseField
var tx: Long = 0
var tx: Long = _
@DatabaseField
var rx: Long = 0
var rx: Long = _
@DatabaseField
val date: java.util.Date = new java.util.Date()
@@ -89,7 +89,7 @@ class Profile {
var userOrder: Long = _
@DatabaseField
var kcp: Boolean = false
var kcp: Boolean = _
@DatabaseField
var kcpPort: Int = 8399
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -179,12 +179,3 @@ object Action {
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
object Route {
val ALL = "all"
val BYPASS_LAN = "bypass-lan"
val BYPASS_CHN = "bypass-china"
val BYPASS_LAN_CHN = "bypass-lan-china"
val GFWLIST = "gfwlist"
val CHINALIST = "china-list"
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -37,16 +37,6 @@ object IOUtils {
}
}
def readString(in: InputStream): String = {
val builder = new StringBuilder()
val buffer = new Array[Byte](BUFFER_SIZE)
while (true) {
val count = in.read(buffer)
if (count >= 0) builder.append(new String(buffer, 0, count)) else return builder.toString()
}
null
}
def writeString(file: String, content: String): Unit =
autoClose(new FileWriter(file))(writer => writer.write(content))
}
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -82,16 +82,6 @@ object Utils {
toast
}
def crossFade(context: Context, from: View, to: View) {
def shortAnimTime = context.getResources.getInteger(android.R.integer.config_shortAnimTime)
to.setAlpha(0)
to.setVisibility(View.VISIBLE)
to.animate().alpha(1).setDuration(shortAnimTime)
from.animate().alpha(0).setDuration(shortAnimTime).setListener(new AnimatorListenerAdapter {
override def onAnimationEnd(animation: Animator): Unit = from.setVisibility(View.GONE)
})
}
def readAllLines(f: File): String = {
val scanner = new Scanner(f)
try {
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -1,7 +1,7 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2016 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
@@ -22,7 +22,6 @@ 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
@@ -51,15 +50,16 @@ class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
}
private var last: Snackbar = _
def remove(index: Int, item: T) {
recycleBin.append((index, item))
def remove(items: (Int, T)*) {
recycleBin.appendAll(items)
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)
.addCallback(removedCallback)
.setAction(R.string.undo, (_ => {
undo(recycleBin.reverseIterator)
recycleBin.clear
}): View.OnClickListener)
last.show()
}
@@ -0,0 +1,152 @@
package scala.collection.mutable
import scala.collection.SortedSetLike
import scala.collection.generic.MutableSortedSetFactory
import scala.collection.immutable.RedBlackTree.Tree
import scala.collection.immutable.{RedBlackTree => RB}
import scala.runtime.ObjectRef
/**
* @define Coll `mutable.SortedList`
* @define coll mutable sorted list
* @factoryInfo
* Companion object of SortedList providing factory related utilities.
* @author Mygod
*/
//noinspection ReferenceMustBePrefixed,ScalaDocUnknownTag
object SortedList extends MutableSortedSetFactory[SortedList] {
/**
* The empty list of this type
*/
def empty[A](implicit ordering: Ordering[A]) = new SortedList[A]()
}
/**
* A mutable SortedList heavily based on SortedSet which "is not designed to enable meaningful subclassing".
*
* Based on: https://github.com/scala/scala/blob/3cc99d7/src/library/scala/collection/mutable/TreeSet.scala
*
* @author Mygod
*/
//noinspection ReferenceMustBePrefixed
class SortedList[A] private(treeRef: ObjectRef[RB.Tree[A, Null]], from: Option[A], until: Option[A])
(implicit val ordering: Ordering[A])
extends SortedSet[A] with SetLike[A, SortedList[A]] with SortedSetLike[A, SortedList[A]] with Set[A] with Serializable {
def insert(elem: A): Boolean = {
val old = size
this += elem
old < size
}
// AbstractSeq
def apply(i: Int): A = RB.nth(treeRef.elem, i).key // Should i out of range result in NullReferenceException?
def length: Int = size
// Buffer
def +=:(elem: A): SortedList.this.type = +=(elem) // prepending is the same as the list is sorted
def remove(n: Int): A = {
val result = apply(n)
remove(result)
result
}
def update(idx: Int, elem: A) {
val x = apply(idx)
if (!ordering.equiv(x, elem)) {
remove(x)
this.add(elem)
}
}
// Optimized methods
override def clear(): Unit = treeRef.elem = null
def indexOf[B >: A](elem: B, from: Int = 0): Int = lookupIndex(treeRef.elem, elem.asInstanceOf[A]) match {
case result if result < from => -1
case result => result
}
override def last: A = RB.greatest(treeRef.elem).key
// Tree helper methods, TODO: expand to non-recursive form?
private def lookupIndex(tree: Tree[A, Null], x: A): Int =
if (tree eq null) -1 else ordering.compare(x, tree.key) match {
case 0 => RB.count(tree.left)
case cmp if cmp < 0 => lookupIndex(tree.left, x)
case _ => lookupIndex(tree.right, x) match {
case i if i < 0 => i
case i => RB.count(tree.left) + 1 + i
}
}
// The following is copied from TreeSet
if (ordering eq null)
throw new NullPointerException("ordering must not be null")
def this()(implicit ordering: Ordering[A]) = this(new ObjectRef(null), None, None)
override def size: Int = RB.countInRange(treeRef.elem, from, until)
override def stringPrefix = "SortedList"
override def empty: SortedList[A] = SortedList.empty
private def pickBound(comparison: (A, A) => A, oldBound: Option[A], newBound: Option[A]) = (newBound, oldBound) match {
case (Some(newB), Some(oldB)) => Some(comparison(newB, oldB))
case (None, _) => oldBound
case _ => newBound
}
override def rangeImpl(fromArg: Option[A], untilArg: Option[A]): SortedList[A] = {
val newFrom = pickBound(ordering.max, fromArg, from)
val newUntil = pickBound(ordering.min, untilArg, until)
new SortedList(treeRef, newFrom, newUntil)
}
override def -=(elem: A): this.type = {
treeRef.elem = RB.delete(treeRef.elem, elem)
this
}
override def +=(elem: A): this.type = {
treeRef.elem = RB.update(treeRef.elem, elem, null, overwrite = false)
this
}
/**
* Thanks to the immutable nature of the
* underlying Tree, we can share it with
* the clone. So clone complexity in time is O(1).
*
*/
override def clone(): SortedList[A] =
new SortedList[A](new ObjectRef(treeRef.elem), from, until)
private val notProjection = !(from.isDefined || until.isDefined)
override def contains(elem: A): Boolean = {
def leftAcceptable: Boolean = from match {
case Some(lb) => ordering.gteq(elem, lb)
case _ => true
}
def rightAcceptable: Boolean = until match {
case Some(ub) => ordering.lt(elem, ub)
case _ => true
}
(notProjection || (leftAcceptable && rightAcceptable)) &&
RB.contains(treeRef.elem, elem)
}
override def iterator: Iterator[A] = iteratorFrom(None)
override def keysIteratorFrom(start: A): Iterator[A] = iteratorFrom(Some(start))
private def iteratorFrom(start: Option[A]) = {
val it = RB.keysIterator(treeRef.elem, pickBound(ordering.max, from, start))
until match {
case None => it
case Some(ub) => it takeWhile (k => ordering.lt(k, ub))
}
}
}