Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4026e5a86 | ||
|
|
fb98b3e4a1 | ||
|
|
f629c432b0 | ||
|
|
4c15d27a99 | ||
|
|
341c85049b | ||
|
|
35d7611524 | ||
|
|
d10acf52ec | ||
|
|
0234acbf5f | ||
|
|
292c76b227 | ||
|
|
a67c29314e | ||
|
|
0fc9c6fb8a | ||
|
|
82a3e429d1 | ||
|
|
b4425e3b06 | ||
|
|
ac2b668a4b | ||
|
|
8374a8cdcc | ||
|
|
7feb049f4b | ||
|
|
5bc7c9f8cc | ||
|
|
115fbcb2fe | ||
|
|
69d2bc1ee5 | ||
|
|
af9e8c786d | ||
|
|
5ca3baac95 | ||
|
|
878c53ee30 | ||
|
|
eb7e608d0b | ||
|
|
ef6a9e7727 | ||
|
|
2edc14a695 | ||
|
|
bc94adef38 | ||
|
|
dd8031519c | ||
|
|
b0412b4f1f | ||
|
|
7e94e25d49 | ||
|
|
068906cd0f | ||
|
|
e24c2256af | ||
|
|
d78e823ce2 | ||
|
|
8e46be5900 | ||
|
|
2e73741825 | ||
|
|
022b6da9c9 | ||
|
|
622cef048c |
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+3760
-3760
File diff suppressed because it is too large
Load Diff
@@ -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="mailto:max.c.lv@gmail.com">max.c.lv@gmail.com</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="mailto:max.c.lv@gmail.com">max.c.lv@gmail.com</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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
Submodule src/main/jni/shadowsocks-libev updated: 31a88839d0...3b4af24fec
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+1
-3
@@ -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"/>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+7
-8
@@ -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
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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 */
|
||||
|
||||
+2
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user