Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
450bde5305 | ||
|
|
d51bf46413 | ||
|
|
a69183cc0f | ||
|
|
62a0bb59c7 | ||
|
|
4eb77c8854 | ||
|
|
839765b724 | ||
|
|
a3f57d2557 | ||
|
|
f5247d627d | ||
|
|
f19f8d8ffa | ||
|
|
adaca76b12 | ||
|
|
010fd6158f | ||
|
|
8b57b53788 | ||
|
|
1db2d81aca | ||
|
|
258cb0c9c9 | ||
|
|
af057d0740 | ||
|
|
52499f5c2d | ||
|
|
230a018326 | ||
|
|
f6f4f45f62 | ||
|
|
f950037003 | ||
|
|
d8179b5b17 | ||
|
|
d701fb4cee | ||
|
|
5549dfd20a | ||
|
|
6978b921bb | ||
|
|
ac28b83a0b | ||
|
|
422924f3f4 | ||
|
|
e1ee53567a | ||
|
|
9c36f84b40 | ||
|
|
143afb052d | ||
|
|
27fc27346c | ||
|
|
02c8be58e3 | ||
|
|
2e1564b7b9 | ||
|
|
250373b92a | ||
|
|
dc439b0802 | ||
|
|
26b6853061 | ||
|
|
a84070c2d3 | ||
|
|
49e30e7b09 | ||
|
|
398db4f407 | ||
|
|
8208ac1510 | ||
|
|
5542f463a6 | ||
|
|
1e750e45e9 | ||
|
|
574f80aefd | ||
|
|
424e1bd1bc | ||
|
|
bbf52c4f1d | ||
|
|
0acfaa12c0 | ||
|
|
1ec80175ea | ||
|
|
2b985686a4 | ||
|
|
095f4831db | ||
|
|
ba75b7af75 | ||
|
|
d5dc3a8379 | ||
|
|
89ddc2f7c9 | ||
|
|
f721b57e00 | ||
|
|
3a785ab75e | ||
|
|
b68689f731 | ||
|
|
cf2f17dfff | ||
|
|
a64eb8ac58 | ||
|
|
9ab9230960 | ||
|
|
8f6357d9cf | ||
|
|
73462b08af | ||
|
|
6d07c03bc7 | ||
|
|
953f59ea43 | ||
|
|
6df5179734 | ||
|
|
2f312486a1 | ||
|
|
88d5c01cb0 | ||
|
|
2b05da3aff | ||
|
|
c5a98a9b12 | ||
|
|
5633ae7b21 | ||
|
|
92af48f528 | ||
|
|
6ca3e8abda | ||
|
|
3a935b3274 | ||
|
|
ea848f3b71 | ||
|
|
53af99eb98 | ||
|
|
9617418e70 | ||
|
|
c09a96a203 | ||
|
|
1ad08c6dba |
@@ -5,7 +5,7 @@ jdk:
|
||||
|
||||
env:
|
||||
global:
|
||||
- NDK_VERSION=r14
|
||||
- NDK_VERSION=r15b
|
||||
- NDK_CCACHE=ccache
|
||||
- GOROOT_BOOTSTRAP=$GOROOT
|
||||
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
|
||||
@@ -13,7 +13,7 @@ env:
|
||||
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
|
||||
|
||||
scala:
|
||||
- 2.11.8
|
||||
- 2.11.11
|
||||
|
||||
before_cache:
|
||||
- find $HOME/.sbt -name "*.lock" | xargs rm
|
||||
@@ -27,7 +27,7 @@ cache:
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- build-tools-25.0.2
|
||||
- build-tools-26.0.0
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
||||
* SBT 0.13.0+
|
||||
* Go 1.4+
|
||||
* Android SDK
|
||||
- Build Tools 25+
|
||||
- Build Tools 26+
|
||||
- Android Support Repository and Google Repository (see `build.sbt` for version)
|
||||
- Android NDK r14+
|
||||
- Android NDK r15+
|
||||
|
||||
### BUILD
|
||||
|
||||
@@ -47,13 +47,13 @@ Translators can go to [POEditor](https://poeditor.com/join/project/u5VHO9vhSf) t
|
||||
<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>libevent: <a href="https://github.com/shadowsocks/libevent/blob/master/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>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/libev/LICENSE">GPLv2</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
|
||||
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
|
||||
<li>libudns: <a href="https://github.com/shadowsocks/libudns/blob/master/COPYING.LGPL">LGPL</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
lazy val commonSettings = Seq(
|
||||
scalaVersion := "2.11.9",
|
||||
scalaVersion := "2.11.11",
|
||||
dexMaxHeap := "4g",
|
||||
|
||||
organization := "com.github.shadowsocks",
|
||||
|
||||
platformTarget := "android-25",
|
||||
platformTarget := "android-26",
|
||||
|
||||
compileOrder := CompileOrder.JavaThenScala,
|
||||
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
|
||||
scalacOptions ++= "-target:jvm-1.7" :: "-Xexperimental" :: Nil,
|
||||
ndkArgs := "-j" :: java.lang.Runtime.getRuntime.availableProcessors.toString :: Nil,
|
||||
ndkAbiFilter := Seq("armeabi-v7a", "arm64-v8a", "x86"),
|
||||
|
||||
proguardVersion := "5.3.3",
|
||||
proguardCache := Seq(),
|
||||
@@ -17,10 +18,12 @@ lazy val commonSettings = Seq(
|
||||
shrinkResources := true,
|
||||
typedResources := false,
|
||||
|
||||
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW")
|
||||
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW"),
|
||||
|
||||
resolvers += "google" at "https://maven.google.com"
|
||||
)
|
||||
|
||||
val supportLibsVersion = "25.3.1"
|
||||
val supportLibsVersion = "26.0.0"
|
||||
lazy val root = Project(id = "shadowsocks-android", base = file("."))
|
||||
.settings(commonSettings)
|
||||
.aggregate(plugin, mobile)
|
||||
|
||||
@@ -4,29 +4,30 @@ enablePlugins(AndroidGms)
|
||||
android.useSupportVectors
|
||||
|
||||
name := "shadowsocks"
|
||||
version := "4.1.6"
|
||||
versionCode := Some(186)
|
||||
version := "4.2.2"
|
||||
versionCode := Some(192)
|
||||
|
||||
proguardOptions ++=
|
||||
"-dontwarn com.evernote.android.job.gcm.**" ::
|
||||
"-dontwarn com.evernote.android.job.util.GcmAvailableHelper" ::
|
||||
"-dontwarn com.google.android.gms.internal.**" ::
|
||||
"-dontwarn com.j256.ormlite.**" ::
|
||||
"-dontwarn okhttp3.**" ::
|
||||
"-dontwarn okio.**" ::
|
||||
"-dontwarn org.xbill.**" ::
|
||||
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
|
||||
"-dontwarn com.evernote.android.job.gcm.**" ::
|
||||
"-dontwarn com.evernote.android.job.util.GcmAvailableHelper" ::
|
||||
"-keep public class com.evernote.android.job.v21.PlatformJobService" ::
|
||||
"-keep public class com.evernote.android.job.v14.PlatformAlarmService" ::
|
||||
"-keep public class com.evernote.android.job.v14.PlatformAlarmReceiver" ::
|
||||
"-keep public class com.evernote.android.job.JobBootReceiver" ::
|
||||
"-keep public class com.evernote.android.job.JobRescheduleService" ::
|
||||
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
|
||||
Nil
|
||||
|
||||
val playServicesVersion = "10.2.1"
|
||||
val playServicesVersion = "11.0.4"
|
||||
resolvers += Resolver.jcenterRepo
|
||||
libraryDependencies ++=
|
||||
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
|
||||
"com.evernote" % "android-job" % "1.1.9" ::
|
||||
"com.evernote" % "android-job" % "1.2.0-alpha3" ::
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
|
||||
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
|
||||
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
|
||||
@@ -34,17 +35,31 @@ libraryDependencies ++=
|
||||
"com.google.firebase" % "firebase-config" % playServicesVersion ::
|
||||
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
|
||||
"com.mikepenz" % "crossfader" % "1.5.0" ::
|
||||
"com.mikepenz" % "fastadapter" % "2.5.2" ::
|
||||
"com.mikepenz" % "iconics-core" % "2.8.3" ::
|
||||
"com.mikepenz" % "materialdrawer" % "5.9.0" ::
|
||||
"com.mikepenz" % "materialize" % "1.0.1" ::
|
||||
"com.squareup.okhttp3" % "okhttp" % "3.7.0" ::
|
||||
"com.mikepenz" % "fastadapter" % "2.6.2" ::
|
||||
"com.mikepenz" % "iconics-core" % "2.8.9" ::
|
||||
"com.mikepenz" % "materialdrawer" % "5.9.4" ::
|
||||
"com.mikepenz" % "materialize" % "1.0.3" ::
|
||||
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
|
||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
|
||||
"dnsjava" % "dnsjava" % "2.1.8" ::
|
||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
|
||||
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
|
||||
"net.glxn.qrgen" % "android" % "2.0" ::
|
||||
Nil
|
||||
|
||||
packagingOptions := PackagingOptions(excludes =
|
||||
"META-INF/maven/com.squareup.okio/okio/pom.properties" ::
|
||||
"META-INF/maven/com.squareup.okio/okio/pom.xml" ::
|
||||
"META-INF/maven/com.squareup.okhttp3/okhttp/pom.properties" ::
|
||||
"META-INF/maven/com.squareup.okhttp3/okhttp/pom.xml" ::
|
||||
Nil)
|
||||
|
||||
lazy val goClean = TaskKey[Unit]("go-clean", "Clean go build dependencies")
|
||||
goClean := {
|
||||
IO.delete(baseDirectory(base => base / "src/overture/.deps").value)
|
||||
IO.delete(baseDirectory(base => base / "src/main/jni/overture").value)
|
||||
}
|
||||
|
||||
lazy val goBuild = TaskKey[Unit]("go-build", "Build go and overture")
|
||||
goBuild := {
|
||||
Process(Seq("mobile/src/overture/make.bash", minSdkVersion.value)) ! streams.value.log match {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
@@ -11,16 +12,22 @@
|
||||
android:required="false"/>
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="19"
|
||||
android:targetSdkVersion="25"/>
|
||||
android:targetSdkVersion="26"/>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="true" />
|
||||
|
||||
<application
|
||||
android:name=".ShadowsocksApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:backupAgent=".ShadowsocksBackupAgent"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:banner="@drawable/ic_start_connected">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.version"
|
||||
@@ -75,6 +82,12 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity
|
||||
android:name=".ScannerActivity"
|
||||
android:label="@string/add_profile_methods_scan_qr_code"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:excludeFromRecents="true"/>
|
||||
|
||||
<activity android:name=".TaskerActivity"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<intent-filter>
|
||||
@@ -87,18 +100,14 @@
|
||||
android:label="@string/quick_toggle"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:taskAffinity=""
|
||||
android:process=":bg"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".ShadowsocksRunnerService"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".ShadowsocksNatService"
|
||||
android:process=":bg"
|
||||
@@ -125,13 +134,13 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".BootReceiver" android:enabled="false">
|
||||
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver">
|
||||
<receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
@@ -150,9 +159,6 @@
|
||||
android:process=":bg"/>
|
||||
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
||||
android:process=":bg"/>
|
||||
|
||||
<!-- Strip useless components -->
|
||||
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" tools:node="remove"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -10,7 +10,4 @@ interface IShadowsocksService {
|
||||
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
|
||||
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
|
||||
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
|
||||
|
||||
oneway void use(in int profileId);
|
||||
void useSync(in int profileId);
|
||||
}
|
||||
|
||||
@@ -291,13 +291,13 @@ along with this program. If not, see <a href="http://www.gnu.org/licenses/">http
|
||||
<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>libevent: <a href="https://github.com/shadowsocks/libevent/blob/master/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>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/libev/LICENSE">GPLv2</a></li>
|
||||
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
|
||||
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
|
||||
<li>libudns: <a href="https://github.com/shadowsocks/libudns/blob/master/COPYING.LGPL">LGPL</a></li>
|
||||
</ul>
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2010 Brave New Software Project, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkProperties;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class Dns {
|
||||
public static String getDnsResolver(Context context) throws Exception {
|
||||
Collection<InetAddress> dnsResolvers = getDnsResolvers(context);
|
||||
if (dnsResolvers.isEmpty()) {
|
||||
throw new Exception("Couldn't find an active DNS resolver");
|
||||
}
|
||||
String dnsResolver = dnsResolvers.iterator().next().toString();
|
||||
if (dnsResolver.startsWith("/")) {
|
||||
dnsResolver = dnsResolver.substring(1);
|
||||
}
|
||||
return dnsResolver;
|
||||
}
|
||||
|
||||
private static Collection<InetAddress> getDnsResolvers(Context context) throws Exception {
|
||||
ArrayList<InetAddress> addresses = new ArrayList<InetAddress>();
|
||||
ConnectivityManager connectivityManager =
|
||||
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
Class<?> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
|
||||
Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties");
|
||||
Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
|
||||
if (linkProperties != null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses");
|
||||
Collection<?> dnses = (Collection<?>)getDnsesMethod.invoke(linkProperties);
|
||||
for (Object dns : dnses) {
|
||||
addresses.add((InetAddress)dns);
|
||||
}
|
||||
} else {
|
||||
addresses.addAll(((LinkProperties) linkProperties).getDnsServers());
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ SHADOWSOCKS_SOURCES := local.c \
|
||||
LOCAL_MODULE := ss-local
|
||||
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_LOCAL \
|
||||
-DUSE_CRYPTO_MBEDTLS -DANDROID -DHAVE_CONFIG_H \
|
||||
-DUSE_CRYPTO_MBEDTLS -DHAVE_CONFIG_H \
|
||||
-DCONNECT_IN_PROGRESS=EINPROGRESS \
|
||||
-I$(LOCAL_PATH)/include/shadowsocks-libev \
|
||||
-I$(LOCAL_PATH)/include \
|
||||
@@ -294,7 +294,7 @@ SHADOWSOCKS_SOURCES := tunnel.c \
|
||||
LOCAL_MODULE := ss-tunnel
|
||||
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
|
||||
LOCAL_CFLAGS := -Wall -O2 -fno-strict-aliasing -DMODULE_TUNNEL \
|
||||
-DUSE_CRYPTO_MBEDTLS -DANDROID -DHAVE_CONFIG_H -DSSTUNNEL_JNI \
|
||||
-DUSE_CRYPTO_MBEDTLS -DHAVE_CONFIG_H -DSSTUNNEL_JNI \
|
||||
-DCONNECT_IN_PROGRESS=EINPROGRESS \
|
||||
-I$(LOCAL_PATH)/libancillary \
|
||||
-I$(LOCAL_PATH)/include \
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gradient xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:startColor="#19000000"
|
||||
android:endColor="#00000000"
|
||||
android:startX="46.15"
|
||||
android:endX="108"
|
||||
android:startY="40.34"
|
||||
android:endY="102.18"
|
||||
android:type="linear"/>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector android:height="108dp" android:viewportHeight="108.0"
|
||||
android:viewportWidth="108.0" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@color/ic_launcher_foreground_shadow"
|
||||
android:pathData="M70.94,35.7l37.06,36.3l0,36l-23.92,0l-53.02,-52.57l39.88,-19.73l0,0"/>
|
||||
<path android:fillAlpha="0.79" android:fillColor="#fff"
|
||||
android:pathData="M70.94,35.7l-6.94,30.53l-15.56,-5l13.74,-16.38l-18.31,15.4l-12.81,-4.82l39.88,-19.73z"/>
|
||||
<path android:fillAlpha="0.79" android:fillColor="#fff"
|
||||
android:pathData="M48.79,64.37l4.96,1.6l-4.94,6.73l-0.02,-8.33z"/>
|
||||
<path android:fillAlpha="0.79" android:fillColor="#fff"
|
||||
android:pathData="M70.94,35.7l-6.94,30.53l-15.56,-5l13.74,-16.38l-18.31,15.4l-12.81,-4.82l39.88,-19.73z"/>
|
||||
<path android:fillAlpha="0.79" android:fillColor="#fff"
|
||||
android:pathData="M48.79,64.37l4.96,1.6l-4.94,6.73l-0.02,-8.33z"/>
|
||||
</vector>
|
||||
@@ -37,7 +37,7 @@
|
||||
<android.support.v7.widget.AppCompatImageView android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:padding="8dp"
|
||||
android:layout_gravity="top"
|
||||
app:srcCompat="@drawable/ic_social_share"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
@@ -47,7 +47,7 @@
|
||||
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:padding="8dp"
|
||||
android:layout_gravity="top"
|
||||
app:srcCompat="@drawable/ic_image_edit"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
@@ -91,6 +91,7 @@
|
||||
<string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手動設定"</string>
|
||||
<string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string>
|
||||
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"削除済み"</item>
|
||||
</plurals>
|
||||
@@ -122,7 +123,7 @@
|
||||
<string name="route_entry_bypass_lan_chn">"LAN 及び中国本土のアドレスを迂回する"</string>
|
||||
<string name="route_entry_chinalist">"中国本土のアドレス以外を迂回する"</string>
|
||||
<string name="proxied_apps_summary_v21">"選択したアプリにプロキシを設定する"</string>
|
||||
<string name="acl_rule_templates_generic">"サブネット/ホスト名 PCRE パターン"</string>
|
||||
<string name="acl_rule_templates_generic">"URL/サブネット/ホスト名 PCRE パターン"</string>
|
||||
<string name="acl_rule_templates_domain">"ドメイン及び全てのサブドメイン"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
@@ -132,4 +133,4 @@
|
||||
<string name="plugin_unknown">"不明なプラグイン"</string>
|
||||
<string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string>
|
||||
<string name="profile_plugin">"プラグイン"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<resources>
|
||||
<string name="quick_toggle">"켜기/끄기"</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="misc_cat">"기타"</string>
|
||||
<!-- misc -->
|
||||
<string name="profile">"프로필"</string>
|
||||
<string name="profile_summary">"다른 프로필로 전환하거나 새 프로필 추가"</string>
|
||||
<string name="nat">"NAT 모드 (지원 중단)"</string>
|
||||
@@ -45,4 +44,5 @@
|
||||
<string name="tcp_fastopen_summary">"루트 권한이 필요합니다"</string>
|
||||
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s < 3.7.1"</string>
|
||||
<string name="udp_dns">"DNS 포워딩"</string>
|
||||
</resources>
|
||||
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,22 +2,28 @@
|
||||
<resources>
|
||||
<string name="quick_toggle">"Подключение"</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="misc_cat">"Разное"</string>
|
||||
<!-- misc -->
|
||||
<string name="profile">"Профиль"</string>
|
||||
<string name="profile_summary">"Переключить на другой профиль или добавить новые"</string>
|
||||
<string name="nat">"Режим NAT (не рекомендуется)"</string>
|
||||
<string name="nat_summary">"Использовать режим NAT вместо VPN. Требует ROOT прав."</string>
|
||||
<string name="stat_summary">"Отправлено: \t\t\t\t\t%3$s\t↑\t%1$s/s
|
||||
<string name="remote_dns">"Удалённый DNS"</string>
|
||||
<string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s/s
|
||||
Получено: \t%4$s\t↓\t%2$s/s"</string>
|
||||
<string name="stat_profiles">"
|
||||
%1$s↑\t%2$s↓"</string>
|
||||
<string name="traffic_summary">"%1$s/s↑\t%2$s/s↓"</string>
|
||||
<string name="connection_test_pending">"Проверить подключение"</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="one">"байт"</item>
|
||||
<item quantity="few">"байта"</item>
|
||||
<item quantity="many">"байт"</item>
|
||||
<item quantity="other">"байт"</item>
|
||||
</plurals>
|
||||
<string name="connection_test_pending">"Проверить соединение"</string>
|
||||
<string name="connection_test_testing">"Проверка…"</string>
|
||||
<string name="connection_test_available">"Успешно: %dms задержка"</string>
|
||||
<string name="connection_test_error">"Сбой проверки интернет подключения: %s"</string>
|
||||
<string name="connection_test_fail">"Интернет Недоступен"</string>
|
||||
<string name="connection_test_available">"Успешно: задержка %d мс"</string>
|
||||
<string name="connection_test_error">"Сбой проверки интернет-соединения: %s"</string>
|
||||
<string name="connection_test_fail">"Интернет недоступен"</string>
|
||||
<string name="connection_test_error_status_code">"Код ошибки: #%d"</string>
|
||||
|
||||
<!-- proxy category -->
|
||||
@@ -30,7 +36,7 @@
|
||||
|
||||
<!-- feature category -->
|
||||
<string name="ipv6">"IPv6 Маршрут"</string>
|
||||
<string name="ipv6_summary">"Перенаправлять IPv6 траффик на удалённый сервер"</string>
|
||||
<string name="ipv6_summary">"Перенаправлять трафик IPv6 на удалённый сервер"</string>
|
||||
<string name="route_list">"Маршрут"</string>
|
||||
<string name="route_entry_gfwlist">"Список GFW"</string>
|
||||
<string name="proxied_apps">"Прокси для выбранных приложений"</string>
|
||||
@@ -42,22 +48,23 @@
|
||||
<string name="auto_connect_summary">"Запускать Shadowsocks при включении"</string>
|
||||
<string name="tcp_fastopen_summary">"Переключение требует наличия ROOT прав"</string>
|
||||
<string name="tcp_fastopen_summary_unsupported">"Неподдерживаемая версия ядра: %s < 3.7.1"</string>
|
||||
<string name="udp_dns">"Проброс DNS"</string>
|
||||
<string name="udp_dns_summary">"Пробрасывать DNS пакеты на удалённый сервер"</string>
|
||||
<string name="udp_dns">"Перенаправление DNS"</string>
|
||||
<string name="udp_dns_summary">"Перенаправлять все DNS-запросы на удалённый сервер"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">"Shadowsocks запущен."</string>
|
||||
<string name="invalid_server">"Неправильное имя сервера"</string>
|
||||
<string name="service_failed">"Ошибка при подключении к удалённому серверу"</string>
|
||||
<string name="nat_deprecated">"ВНИМАНИЕ:режим NAT не работает в Android 5.0"</string>
|
||||
<string name="nat_deprecated">"ВНИМАНИЕ: режим NAT не рекомендуется, начиная с Android 5.0"</string>
|
||||
<string name="nat_no_root">"Режим NAT требует наличия ROOT прав"</string>
|
||||
<string name="switch_to_vpn">"Переключить на режим VPN"</string>
|
||||
<string name="stop">"Остановить"</string>
|
||||
<string name="stopping">"Останавливается…"</string>
|
||||
<string name="reboot_required">"Сбой запуска VPN сервиса. Требуется перезагрузка Вашего устройства."</string>
|
||||
<string name="reboot_required">"Не удалось запустить службу VPN. Возможно, требуется перезагрузить ваше устройство."</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">"Закрыть"</string>
|
||||
<string name="profile_empty">"Пожалуйста, выберите профиль"</string>
|
||||
<string name="proxy_empty">"Прокси/Пароль не должны быть пустыми"</string>
|
||||
<string name="connect">"Подключить"</string>
|
||||
<string name="recovering">"Сброс…"</string>
|
||||
@@ -69,6 +76,7 @@
|
||||
<string name="recovery">"Сброс"</string>
|
||||
<string name="about">"О приложении"</string>
|
||||
<string name="about_title">"Shadowsocks %s"</string>
|
||||
<string name="edit">"Изменить"</string>
|
||||
<string name="share">"Поделиться"</string>
|
||||
<string name="add_profile">"Добавить Профиль"</string>
|
||||
<string name="action_apply_all">"Применить настройки для всех профилей"</string>
|
||||
@@ -78,19 +86,27 @@
|
||||
<string name="action_export_err">"Сбой экспорта."</string>
|
||||
<string name="action_import_msg">"Успешный импорт!"</string>
|
||||
<string name="action_import_err">"Сбой импорта."</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_config">"Настройка профиля"</string>
|
||||
<string name="delete">"Удалить"</string>
|
||||
<string name="delete_confirm_prompt">"Вы уверены, что хотите удалить этот профиль?"</string>
|
||||
<string name="share_qr_nfc">"QR-код/NFC"</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_methods_scan_qr_code">"Сканировать QR-код"</string>
|
||||
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
|
||||
<string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string>
|
||||
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">"Удалён"</item>
|
||||
<item quantity="few">"%d элементы удалены"</item>
|
||||
<item quantity="many">"%d элементы удалены"</item>
|
||||
<item quantity="other">"%d элементы удалены"</item>
|
||||
<item quantity="one">"Удалено"</item>
|
||||
<item quantity="few">"Удалено %d элемента"</item>
|
||||
<item quantity="many">"Удалено %d элементов"</item>
|
||||
<item quantity="other">"Удалено %d элементов"</item>
|
||||
</plurals>
|
||||
<string name="undo">"Вернуть"</string>
|
||||
|
||||
<!-- tasker -->
|
||||
<string name="toggle_service_state">"Запустить сервисStart the service"</string>
|
||||
<string name="toggle_service_state">"Запустить службу"</string>
|
||||
<string name="start_service_default">"Подключиться к текущему серверу"</string>
|
||||
<string name="start_service">"Подключиться к %s"</string>
|
||||
<string name="stop_service">"Переключить на %s"</string>
|
||||
@@ -99,10 +115,30 @@
|
||||
<!-- status -->
|
||||
<string name="sent">"Отправлено:"</string>
|
||||
<string name="received">"Получено:"</string>
|
||||
<string name="connecting">"Соединение..."</string>
|
||||
<string name="vpn_connected">"Подключено, нажмите для проверки соединения"</string>
|
||||
<string name="nat_connected">"Подключено"</string>
|
||||
<string name="not_connected">"Не подключено"</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">"Пользовательские правила"</string>
|
||||
<string name="action_selection">"Выделение..."</string>
|
||||
<string name="action_add_rule">"Добавить правило..."</string>
|
||||
<string name="edit_rule">"Редактировать правило"</string>
|
||||
<string name="route_entry_all">"Все"</string>
|
||||
<string name="route_entry_bypass_lan">"Обход LAN"</string>
|
||||
<string name="route_entry_bypass_chn">"Обходной Китай"</string>
|
||||
<string name="route_entry_bypass_lan_chn">"Обход LAN и Китай"</string>
|
||||
<string name="route_entry_bypass_lan">"Все, кроме LAN"</string>
|
||||
<string name="route_entry_bypass_chn">"Все, кроме Китая"</string>
|
||||
<string name="route_entry_bypass_lan_chn">"Все, кроме LAN и Китая"</string>
|
||||
<string name="route_entry_chinalist">"Список Китай"</string>
|
||||
<string name="proxied_apps_summary_v21">"Установить прокси для выбранных приложений"</string>
|
||||
</resources>
|
||||
<string name="acl_rule_templates_generic">"URL/Подсеть/Регулярное выражение (PCRE) имени хоста"</string>
|
||||
<string name="acl_rule_templates_domain">"Доменное имя и все его поддомены"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
<string name="plugin">"Плагин"</string>
|
||||
<string name="plugin_configure">"Настроить..."</string>
|
||||
<string name="plugin_disabled">"Отключён"</string>
|
||||
<string name="plugin_unknown">"Неизвестный плагин %s"</string>
|
||||
<string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string>
|
||||
<string name="profile_plugin">"Плагин: %s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
<string name="add_profile_methods_scan_qr_code">"扫描二维码"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手动设置"</string>
|
||||
<string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string>
|
||||
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"已删除 %d 项"</item>
|
||||
</plurals>
|
||||
@@ -123,7 +124,7 @@
|
||||
<string name="route_entry_bypass_lan_chn">"绕过局域网及中国大陆地址"</string>
|
||||
<string name="route_entry_chinalist">"仅代理中国大陆地址"</string>
|
||||
<string name="proxied_apps_summary_v21">"为应用程序分别设置代理"</string>
|
||||
<string name="acl_rule_templates_generic">"子网/域名 PCRE 正则表达式"</string>
|
||||
<string name="acl_rule_templates_generic">"URL/子网/域名 PCRE 正则表达式"</string>
|
||||
<string name="acl_rule_templates_domain">"域名及其子域名"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
@@ -133,4 +134,4 @@
|
||||
<string name="plugin_unknown">"未知插件 %s"</string>
|
||||
<string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string>
|
||||
<string name="profile_plugin">"插件:%s"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<resources>
|
||||
<string name="quick_toggle">"切換"</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="misc_cat">"其他"</string>
|
||||
<!-- misc -->
|
||||
<string name="profile">"設定檔"</string>
|
||||
<string name="profile_summary">"切換至其他設定檔或新增新設定檔"</string>
|
||||
<string name="nat">"NAT 模式 (已過時)"</string>
|
||||
@@ -91,6 +90,7 @@
|
||||
<string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手動設定"</string>
|
||||
<string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string>
|
||||
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"已移除 %d 項"</item>
|
||||
</plurals>
|
||||
@@ -122,6 +122,14 @@
|
||||
<string name="route_entry_bypass_lan_chn">"略過區域網路及中國大陸"</string>
|
||||
<string name="route_entry_chinalist">"China List"</string>
|
||||
<string name="proxied_apps_summary_v21">"為已選擇的應用程式設定 Proxy"</string>
|
||||
<string name="acl_rule_templates_generic">"子網路/主機名稱 PCRE 模式"</string>
|
||||
<string name="acl_rule_templates_generic">"URL/子網路/主機名稱 PCRE 模式"</string>
|
||||
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
|
||||
</resources>
|
||||
|
||||
<!-- plugin -->
|
||||
<string name="plugin">"插件"</string>
|
||||
<string name="plugin_configure">"配置..."</string>
|
||||
<string name="plugin_disabled">"未啟用"</string>
|
||||
<string name="plugin_unknown">"未知插件 %s"</string>
|
||||
<string name="plugin_untrusted">"警告:該插件似乎不是來自一個已知的可信任源。"</string>
|
||||
<string name="profile_plugin">"插件: %s"</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#7488A1</color>
|
||||
</resources>
|
||||
@@ -56,6 +56,8 @@
|
||||
<string name="udp_dns_summary">Forward all DNS requests to remote</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="service_vpn">VPN Service</string>
|
||||
<string name="service_nat">NAT Service</string>
|
||||
<string name="forward_success">Shadowsocks started.</string>
|
||||
<string name="invalid_server">Invalid server name</string>
|
||||
<string name="service_failed">Failed to connect the remote server</string>
|
||||
@@ -104,6 +106,7 @@
|
||||
<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_not_installed">Please install any ZXing-compliant QR code scanning app.</string>
|
||||
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">Removed</item>
|
||||
<item quantity="other">%d items removed</item>
|
||||
@@ -129,7 +132,7 @@
|
||||
<string name="custom_rules">Custom rules</string>
|
||||
<string name="action_selection">Selection…</string>
|
||||
<string name="action_add_rule">Add rule(s)…</string>
|
||||
<string name="acl_rule_templates_generic">Subnet/Hostname PCRE pattern</string>
|
||||
<string name="acl_rule_templates_generic">URL, Subnet or Hostname PCRE pattern</string>
|
||||
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
|
||||
<string name="edit_rule">Edit rule</string>
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false"/>
|
||||
</network-security-config>
|
||||
@@ -34,7 +34,7 @@ class AboutFragment extends ToolbarFragment {
|
||||
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
|
||||
val web = view.findViewById(R.id.web_view).asInstanceOf[WebView]
|
||||
val web = view.findViewById[WebView](R.id.web_view)
|
||||
web.loadUrl("file:///android_asset/pages/about.html")
|
||||
web.setWebViewClient(new WebViewClient() {
|
||||
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
|
||||
|
||||
@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
import AppManager._
|
||||
|
||||
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
||||
private val icon = itemView.findViewById(R.id.itemicon).asInstanceOf[ImageView]
|
||||
private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
|
||||
private val icon = itemView.findViewById[ImageView](R.id.itemicon)
|
||||
private val check = itemView.findViewById[Switch](R.id.itemcheck)
|
||||
private var item: ProxiedApp = _
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
proxiedApps.add(item.packageName)
|
||||
check.setChecked(true)
|
||||
}
|
||||
if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
|
||||
if (!appsLoading.get) {
|
||||
app.dataStore.individual = proxiedApps.mkString("\n")
|
||||
app.dataStore.dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +121,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
private val appsLoading = new AtomicBoolean
|
||||
private var handler: Handler = _
|
||||
|
||||
private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) =
|
||||
private def initProxiedApps(str: String = app.dataStore.individual) =
|
||||
proxiedApps = str.split('\n').to[mutable.HashSet]
|
||||
|
||||
override def onDestroy() {
|
||||
@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
case R.id.action_apply_all =>
|
||||
app.profileManager.getAllProfiles match {
|
||||
case Some(profiles) =>
|
||||
val proxiedAppString = app.settings.getString(Key.individual, null)
|
||||
val proxiedAppString = app.dataStore.individual
|
||||
profiles.foreach(p => {
|
||||
p.individual = proxiedAppString
|
||||
app.profileManager.updateProfile(p)
|
||||
@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
||||
}
|
||||
return true
|
||||
case R.id.action_export =>
|
||||
val bypass = app.settings.getBoolean(Key.bypass, false)
|
||||
val proxiedAppString = app.settings.getString(Key.individual, null)
|
||||
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
|
||||
app.dataStore.bypass + "\n" + app.dataStore.individual))
|
||||
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
case R.id.action_import =>
|
||||
@@ -163,7 +164,8 @@ 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).putBoolean(Key.dirty, true).apply()
|
||||
app.dataStore.individual = apps
|
||||
app.dataStore.dirty = true
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
appListView.setVisibility(View.GONE)
|
||||
fastScroller.setVisibility(View.GONE)
|
||||
@@ -202,18 +204,23 @@ 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).putBoolean(Key.dirty, true).apply()
|
||||
if (!app.dataStore.proxyApps) {
|
||||
app.dataStore.proxyApps = true
|
||||
app.dataStore.dirty = true
|
||||
}
|
||||
findViewById(R.id.onSwitch).asInstanceOf[Switch]
|
||||
.setOnCheckedChangeListener((_, checked) => {
|
||||
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply()
|
||||
app.dataStore.proxyApps = checked
|
||||
app.dataStore.dirty = true
|
||||
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).putBoolean(Key.dirty, true).apply())
|
||||
bypassSwitch.setChecked(app.dataStore.bypass)
|
||||
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
|
||||
app.dataStore.bypass = checked
|
||||
app.dataStore.dirty = true
|
||||
})
|
||||
|
||||
initProxiedApps()
|
||||
loadingView = findViewById(R.id.loading)
|
||||
|
||||
@@ -21,15 +21,14 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import java.io.{File, IOException}
|
||||
import java.net.{Inet6Address, InetAddress}
|
||||
import java.util
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.{Timer, TimerTask}
|
||||
import java.net.InetAddress
|
||||
import java.net.Inet6Address
|
||||
|
||||
import android.app.Service
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.{Handler, IBinder, RemoteCallbackList}
|
||||
import android.content._
|
||||
import android.os.{Build, Handler, IBinder, RemoteCallbackList}
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
@@ -65,9 +64,12 @@ trait BaseService extends Service {
|
||||
lazy val handler = new Handler(getMainLooper)
|
||||
lazy val restartHanlder = new Handler(getMainLooper)
|
||||
|
||||
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
stopRunner(stopService = true)
|
||||
private var notification: ShadowsocksNotification = _
|
||||
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
|
||||
case Action.RELOAD => forceLoad()
|
||||
case _ =>
|
||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
stopRunner(stopService = true)
|
||||
}
|
||||
var closeReceiverRegistered: Boolean = _
|
||||
|
||||
@@ -103,20 +105,6 @@ trait BaseService extends Service {
|
||||
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
||||
callbacks.unregister(cb)
|
||||
}
|
||||
|
||||
override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else {
|
||||
val profile = app.profileManager.getProfile(profileId).orNull
|
||||
if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match {
|
||||
case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
|
||||
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
|
||||
stopRunner(stopService = false)
|
||||
startRunner(profile)
|
||||
}
|
||||
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
|
||||
}
|
||||
})
|
||||
|
||||
override def useSync(profileId: Int): Unit = use(profileId)
|
||||
}
|
||||
|
||||
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
||||
@@ -124,9 +112,18 @@ trait BaseService extends Service {
|
||||
false
|
||||
} else true
|
||||
|
||||
def connect() {
|
||||
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
|
||||
def forceLoad(): Unit = app.currentProfile.orNull match {
|
||||
case null => stopRunner(stopService = true, getString(R.string.profile_empty))
|
||||
case p => if (checkProfile(p)) state match {
|
||||
case State.STOPPED => startRunner()
|
||||
case State.CONNECTED =>
|
||||
stopRunner(stopService = false)
|
||||
startRunner()
|
||||
case s => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + s)
|
||||
}
|
||||
}
|
||||
|
||||
def connect() {
|
||||
if (profile.host == "198.199.101.152") {
|
||||
val client = new OkHttpClient.Builder()
|
||||
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
|
||||
@@ -154,42 +151,15 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
|
||||
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
|
||||
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true)
|
||||
|
||||
plugin = new PluginConfiguration(profile.plugin).selectedOptions
|
||||
pluginPath = PluginManager.init(plugin)
|
||||
}
|
||||
|
||||
def startRunner(profile: Profile) {
|
||||
this.profile = profile
|
||||
|
||||
startService(new Intent(this, getClass))
|
||||
TrafficMonitor.reset()
|
||||
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
|
||||
trafficMonitorThread.start()
|
||||
|
||||
if (!closeReceiverRegistered) {
|
||||
// register close receiver
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
registerReceiver(closeReceiver, filter)
|
||||
closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
app.track(getClass.getSimpleName, "start")
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
|
||||
Utils.ThrowableFuture(try connect() catch {
|
||||
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
|
||||
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
|
||||
case exc: Throwable =>
|
||||
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
|
||||
exc.printStackTrace()
|
||||
app.track(exc)
|
||||
})
|
||||
}
|
||||
def createNotification(): ShadowsocksNotification
|
||||
def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
|
||||
else startService(new Intent(this, getClass))
|
||||
|
||||
def stopRunner(stopService: Boolean, msg: String = null) {
|
||||
// clean up recevier
|
||||
@@ -198,6 +168,8 @@ trait BaseService extends Service {
|
||||
closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
if (notification != null) notification.destroy()
|
||||
|
||||
// Make sure update total traffic when stopping the runner
|
||||
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
|
||||
@@ -276,7 +248,49 @@ trait BaseService extends Service {
|
||||
}
|
||||
|
||||
// Service of shadowsocks should always be started explicitly
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
|
||||
state match {
|
||||
case State.STOPPED | State.IDLE =>
|
||||
case _ => return Service.START_NOT_STICKY // ignore request
|
||||
}
|
||||
|
||||
profile = app.currentProfile.orNull
|
||||
if (profile == null) {
|
||||
stopRunner(stopService = true)
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
|
||||
|
||||
TrafficMonitor.reset()
|
||||
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
|
||||
trafficMonitorThread.start()
|
||||
|
||||
if (!closeReceiverRegistered) {
|
||||
// register close receiver
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(Action.RELOAD)
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
registerReceiver(closeReceiver, filter)
|
||||
closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
notification = createNotification()
|
||||
app.track(getClass.getSimpleName, "start")
|
||||
|
||||
changeState(State.CONNECTING)
|
||||
|
||||
Utils.ThrowableFuture(try connect() catch {
|
||||
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
|
||||
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
|
||||
case exc: Throwable =>
|
||||
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
|
||||
exc.printStackTrace()
|
||||
app.track(exc)
|
||||
})
|
||||
Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
protected def changeState(s: Int, msg: String = null) {
|
||||
val handler = new Handler(getMainLooper)
|
||||
@@ -317,7 +331,7 @@ trait BaseService extends Service {
|
||||
|
||||
protected final def buildOvertureConfig(file: String): String = {
|
||||
val config = new JSONObject()
|
||||
.put("BindAddress", ":" + (profile.localPort + 53))
|
||||
.put("BindAddress", "127.0.0.1:" + (profile.localPort + 53))
|
||||
.put("RedirectIPv6Record", true)
|
||||
.put("DomainBase64Decode", true)
|
||||
.put("HostsFile", "hosts")
|
||||
@@ -341,12 +355,21 @@ trait BaseService extends Service {
|
||||
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
|
||||
case (dns, i) => makeDns("UserDef-" + i, dns.trim)
|
||||
})
|
||||
val localDns = new JSONArray(Array(
|
||||
makeDns("Primary-1", "119.29.29.29", edns = false),
|
||||
makeDns("Primary-2", "114.114.114.114", edns = false)
|
||||
))
|
||||
|
||||
try {
|
||||
val localLinkDns = com.github.shadowsocks.utils.Dns.getDnsResolver(this)
|
||||
localDns.put(makeDns("Primary-3", localLinkDns, edns = false))
|
||||
} catch {
|
||||
case _: Exception => // Ignore
|
||||
}
|
||||
|
||||
profile.route match {
|
||||
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
|
||||
.put("PrimaryDNS", new JSONArray(Array(
|
||||
makeDns("Primary-1", "119.29.29.29", edns = false),
|
||||
makeDns("Primary-2", "114.114.114.114", edns = false)
|
||||
)))
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
.put("IPNetworkFile", "china_ip_list.txt")
|
||||
.put("DomainFile", "gfwlist.txt")
|
||||
|
||||
@@ -20,27 +20,24 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import be.mygod.preference.PreferenceFragment
|
||||
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
|
||||
|
||||
class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
|
||||
class GlobalConfigFragment extends PreferenceFragment with OnPreferenceDataStoreChangeListener {
|
||||
override def onCreatePreferences(bundle: Bundle, key: String) {
|
||||
getPreferenceManager.setPreferenceDataStore(app.dataStore)
|
||||
addPreferencesFromResource(R.xml.pref_global)
|
||||
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
|
||||
switch.setOnPreferenceChangeListener((_, value) => {
|
||||
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
|
||||
true
|
||||
})
|
||||
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
|
||||
BootReceiver.setEnabled(getActivity, true)
|
||||
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
|
||||
}
|
||||
switch.setChecked(BootReceiver.getEnabled(getActivity))
|
||||
|
||||
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
|
||||
@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha
|
||||
tfo.setEnabled(false)
|
||||
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
|
||||
}
|
||||
app.settings.registerOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
override def onDestroy() {
|
||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.unregisterChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
|
||||
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false))
|
||||
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
|
||||
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false))
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.immutable.Stream
|
||||
|
||||
class StreamLogger(is: InputStream, tag: String) extends Thread {
|
||||
class StreamLogger(is: InputStream, tag: String, logger: (String, String) => Int) extends Thread {
|
||||
override def run(): Unit = autoClose(new BufferedReader(new InputStreamReader(is)))(br =>
|
||||
try Stream.continually(br.readLine()).takeWhile(_ != null).foreach(Log.i(tag, _)) catch {
|
||||
try Stream.continually(br.readLine()).takeWhile(_ != null).foreach(logger(tag, _)) catch {
|
||||
case _: IOException =>
|
||||
})
|
||||
}
|
||||
@@ -68,8 +68,8 @@ class GuardedProcess(cmd: String*) {
|
||||
.directory(app.getFilesDir)
|
||||
.start()
|
||||
|
||||
val is = process.getInputStream
|
||||
new StreamLogger(is, TAG).start()
|
||||
new StreamLogger(process.getInputStream(), TAG, Log.i).start()
|
||||
new StreamLogger(process.getErrorStream(), TAG, Log.e).start()
|
||||
|
||||
if (callback == null) callback = onRestartCallback else callback()
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import java.util.Locale
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.app.{Activity, ProgressDialog}
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content._
|
||||
import android.net.{Uri, VpnService}
|
||||
import android.nfc.{NdefMessage, NfcAdapter}
|
||||
@@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar}
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.content.res.AppCompatResources
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.mikepenz.crossfader.Crossfader
|
||||
@@ -67,7 +68,7 @@ object MainActivity {
|
||||
}
|
||||
|
||||
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
|
||||
with OnSharedPreferenceChangeListener {
|
||||
with OnPreferenceDataStoreChangeListener {
|
||||
import MainActivity._
|
||||
|
||||
// UI
|
||||
@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
|
||||
|
||||
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
|
||||
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false).apply()): View.OnClickListener)
|
||||
if (state == State.STOPPED) app.dataStore.isNAT = false): View.OnClickListener)
|
||||
|
||||
override def binderDied(): Unit = handler.post(() => {
|
||||
detachService()
|
||||
@@ -174,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
})
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
|
||||
case Activity.RESULT_OK => bgService.use(app.profileId)
|
||||
case Activity.RESULT_OK => Utils.startSsService(this)
|
||||
case _ => Log.e(TAG, "Failed to start VpnService")
|
||||
}
|
||||
|
||||
@@ -246,7 +247,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
} else drawer = drawerBuilder.build()
|
||||
|
||||
val header = drawer.getHeader
|
||||
val title = header.findViewById(R.id.drawer_title).asInstanceOf[TextView]
|
||||
val title = header.findViewById[TextView](R.id.drawer_title)
|
||||
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
|
||||
if (tf != null) title.setTypeface(tf)
|
||||
|
||||
@@ -256,7 +257,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
|
||||
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
|
||||
rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView]
|
||||
findViewById(R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
|
||||
findViewById[View](R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
|
||||
testCount += 1
|
||||
statusText.setText(R.string.connection_test_testing)
|
||||
val id = testCount // it would change by other code
|
||||
@@ -298,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
|
||||
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
|
||||
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
|
||||
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
|
||||
if (app.isNatEnabled) bgService.use(app.profileId) else {
|
||||
fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
|
||||
if (app.isNatEnabled) Utils.startSsService(this) else {
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
|
||||
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
|
||||
@@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
|
||||
changeState(State.IDLE) // reset everything to init state
|
||||
handler.post(() => attachService(callback))
|
||||
app.settings.registerOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.registerChangeListener(this)
|
||||
|
||||
val intent = getIntent
|
||||
if (intent != null) handleShareIntent(intent)
|
||||
@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
.show()
|
||||
}
|
||||
|
||||
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
|
||||
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
|
||||
case Key.isNAT => handler.post(() => {
|
||||
detachService()
|
||||
attachService(callback)
|
||||
@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
|
||||
case DRAWER_RECOVERY =>
|
||||
app.track("GlobalConfigFragment", "reset")
|
||||
if (bgService != null) bgService.use(-1)
|
||||
Utils.stopSsService(this)
|
||||
val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
|
||||
val handler = new Handler {
|
||||
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
|
||||
@@ -384,7 +385,6 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
displayFragment(new AboutFragment)
|
||||
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
|
||||
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
|
||||
case _ => // ignore
|
||||
}
|
||||
true // unexpected cases will throw exception
|
||||
}
|
||||
@@ -425,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
|
||||
override def onDestroy() {
|
||||
super.onDestroy()
|
||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.unregisterChangeListener(this)
|
||||
detachService()
|
||||
new BackupManager(this).dataChanged()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
|
||||
@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity {
|
||||
toolbar.setOnMenuItemClickListener(child)
|
||||
}
|
||||
|
||||
override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this)
|
||||
override def onBackPressed(): Unit = if (app.dataStore.dirty) new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsaved_changes_prompt)
|
||||
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
|
||||
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content._
|
||||
import android.os.{Build, Bundle, UserManager}
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.{Preference, PreferenceDataStore}
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||
import android.text.TextUtils
|
||||
import android.view.MenuItem
|
||||
@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.plugin._
|
||||
import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment}
|
||||
import com.github.shadowsocks.preference.{IconListPreference, OnPreferenceDataStoreChangeListener, PluginConfigurationDialogFragment}
|
||||
import com.github.shadowsocks.utils.{Action, Key, Utils}
|
||||
|
||||
object ProfileConfigFragment {
|
||||
@@ -43,7 +42,7 @@ object ProfileConfigFragment {
|
||||
}
|
||||
|
||||
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
|
||||
with OnSharedPreferenceChangeListener {
|
||||
with OnPreferenceDataStoreChangeListener {
|
||||
import ProfileConfigFragment._
|
||||
|
||||
private var profile: Profile = _
|
||||
@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
private var pluginConfiguration: PluginConfiguration = _
|
||||
|
||||
override def onCreatePreferences(bundle: Bundle, key: String) {
|
||||
getPreferenceManager.setPreferenceDataStore(app.dataStore)
|
||||
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
|
||||
case Some(p) =>
|
||||
profile = p
|
||||
profile.serialize(app.editor).apply()
|
||||
profile.serialize(app.dataStore)
|
||||
case None => getActivity.finish()
|
||||
}
|
||||
addPreferencesFromResource(R.xml.pref_profile)
|
||||
@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
plugin.setOnPreferenceChangeListener((_, value) => {
|
||||
val selected = value.asInstanceOf[String]
|
||||
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
|
||||
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
|
||||
app.dataStore.plugin = pluginConfiguration.toString
|
||||
app.dataStore.dirty = true
|
||||
pluginConfigure.setEnabled(!TextUtils.isEmpty(selected))
|
||||
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
|
||||
if (!PluginManager.fetchPlugins()(selected).trusted)
|
||||
@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
|
||||
initPlugins()
|
||||
app.listenForPackageChanges(initPlugins())
|
||||
app.settings.registerOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
def initPlugins() {
|
||||
@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
|
||||
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
|
||||
plugin.entryPackageNames = plugins.map(_._2.packageName).toArray
|
||||
pluginConfiguration = new PluginConfiguration(app.settings.getString(Key.plugin, null))
|
||||
pluginConfiguration = new PluginConfiguration(app.dataStore.plugin)
|
||||
plugin.setValue(pluginConfiguration.selected)
|
||||
plugin.init()
|
||||
plugin.checkSummary()
|
||||
@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
val selected = pluginConfiguration.selected
|
||||
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
|
||||
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
|
||||
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
|
||||
app.dataStore.plugin = pluginConfiguration.toString
|
||||
app.dataStore.dirty = true
|
||||
true
|
||||
} catch {
|
||||
case exc: IllegalArgumentException =>
|
||||
@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
false
|
||||
}
|
||||
|
||||
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
|
||||
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
|
||||
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
|
||||
if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
|
||||
|
||||
override def onDestroy() {
|
||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
||||
app.dataStore.unregisterChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override def onResume() {
|
||||
super.onResume()
|
||||
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
|
||||
isProxyApps.setChecked(app.dataStore.proxyApps) // fetch proxyApps updated by AppManager
|
||||
}
|
||||
|
||||
private def showPluginEditor() {
|
||||
@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
||||
}
|
||||
|
||||
def saveAndExit() {
|
||||
profile.deserialize(app.settings)
|
||||
profile.deserialize(app.dataStore)
|
||||
app.profileManager.updateProfile(profile)
|
||||
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
|
||||
getActivity.finish()
|
||||
|
||||
@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
case _ => false
|
||||
}
|
||||
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
|
||||
case State.CONNECTED => id != app.profileId
|
||||
case State.CONNECTED => id != app.dataStore.profileId
|
||||
case State.STOPPED => true
|
||||
case _ => false
|
||||
}
|
||||
@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
|
||||
var item: Profile = _
|
||||
|
||||
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
|
||||
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
|
||||
private val edit = itemView.findViewById(R.id.edit)
|
||||
private val text1 = itemView.findViewById[TextView](android.R.id.text1)
|
||||
private val text2 = itemView.findViewById[TextView](android.R.id.text2)
|
||||
private val traffic = itemView.findViewById[TextView](R.id.traffic)
|
||||
private val edit = itemView.findViewById[View](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)
|
||||
itemView.findViewById[View](R.id.indicator).setBackgroundResource(R.drawable.background_profile)
|
||||
|
||||
private var adView: NativeExpressAdView = _
|
||||
|
||||
{
|
||||
val share = itemView.findViewById(R.id.share)
|
||||
val share = itemView.findViewById[View](R.id.share)
|
||||
share.setOnClickListener(_ => {
|
||||
val popup = new PopupMenu(getActivity, share)
|
||||
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
|
||||
@@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
|
||||
}
|
||||
|
||||
if (item.id == app.profileId) {
|
||||
if (item.id == app.dataStore.profileId) {
|
||||
itemView.setSelected(true)
|
||||
selectedItem = this
|
||||
} else {
|
||||
@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
adView.setLayoutParams(params)
|
||||
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
|
||||
adView.setAdSize(new AdSize(328, 132))
|
||||
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
|
||||
itemView.findViewById[LinearLayout](R.id.content).addView(adView)
|
||||
|
||||
// Demographics
|
||||
val random = new Random()
|
||||
@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
|
||||
def onClick(v: View): Unit = if (isEnabled) {
|
||||
val activity = getActivity.asInstanceOf[MainActivity]
|
||||
val old = app.profileId
|
||||
val old = app.dataStore.profileId
|
||||
app.switchProfile(item.id)
|
||||
profilesAdapter.refreshId(old)
|
||||
itemView.setSelected(true)
|
||||
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
|
||||
if (activity.state == State.CONNECTED) Utils.reloadSsService(activity)
|
||||
}
|
||||
|
||||
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
|
||||
@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
if (index >= 0) {
|
||||
profiles.remove(index)
|
||||
notifyItemRemoved(index)
|
||||
if (id == app.profileId) app.profileId(0) // switch to null profile
|
||||
if (id == app.dataStore.profileId) app.dataStore.profileId = 0 // switch to null profile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,12 +278,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
toolbar.inflateMenu(R.menu.profile_manager_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
|
||||
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
||||
if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
|
||||
val profilesList = view.findViewById[RecyclerView](R.id.list)
|
||||
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
|
||||
case (profile, i) if profile.id == app.dataStore.profileId => i
|
||||
}.getOrElse(-1))
|
||||
val animator = new DefaultItemAnimator()
|
||||
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
|
||||
@@ -352,21 +352,16 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
||||
|
||||
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
|
||||
case R.id.action_scan_qr_code =>
|
||||
def installScanner() = {
|
||||
Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
|
||||
try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=tw.com.quickmark"))) catch {
|
||||
case exc: ActivityNotFoundException => exc.printStackTrace()
|
||||
}
|
||||
}
|
||||
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 => installScanner()
|
||||
case _: ActivityNotFoundException =>
|
||||
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
|
||||
case e: SecurityException =>
|
||||
e.printStackTrace()
|
||||
app.track(e)
|
||||
installScanner()
|
||||
startActivity(new Intent(getActivity, classOf[ScannerActivity]))
|
||||
}
|
||||
true
|
||||
case R.id.action_import =>
|
||||
|
||||
@@ -24,6 +24,8 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.{Build, Bundle}
|
||||
import android.support.v4.content.pm.{ShortcutInfoCompat, ShortcutManagerCompat}
|
||||
import android.support.v4.graphics.drawable.IconCompat
|
||||
import com.github.shadowsocks.utils.{State, Utils}
|
||||
|
||||
/**
|
||||
@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext {
|
||||
super.onCreate(savedInstanceState)
|
||||
getIntent.getAction match {
|
||||
case Intent.ACTION_CREATE_SHORTCUT =>
|
||||
setResult(Activity.RESULT_OK, new Intent()
|
||||
.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut]))
|
||||
.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle))
|
||||
.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
|
||||
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher)))
|
||||
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
|
||||
new ShortcutInfoCompat.Builder(this, "toggle")
|
||||
.setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
|
||||
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
|
||||
.setShortLabel(getString(R.string.quick_toggle))
|
||||
.build()))
|
||||
finish()
|
||||
case _ =>
|
||||
attachService()
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*******************************************************************************/
|
||||
/* */
|
||||
/* Copyright (C) 2016 by Max Lv <max.c.lv@gmail.com> */
|
||||
/* Copyright (C) 2016 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 */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
/* (at your option) any later version. */
|
||||
/* */
|
||||
/* This program is distributed in the hope that it will be useful, */
|
||||
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
|
||||
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
|
||||
/* GNU General Public License for more details. */
|
||||
/* */
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
/* */
|
||||
/*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.{Activity, TaskStackBuilder}
|
||||
import android.content.Intent
|
||||
import android.content.pm.{PackageManager, ShortcutManager}
|
||||
import android.os.{Build, Bundle}
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.Parser
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
|
||||
object ScannerActivity {
|
||||
private final val MY_PERMISSIONS_REQUEST_CAMERA = 1
|
||||
}
|
||||
|
||||
class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
|
||||
import ScannerActivity._
|
||||
|
||||
private var scannerView: ZXingScannerView = _
|
||||
|
||||
override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
|
||||
grantResults: Array[Int]) {
|
||||
if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA) {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.length > 0
|
||||
&& grantResults(0) == PackageManager.PERMISSION_GRANTED) {
|
||||
scannerView.setResultHandler(this)
|
||||
scannerView.startCamera()
|
||||
} else {
|
||||
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def navigateUp() {
|
||||
val intent = getParentActivityIntent
|
||||
if (shouldUpRecreateTask(intent) || isTaskRoot)
|
||||
TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
|
||||
else finish()
|
||||
}
|
||||
|
||||
override def onCreate(state: Bundle) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.layout_scanner)
|
||||
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar.setTitle(getTitle)
|
||||
toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
|
||||
toolbar.setNavigationOnClickListener(_ => navigateUp())
|
||||
scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
|
||||
if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
|
||||
}
|
||||
|
||||
override def onResume() {
|
||||
super.onResume()
|
||||
val permissionCheck = ContextCompat.checkSelfPermission(this,
|
||||
android.Manifest.permission.CAMERA)
|
||||
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
|
||||
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
|
||||
scannerView.startCamera() // Start camera on resume
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
override def onPause() {
|
||||
super.onPause()
|
||||
scannerView.stopCamera() // Stop camera on pause
|
||||
}
|
||||
|
||||
override def handleResult(rawResult: Result) = {
|
||||
val uri = rawResult.getText
|
||||
if (!TextUtils.isEmpty(uri))
|
||||
Parser.findAll(uri).foreach(app.profileManager.createProfile)
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException}
|
||||
import java.util.Locale
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.{Application, NotificationChannel, NotificationManager}
|
||||
import android.content._
|
||||
import android.content.res.Configuration
|
||||
import android.os.{Build, LocaleList}
|
||||
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.preference.OrmLitePreferenceDataStore
|
||||
import com.github.shadowsocks.utils.CloseUtils._
|
||||
import com.github.shadowsocks.utils._
|
||||
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
|
||||
@@ -42,6 +42,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
||||
import com.j256.ormlite.logger.LocalLog
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
object ShadowsocksApplication {
|
||||
@@ -59,13 +60,13 @@ object ShadowsocksApplication {
|
||||
class ShadowsocksApplication extends Application {
|
||||
import ShadowsocksApplication._
|
||||
|
||||
lazy val remoteConfig = FirebaseRemoteConfig.getInstance()
|
||||
lazy val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
|
||||
lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
|
||||
lazy val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
lazy val editor: SharedPreferences.Editor = settings.edit
|
||||
lazy val profileManager = new ProfileManager(new DBHelper(this))
|
||||
private lazy val dbHelper = new DBHelper(this)
|
||||
lazy val profileManager = new ProfileManager(dbHelper)
|
||||
lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
|
||||
|
||||
def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false)
|
||||
def isNatEnabled: Boolean = dataStore.isNAT
|
||||
def isVpnEnabled: Boolean = !isNatEnabled
|
||||
|
||||
// send event
|
||||
@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application {
|
||||
.setFatal(false)
|
||||
.build())
|
||||
|
||||
def profileId: Int = settings.getInt(Key.id, 0)
|
||||
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
|
||||
def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
|
||||
def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
|
||||
|
||||
def switchProfile(id: Int): Profile = {
|
||||
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
|
||||
profileId(result.id)
|
||||
dataStore.profileId = result.id
|
||||
result
|
||||
}
|
||||
|
||||
@@ -148,12 +147,17 @@ class ShadowsocksApplication extends Application {
|
||||
checkChineseLocale(getResources.getConfiguration)
|
||||
|
||||
FirebaseApp.initializeApp(this)
|
||||
remoteConfig.setDefaults(R.xml.default_configs);
|
||||
remoteConfig.setDefaults(R.xml.default_configs)
|
||||
remoteConfig.fetch().addOnCompleteListener(task => if (task.isSuccessful) remoteConfig.activateFetched())
|
||||
|
||||
JobManager.create(this).addJobCreator(DonaldTrump)
|
||||
|
||||
TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
TcpFastOpen.enabled(dataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) getSystemService(classOf[NotificationManager]).createNotificationChannels(List(
|
||||
new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN),
|
||||
new NotificationChannel("service-nat", getText(R.string.service_nat), NotificationManager.IMPORTANCE_LOW)
|
||||
))
|
||||
}
|
||||
|
||||
def crashRecovery() {
|
||||
@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application {
|
||||
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
|
||||
IOUtils.copy(in, out)))
|
||||
}
|
||||
editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply()
|
||||
dataStore.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE)
|
||||
}
|
||||
|
||||
def updateAssets(): Unit = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
|
||||
def updateAssets(): Unit = if (dataStore.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
|
||||
|
||||
def listenForPackageChanges(callback: => Unit): BroadcastReceiver = {
|
||||
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
||||
|
||||
@@ -20,26 +20,11 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.backup.{BackupAgentHelper, FileBackupHelper, SharedPreferencesBackupHelper}
|
||||
import android.app.backup.{BackupAgentHelper, FileBackupHelper}
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.database.DBHelper
|
||||
|
||||
class ShadowsocksBackupAgent extends BackupAgentHelper {
|
||||
|
||||
// The names of the SharedPreferences groups that the application maintains. These
|
||||
// are the same strings that are passed to getSharedPreferences(String, int).
|
||||
val PREFS_DISPLAY = "com.github.shadowsocks_preferences"
|
||||
|
||||
// An arbitrary string used within the BackupAgentHelper implementation to
|
||||
// identify the SharedPreferencesBackupHelper's data.
|
||||
val MY_PREFS_BACKUP_KEY = "com.github.shadowsocks"
|
||||
|
||||
val DATABASE = "com.github.shadowsocks.database.profile"
|
||||
|
||||
override def onCreate() {
|
||||
val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY)
|
||||
addHelper(MY_PREFS_BACKUP_KEY, helper)
|
||||
addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE,
|
||||
Acl.CUSTOM_RULES + ".acl"))
|
||||
}
|
||||
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
|
||||
new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ import java.io.File
|
||||
import java.net.{Inet6Address, InetAddress}
|
||||
import java.util.Locale
|
||||
|
||||
import android.app.Service
|
||||
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.acl.{Acl, AclSyncJob}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils._
|
||||
import eu.chainfire.libsuperuser.Shell
|
||||
@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
|
||||
val CMD_IPTABLES_DNAT_ADD_SOCKS =
|
||||
"iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123"
|
||||
|
||||
private var notification: ShadowsocksNotification = _
|
||||
val myUid: Int = android.os.Process.myUid()
|
||||
|
||||
var sslocalProcess: GuardedProcess = _
|
||||
@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService {
|
||||
su.addCommand((init_sb ++ http_sb).toArray)
|
||||
}
|
||||
|
||||
override def startRunner(profile: Profile): Unit = if (su == null)
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (su == null) {
|
||||
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
|
||||
if (exitCode == 0) super.startRunner(profile) else {
|
||||
if (exitCode == 0) super.onStartCommand(intent, flags, startId) else {
|
||||
if (su != null) {
|
||||
su.close()
|
||||
su = null
|
||||
}
|
||||
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
|
||||
})
|
||||
Service.START_NOT_STICKY
|
||||
} else super.onStartCommand(intent, flags, startId)
|
||||
|
||||
override def connect() {
|
||||
super.connect()
|
||||
@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService {
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
changeState(State.CONNECTED)
|
||||
notification = new ShadowsocksNotification(this, profile.name, true)
|
||||
}
|
||||
|
||||
override def stopRunner(stopService: Boolean, msg: String = null) {
|
||||
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-nat", true)
|
||||
|
||||
if (notification != null) notification.destroy()
|
||||
override def stopRunner(stopService: Boolean, msg: String = null) {
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPING)
|
||||
|
||||
@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager}
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.os.BuildCompat
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
|
||||
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
|
||||
class ShadowsocksNotification(private val service: BaseService, profileName: String,
|
||||
channel: String, visible: Boolean = false) {
|
||||
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
||||
private lazy val callback = new Stub {
|
||||
@@ -52,7 +54,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
||||
private var lockReceiver: BroadcastReceiver = _
|
||||
private var callbackRegistered: Boolean = _
|
||||
|
||||
private val builder = new NotificationCompat.Builder(service)
|
||||
private val builder = new NotificationCompat.Builder(service, channel)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.net.VpnService
|
||||
import android.os.{Bundle, Handler}
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.Utils
|
||||
|
||||
object ShadowsocksRunnerActivity {
|
||||
private final val TAG = "ShadowsocksRunnerActivity"
|
||||
@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
|
||||
def startBackgroundService() {
|
||||
if (app.isNatEnabled) {
|
||||
bgService.use(app.profileId)
|
||||
Utils.startSsService(this)
|
||||
finish()
|
||||
} else {
|
||||
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
|
||||
@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
} else {
|
||||
attachService()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override def onDestroy() {
|
||||
@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
||||
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||
resultCode match {
|
||||
case Activity.RESULT_OK =>
|
||||
if (bgService != null) {
|
||||
bgService.use(app.profileId)
|
||||
}
|
||||
case Activity.RESULT_OK => Utils.startSsService(this)
|
||||
case _ =>
|
||||
Log.e(TAG, "Failed to start VpnService")
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*******************************************************************************/
|
||||
/* */
|
||||
/* 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 */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
/* (at your option) any later version. */
|
||||
/* */
|
||||
/* This program is distributed in the hope that it will be useful, */
|
||||
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
|
||||
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
|
||||
/* GNU General Public License for more details. */
|
||||
/* */
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
/* */
|
||||
/*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.{Handler, IBinder}
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
|
||||
class ShadowsocksRunnerService extends Service with ServiceBoundContext {
|
||||
val handler = new Handler()
|
||||
|
||||
override def onBind(intent: Intent): IBinder = {
|
||||
null
|
||||
}
|
||||
|
||||
override def onServiceConnected() {
|
||||
handler.postDelayed(() => {
|
||||
if (bgService != null) {
|
||||
if (app.isNatEnabled) startBackgroundService()
|
||||
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
|
||||
handler.postDelayed(() => stopSelf(), 3000)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
def startBackgroundService(): Unit = bgService.useSync(app.profileId)
|
||||
|
||||
override def onCreate() {
|
||||
super.onCreate()
|
||||
attachService()
|
||||
}
|
||||
|
||||
override def onDestroy() {
|
||||
super.onDestroy()
|
||||
detachService()
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ package com.github.shadowsocks
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
import android.app.Service
|
||||
import android.content._
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.net.VpnService
|
||||
@@ -30,7 +31,6 @@ import android.os._
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.utils._
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
|
||||
var conn: ParcelFileDescriptor = _
|
||||
var vpnThread: ShadowsocksVpnThread = _
|
||||
private var notification: ShadowsocksNotification = _
|
||||
|
||||
var sslocalProcess: GuardedProcess = _
|
||||
var overtureProcess: GuardedProcess = _
|
||||
@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
vpnThread = null
|
||||
}
|
||||
|
||||
if (notification != null) notification.destroy()
|
||||
|
||||
// channge the state
|
||||
changeState(State.STOPPING)
|
||||
|
||||
@@ -103,20 +100,19 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
override def startRunner(profile: Profile) {
|
||||
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
|
||||
// ensure the VPNService is prepared
|
||||
if (VpnService.prepare(this) != null) {
|
||||
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(i)
|
||||
stopRunner(stopService = true)
|
||||
return
|
||||
}
|
||||
|
||||
super.startRunner(profile)
|
||||
Service.START_NOT_STICKY
|
||||
} else super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn")
|
||||
|
||||
override def connect() {
|
||||
super.connect()
|
||||
|
||||
@@ -137,13 +133,11 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
|
||||
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
|
||||
AclSyncJob.schedule(profile.route)
|
||||
|
||||
notification = new ShadowsocksNotification(this, profile.name)
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
def handleConnection() {
|
||||
|
||||
|
||||
startShadowsocksDaemon()
|
||||
|
||||
if (!profile.udpdns) {
|
||||
@@ -214,7 +208,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_CHN) {
|
||||
if (profile.route == Acl.ALL
|
||||
|| profile.route == Acl.BYPASS_CHN
|
||||
|| profile.route == Acl.CUSTOM_RULES) {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
} else {
|
||||
getResources.getStringArray(R.array.bypass_private_route).foreach(cidr => {
|
||||
@@ -245,7 +241,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
||||
cmd += "--enable-udprelay"
|
||||
|
||||
if (!profile.udpdns)
|
||||
cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"),
|
||||
cmd += ("--dnsgw", "%s:%d".formatLocal(Locale.ENGLISH, "127.0.0.1",
|
||||
profile.localPort + 53))
|
||||
|
||||
tun2socksProcess = new GuardedProcess(cmd: _*).start(() => sendFd(fd))
|
||||
|
||||
@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity {
|
||||
typedArray.recycle()
|
||||
}
|
||||
private var item: Profile = _
|
||||
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
|
||||
private val text = itemView.findViewById[CheckedTextView](android.R.id.text1)
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
def bindDefault() {
|
||||
|
||||
@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
class TaskerReceiver extends BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent) {
|
||||
val settings = TaskerSettings.fromIntent(intent)
|
||||
var changed = false
|
||||
app.profileManager.getProfile(settings.profileId) match {
|
||||
case Some(_) => app.switchProfile(settings.profileId)
|
||||
case Some(_) => if (app.dataStore.profileId != settings.profileId) {
|
||||
app.switchProfile(settings.profileId)
|
||||
changed = true
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context)
|
||||
if (settings.switchOn) {
|
||||
Utils.startSsService(context)
|
||||
if (changed) Utils.reloadSsService(context)
|
||||
} else Utils.stopSsService(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ToolbarFragment extends Fragment {
|
||||
|
||||
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
toolbar = view.findViewById[Toolbar](R.id.toolbar)
|
||||
val activity = getActivity.asInstanceOf[MainActivity]
|
||||
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.github.shadowsocks.acl
|
||||
|
||||
import java.io.{File, FileNotFoundException}
|
||||
import java.io.{File, FileNotFoundException, IOException}
|
||||
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.IOUtils
|
||||
@@ -22,24 +22,11 @@ class Acl {
|
||||
val bypassHostnames = new mutable.SortedList[String]()
|
||||
val proxyHostnames = new mutable.SortedList[String]()
|
||||
val subnets = new mutable.SortedList[Subnet]()
|
||||
val urls = new mutable.SortedList[String]()
|
||||
@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 isUrl(url: String): Boolean = url.startsWith("http://") || url.startsWith("https://")
|
||||
|
||||
def fromAcl(other: Acl): Acl = {
|
||||
bypassHostnames.clear()
|
||||
@@ -48,21 +35,35 @@ class Acl {
|
||||
proxyHostnames ++= other.proxyHostnames
|
||||
subnets.clear()
|
||||
subnets ++= other.subnets
|
||||
urls.clear()
|
||||
urls ++= other.urls
|
||||
bypass = other.bypass
|
||||
this
|
||||
}
|
||||
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
|
||||
def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
this.subnets.clear()
|
||||
this.urls.clear()
|
||||
bypass = defaultBypass
|
||||
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
|
||||
lazy val proxySubnets = new mutable.SortedList[Subnet]()
|
||||
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
|
||||
for (line <- value.getLines()) (line.indexOf('#') match {
|
||||
case -1 => line
|
||||
case index => line.substring(0, index) // trim comments
|
||||
var in_urls = false
|
||||
for (line <- value.getLines()) (line.trim.indexOf('#') match {
|
||||
case 0 => {
|
||||
line.indexOf("NETWORK_ACL_BEGIN") match {
|
||||
case -1 =>
|
||||
case index => in_urls = true
|
||||
}
|
||||
line.indexOf("NETWORK_ACL_END") match {
|
||||
case -1 =>
|
||||
case index => in_urls = false
|
||||
}
|
||||
"" // ignore any comment lines
|
||||
}
|
||||
case index => if (!in_urls) line else ""
|
||||
}).trim match {
|
||||
case "[outbound_block_list]" =>
|
||||
hostnames = null
|
||||
@@ -76,7 +77,11 @@ class Acl {
|
||||
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 _: IllegalArgumentException => if (isUrl(input)) {
|
||||
urls += input
|
||||
} else {
|
||||
hostnames += input
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
@@ -85,9 +90,24 @@ class Acl {
|
||||
}
|
||||
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
|
||||
|
||||
override def toString: String = {
|
||||
def getAclString(network: Boolean): String = {
|
||||
val result = new StringBuilder()
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
if (urls.nonEmpty) {
|
||||
result.append(urls.mkString("\n"))
|
||||
result.append("\n")
|
||||
if (network) {
|
||||
result.append("#NETWORK_ACL_BEGIN\n")
|
||||
try {
|
||||
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
|
||||
} catch {
|
||||
case e: IOException => // ignore
|
||||
}
|
||||
result.append("#NETWORK_ACL_END\n")
|
||||
}
|
||||
}
|
||||
if (result.isEmpty) {
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
}
|
||||
val (bypassList, proxyList) =
|
||||
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
|
||||
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
|
||||
@@ -104,6 +124,10 @@ class Acl {
|
||||
result.toString
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
getAclString(false)
|
||||
}
|
||||
|
||||
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
|
||||
|
||||
// Don't change: dummy fields for OrmLite interaction
|
||||
@@ -134,9 +158,9 @@ object 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(getFile(id), acl.toString)
|
||||
def save(id: String, acl: Acl, network: Boolean = false): Unit = {
|
||||
IOUtils.writeString(getFile(id), acl.getAclString(network))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,6 @@ object AclSyncJob {
|
||||
}
|
||||
|
||||
class AclSyncJob(route: String) extends Job {
|
||||
var rescheduled = 0
|
||||
|
||||
override def onRunJob(params: Params): Result = {
|
||||
try {
|
||||
//noinspection JavaAccessorMethodCalledAsEmptyParen
|
||||
@@ -57,11 +55,7 @@ class AclSyncJob(route: String) extends Job {
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
e.printStackTrace()
|
||||
rescheduled += 1
|
||||
if (rescheduled < AclSyncJob.MAX_RESCHEDULE)
|
||||
Result.RESCHEDULE
|
||||
else
|
||||
Result.FAILURE
|
||||
if (params.getFailureCount < AclSyncJob.MAX_RESCHEDULE) Result.RESCHEDULE else Result.FAILURE
|
||||
case e: Exception => // unknown failures, probably shouldn't retry
|
||||
e.printStackTrace()
|
||||
Result.FAILURE
|
||||
|
||||
@@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
|
||||
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]
|
||||
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
|
||||
val editText = view.findViewById[EditText](R.id.content)
|
||||
PATTERN_DOMAIN.findFirstMatchIn(text) match {
|
||||
case Some(m) =>
|
||||
templateSelector.setSelection(DOMAIN)
|
||||
@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
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]
|
||||
private val text = itemView.findViewById[TextView](android.R.id.text1)
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnLongClickListener(this)
|
||||
itemView.setBackgroundResource(R.drawable.background_selectable)
|
||||
@@ -125,22 +125,34 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
})
|
||||
}
|
||||
|
||||
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
|
||||
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.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))
|
||||
val j = i - acl.urls.size
|
||||
val k = i - acl.subnets.size - acl.urls.size
|
||||
if (j < 0)
|
||||
vh.bind(acl.urls(i))
|
||||
else if (k < 0)
|
||||
vh.bind(acl.subnets(j))
|
||||
else
|
||||
vh.bind(acl.proxyHostnames(k))
|
||||
}
|
||||
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
|
||||
.inflate(android.R.layout.simple_list_item_1, vg, false))
|
||||
|
||||
def addUrl(url: String): Int = if (acl.urls.add(url)) {
|
||||
val index = acl.urls.indexOf(url)
|
||||
notifyItemInserted(index)
|
||||
apply()
|
||||
index
|
||||
} else -1
|
||||
def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) {
|
||||
val index = acl.subnets.indexOf(subnet)
|
||||
val index = acl.subnets.indexOf(subnet) + acl.urls.size
|
||||
notifyItemInserted(index)
|
||||
apply()
|
||||
index
|
||||
} else -1
|
||||
def addHostname(hostname: String): Int = if (acl.proxyHostnames.add(hostname)) {
|
||||
val index = acl.proxyHostnames.indexOf(hostname) + acl.subnets.size
|
||||
val index = acl.proxyHostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size
|
||||
notifyItemInserted(index)
|
||||
apply()
|
||||
index
|
||||
@@ -148,6 +160,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
def addToProxy(input: String): Int = {
|
||||
val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
|
||||
var result = -1
|
||||
for (url <- acl.urls) result = addUrl(url)
|
||||
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
|
||||
if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
|
||||
result
|
||||
@@ -164,26 +177,36 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
}
|
||||
|
||||
def remove(i: Int) {
|
||||
val j = i - acl.subnets.size
|
||||
val j = i - acl.urls.size
|
||||
val k = i - acl.urls.size - acl.subnets.size
|
||||
if (j < 0) {
|
||||
undoManager.remove((i, acl.subnets(i)))
|
||||
acl.subnets.remove(i)
|
||||
undoManager.remove((i, acl.urls(i)))
|
||||
acl.urls.remove(i)
|
||||
} else if (k < 0) {
|
||||
undoManager.remove((i, acl.subnets(j)))
|
||||
acl.subnets.remove(j)
|
||||
} else {
|
||||
undoManager.remove((j, acl.proxyHostnames(j)))
|
||||
acl.proxyHostnames.remove(j)
|
||||
undoManager.remove((i, acl.proxyHostnames(k)))
|
||||
acl.proxyHostnames.remove(k)
|
||||
}
|
||||
notifyItemRemoved(i)
|
||||
apply()
|
||||
}
|
||||
def remove(item: AnyRef): Unit = item match {
|
||||
case subnet: Subnet =>
|
||||
notifyItemRemoved(acl.subnets.indexOf(subnet))
|
||||
notifyItemRemoved(acl.subnets.indexOf(subnet) + acl.urls.size)
|
||||
acl.subnets.remove(subnet)
|
||||
apply()
|
||||
case hostname: String =>
|
||||
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
|
||||
case hostname: String => if (acl.isUrl(hostname)) {
|
||||
notifyItemRemoved(acl.urls.indexOf(hostname))
|
||||
acl.urls.remove(hostname)
|
||||
apply()
|
||||
} else {
|
||||
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname)
|
||||
+ acl.urls.size + acl.subnets.size)
|
||||
acl.proxyHostnames.remove(hostname)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
def removeSelected() {
|
||||
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
|
||||
@@ -192,12 +215,19 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
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 hostname: String => if (acl.isUrl(hostname)) {
|
||||
if (acl.urls.insert(hostname)) {
|
||||
notifyItemInserted(acl.urls.indexOf(hostname))
|
||||
apply()
|
||||
}
|
||||
} else {
|
||||
if (acl.proxyHostnames.insert(hostname)) {
|
||||
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
|
||||
notifyItemInserted(acl.subnets.indexOf(subnet))
|
||||
notifyItemInserted(acl.subnets.indexOf(subnet) + acl.urls.size)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
@@ -252,12 +282,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
selectionItem = toolbar.getMenu.findItem(R.id.selection)
|
||||
selectionItem.setVisible(selectedItems.nonEmpty)
|
||||
list = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
||||
list = view.findViewById(R.id.list)
|
||||
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)
|
||||
view.findViewById[FastScroller](R.id.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 =
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
|
||||
package com.github.shadowsocks.database
|
||||
|
||||
import android.content.Context
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import android.content.{Context, SharedPreferences}
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.preference.PreferenceManager
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
||||
import com.j256.ormlite.dao.Dao
|
||||
import com.j256.ormlite.support.ConnectionSource
|
||||
@@ -46,17 +50,20 @@ object DBHelper {
|
||||
}
|
||||
|
||||
class DBHelper(val context: Context)
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) {
|
||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) {
|
||||
import DBHelper._
|
||||
|
||||
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
|
||||
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
|
||||
|
||||
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
||||
TableUtils.createTable(connectionSource, classOf[Profile])
|
||||
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
|
||||
}
|
||||
|
||||
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
||||
TableUtils.dropTable(connectionSource, classOf[Profile], true)
|
||||
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
|
||||
onCreate(database, connectionSource)
|
||||
}
|
||||
|
||||
@@ -132,6 +139,20 @@ class DBHelper(val context: Context)
|
||||
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
}
|
||||
|
||||
if (oldVersion < 23) {
|
||||
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
|
||||
import KeyValuePair._
|
||||
val old = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
|
||||
ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array()))
|
||||
kvPairDao.createOrUpdate(new KeyValuePair(Key.isNAT, TYPE_BOOLEAN,
|
||||
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.isNAT, false)) 1 else 0).toByte).array()))
|
||||
kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN,
|
||||
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array()))
|
||||
kvPairDao.createOrUpdate(new KeyValuePair(Key.currentVersionCode, TYPE_INT,
|
||||
ByteBuffer.allocate(4).putInt(-1).array()))
|
||||
}
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
app.track(ex)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.github.shadowsocks.database
|
||||
|
||||
import com.j256.ormlite.field.{DataType, DatabaseField}
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
object KeyValuePair {
|
||||
val TYPE_UNINITIALIZED = 0
|
||||
val TYPE_BOOLEAN = 1
|
||||
val TYPE_FLOAT = 2
|
||||
val TYPE_INT = 3
|
||||
val TYPE_LONG = 4
|
||||
val TYPE_STRING = 5
|
||||
val TYPE_STRING_SET = 6
|
||||
}
|
||||
|
||||
class KeyValuePair {
|
||||
@DatabaseField(id = true)
|
||||
var key: String = _
|
||||
@DatabaseField
|
||||
var valueType: Int = _
|
||||
@DatabaseField(dataType = DataType.BYTE_ARRAY)
|
||||
var value: Array[Byte] = _
|
||||
|
||||
def this(key: String, valueType: Int, value: Array[Byte]) = {
|
||||
this()
|
||||
this.key = key
|
||||
this.valueType = valueType
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,11 @@ package com.github.shadowsocks.database
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.util.Base64
|
||||
import com.github.shadowsocks.plugin.PluginConfiguration
|
||||
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
|
||||
import com.github.shadowsocks.utils.Key
|
||||
import com.j256.ormlite.field.{DataType, DatabaseField}
|
||||
|
||||
@@ -108,37 +108,38 @@ class Profile {
|
||||
}
|
||||
override def toString: String = toUri.toString
|
||||
|
||||
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
|
||||
.putString(Key.name, name)
|
||||
.putString(Key.host, host)
|
||||
.putInt(Key.localPort, localPort)
|
||||
.putInt(Key.remotePort, remotePort)
|
||||
.putString(Key.password, password)
|
||||
.putString(Key.route, route)
|
||||
.putString(Key.remoteDns, remoteDns)
|
||||
.putString(Key.method, method)
|
||||
.putBoolean(Key.proxyApps, proxyApps)
|
||||
.putBoolean(Key.bypass, bypass)
|
||||
.putBoolean(Key.udpdns, udpdns)
|
||||
.putBoolean(Key.ipv6, ipv6)
|
||||
.putString(Key.individual, individual)
|
||||
.putString(Key.plugin, plugin)
|
||||
.remove(Key.dirty)
|
||||
def deserialize(pref: SharedPreferences) {
|
||||
def serialize(store: OrmLitePreferenceDataStore) {
|
||||
store.putString(Key.name, name)
|
||||
store.putString(Key.host, host)
|
||||
store.putInt(Key.localPort, localPort)
|
||||
store.putInt(Key.remotePort, remotePort)
|
||||
store.putString(Key.password, password)
|
||||
store.putString(Key.route, route)
|
||||
store.putString(Key.remoteDns, remoteDns)
|
||||
store.putString(Key.method, method)
|
||||
store.proxyApps = proxyApps
|
||||
store.bypass = bypass
|
||||
store.putBoolean(Key.udpdns, udpdns)
|
||||
store.putBoolean(Key.ipv6, ipv6)
|
||||
store.individual = individual
|
||||
store.plugin = plugin
|
||||
store.remove(Key.dirty)
|
||||
}
|
||||
def deserialize(store: OrmLitePreferenceDataStore) {
|
||||
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
|
||||
name = pref.getString(Key.name, null)
|
||||
host = pref.getString(Key.host, null)
|
||||
localPort = pref.getInt(Key.localPort, 0)
|
||||
remotePort = pref.getInt(Key.remotePort, 0)
|
||||
password = pref.getString(Key.password, null)
|
||||
method = pref.getString(Key.method, null)
|
||||
route = pref.getString(Key.route, null)
|
||||
remoteDns = pref.getString(Key.remoteDns, null)
|
||||
proxyApps = pref.getBoolean(Key.proxyApps, false)
|
||||
bypass = pref.getBoolean(Key.bypass, false)
|
||||
udpdns = pref.getBoolean(Key.udpdns, false)
|
||||
ipv6 = pref.getBoolean(Key.ipv6, false)
|
||||
individual = pref.getString(Key.individual, null)
|
||||
plugin = pref.getString(Key.plugin, null)
|
||||
name = store.getString(Key.name, null)
|
||||
host = store.getString(Key.host, null)
|
||||
localPort = store.getInt(Key.localPort, 0)
|
||||
remotePort = store.getInt(Key.remotePort, 0)
|
||||
password = store.getString(Key.password, null)
|
||||
method = store.getString(Key.method, null)
|
||||
route = store.getString(Key.route, null)
|
||||
remoteDns = store.getString(Key.remoteDns, null)
|
||||
proxyApps = store.proxyApps
|
||||
bypass = store.bypass
|
||||
udpdns = store.getBoolean(Key.udpdns, false)
|
||||
ipv6 = store.getBoolean(Key.ipv6, false)
|
||||
individual = store.individual
|
||||
plugin = store.plugin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
|
||||
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
|
||||
with View.OnClickListener with View.OnLongClickListener {
|
||||
private var index: Int = _
|
||||
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
|
||||
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
|
||||
private val text1 = view.findViewById[TextView](android.R.id.text1)
|
||||
private val text2 = view.findViewById[TextView](android.R.id.text2)
|
||||
private val icon = view.findViewById[ImageView](android.R.id.icon)
|
||||
view.setOnClickListener(this)
|
||||
view.setOnLongClickListener(this)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.github.shadowsocks.preference
|
||||
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
trait OnPreferenceDataStoreChangeListener {
|
||||
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.github.shadowsocks.preference
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util
|
||||
|
||||
import android.support.v7.preference.PreferenceDataStore
|
||||
import com.github.shadowsocks.database.{DBHelper, KeyValuePair}
|
||||
import com.github.shadowsocks.utils.Key
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* @author Mygod
|
||||
*/
|
||||
//noinspection AccessorLikeMethodIsUnit
|
||||
final class OrmLitePreferenceDataStore(dbHelper: DBHelper) extends PreferenceDataStore {
|
||||
import KeyValuePair._
|
||||
|
||||
override def getBoolean(key: String, defValue: Boolean = false): Boolean = dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair =>
|
||||
if (pair.valueType == TYPE_BOOLEAN) ByteBuffer.wrap(pair.value).get() != 0 else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
override def getFloat(key: String, defValue: Float): Float = dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair =>
|
||||
if (pair.valueType == TYPE_FLOAT) ByteBuffer.wrap(pair.value).getFloat() else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
override def getInt(key: String, defValue: Int): Int = dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair =>
|
||||
if (pair.valueType == TYPE_INT) ByteBuffer.wrap(pair.value).getInt() else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
override def getLong(key: String, defValue: Long): Long = dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair =>
|
||||
if (pair.valueType == TYPE_LONG) ByteBuffer.wrap(pair.value).getLong() else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
override def getString(key: String, defValue: String = null): String = dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair =>
|
||||
if (pair.valueType == TYPE_STRING) new String(pair.value) else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
override def getStringSet(key: String, defValue: util.Set[String]): util.Set[String] =
|
||||
dbHelper.kvPairDao.queryForId(key) match {
|
||||
case pair: KeyValuePair => if (pair.valueType == TYPE_STRING_SET) {
|
||||
val buffer = ByteBuffer.wrap(pair.value)
|
||||
val result = new util.HashSet[String]()
|
||||
while (buffer.hasRemaining) {
|
||||
val chArr = new Array[Byte](buffer.getInt)
|
||||
buffer.get(chArr)
|
||||
result.add(new String(chArr))
|
||||
}
|
||||
result
|
||||
} else defValue
|
||||
case _ => defValue
|
||||
}
|
||||
|
||||
override def putBoolean(key: String, value: Boolean) {
|
||||
dbHelper.kvPairDao.createOrUpdate(
|
||||
new KeyValuePair(key, TYPE_BOOLEAN, ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte).array()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override def putFloat(key: String, value: Float) {
|
||||
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_FLOAT, ByteBuffer.allocate(4).putFloat(value).array()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override def putInt(key: String, value: Int) {
|
||||
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_INT, ByteBuffer.allocate(4).putInt(value).array()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override def putLong(key: String, value: Long) {
|
||||
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_LONG, ByteBuffer.allocate(8).putLong(value).array()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override def putString(key: String, value: String) {
|
||||
value match {
|
||||
case null => remove(key)
|
||||
case _ => dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING, value.getBytes()))
|
||||
}
|
||||
fireChangeListener(key)
|
||||
}
|
||||
override def putStringSet(key: String, value: util.Set[String]) {
|
||||
val stream = new ByteArrayOutputStream()
|
||||
for (v <- value) {
|
||||
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
|
||||
stream.write(v.getBytes())
|
||||
}
|
||||
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING_SET, stream.toByteArray))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
def remove(key: String): Int = dbHelper.kvPairDao.deleteById(key)
|
||||
|
||||
private var listeners: Set[OnPreferenceDataStoreChangeListener] = Set.empty
|
||||
private def fireChangeListener(key: String) = listeners.foreach(_.onPreferenceDataStoreChanged(this, key))
|
||||
def registerChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners += listener
|
||||
def unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners -= listener
|
||||
|
||||
def profileId: Int = getInt(Key.id, 0)
|
||||
def profileId_=(i: Int): Unit = putInt(Key.id, i)
|
||||
def isNAT: Boolean = getBoolean(Key.isNAT)
|
||||
def isNAT_=(value: Boolean): Unit = putBoolean(Key.isNAT, value)
|
||||
|
||||
def proxyApps: Boolean = getBoolean(Key.proxyApps)
|
||||
def proxyApps_=(value: Boolean): Unit = putBoolean(Key.proxyApps, value)
|
||||
def bypass: Boolean = getBoolean(Key.bypass)
|
||||
def bypass_=(value: Boolean): Unit = putBoolean(Key.bypass, value)
|
||||
def individual: String = getString(Key.individual)
|
||||
def individual_=(value: String): Unit = putString(Key.individual, value)
|
||||
def plugin: String = getString(Key.plugin)
|
||||
def plugin_=(value: String): Unit = putString(Key.plugin, value)
|
||||
def dirty: Boolean = getBoolean(Key.dirty)
|
||||
def dirty_=(value: Boolean): Unit = putBoolean(Key.dirty, value)
|
||||
}
|
||||
@@ -94,6 +94,7 @@ object State {
|
||||
object Action {
|
||||
final val SERVICE = "com.github.shadowsocks.SERVICE"
|
||||
final val CLOSE = "com.github.shadowsocks.CLOSE"
|
||||
final val RELOAD = "com.github.shadowsocks.RELOAD"
|
||||
|
||||
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import android.view.View.MeasureSpec
|
||||
import android.view.{Gravity, View, Window}
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
|
||||
import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService}
|
||||
import org.xbill.DNS._
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
@@ -137,10 +137,11 @@ object Utils {
|
||||
}
|
||||
|
||||
def startSsService(context: Context) {
|
||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
||||
context.startService(intent)
|
||||
val intent =
|
||||
new Intent(context, if (app.dataStore.isNAT) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService])
|
||||
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
|
||||
}
|
||||
|
||||
def reloadSsService(context: Context): Unit = context.sendBroadcast(new Intent(Action.RELOAD))
|
||||
def stopSsService(context: Context) {
|
||||
val intent = new Intent(Action.CLOSE)
|
||||
context.sendBroadcast(intent)
|
||||
|
||||
@@ -4,7 +4,7 @@ function try () {
|
||||
"$@" || exit -1
|
||||
}
|
||||
|
||||
[ -z "$ANDROID_NDK_HOME" ] && ANDROID_NDK_HOME=~/android-ndk-r12b
|
||||
[ -z "$ANDROID_NDK_HOME" ] && ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
MIN_API=$1
|
||||
@@ -15,13 +15,13 @@ ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm
|
||||
ANDROID_ARM64_TOOLCHAIN=$DEPS/android-toolchain-21-arm64
|
||||
ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-x86
|
||||
|
||||
ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-gcc
|
||||
ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-clang
|
||||
ANDROID_ARM_STRIP=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-strip
|
||||
|
||||
ANDROID_ARM64_CC=$ANDROID_ARM64_TOOLCHAIN/bin/aarch64-linux-android-gcc
|
||||
ANDROID_ARM64_CC=$ANDROID_ARM64_TOOLCHAIN/bin/aarch64-linux-android-clang
|
||||
ANDROID_ARM64_STRIP=$ANDROID_ARM64_TOOLCHAIN/bin/aarch64-linux-android-strip
|
||||
|
||||
ANDROID_X86_CC=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-gcc
|
||||
ANDROID_X86_CC=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-clang
|
||||
ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip
|
||||
|
||||
try mkdir -p $DEPS $TARGET/armeabi-v7a $TARGET/x86 $TARGET/arm64-v8a
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
* 0.0.4:
|
||||
* Enlarge text size of number pickers;
|
||||
* Update support library version to 26.0.0.
|
||||
* 0.0.3:
|
||||
* Update support library version to 25.2.0.
|
||||
* 0.0.2:
|
||||
|
||||
@@ -36,7 +36,7 @@ First you need to add this library to your dependencies. This library is written
|
||||
and it's most convenient to use it with SBT:
|
||||
|
||||
```scala
|
||||
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2"
|
||||
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4"
|
||||
```
|
||||
|
||||
### Native binary configuration
|
||||
|
||||
@@ -2,7 +2,7 @@ enablePlugins(AndroidLib)
|
||||
android.useSupportVectors
|
||||
|
||||
name := "plugin"
|
||||
version := "0.0.3"
|
||||
version := "0.0.4"
|
||||
|
||||
pomExtra in Global := {
|
||||
<url>https://github.com/shadowsocks/shadowsocks-android</url>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
package="com.github.shadowsocks.plugin">
|
||||
<uses-sdk
|
||||
android:minSdkVersion="19"
|
||||
android:targetSdkVersion="25"/>
|
||||
android:targetSdkVersion="26"/>
|
||||
<application
|
||||
android:theme="@style/Theme.Shadowsocks">
|
||||
<meta-data android:name="com.github.shadowsocks.plugin.version"
|
||||
android:value="0.0.2"/>
|
||||
android:value="0.0.4"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -6,4 +6,10 @@
|
||||
|
||||
<!-- feature category -->
|
||||
<string name="feature_cat">"Дополнительные Настройки"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="yes">"Да"</string>
|
||||
<string name="no">"Нет"</string>
|
||||
<string name="unsaved_changes_prompt">"Изменения не сохранены. Сохранить?"</string>
|
||||
<string name="apply">"Применить"</string>
|
||||
</resources>
|
||||
@@ -40,6 +40,6 @@
|
||||
<item name="android:scrollbars">vertical</item>
|
||||
</style>
|
||||
<style name="NumberPickerStyle">
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textSize">18sp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -60,7 +60,8 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P
|
||||
}
|
||||
|
||||
override def setText(text: String): Unit = {
|
||||
val old = getText
|
||||
super.setText(text)
|
||||
notifyChanged()
|
||||
if (old != text) notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
|
||||
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
|
||||
if (oldParent eq view) return
|
||||
if (oldParent != null) oldParent.removeView(editText)
|
||||
val oldEdit = view.findViewById(android.R.id.edit)
|
||||
val oldEdit = view.findViewById[View](android.R.id.edit)
|
||||
if (oldEdit == null) return
|
||||
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
|
||||
if (container == null) return
|
||||
|
||||
@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
|
||||
view.setBackground(background)
|
||||
ViewCompat.setPaddingRelative(view, s, t, e, b)
|
||||
}
|
||||
val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup]
|
||||
val widgetFrame = view.findViewById[ViewGroup](android.R.id.widget_frame)
|
||||
if (widgetFrame != null) {
|
||||
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
|
||||
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||
|
||||
|
||||