Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f19f8d8ffa | ||
|
|
adaca76b12 | ||
|
|
010fd6158f | ||
|
|
8b57b53788 | ||
|
|
1db2d81aca | ||
|
|
258cb0c9c9 | ||
|
|
af057d0740 | ||
|
|
52499f5c2d | ||
|
|
230a018326 | ||
|
|
f950037003 | ||
|
|
d8179b5b17 | ||
|
|
d701fb4cee | ||
|
|
5549dfd20a | ||
|
|
6978b921bb | ||
|
|
ac28b83a0b | ||
|
|
422924f3f4 | ||
|
|
e1ee53567a | ||
|
|
9c36f84b40 | ||
|
|
143afb052d | ||
|
|
27fc27346c | ||
|
|
02c8be58e3 | ||
|
|
2e1564b7b9 | ||
|
|
250373b92a | ||
|
|
dc439b0802 | ||
|
|
49e30e7b09 | ||
|
|
1e750e45e9 | ||
|
|
574f80aefd | ||
|
|
424e1bd1bc | ||
|
|
bbf52c4f1d | ||
|
|
0acfaa12c0 | ||
|
|
1ec80175ea | ||
|
|
2b985686a4 | ||
|
|
6ca3e8abda | ||
|
|
3a935b3274 | ||
|
|
ea848f3b71 | ||
|
|
53af99eb98 | ||
|
|
9617418e70 | ||
|
|
c09a96a203 | ||
|
|
1ad08c6dba |
@@ -5,7 +5,7 @@ jdk:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- NDK_VERSION=r14
|
- NDK_VERSION=r15b
|
||||||
- NDK_CCACHE=ccache
|
- NDK_CCACHE=ccache
|
||||||
- GOROOT_BOOTSTRAP=$GOROOT
|
- GOROOT_BOOTSTRAP=$GOROOT
|
||||||
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
|
- 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}
|
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
|
||||||
|
|
||||||
scala:
|
scala:
|
||||||
- 2.11.8
|
- 2.11.11
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- find $HOME/.sbt -name "*.lock" | xargs rm
|
- find $HOME/.sbt -name "*.lock" | xargs rm
|
||||||
@@ -27,7 +27,7 @@ cache:
|
|||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- tools
|
- tools
|
||||||
- build-tools-25.0.2
|
- build-tools-26.0.0
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
|
|||||||
* SBT 0.13.0+
|
* SBT 0.13.0+
|
||||||
* Go 1.4+
|
* Go 1.4+
|
||||||
* Android SDK
|
* Android SDK
|
||||||
- Build Tools 25+
|
- Build Tools 26+
|
||||||
- Android Support Repository and Google Repository (see `build.sbt` for version)
|
- Android Support Repository and Google Repository (see `build.sbt` for version)
|
||||||
- Android NDK r14+
|
- Android NDK r15+
|
||||||
|
|
||||||
### BUILD
|
### BUILD
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
lazy val commonSettings = Seq(
|
lazy val commonSettings = Seq(
|
||||||
scalaVersion := "2.11.9",
|
scalaVersion := "2.11.11",
|
||||||
dexMaxHeap := "4g",
|
dexMaxHeap := "4g",
|
||||||
|
|
||||||
organization := "com.github.shadowsocks",
|
organization := "com.github.shadowsocks",
|
||||||
|
|
||||||
platformTarget := "android-25",
|
platformTarget := "android-26",
|
||||||
|
|
||||||
compileOrder := CompileOrder.JavaThenScala,
|
compileOrder := CompileOrder.JavaThenScala,
|
||||||
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
|
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
|
||||||
@@ -18,10 +18,12 @@ lazy val commonSettings = Seq(
|
|||||||
shrinkResources := true,
|
shrinkResources := true,
|
||||||
typedResources := false,
|
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("."))
|
lazy val root = Project(id = "shadowsocks-android", base = file("."))
|
||||||
.settings(commonSettings)
|
.settings(commonSettings)
|
||||||
.aggregate(plugin, mobile)
|
.aggregate(plugin, mobile)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
|
|||||||
android.useSupportVectors
|
android.useSupportVectors
|
||||||
|
|
||||||
name := "shadowsocks"
|
name := "shadowsocks"
|
||||||
version := "4.1.8"
|
version := "4.2.0"
|
||||||
versionCode := Some(188)
|
versionCode := Some(190)
|
||||||
|
|
||||||
proguardOptions ++=
|
proguardOptions ++=
|
||||||
"-dontwarn com.google.android.gms.internal.**" ::
|
"-dontwarn com.google.android.gms.internal.**" ::
|
||||||
@@ -14,13 +14,20 @@ proguardOptions ++=
|
|||||||
"-dontwarn okio.**" ::
|
"-dontwarn okio.**" ::
|
||||||
"-dontwarn org.xbill.**" ::
|
"-dontwarn org.xbill.**" ::
|
||||||
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
|
"-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" ::
|
||||||
Nil
|
Nil
|
||||||
|
|
||||||
val playServicesVersion = "10.2.6"
|
val playServicesVersion = "11.0.4"
|
||||||
resolvers += Resolver.jcenterRepo
|
resolvers += Resolver.jcenterRepo
|
||||||
libraryDependencies ++=
|
libraryDependencies ++=
|
||||||
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
|
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
|
||||||
"com.evernote" % "android-job" % "1.1.10" ::
|
"com.evernote" % "android-job" % "1.2.0-alpha3" ::
|
||||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
|
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
|
||||||
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
|
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
|
||||||
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
|
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
|
||||||
@@ -28,15 +35,15 @@ libraryDependencies ++=
|
|||||||
"com.google.firebase" % "firebase-config" % playServicesVersion ::
|
"com.google.firebase" % "firebase-config" % playServicesVersion ::
|
||||||
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
|
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
|
||||||
"com.mikepenz" % "crossfader" % "1.5.0" ::
|
"com.mikepenz" % "crossfader" % "1.5.0" ::
|
||||||
"com.mikepenz" % "fastadapter" % "2.5.2" ::
|
"com.mikepenz" % "fastadapter" % "2.6.2" ::
|
||||||
"com.mikepenz" % "iconics-core" % "2.8.4" ::
|
"com.mikepenz" % "iconics-core" % "2.8.9" ::
|
||||||
"com.mikepenz" % "materialdrawer" % "5.9.1" ::
|
"com.mikepenz" % "materialdrawer" % "5.9.4" ::
|
||||||
"com.mikepenz" % "materialize" % "1.0.1" ::
|
"com.mikepenz" % "materialize" % "1.0.3" ::
|
||||||
"com.squareup.okhttp3" % "okhttp" % "3.8.0" ::
|
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
|
||||||
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
|
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
|
||||||
"dnsjava" % "dnsjava" % "2.1.8" ::
|
"dnsjava" % "dnsjava" % "2.1.8" ::
|
||||||
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
|
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
|
||||||
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
|
"me.dm7.barcodescanner" % "zxing" % "1.9.5" ::
|
||||||
"net.glxn.qrgen" % "android" % "2.0" ::
|
"net.glxn.qrgen" % "android" % "2.0" ::
|
||||||
Nil
|
Nil
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,17 @@
|
|||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="19"
|
android:minSdkVersion="19"
|
||||||
android:targetSdkVersion="25"/>
|
android:targetSdkVersion="26"/>
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ShadowsocksApplication"
|
android:name=".ShadowsocksApplication"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:backupAgent=".ShadowsocksBackupAgent"
|
android:backupAgent=".ShadowsocksBackupAgent"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:banner="@drawable/ic_start_connected">
|
android:banner="@drawable/ic_start_connected">
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.version"
|
<meta-data android:name="com.google.android.gms.version"
|
||||||
@@ -96,18 +100,14 @@
|
|||||||
android:label="@string/quick_toggle"
|
android:label="@string/quick_toggle"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
|
android:process=":bg"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".ShadowsocksRunnerService"
|
|
||||||
android:exported="false">
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".ShadowsocksNatService"
|
android:name=".ShadowsocksNatService"
|
||||||
android:process=":bg"
|
android:process=":bg"
|
||||||
@@ -134,13 +134,13 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".BootReceiver" android:enabled="false">
|
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver">
|
<receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -159,9 +159,6 @@
|
|||||||
android:process=":bg"/>
|
android:process=":bg"/>
|
||||||
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
||||||
android:process=":bg"/>
|
android:process=":bg"/>
|
||||||
|
|
||||||
<!-- Strip useless components -->
|
|
||||||
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" tools:node="remove"/>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -10,7 +10,4 @@ interface IShadowsocksService {
|
|||||||
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
|
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
|
||||||
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
|
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
|
||||||
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
|
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
|
||||||
|
|
||||||
oneway void use(in int profileId);
|
|
||||||
void useSync(in int profileId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.support.v7.widget.AppCompatImageView android:id="@+id/share"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="4dp"
|
android:padding="8dp"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
app:srcCompat="@drawable/ic_social_share"
|
app:srcCompat="@drawable/ic_social_share"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
|
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="4dp"
|
android:padding="8dp"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
app:srcCompat="@drawable/ic_image_edit"
|
app:srcCompat="@drawable/ic_image_edit"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
|||||||
@@ -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 |
@@ -123,7 +123,7 @@
|
|||||||
<string name="route_entry_bypass_lan_chn">"LAN 及び中国本土のアドレスを迂回する"</string>
|
<string name="route_entry_bypass_lan_chn">"LAN 及び中国本土のアドレスを迂回する"</string>
|
||||||
<string name="route_entry_chinalist">"中国本土のアドレス以外を迂回する"</string>
|
<string name="route_entry_chinalist">"中国本土のアドレス以外を迂回する"</string>
|
||||||
<string name="proxied_apps_summary_v21">"選択したアプリにプロキシを設定する"</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>
|
<string name="acl_rule_templates_domain">"ドメイン及び全てのサブドメイン"</string>
|
||||||
|
|
||||||
<!-- plugin -->
|
<!-- plugin -->
|
||||||
|
|||||||
@@ -45,4 +45,4 @@
|
|||||||
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s < 3.7.1"</string>
|
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s < 3.7.1"</string>
|
||||||
<string name="udp_dns">"DNS 포워딩"</string>
|
<string name="udp_dns">"DNS 포워딩"</string>
|
||||||
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
|
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
<string name="route_entry_bypass_lan_chn">"Все, кроме LAN и Китая"</string>
|
<string name="route_entry_bypass_lan_chn">"Все, кроме LAN и Китая"</string>
|
||||||
<string name="route_entry_chinalist">"Список Китай"</string>
|
<string name="route_entry_chinalist">"Список Китай"</string>
|
||||||
<string name="proxied_apps_summary_v21">"Установить прокси для выбранных приложений"</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>
|
<string name="acl_rule_templates_domain">"Доменное имя и все его поддомены"</string>
|
||||||
|
|
||||||
<!-- plugin -->
|
<!-- plugin -->
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
<string name="route_entry_bypass_lan_chn">"绕过局域网及中国大陆地址"</string>
|
<string name="route_entry_bypass_lan_chn">"绕过局域网及中国大陆地址"</string>
|
||||||
<string name="route_entry_chinalist">"仅代理中国大陆地址"</string>
|
<string name="route_entry_chinalist">"仅代理中国大陆地址"</string>
|
||||||
<string name="proxied_apps_summary_v21">"为应用程序分别设置代理"</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>
|
<string name="acl_rule_templates_domain">"域名及其子域名"</string>
|
||||||
|
|
||||||
<!-- plugin -->
|
<!-- plugin -->
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
<string name="route_entry_bypass_lan_chn">"略過區域網路及中國大陸"</string>
|
<string name="route_entry_bypass_lan_chn">"略過區域網路及中國大陸"</string>
|
||||||
<string name="route_entry_chinalist">"China List"</string>
|
<string name="route_entry_chinalist">"China List"</string>
|
||||||
<string name="proxied_apps_summary_v21">"為已選擇的應用程式設定 Proxy"</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>
|
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
|
||||||
|
|
||||||
<!-- plugin -->
|
<!-- plugin -->
|
||||||
|
|||||||
@@ -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>
|
<string name="udp_dns_summary">Forward all DNS requests to remote</string>
|
||||||
|
|
||||||
<!-- notification category -->
|
<!-- 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="forward_success">Shadowsocks started.</string>
|
||||||
<string name="invalid_server">Invalid server name</string>
|
<string name="invalid_server">Invalid server name</string>
|
||||||
<string name="service_failed">Failed to connect the remote server</string>
|
<string name="service_failed">Failed to connect the remote server</string>
|
||||||
@@ -130,7 +132,7 @@
|
|||||||
<string name="custom_rules">Custom rules</string>
|
<string name="custom_rules">Custom rules</string>
|
||||||
<string name="action_selection">Selection…</string>
|
<string name="action_selection">Selection…</string>
|
||||||
<string name="action_add_rule">Add rule(s)…</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="acl_rule_templates_domain">Domain name and all its subdomain names</string>
|
||||||
<string name="edit_rule">Edit rule</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) {
|
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
|
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.loadUrl("file:///android_asset/pages/about.html")
|
||||||
web.setWebViewClient(new WebViewClient() {
|
web.setWebViewClient(new WebViewClient() {
|
||||||
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
|
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
|||||||
import AppManager._
|
import AppManager._
|
||||||
|
|
||||||
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
|
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 icon = itemView.findViewById[ImageView](R.id.itemicon)
|
||||||
private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
|
private val check = itemView.findViewById[Switch](R.id.itemcheck)
|
||||||
private var item: ProxiedApp = _
|
private var item: ProxiedApp = _
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
||||||
|
|
||||||
@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
|||||||
proxiedApps.add(item.packageName)
|
proxiedApps.add(item.packageName)
|
||||||
check.setChecked(true)
|
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 val appsLoading = new AtomicBoolean
|
||||||
private var handler: Handler = _
|
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]
|
proxiedApps = str.split('\n').to[mutable.HashSet]
|
||||||
|
|
||||||
override def onDestroy() {
|
override def onDestroy() {
|
||||||
@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
|||||||
case R.id.action_apply_all =>
|
case R.id.action_apply_all =>
|
||||||
app.profileManager.getAllProfiles match {
|
app.profileManager.getAllProfiles match {
|
||||||
case Some(profiles) =>
|
case Some(profiles) =>
|
||||||
val proxiedAppString = app.settings.getString(Key.individual, null)
|
val proxiedAppString = app.dataStore.individual
|
||||||
profiles.foreach(p => {
|
profiles.foreach(p => {
|
||||||
p.individual = proxiedAppString
|
p.individual = proxiedAppString
|
||||||
app.profileManager.updateProfile(p)
|
app.profileManager.updateProfile(p)
|
||||||
@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
case R.id.action_export =>
|
case R.id.action_export =>
|
||||||
val bypass = app.settings.getBoolean(Key.bypass, false)
|
clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
|
||||||
val proxiedAppString = app.settings.getString(Key.individual, null)
|
app.dataStore.bypass + "\n" + app.dataStore.individual))
|
||||||
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||||
return true
|
return true
|
||||||
case R.id.action_import =>
|
case R.id.action_import =>
|
||||||
@@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
|
|||||||
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
|
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
|
||||||
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
|
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
|
||||||
bypassSwitch.setChecked(enabled.toBoolean)
|
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()
|
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||||
appListView.setVisibility(View.GONE)
|
appListView.setVisibility(View.GONE)
|
||||||
fastScroller.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.inflateMenu(R.menu.app_manager_menu)
|
||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
if (!app.settings.getBoolean(Key.proxyApps, false))
|
if (!app.dataStore.proxyApps) {
|
||||||
app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply()
|
app.dataStore.proxyApps = true
|
||||||
|
app.dataStore.dirty = true
|
||||||
|
}
|
||||||
findViewById(R.id.onSwitch).asInstanceOf[Switch]
|
findViewById(R.id.onSwitch).asInstanceOf[Switch]
|
||||||
.setOnCheckedChangeListener((_, checked) => {
|
.setOnCheckedChangeListener((_, checked) => {
|
||||||
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply()
|
app.dataStore.proxyApps = checked
|
||||||
|
app.dataStore.dirty = true
|
||||||
finish()
|
finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
|
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
|
||||||
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
|
bypassSwitch.setChecked(app.dataStore.bypass)
|
||||||
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
|
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
|
||||||
app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply())
|
app.dataStore.bypass = checked
|
||||||
|
app.dataStore.dirty = true
|
||||||
|
})
|
||||||
|
|
||||||
initProxiedApps()
|
initProxiedApps()
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|||||||
@@ -21,15 +21,14 @@
|
|||||||
package com.github.shadowsocks
|
package com.github.shadowsocks
|
||||||
|
|
||||||
import java.io.{File, IOException}
|
import java.io.{File, IOException}
|
||||||
|
import java.net.{Inet6Address, InetAddress}
|
||||||
import java.util
|
import java.util
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.{Timer, TimerTask}
|
import java.util.{Timer, TimerTask}
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.Inet6Address
|
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
import android.content._
|
||||||
import android.os.{Handler, IBinder, RemoteCallbackList}
|
import android.os.{Build, Handler, IBinder, RemoteCallbackList}
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -65,9 +64,12 @@ trait BaseService extends Service {
|
|||||||
lazy val handler = new Handler(getMainLooper)
|
lazy val handler = new Handler(getMainLooper)
|
||||||
lazy val restartHanlder = new Handler(getMainLooper)
|
lazy val restartHanlder = new Handler(getMainLooper)
|
||||||
|
|
||||||
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
|
private var notification: ShadowsocksNotification = _
|
||||||
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
|
||||||
stopRunner(stopService = true)
|
case Action.RELOAD => forceLoad()
|
||||||
|
case _ =>
|
||||||
|
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||||
|
stopRunner(stopService = true)
|
||||||
}
|
}
|
||||||
var closeReceiverRegistered: Boolean = _
|
var closeReceiverRegistered: Boolean = _
|
||||||
|
|
||||||
@@ -103,20 +105,6 @@ trait BaseService extends Service {
|
|||||||
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
||||||
callbacks.unregister(cb)
|
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)) {
|
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
||||||
@@ -124,9 +112,18 @@ trait BaseService extends Service {
|
|||||||
false
|
false
|
||||||
} else true
|
} else true
|
||||||
|
|
||||||
def connect() {
|
def forceLoad(): Unit = app.currentProfile.orNull match {
|
||||||
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
|
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") {
|
if (profile.host == "198.199.101.152") {
|
||||||
val client = new OkHttpClient.Builder()
|
val client = new OkHttpClient.Builder()
|
||||||
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
|
.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
|
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
|
plugin = new PluginConfiguration(profile.plugin).selectedOptions
|
||||||
pluginPath = PluginManager.init(plugin)
|
pluginPath = PluginManager.init(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
def startRunner(profile: Profile) {
|
def createNotification(): ShadowsocksNotification
|
||||||
this.profile = profile
|
def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
|
||||||
|
else startService(new Intent(this, getClass))
|
||||||
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 stopRunner(stopService: Boolean, msg: String = null) {
|
def stopRunner(stopService: Boolean, msg: String = null) {
|
||||||
// clean up recevier
|
// clean up recevier
|
||||||
@@ -198,6 +168,8 @@ trait BaseService extends Service {
|
|||||||
closeReceiverRegistered = false
|
closeReceiverRegistered = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notification != null) notification.destroy()
|
||||||
|
|
||||||
// Make sure update total traffic when stopping the runner
|
// Make sure update total traffic when stopping the runner
|
||||||
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||||
|
|
||||||
@@ -276,7 +248,49 @@ trait BaseService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Service of shadowsocks should always be started explicitly
|
// 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) {
|
protected def changeState(s: Int, msg: String = null) {
|
||||||
val handler = new Handler(getMainLooper)
|
val handler = new Handler(getMainLooper)
|
||||||
@@ -341,12 +355,21 @@ trait BaseService extends Service {
|
|||||||
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
|
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
|
||||||
case (dns, i) => makeDns("UserDef-" + i, dns.trim)
|
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 {
|
profile.route match {
|
||||||
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
|
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
|
||||||
.put("PrimaryDNS", new JSONArray(Array(
|
.put("PrimaryDNS", localDns)
|
||||||
makeDns("Primary-1", "119.29.29.29", edns = false),
|
|
||||||
makeDns("Primary-2", "114.114.114.114", edns = false)
|
|
||||||
)))
|
|
||||||
.put("AlternativeDNS", remoteDns)
|
.put("AlternativeDNS", remoteDns)
|
||||||
.put("IPNetworkFile", "china_ip_list.txt")
|
.put("IPNetworkFile", "china_ip_list.txt")
|
||||||
.put("DomainFile", "gfwlist.txt")
|
.put("DomainFile", "gfwlist.txt")
|
||||||
|
|||||||
@@ -20,27 +20,24 @@
|
|||||||
|
|
||||||
package com.github.shadowsocks
|
package com.github.shadowsocks
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v14.preference.SwitchPreference
|
import android.support.v14.preference.SwitchPreference
|
||||||
|
import android.support.v7.preference.PreferenceDataStore
|
||||||
import be.mygod.preference.PreferenceFragment
|
import be.mygod.preference.PreferenceFragment
|
||||||
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
|
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
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) {
|
override def onCreatePreferences(bundle: Bundle, key: String) {
|
||||||
|
getPreferenceManager.setPreferenceDataStore(app.dataStore)
|
||||||
addPreferencesFromResource(R.xml.pref_global)
|
addPreferencesFromResource(R.xml.pref_global)
|
||||||
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
|
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
|
||||||
switch.setOnPreferenceChangeListener((_, value) => {
|
switch.setOnPreferenceChangeListener((_, value) => {
|
||||||
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
|
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
|
|
||||||
BootReceiver.setEnabled(getActivity, true)
|
|
||||||
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
|
|
||||||
}
|
|
||||||
switch.setChecked(BootReceiver.getEnabled(getActivity))
|
switch.setChecked(BootReceiver.getEnabled(getActivity))
|
||||||
|
|
||||||
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
|
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
|
||||||
@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha
|
|||||||
tfo.setEnabled(false)
|
tfo.setEnabled(false)
|
||||||
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
|
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() {
|
override def onDestroy() {
|
||||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
app.dataStore.unregisterChangeListener(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
|
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
|
||||||
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false))
|
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false))
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import java.util.Locale
|
|||||||
|
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.app.{Activity, ProgressDialog}
|
import android.app.{Activity, ProgressDialog}
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
import android.content._
|
import android.content._
|
||||||
import android.net.{Uri, VpnService}
|
import android.net.{Uri, VpnService}
|
||||||
import android.nfc.{NdefMessage, NfcAdapter}
|
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.v4.content.ContextCompat
|
||||||
import android.support.v7.app.AlertDialog
|
import android.support.v7.app.AlertDialog
|
||||||
import android.support.v7.content.res.AppCompatResources
|
import android.support.v7.content.res.AppCompatResources
|
||||||
|
import android.support.v7.preference.PreferenceDataStore
|
||||||
import android.support.v7.widget.RecyclerView.ViewHolder
|
import android.support.v7.widget.RecyclerView.ViewHolder
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle
|
|||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
|
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||||
|
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
|
||||||
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
|
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
|
||||||
import com.github.shadowsocks.utils._
|
import com.github.shadowsocks.utils._
|
||||||
import com.mikepenz.crossfader.Crossfader
|
import com.mikepenz.crossfader.Crossfader
|
||||||
@@ -67,7 +68,7 @@ object MainActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
|
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
|
||||||
with OnSharedPreferenceChangeListener {
|
with OnPreferenceDataStoreChangeListener {
|
||||||
import MainActivity._
|
import MainActivity._
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
|||||||
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
|
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
|
||||||
|
|
||||||
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
|
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(() => {
|
override def binderDied(): Unit = handler.post(() => {
|
||||||
detachService()
|
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 {
|
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")
|
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()
|
} else drawer = drawerBuilder.build()
|
||||||
|
|
||||||
val header = drawer.getHeader
|
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")
|
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
|
||||||
if (tf != null) title.setTypeface(tf)
|
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]
|
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
|
||||||
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
|
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
|
||||||
rxRateText = findViewById(R.id.rxRate).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
|
testCount += 1
|
||||||
statusText.setText(R.string.connection_test_testing)
|
statusText.setText(R.string.connection_test_testing)
|
||||||
val id = testCount // it would change by other code
|
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]
|
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
|
||||||
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
|
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
|
||||||
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
|
fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
|
||||||
if (app.isNatEnabled) bgService.use(app.profileId) else {
|
if (app.isNatEnabled) Utils.startSsService(this) else {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
|
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
|
||||||
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
|
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
|
changeState(State.IDLE) // reset everything to init state
|
||||||
handler.post(() => attachService(callback))
|
handler.post(() => attachService(callback))
|
||||||
app.settings.registerOnSharedPreferenceChangeListener(this)
|
app.dataStore.registerChangeListener(this)
|
||||||
|
|
||||||
val intent = getIntent
|
val intent = getIntent
|
||||||
if (intent != null) handleShareIntent(intent)
|
if (intent != null) handleShareIntent(intent)
|
||||||
@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
|
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
|
||||||
case Key.isNAT => handler.post(() => {
|
case Key.isNAT => handler.post(() => {
|
||||||
detachService()
|
detachService()
|
||||||
attachService(callback)
|
attachService(callback)
|
||||||
@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
|||||||
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
|
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
|
||||||
case DRAWER_RECOVERY =>
|
case DRAWER_RECOVERY =>
|
||||||
app.track("GlobalConfigFragment", "reset")
|
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 dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
|
||||||
val handler = new Handler {
|
val handler = new Handler {
|
||||||
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
|
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
|
||||||
@@ -424,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
|||||||
|
|
||||||
override def onDestroy() {
|
override def onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
app.dataStore.unregisterChangeListener(this)
|
||||||
detachService()
|
detachService()
|
||||||
new BackupManager(this).dataChanged()
|
new BackupManager(this).dataChanged()
|
||||||
handler.removeCallbacksAndMessages(null)
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity {
|
|||||||
toolbar.setOnMenuItemClickListener(child)
|
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)
|
.setTitle(R.string.unsaved_changes_prompt)
|
||||||
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
|
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
|
||||||
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
|
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
package com.github.shadowsocks
|
package com.github.shadowsocks
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
|
||||||
import android.content._
|
import android.content._
|
||||||
import android.os.{Build, Bundle, UserManager}
|
import android.os.{Build, Bundle, UserManager}
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v14.preference.SwitchPreference
|
import android.support.v14.preference.SwitchPreference
|
||||||
import android.support.v7.app.AlertDialog
|
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.support.v7.widget.Toolbar.OnMenuItemClickListener
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment}
|
|||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
import com.github.shadowsocks.database.Profile
|
import com.github.shadowsocks.database.Profile
|
||||||
import com.github.shadowsocks.plugin._
|
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}
|
import com.github.shadowsocks.utils.{Action, Key, Utils}
|
||||||
|
|
||||||
object ProfileConfigFragment {
|
object ProfileConfigFragment {
|
||||||
@@ -43,7 +42,7 @@ object ProfileConfigFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
|
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
|
||||||
with OnSharedPreferenceChangeListener {
|
with OnPreferenceDataStoreChangeListener {
|
||||||
import ProfileConfigFragment._
|
import ProfileConfigFragment._
|
||||||
|
|
||||||
private var profile: Profile = _
|
private var profile: Profile = _
|
||||||
@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
private var pluginConfiguration: PluginConfiguration = _
|
private var pluginConfiguration: PluginConfiguration = _
|
||||||
|
|
||||||
override def onCreatePreferences(bundle: Bundle, key: String) {
|
override def onCreatePreferences(bundle: Bundle, key: String) {
|
||||||
|
getPreferenceManager.setPreferenceDataStore(app.dataStore)
|
||||||
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
|
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
|
||||||
case Some(p) =>
|
case Some(p) =>
|
||||||
profile = p
|
profile = p
|
||||||
profile.serialize(app.editor).apply()
|
profile.serialize(app.dataStore)
|
||||||
case None => getActivity.finish()
|
case None => getActivity.finish()
|
||||||
}
|
}
|
||||||
addPreferencesFromResource(R.xml.pref_profile)
|
addPreferencesFromResource(R.xml.pref_profile)
|
||||||
@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
plugin.setOnPreferenceChangeListener((_, value) => {
|
plugin.setOnPreferenceChangeListener((_, value) => {
|
||||||
val selected = value.asInstanceOf[String]
|
val selected = value.asInstanceOf[String]
|
||||||
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
|
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.setEnabled(!TextUtils.isEmpty(selected))
|
||||||
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
|
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
|
||||||
if (!PluginManager.fetchPlugins()(selected).trusted)
|
if (!PluginManager.fetchPlugins()(selected).trusted)
|
||||||
@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
|
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
|
||||||
initPlugins()
|
initPlugins()
|
||||||
app.listenForPackageChanges(initPlugins())
|
app.listenForPackageChanges(initPlugins())
|
||||||
app.settings.registerOnSharedPreferenceChangeListener(this)
|
app.dataStore.registerChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
def initPlugins() {
|
def initPlugins() {
|
||||||
@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
|
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
|
||||||
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
|
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
|
||||||
plugin.entryPackageNames = plugins.map(_._2.packageName).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.setValue(pluginConfiguration.selected)
|
||||||
plugin.init()
|
plugin.init()
|
||||||
plugin.checkSummary()
|
plugin.checkSummary()
|
||||||
@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
val selected = pluginConfiguration.selected
|
val selected = pluginConfiguration.selected
|
||||||
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
|
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
|
||||||
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
|
(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
|
true
|
||||||
} catch {
|
} catch {
|
||||||
case exc: IllegalArgumentException =>
|
case exc: IllegalArgumentException =>
|
||||||
@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
|
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
|
||||||
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
|
if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
|
||||||
|
|
||||||
override def onDestroy() {
|
override def onDestroy() {
|
||||||
app.settings.unregisterOnSharedPreferenceChangeListener(this)
|
app.dataStore.unregisterChangeListener(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onResume() {
|
override def onResume() {
|
||||||
super.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() {
|
private def showPluginEditor() {
|
||||||
@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
|
|||||||
}
|
}
|
||||||
|
|
||||||
def saveAndExit() {
|
def saveAndExit() {
|
||||||
profile.deserialize(app.settings)
|
profile.deserialize(app.dataStore)
|
||||||
app.profileManager.updateProfile(profile)
|
app.profileManager.updateProfile(profile)
|
||||||
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
|
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
|
||||||
getActivity.finish()
|
getActivity.finish()
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
|||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
|
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 State.STOPPED => true
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
|||||||
|
|
||||||
var item: Profile = _
|
var item: Profile = _
|
||||||
|
|
||||||
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
private val text1 = itemView.findViewById[TextView](android.R.id.text1)
|
||||||
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
|
private val text2 = itemView.findViewById[TextView](android.R.id.text2)
|
||||||
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
|
private val traffic = itemView.findViewById[TextView](R.id.traffic)
|
||||||
private val edit = itemView.findViewById(R.id.edit)
|
private val edit = itemView.findViewById[View](R.id.edit)
|
||||||
edit.setOnClickListener(_ => startConfig(item.id))
|
edit.setOnClickListener(_ => startConfig(item.id))
|
||||||
edit.setOnLongClickListener(cardButtonLongClickListener)
|
edit.setOnLongClickListener(cardButtonLongClickListener)
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
||||||
// it will not take effect unless set in code
|
// 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 = _
|
private var adView: NativeExpressAdView = _
|
||||||
|
|
||||||
{
|
{
|
||||||
val share = itemView.findViewById(R.id.share)
|
val share = itemView.findViewById[View](R.id.share)
|
||||||
share.setOnClickListener(_ => {
|
share.setOnClickListener(_ => {
|
||||||
val popup = new PopupMenu(getActivity, share)
|
val popup = new PopupMenu(getActivity, share)
|
||||||
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
|
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)))
|
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.id == app.profileId) {
|
if (item.id == app.dataStore.profileId) {
|
||||||
itemView.setSelected(true)
|
itemView.setSelected(true)
|
||||||
selectedItem = this
|
selectedItem = this
|
||||||
} else {
|
} else {
|
||||||
@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
|||||||
adView.setLayoutParams(params)
|
adView.setLayoutParams(params)
|
||||||
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
|
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
|
||||||
adView.setAdSize(new AdSize(328, 132))
|
adView.setAdSize(new AdSize(328, 132))
|
||||||
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
|
itemView.findViewById[LinearLayout](R.id.content).addView(adView)
|
||||||
|
|
||||||
// Demographics
|
// Demographics
|
||||||
val random = new Random()
|
val random = new Random()
|
||||||
@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
|||||||
|
|
||||||
def onClick(v: View): Unit = if (isEnabled) {
|
def onClick(v: View): Unit = if (isEnabled) {
|
||||||
val activity = getActivity.asInstanceOf[MainActivity]
|
val activity = getActivity.asInstanceOf[MainActivity]
|
||||||
val old = app.profileId
|
val old = app.dataStore.profileId
|
||||||
app.switchProfile(item.id)
|
app.switchProfile(item.id)
|
||||||
profilesAdapter.refreshId(old)
|
profilesAdapter.refreshId(old)
|
||||||
itemView.setSelected(true)
|
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 {
|
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
|
||||||
@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
profiles.remove(index)
|
profiles.remove(index)
|
||||||
notifyItemRemoved(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.inflateMenu(R.menu.profile_manager_menu)
|
||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
|
if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
|
||||||
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
|
val profilesList = view.findViewById[RecyclerView](R.id.list)
|
||||||
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
|
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
|
||||||
profilesList.setLayoutManager(layoutManager)
|
profilesList.setLayoutManager(layoutManager)
|
||||||
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
|
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))
|
}.getOrElse(-1))
|
||||||
val animator = new DefaultItemAnimator()
|
val animator = new DefaultItemAnimator()
|
||||||
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
|
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ShortcutManager
|
import android.content.pm.ShortcutManager
|
||||||
import android.os.{Build, Bundle}
|
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}
|
import com.github.shadowsocks.utils.{State, Utils}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
getIntent.getAction match {
|
getIntent.getAction match {
|
||||||
case Intent.ACTION_CREATE_SHORTCUT =>
|
case Intent.ACTION_CREATE_SHORTCUT =>
|
||||||
setResult(Activity.RESULT_OK, new Intent()
|
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
|
||||||
.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut]))
|
new ShortcutInfoCompat.Builder(this, "toggle")
|
||||||
.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle))
|
.setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
|
||||||
.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
|
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
|
||||||
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher)))
|
.setShortLabel(getString(R.string.quick_toggle))
|
||||||
|
.build()))
|
||||||
finish()
|
finish()
|
||||||
case _ =>
|
case _ =>
|
||||||
attachService()
|
attachService()
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException}
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.{Application, NotificationChannel, NotificationManager}
|
||||||
import android.content._
|
import android.content._
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.{Build, LocaleList}
|
import android.os.{Build, LocaleList}
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import android.support.v7.app.AppCompatDelegate
|
import android.support.v7.app.AppCompatDelegate
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
import com.github.shadowsocks.acl.DonaldTrump
|
import com.github.shadowsocks.acl.DonaldTrump
|
||||||
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
|
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.CloseUtils._
|
||||||
import com.github.shadowsocks.utils._
|
import com.github.shadowsocks.utils._
|
||||||
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
|
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 com.j256.ormlite.logger.LocalLog
|
||||||
import eu.chainfire.libsuperuser.Shell
|
import eu.chainfire.libsuperuser.Shell
|
||||||
|
|
||||||
|
import scala.collection.JavaConversions._
|
||||||
import scala.collection.mutable.ArrayBuffer
|
import scala.collection.mutable.ArrayBuffer
|
||||||
|
|
||||||
object ShadowsocksApplication {
|
object ShadowsocksApplication {
|
||||||
@@ -59,13 +60,13 @@ object ShadowsocksApplication {
|
|||||||
class ShadowsocksApplication extends Application {
|
class ShadowsocksApplication extends Application {
|
||||||
import ShadowsocksApplication._
|
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 tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
|
||||||
lazy val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
private lazy val dbHelper = new DBHelper(this)
|
||||||
lazy val editor: SharedPreferences.Editor = settings.edit
|
lazy val profileManager = new ProfileManager(dbHelper)
|
||||||
lazy val profileManager = new ProfileManager(new DBHelper(this))
|
lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
|
||||||
|
|
||||||
def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false)
|
def isNatEnabled: Boolean = dataStore.isNAT
|
||||||
def isVpnEnabled: Boolean = !isNatEnabled
|
def isVpnEnabled: Boolean = !isNatEnabled
|
||||||
|
|
||||||
// send event
|
// send event
|
||||||
@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application {
|
|||||||
.setFatal(false)
|
.setFatal(false)
|
||||||
.build())
|
.build())
|
||||||
|
|
||||||
def profileId: Int = settings.getInt(Key.id, 0)
|
def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
|
||||||
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
|
|
||||||
def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
|
|
||||||
|
|
||||||
def switchProfile(id: Int): Profile = {
|
def switchProfile(id: Int): Profile = {
|
||||||
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
|
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
|
||||||
profileId(result.id)
|
dataStore.profileId = result.id
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +152,12 @@ class ShadowsocksApplication extends Application {
|
|||||||
|
|
||||||
JobManager.create(this).addJobCreator(DonaldTrump)
|
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() {
|
def crashRecovery() {
|
||||||
@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application {
|
|||||||
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
|
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
|
||||||
IOUtils.copy(in, 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 = {
|
def listenForPackageChanges(callback: => Unit): BroadcastReceiver = {
|
||||||
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
|||||||
@@ -20,26 +20,11 @@
|
|||||||
|
|
||||||
package com.github.shadowsocks
|
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.acl.Acl
|
||||||
import com.github.shadowsocks.database.DBHelper
|
import com.github.shadowsocks.database.DBHelper
|
||||||
|
|
||||||
class ShadowsocksBackupAgent extends BackupAgentHelper {
|
class ShadowsocksBackupAgent extends BackupAgentHelper {
|
||||||
|
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
|
||||||
// The names of the SharedPreferences groups that the application maintains. These
|
new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
|
||||||
// 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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ import java.io.File
|
|||||||
import java.net.{Inet6Address, InetAddress}
|
import java.net.{Inet6Address, InetAddress}
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
import android.content._
|
import android.content._
|
||||||
import android.os._
|
import android.os._
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
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.database.Profile
|
||||||
import com.github.shadowsocks.utils._
|
import com.github.shadowsocks.utils._
|
||||||
import eu.chainfire.libsuperuser.Shell
|
import eu.chainfire.libsuperuser.Shell
|
||||||
@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
|
|||||||
val CMD_IPTABLES_DNAT_ADD_SOCKS =
|
val CMD_IPTABLES_DNAT_ADD_SOCKS =
|
||||||
"iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123"
|
"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()
|
val myUid: Int = android.os.Process.myUid()
|
||||||
|
|
||||||
var sslocalProcess: GuardedProcess = _
|
var sslocalProcess: GuardedProcess = _
|
||||||
@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService {
|
|||||||
su.addCommand((init_sb ++ http_sb).toArray)
|
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, _) =>
|
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) {
|
if (su != null) {
|
||||||
su.close()
|
su.close()
|
||||||
su = null
|
su = null
|
||||||
}
|
}
|
||||||
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
|
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
|
||||||
})
|
})
|
||||||
|
Service.START_NOT_STICKY
|
||||||
|
} else super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
override def connect() {
|
override def connect() {
|
||||||
super.connect()
|
super.connect()
|
||||||
@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService {
|
|||||||
AclSyncJob.schedule(profile.route)
|
AclSyncJob.schedule(profile.route)
|
||||||
|
|
||||||
changeState(State.CONNECTED)
|
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
|
// channge the state
|
||||||
changeState(State.STOPPING)
|
changeState(State.STOPPING)
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager}
|
|||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import android.support.v4.app.NotificationCompat.BigTextStyle
|
import android.support.v4.app.NotificationCompat.BigTextStyle
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v4.os.BuildCompat
|
||||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
|
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
|
||||||
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
|
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mygod
|
* @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 val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
|
||||||
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
||||||
private lazy val callback = new Stub {
|
private lazy val callback = new Stub {
|
||||||
@@ -52,7 +54,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
|
|||||||
private var lockReceiver: BroadcastReceiver = _
|
private var lockReceiver: BroadcastReceiver = _
|
||||||
private var callbackRegistered: Boolean = _
|
private var callbackRegistered: Boolean = _
|
||||||
|
|
||||||
private val builder = new NotificationCompat.Builder(service)
|
private val builder = new NotificationCompat.Builder(service, channel)
|
||||||
.setWhen(0)
|
.setWhen(0)
|
||||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||||
.setTicker(service.getString(R.string.forward_success))
|
.setTicker(service.getString(R.string.forward_success))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.net.VpnService
|
|||||||
import android.os.{Bundle, Handler}
|
import android.os.{Bundle, Handler}
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
|
import com.github.shadowsocks.utils.Utils
|
||||||
|
|
||||||
object ShadowsocksRunnerActivity {
|
object ShadowsocksRunnerActivity {
|
||||||
private final val TAG = "ShadowsocksRunnerActivity"
|
private final val TAG = "ShadowsocksRunnerActivity"
|
||||||
@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
|||||||
|
|
||||||
def startBackgroundService() {
|
def startBackgroundService() {
|
||||||
if (app.isNatEnabled) {
|
if (app.isNatEnabled) {
|
||||||
bgService.use(app.profileId)
|
Utils.startSsService(this)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
|
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
|
||||||
@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
|||||||
} else {
|
} else {
|
||||||
attachService()
|
attachService()
|
||||||
}
|
}
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onDestroy() {
|
override def onDestroy() {
|
||||||
@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
|
|||||||
|
|
||||||
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||||
resultCode match {
|
resultCode match {
|
||||||
case Activity.RESULT_OK =>
|
case Activity.RESULT_OK => Utils.startSsService(this)
|
||||||
if (bgService != null) {
|
|
||||||
bgService.use(app.profileId)
|
|
||||||
}
|
|
||||||
case _ =>
|
case _ =>
|
||||||
Log.e(TAG, "Failed to start VpnService")
|
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.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
import android.content._
|
import android.content._
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
@@ -30,7 +31,6 @@ import android.os._
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
|
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
|
||||||
import com.github.shadowsocks.database.Profile
|
|
||||||
import com.github.shadowsocks.utils._
|
import com.github.shadowsocks.utils._
|
||||||
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
import scala.collection.mutable.ArrayBuffer
|
||||||
@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
|||||||
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
|
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
|
||||||
var conn: ParcelFileDescriptor = _
|
var conn: ParcelFileDescriptor = _
|
||||||
var vpnThread: ShadowsocksVpnThread = _
|
var vpnThread: ShadowsocksVpnThread = _
|
||||||
private var notification: ShadowsocksNotification = _
|
|
||||||
|
|
||||||
var sslocalProcess: GuardedProcess = _
|
var sslocalProcess: GuardedProcess = _
|
||||||
var overtureProcess: GuardedProcess = _
|
var overtureProcess: GuardedProcess = _
|
||||||
@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
|||||||
vpnThread = null
|
vpnThread = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification != null) notification.destroy()
|
|
||||||
|
|
||||||
// channge the state
|
// channge the state
|
||||||
changeState(State.STOPPING)
|
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
|
// ensure the VPNService is prepared
|
||||||
if (VpnService.prepare(this) != null) {
|
if (VpnService.prepare(this) != null) {
|
||||||
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
|
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
|
||||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
stopRunner(stopService = true)
|
stopRunner(stopService = true)
|
||||||
return
|
Service.START_NOT_STICKY
|
||||||
}
|
} else super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
super.startRunner(profile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn")
|
||||||
|
|
||||||
override def connect() {
|
override def connect() {
|
||||||
super.connect()
|
super.connect()
|
||||||
|
|
||||||
@@ -137,8 +133,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
|
|||||||
|
|
||||||
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
|
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
|
||||||
AclSyncJob.schedule(profile.route)
|
AclSyncJob.schedule(profile.route)
|
||||||
|
|
||||||
notification = new ShadowsocksNotification(this, profile.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called when the activity is first created. */
|
/** Called when the activity is first created. */
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity {
|
|||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
}
|
}
|
||||||
private var item: Profile = _
|
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)
|
itemView.setOnClickListener(this)
|
||||||
|
|
||||||
def bindDefault() {
|
def bindDefault() {
|
||||||
|
|||||||
@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app
|
|||||||
class TaskerReceiver extends BroadcastReceiver {
|
class TaskerReceiver extends BroadcastReceiver {
|
||||||
override def onReceive(context: Context, intent: Intent) {
|
override def onReceive(context: Context, intent: Intent) {
|
||||||
val settings = TaskerSettings.fromIntent(intent)
|
val settings = TaskerSettings.fromIntent(intent)
|
||||||
|
var changed = false
|
||||||
app.profileManager.getProfile(settings.profileId) match {
|
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 _ =>
|
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) {
|
override def onViewCreated(view: View, savedInstanceState: Bundle) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
toolbar = view.findViewById[Toolbar](R.id.toolbar)
|
||||||
val activity = getActivity.asInstanceOf[MainActivity]
|
val activity = getActivity.asInstanceOf[MainActivity]
|
||||||
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
|
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.github.shadowsocks.acl
|
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.ShadowsocksApplication.app
|
||||||
import com.github.shadowsocks.utils.IOUtils
|
import com.github.shadowsocks.utils.IOUtils
|
||||||
@@ -22,25 +22,10 @@ class Acl {
|
|||||||
val bypassHostnames = new mutable.SortedList[String]()
|
val bypassHostnames = new mutable.SortedList[String]()
|
||||||
val proxyHostnames = new mutable.SortedList[String]()
|
val proxyHostnames = new mutable.SortedList[String]()
|
||||||
val subnets = new mutable.SortedList[Subnet]()
|
val subnets = new mutable.SortedList[Subnet]()
|
||||||
|
val urls = new mutable.SortedList[String]()
|
||||||
@DatabaseField
|
@DatabaseField
|
||||||
var bypass: Boolean = _
|
var bypass: Boolean = _
|
||||||
|
|
||||||
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
|
|
||||||
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
|
|
||||||
def getSubnetsString: String = subnets.mkString("\n")
|
|
||||||
def setBypassHostnamesString(value: String) {
|
|
||||||
bypassHostnames.clear()
|
|
||||||
bypassHostnames ++= value.split("\n")
|
|
||||||
}
|
|
||||||
def setProxyHostnamesString(value: String) {
|
|
||||||
proxyHostnames.clear()
|
|
||||||
proxyHostnames ++= value.split("\n")
|
|
||||||
}
|
|
||||||
def setSubnetsString(value: String) {
|
|
||||||
subnets.clear()
|
|
||||||
subnets ++= value.split("\n").map(Subnet.fromString)
|
|
||||||
}
|
|
||||||
|
|
||||||
def fromAcl(other: Acl): Acl = {
|
def fromAcl(other: Acl): Acl = {
|
||||||
bypassHostnames.clear()
|
bypassHostnames.clear()
|
||||||
bypassHostnames ++= other.bypassHostnames
|
bypassHostnames ++= other.bypassHostnames
|
||||||
@@ -48,21 +33,28 @@ class Acl {
|
|||||||
proxyHostnames ++= other.proxyHostnames
|
proxyHostnames ++= other.proxyHostnames
|
||||||
subnets.clear()
|
subnets.clear()
|
||||||
subnets ++= other.subnets
|
subnets ++= other.subnets
|
||||||
|
urls.clear()
|
||||||
|
urls ++= other.urls
|
||||||
bypass = other.bypass
|
bypass = other.bypass
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
|
def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
|
||||||
bypassHostnames.clear()
|
bypassHostnames.clear()
|
||||||
proxyHostnames.clear()
|
proxyHostnames.clear()
|
||||||
this.subnets.clear()
|
this.subnets.clear()
|
||||||
|
this.urls.clear()
|
||||||
bypass = defaultBypass
|
bypass = defaultBypass
|
||||||
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
|
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
|
||||||
lazy val proxySubnets = new mutable.SortedList[Subnet]()
|
lazy val proxySubnets = new mutable.SortedList[Subnet]()
|
||||||
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
|
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
|
||||||
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
|
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
|
||||||
|
var in_urls = false
|
||||||
for (line <- value.getLines()) (line.indexOf('#') match {
|
for (line <- value.getLines()) (line.indexOf('#') match {
|
||||||
case -1 => line
|
case -1 => if (!in_urls) line else ""
|
||||||
case index => line.substring(0, index) // trim comments
|
case index =>
|
||||||
|
if (line.contains("URLS_BEGIN")) in_urls = true
|
||||||
|
if (line.contains("URLS_END")) in_urls = false
|
||||||
|
line.substring(0, index) // trim comments
|
||||||
}).trim match {
|
}).trim match {
|
||||||
case "[outbound_block_list]" =>
|
case "[outbound_block_list]" =>
|
||||||
hostnames = null
|
hostnames = null
|
||||||
@@ -76,7 +68,8 @@ class Acl {
|
|||||||
case "[reject_all]" | "[bypass_all]" => bypass = true
|
case "[reject_all]" | "[bypass_all]" => bypass = true
|
||||||
case "[accept_all]" | "[proxy_all]" => bypass = false
|
case "[accept_all]" | "[proxy_all]" => bypass = false
|
||||||
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
|
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
|
||||||
case _: IllegalArgumentException => hostnames += input
|
case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://"))
|
||||||
|
urls += input else hostnames += input
|
||||||
}
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
@@ -85,9 +78,24 @@ class Acl {
|
|||||||
}
|
}
|
||||||
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
|
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()
|
val result = new StringBuilder()
|
||||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
if (urls.nonEmpty) {
|
||||||
|
result.append("#URLS_BEGIN\n")
|
||||||
|
result.append(urls.mkString("\n"))
|
||||||
|
if (network) {
|
||||||
|
try {
|
||||||
|
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
|
||||||
|
} catch {
|
||||||
|
case _: IOException => // ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.append("#URLS_END\n")
|
||||||
|
|
||||||
|
}
|
||||||
|
if (result.isEmpty) {
|
||||||
|
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||||
|
}
|
||||||
val (bypassList, proxyList) =
|
val (bypassList, proxyList) =
|
||||||
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
|
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
|
||||||
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
|
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
|
||||||
@@ -104,6 +112,8 @@ class Acl {
|
|||||||
result.toString
|
result.toString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def toString: String = getAclString(false)
|
||||||
|
|
||||||
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
|
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
|
||||||
|
|
||||||
// Don't change: dummy fields for OrmLite interaction
|
// Don't change: dummy fields for OrmLite interaction
|
||||||
@@ -134,9 +144,9 @@ object Acl {
|
|||||||
try acl.fromId(CUSTOM_RULES) catch {
|
try acl.fromId(CUSTOM_RULES) catch {
|
||||||
case _: FileNotFoundException =>
|
case _: FileNotFoundException =>
|
||||||
}
|
}
|
||||||
acl.bypass = true
|
|
||||||
acl.bypassHostnames.clear() // everything is bypassed
|
|
||||||
acl
|
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,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
|||||||
|
|
||||||
private def createAclRuleDialog(text: CharSequence = "") = {
|
private def createAclRuleDialog(text: CharSequence = "") = {
|
||||||
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
|
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
|
||||||
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
|
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
|
||||||
val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
|
val editText = view.findViewById[EditText](R.id.content)
|
||||||
PATTERN_DOMAIN.findFirstMatchIn(text) match {
|
PATTERN_DOMAIN.findFirstMatchIn(text) match {
|
||||||
case Some(m) =>
|
case Some(m) =>
|
||||||
templateSelector.setSelection(DOMAIN)
|
templateSelector.setSelection(DOMAIN)
|
||||||
@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
|||||||
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
|
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
|
||||||
with View.OnClickListener with View.OnLongClickListener {
|
with View.OnClickListener with View.OnLongClickListener {
|
||||||
var item: AnyRef = _
|
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.setOnClickListener(this)
|
||||||
itemView.setOnLongClickListener(this)
|
itemView.setOnLongClickListener(this)
|
||||||
itemView.setBackgroundResource(R.drawable.background_selectable)
|
itemView.setBackgroundResource(R.drawable.background_selectable)
|
||||||
@@ -252,12 +252,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
|||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
selectionItem = toolbar.getMenu.findItem(R.id.selection)
|
selectionItem = toolbar.getMenu.findItem(R.id.selection)
|
||||||
selectionItem.setVisible(selectedItems.nonEmpty)
|
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.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
|
||||||
list.setItemAnimator(new DefaultItemAnimator)
|
list.setItemAnimator(new DefaultItemAnimator)
|
||||||
list.setAdapter(adapter)
|
list.setAdapter(adapter)
|
||||||
val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
|
view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list)
|
||||||
fastScroller.setRecyclerView(list)
|
|
||||||
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
|
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
|
||||||
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
|
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
|
||||||
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
|
||||||
|
|||||||
@@ -20,10 +20,14 @@
|
|||||||
|
|
||||||
package com.github.shadowsocks.database
|
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.content.pm.ApplicationInfo
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
|
import com.github.shadowsocks.utils.Key
|
||||||
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
|
||||||
import com.j256.ormlite.dao.Dao
|
import com.j256.ormlite.dao.Dao
|
||||||
import com.j256.ormlite.support.ConnectionSource
|
import com.j256.ormlite.support.ConnectionSource
|
||||||
@@ -46,17 +50,20 @@ object DBHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DBHelper(val context: Context)
|
class DBHelper(val context: Context)
|
||||||
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) {
|
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) {
|
||||||
import DBHelper._
|
import DBHelper._
|
||||||
|
|
||||||
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
|
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
|
||||||
|
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
|
||||||
|
|
||||||
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
||||||
TableUtils.createTable(connectionSource, classOf[Profile])
|
TableUtils.createTable(connectionSource, classOf[Profile])
|
||||||
|
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
|
||||||
}
|
}
|
||||||
|
|
||||||
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
|
||||||
TableUtils.dropTable(connectionSource, classOf[Profile], true)
|
TableUtils.dropTable(connectionSource, classOf[Profile], true)
|
||||||
|
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
|
||||||
onCreate(database, connectionSource)
|
onCreate(database, connectionSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +139,19 @@ class DBHelper(val context: Context)
|
|||||||
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
|
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
|
||||||
profileDao.executeRawNoArgs("COMMIT;")
|
profileDao.executeRawNoArgs("COMMIT;")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 23) {
|
||||||
|
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 {
|
} catch {
|
||||||
case ex: Exception =>
|
case ex: Exception =>
|
||||||
app.track(ex)
|
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 java.util.Locale
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import com.github.shadowsocks.plugin.PluginConfiguration
|
import com.github.shadowsocks.plugin.PluginConfiguration
|
||||||
|
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
|
||||||
import com.github.shadowsocks.utils.Key
|
import com.github.shadowsocks.utils.Key
|
||||||
import com.j256.ormlite.field.{DataType, DatabaseField}
|
import com.j256.ormlite.field.{DataType, DatabaseField}
|
||||||
|
|
||||||
@@ -108,37 +108,38 @@ class Profile {
|
|||||||
}
|
}
|
||||||
override def toString: String = toUri.toString
|
override def toString: String = toUri.toString
|
||||||
|
|
||||||
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
|
def serialize(store: OrmLitePreferenceDataStore) {
|
||||||
.putString(Key.name, name)
|
store.putString(Key.name, name)
|
||||||
.putString(Key.host, host)
|
store.putString(Key.host, host)
|
||||||
.putInt(Key.localPort, localPort)
|
store.putInt(Key.localPort, localPort)
|
||||||
.putInt(Key.remotePort, remotePort)
|
store.putInt(Key.remotePort, remotePort)
|
||||||
.putString(Key.password, password)
|
store.putString(Key.password, password)
|
||||||
.putString(Key.route, route)
|
store.putString(Key.route, route)
|
||||||
.putString(Key.remoteDns, remoteDns)
|
store.putString(Key.remoteDns, remoteDns)
|
||||||
.putString(Key.method, method)
|
store.putString(Key.method, method)
|
||||||
.putBoolean(Key.proxyApps, proxyApps)
|
store.proxyApps = proxyApps
|
||||||
.putBoolean(Key.bypass, bypass)
|
store.bypass = bypass
|
||||||
.putBoolean(Key.udpdns, udpdns)
|
store.putBoolean(Key.udpdns, udpdns)
|
||||||
.putBoolean(Key.ipv6, ipv6)
|
store.putBoolean(Key.ipv6, ipv6)
|
||||||
.putString(Key.individual, individual)
|
store.individual = individual
|
||||||
.putString(Key.plugin, plugin)
|
store.plugin = plugin
|
||||||
.remove(Key.dirty)
|
store.remove(Key.dirty)
|
||||||
def deserialize(pref: SharedPreferences) {
|
}
|
||||||
|
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
|
// 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)
|
name = store.getString(Key.name, null)
|
||||||
host = pref.getString(Key.host, null)
|
host = store.getString(Key.host, null)
|
||||||
localPort = pref.getInt(Key.localPort, 0)
|
localPort = store.getInt(Key.localPort, 0)
|
||||||
remotePort = pref.getInt(Key.remotePort, 0)
|
remotePort = store.getInt(Key.remotePort, 0)
|
||||||
password = pref.getString(Key.password, null)
|
password = store.getString(Key.password, null)
|
||||||
method = pref.getString(Key.method, null)
|
method = store.getString(Key.method, null)
|
||||||
route = pref.getString(Key.route, null)
|
route = store.getString(Key.route, null)
|
||||||
remoteDns = pref.getString(Key.remoteDns, null)
|
remoteDns = store.getString(Key.remoteDns, null)
|
||||||
proxyApps = pref.getBoolean(Key.proxyApps, false)
|
proxyApps = store.proxyApps
|
||||||
bypass = pref.getBoolean(Key.bypass, false)
|
bypass = store.bypass
|
||||||
udpdns = pref.getBoolean(Key.udpdns, false)
|
udpdns = store.getBoolean(Key.udpdns, false)
|
||||||
ipv6 = pref.getBoolean(Key.ipv6, false)
|
ipv6 = store.getBoolean(Key.ipv6, false)
|
||||||
individual = pref.getString(Key.individual, null)
|
individual = store.individual
|
||||||
plugin = pref.getString(Key.plugin, null)
|
plugin = store.plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
|
|||||||
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
|
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
|
||||||
with View.OnClickListener with View.OnLongClickListener {
|
with View.OnClickListener with View.OnLongClickListener {
|
||||||
private var index: Int = _
|
private var index: Int = _
|
||||||
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
private val text1 = view.findViewById[TextView](android.R.id.text1)
|
||||||
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
|
private val text2 = view.findViewById[TextView](android.R.id.text2)
|
||||||
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
|
private val icon = view.findViewById[ImageView](android.R.id.icon)
|
||||||
view.setOnClickListener(this)
|
view.setOnClickListener(this)
|
||||||
view.setOnLongClickListener(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 {
|
object Action {
|
||||||
final val SERVICE = "com.github.shadowsocks.SERVICE"
|
final val SERVICE = "com.github.shadowsocks.SERVICE"
|
||||||
final val CLOSE = "com.github.shadowsocks.CLOSE"
|
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"
|
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.view.{Gravity, View, Window}
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||||
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
|
import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService}
|
||||||
import org.xbill.DNS._
|
import org.xbill.DNS._
|
||||||
|
|
||||||
import scala.collection.JavaConversions._
|
import scala.collection.JavaConversions._
|
||||||
@@ -137,10 +137,11 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def startSsService(context: Context) {
|
def startSsService(context: Context) {
|
||||||
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
|
val intent =
|
||||||
context.startService(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) {
|
def stopSsService(context: Context) {
|
||||||
val intent = new Intent(Action.CLOSE)
|
val intent = new Intent(Action.CLOSE)
|
||||||
context.sendBroadcast(intent)
|
context.sendBroadcast(intent)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
* 0.0.4:
|
||||||
|
* Enlarge text size of number pickers;
|
||||||
|
* Update support library version to 26.0.0.
|
||||||
* 0.0.3:
|
* 0.0.3:
|
||||||
* Update support library version to 25.2.0.
|
* Update support library version to 25.2.0.
|
||||||
* 0.0.2:
|
* 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:
|
and it's most convenient to use it with SBT:
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2"
|
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Native binary configuration
|
### Native binary configuration
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ enablePlugins(AndroidLib)
|
|||||||
android.useSupportVectors
|
android.useSupportVectors
|
||||||
|
|
||||||
name := "plugin"
|
name := "plugin"
|
||||||
version := "0.0.3"
|
version := "0.0.4-SNAPSHOT"
|
||||||
|
|
||||||
pomExtra in Global := {
|
pomExtra in Global := {
|
||||||
<url>https://github.com/shadowsocks/shadowsocks-android</url>
|
<url>https://github.com/shadowsocks/shadowsocks-android</url>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
package="com.github.shadowsocks.plugin">
|
package="com.github.shadowsocks.plugin">
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="19"
|
android:minSdkVersion="19"
|
||||||
android:targetSdkVersion="25"/>
|
android:targetSdkVersion="26"/>
|
||||||
<application
|
<application
|
||||||
android:theme="@style/Theme.Shadowsocks">
|
android:theme="@style/Theme.Shadowsocks">
|
||||||
<meta-data android:name="com.github.shadowsocks.plugin.version"
|
<meta-data android:name="com.github.shadowsocks.plugin.version"
|
||||||
android:value="0.0.2"/>
|
android:value="0.0.4"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -40,6 +40,6 @@
|
|||||||
<item name="android:scrollbars">vertical</item>
|
<item name="android:scrollbars">vertical</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="NumberPickerStyle">
|
<style name="NumberPickerStyle">
|
||||||
<item name="android:textSize">14sp</item>
|
<item name="android:textSize">18sp</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P
|
|||||||
}
|
}
|
||||||
|
|
||||||
override def setText(text: String): Unit = {
|
override def setText(text: String): Unit = {
|
||||||
|
val old = getText
|
||||||
super.setText(text)
|
super.setText(text)
|
||||||
notifyChanged()
|
if (old != text) notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
|
|||||||
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
|
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
|
||||||
if (oldParent eq view) return
|
if (oldParent eq view) return
|
||||||
if (oldParent != null) oldParent.removeView(editText)
|
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
|
if (oldEdit == null) return
|
||||||
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
|
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
|
||||||
if (container == null) return
|
if (container == null) return
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
|
|||||||
view.setBackground(background)
|
view.setBackground(background)
|
||||||
ViewCompat.setPaddingRelative(view, s, t, e, b)
|
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) {
|
if (widgetFrame != null) {
|
||||||
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
|
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
|
||||||
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
|
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")
|
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||||
|
|
||||||
|
|||||||