Compare commits

..
66 Commits
Author SHA1 Message Date
Max Lv bb25df8692 Bump version 2017-09-06 18:48:02 -07:00
Max Lv e8b5953701 Merge pull request #1364 from Mygod/url-rules
Rewrite network ACLs
2017-09-04 15:29:20 +08:00
Mygod b8d7ade44d Rewrite network ACLs
* A cleaner format;
* Provides better behavior when importing a non-compatible ACL;
* Supports importing URL from a imported file with up to 10 layers.
2017-09-03 17:32:32 -07:00
Mygod d7183fe147 Update strings.xml (POEditor.com)
Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)
2017-09-03 15:43:05 -07:00
Mygod 8b870877d5 Add persian language 2017-09-03 15:26:21 -07:00
Mygod 024628483e Ignore gen directories 2017-09-03 15:25:02 -07:00
Mygod 73fe14d643 Revert "Remove obsolete items in FAQ"
This reverts commit 068906cd0f.
2017-08-31 20:09:18 -07:00
Mygod 512cb06671 Update dependencies 2017-08-31 19:57:49 -07:00
Max Lv 3d069b10ee Fix #1349 2017-08-29 02:06:35 -07:00
Max Lv 4490d95c7f Bump version 2017-08-28 22:11:38 -07:00
Max Lv 390cb522d5 Fix #1351 2017-08-22 03:10:27 -07:00
Max Lv b14fdcaed9 Fix the missing selected rules 2017-08-20 18:14:07 +08:00
Max Lv b6adeda924 Add local DNS to CHINALIST routing. #1348 2017-08-20 17:41:30 +08:00
Max Lv 253286c482 Bump version 2017-08-15 12:02:03 +08:00
Max Lv f25406b86c Protect table creation with transaction 2017-08-10 08:39:49 +08:00
Max Lv 2c8718163a Add new lines for remote ACL 2017-08-08 07:45:56 +08:00
Max Lv 450bde5305 Bump version 2017-08-07 09:10:45 +08:00
Max Lv d51bf46413 Refine Custom Rules 2017-08-03 22:47:55 +08:00
Max Lv a69183cc0f Move all network rules to new block 2017-08-03 22:15:49 +08:00
Max Lv 62a0bb59c7 Refine URL based ACL 2017-08-03 21:35:25 +08:00
Max Lv 4eb77c8854 Bump version 2017-08-03 20:30:20 +08:00
Max Lv 839765b724 Fix #1325 2017-08-03 20:24:22 +08:00
Max Lv a3f57d2557 Revert dm7 scanner back to 1.9.3
The latest 1.9.5 version seems to have bugs on Android O.
2017-07-31 10:19:06 +08:00
Max Lv f5247d627d Bump version 2017-07-30 15:46:34 +08:00
Max Lv f19f8d8ffa Fix a crash when no profile selected 2017-07-30 15:32:48 +08:00
Mygod adaca76b12 Update plugin changes 2017-07-29 15:50:24 +08:00
Mygod 010fd6158f Fix profile name display in notification 2017-07-29 14:43:42 +08:00
Mygod 8b57b53788 Misc refinements 2017-07-29 14:36:51 +08:00
Mygod 1db2d81aca Update dependencies 2017-07-29 14:36:37 +08:00
Max Lv 258cb0c9c9 Enlarge the button's padding 2017-07-28 12:58:31 +08:00
Max Lv af057d0740 Add missing proguard rules #1300 2017-07-19 16:22:11 +08:00
Max Lv 52499f5c2d Add local DNS to dns list. #1281 2017-07-19 16:22:03 +08:00
Max Lv 230a018326 Allow adding URLs to custom rules. #1276 2017-07-19 16:21:53 +08:00
Max Lv f6f4f45f62 Add missing proguard rules #1300 2017-07-19 16:08:55 +08:00
Max Lv f950037003 Merge pull request #1258 from shadowsocks/o-preview
Support for Android O preview
2017-07-19 01:01:24 -07:00
Max Lv d8179b5b17 Fix a crash caused by evernote/android-job 2017-07-19 09:17:18 +08:00
Max Lv d701fb4cee Enlarge the text size of number picker 2017-07-19 09:16:56 +08:00
Max Lv 5549dfd20a Bump version 2017-07-16 16:04:53 +08:00
Max Lv 6978b921bb Fix a crash in RunnerActivity 2017-07-16 16:04:53 +08:00
Mygod ac28b83a0b Opt out of cleartext traffic 2017-07-16 15:52:55 +08:00
Mygod 422924f3f4 Opt-in for safe browsing 2017-07-16 15:36:20 +08:00
Mygod e1ee53567a Implement adaptive icons 2017-07-16 15:08:52 +08:00
Mygod 9c36f84b40 Update shortcut APIs 2017-07-15 02:10:00 +08:00
Mygod 143afb052d Use startService calls instead of AIDL to start services
Because our data store is process-safe, we can remove the `use` function
entirely and move more components to :bg process.
2017-07-15 01:52:53 +08:00
Mygod 27fc27346c Replace SharedPreferences with SQLite
Other changes included:

* Fix a bug where dirty flag is not set correctly when changing per-app
  proxy settings;
* Old auto connect settings in SharedPreference is now ignored.
2017-07-15 01:16:37 +08:00
Mygod 02c8be58e3 Remove obsolete strip in AndroidManifest 2017-07-15 01:01:34 +08:00
Mygod 2e1564b7b9 Remove usage of BuildCompat 2017-07-14 11:46:03 +08:00
Mygod 250373b92a Update Travis and README 2017-07-14 11:44:00 +08:00
Mygod dc439b0802 Update to API 26 2017-07-14 11:05:57 +08:00
Max Lv 26b6853061 Add local DNS to dns list. #1281 2017-06-18 15:59:47 +08:00
Max Lv a84070c2d3 Allow adding URLs to custom rules. #1276 2017-06-14 15:27:35 +08:00
Max Lv 49e30e7b09 Add back internal QR scanner. #1273 2017-06-05 09:28:34 +08:00
Max Lv 1e750e45e9 Roll back to Google's barcode scanner 2017-05-29 17:46:42 +08:00
Max Lv 574f80aefd Fix a crash caused by new background limit on Android O 2017-05-26 18:01:07 +08:00
Mygod 424e1bd1bc Update to Android O preview 2
* Update support library to beta1;
* Adapt to new foreground service policy;
* Add a separate notification channel for NAT service;
* Adapt to `findViewById` method signature changes.

Possible issues: NAT notification will possibly not hide in lockscreen.
2017-05-18 14:56:26 +08:00
Mygod bbf52c4f1d Add support for notification channels for O preview 2017-05-18 14:35:29 +08:00
Mygod 0acfaa12c0 Use the new findViewById APIs 2017-05-18 14:35:29 +08:00
Mygod 1ec80175ea Update target SDK to O preview 2017-05-18 14:35:29 +08:00
Mygod 2b985686a4 Merge branch 'master' into develop 2017-05-18 14:35:18 +08:00
Mygod 6ca3e8abda Update scala to 2.11.11 2017-04-21 11:07:14 +08:00
Mygod 3a935b3274 Update scala to 2.11.10
It seems there will be 2.11.11 coming out soon however.

Source:
https://github.com/scala/scala-dev/issues/359#issuecomment-293474247
2017-04-14 00:14:45 +08:00
Mygod ea848f3b71 Merge branch 'master' into develop 2017-04-14 00:14:14 +08:00
Mygod 53af99eb98 Update support lib 2017-03-30 21:56:02 +08:00
Mygod 9617418e70 Stop calling notifyChanged over aggressively in EditTextPreference 2017-03-15 19:24:18 +08:00
Mygod c09a96a203 Bump plugin lib version 2017-03-15 19:24:18 +08:00
Mygod 1ad08c6dba Update support lib version 2017-03-15 19:24:18 +08:00
70 changed files with 954 additions and 426 deletions
+10
View File
@@ -14,6 +14,11 @@ Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issu
* Tap the number to enter the port you wish to use; (if the keyboard doesn't pop up automatically for some reason)
* Use Tasker integration to create a desktop widget.
### How to add QR code from local gallery?
Scan it with a third-party scanner like [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) and click the `ss` url.
### Why is NAT mode deprecated?
1. Requiring ROOT permission;
@@ -47,3 +52,8 @@ So if you notice a significant increase in battery usage after you use Shadowsoc
### It works fine under Wi-Fi but can't connect through cellular data?
Allow this app to consume background data in app settings.
### Why Camera permission is required on devices below Android 6.0?
To scan the QR code through the integrated QR scanner.
+1
View File
@@ -6,6 +6,7 @@ local.sbt
#Intellij IDEA
.idea/
.d
gen/
mobile/src/main/libs
mobile/src/main/obj
+3 -3
View File
@@ -5,7 +5,7 @@ jdk:
env:
global:
- NDK_VERSION=r14
- NDK_VERSION=r15b
- NDK_CCACHE=ccache
- GOROOT_BOOTSTRAP=$GOROOT
- ANDROID_NDK_HOME=$HOME/.android/android-ndk-${NDK_VERSION}
@@ -13,7 +13,7 @@ env:
- PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH}
scala:
- 2.11.8
- 2.11.11
before_cache:
- find $HOME/.sbt -name "*.lock" | xargs rm
@@ -27,7 +27,7 @@ cache:
android:
components:
- tools
- build-tools-25.0.2
- build-tools-26.0.0
- extra-android-m2repository
- extra-google-m2repository
+2 -2
View File
@@ -14,9 +14,9 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* SBT 0.13.0+
* Go 1.4+
* Android SDK
- Build Tools 25+
- Build Tools 26+
- Android Support Repository and Google Repository (see `build.sbt` for version)
- Android NDK r14+
- Android NDK r15+
### BUILD
+6 -4
View File
@@ -1,10 +1,10 @@
lazy val commonSettings = Seq(
scalaVersion := "2.11.9",
scalaVersion := "2.11.11",
dexMaxHeap := "4g",
organization := "com.github.shadowsocks",
platformTarget := "android-25",
platformTarget := "android-26",
compileOrder := CompileOrder.JavaThenScala,
javacOptions ++= "-source" :: "1.7" :: "-target" :: "1.7" :: Nil,
@@ -18,10 +18,12 @@ lazy val commonSettings = Seq(
shrinkResources := true,
typedResources := false,
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW")
resConfigs := Seq("fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"),
resolvers += "google" at "https://maven.google.com"
)
val supportLibsVersion = "25.3.1"
val supportLibsVersion = "26.0.0"
lazy val root = Project(id = "shadowsocks-android", base = file("."))
.settings(commonSettings)
.aggregate(plugin, mobile)
+17 -10
View File
@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
android.useSupportVectors
name := "shadowsocks"
version := "4.1.8"
versionCode := Some(188)
version := "4.2.5"
versionCode := Some(195)
proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" ::
@@ -14,13 +14,20 @@ proguardOptions ++=
"-dontwarn okio.**" ::
"-dontwarn org.xbill.**" ::
"-keep class com.github.shadowsocks.JniHelper { *; }" ::
"-dontwarn com.evernote.android.job.gcm.**" ::
"-dontwarn com.evernote.android.job.util.GcmAvailableHelper" ::
"-keep public class com.evernote.android.job.v21.PlatformJobService" ::
"-keep public class com.evernote.android.job.v14.PlatformAlarmService" ::
"-keep public class com.evernote.android.job.v14.PlatformAlarmReceiver" ::
"-keep public class com.evernote.android.job.JobBootReceiver" ::
"-keep public class com.evernote.android.job.JobRescheduleService" ::
Nil
val playServicesVersion = "10.2.6"
val playServicesVersion = "11.2.0"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.1.10" ::
"com.evernote" % "android-job" % "1.2.0-alpha4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
@@ -28,15 +35,15 @@ libraryDependencies ++=
"com.google.firebase" % "firebase-config" % playServicesVersion ::
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.5.2" ::
"com.mikepenz" % "iconics-core" % "2.8.4" ::
"com.mikepenz" % "materialdrawer" % "5.9.1" ::
"com.mikepenz" % "materialize" % "1.0.1" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.0" ::
"com.mikepenz" % "fastadapter" % "2.6.3" ::
"com.mikepenz" % "iconics-core" % "2.9.3" ::
"com.mikepenz" % "materialdrawer" % "5.9.5" ::
"com.mikepenz" % "materialize" % "1.0.3" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.8" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
+9 -12
View File
@@ -17,13 +17,17 @@
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25"/>
android:targetSdkVersion="26"/>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="true" />
<application
android:name=".ShadowsocksApplication"
android:icon="@mipmap/ic_launcher"
android:backupAgent=".ShadowsocksBackupAgent"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:banner="@drawable/ic_start_connected">
<meta-data android:name="com.google.android.gms.version"
@@ -96,18 +100,14 @@
android:label="@string/quick_toggle"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:taskAffinity=""
android:process=":bg"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
<service
android:name=".ShadowsocksRunnerService"
android:exported="false">
</service>
<service
android:name=".ShadowsocksNatService"
android:process=":bg"
@@ -134,13 +134,13 @@
</intent-filter>
</service>
<receiver android:name=".BootReceiver" android:enabled="false">
<receiver android:name=".BootReceiver" android:process=":bg" android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:name=".TaskerReceiver" tools:ignore="ExportedReceiver">
<receiver android:name=".TaskerReceiver" android:process=":bg" tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
@@ -159,9 +159,6 @@
android:process=":bg"/>
<receiver android:name="com.evernote.android.job.JobBootReceiver"
android:process=":bg"/>
<!-- Strip useless components -->
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" tools:node="remove"/>
</application>
</manifest>
@@ -10,7 +10,4 @@ interface IShadowsocksService {
oneway void startListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void stopListeningForBandwidth(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in int profileId);
void useSync(in int profileId);
}
Binary file not shown.

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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:padding="8dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_social_share"
android:background="?attr/selectableItemBackgroundBorderless"
@@ -47,7 +47,7 @@
<android.support.v7.widget.AppCompatImageView android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:padding="8dp"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_image_edit"
android:background="?attr/selectableItemBackgroundBorderless"
@@ -0,0 +1,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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"شدوساکس"</string>
<!-- misc -->
<string name="profile">"پروفایل"</string>
<string name="connection_test_testing">"در حال آزمایش..."</string>
<!-- proxy category -->
<string name="profile_name">"نام پروفایل"</string>
<string name="proxy">"سرور"</string>
<string name="sitekey">"کلمه عبور"</string>
<string name="enc_method">"طریقه رمزگذاری"</string>
<string name="on">"روشن"</string>
<string name="auto_connect">"وصل‌شدن اتوماتیک"</string>
<string name="auto_connect_summary">"فعال‌شدن شدوساکس لحظه روشن‌شدن گوشی"</string>
<!-- notification category -->
<string name="forward_success">"شدوساکس فعال شد."</string>
<string name="invalid_server">"آدرس سرور نامعتبر است"</string>
<string name="service_failed">"اتصال به سرور ناموفق بود!"</string>
<string name="stop">"غیرفعال‌کردن"</string>
<string name="stopping">"در حال قطع‌کردن اتصال..."</string>
<string name="profile_invalid_input">"هیچ پروفایل معتبری یافت نشد!"</string>
<!-- alert category -->
<string name="close">"خروج"</string>
<string name="profile_empty">"لطفا یک پروفایل انتخاب کنید."</string>
<string name="proxy_empty">"پروکسی یا کلمه عبور نباید خالی باشند!"</string>
<string name="connect">"اتصال"</string>
<!-- menu category -->
<string name="profiles">"پروفایل‌ها"</string>
<string name="settings">"تنظیمات"</string>
<string name="faq">"سوالات مداول"</string>
<string name="recovery">"ریست"</string>
<string name="about">"درباره ما"</string>
<string name="edit">"ویرایش"</string>
<string name="share">"به اشتراک گذاری"</string>
<string name="add_profile">"افزودن پروفایل"</string>
<string name="delete">"حذف"</string>
<string name="delete_confirm_prompt">"آیا مطمئن هستید که می‌خواهید این پروفایل را حذف کنید؟"</string>
<string name="share_qr_nfc">"NFC یا QR Code"</string>
<string name="add_profile_methods_scan_qr_code">"اسکن‌کردن QR Code"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
<!-- tasker -->
<string name="toggle_service_state">"فعال‌کردن سرویس"</string>
<string name="start_service_default">"وصل‌شدن به سرور فعلی"</string>
<string name="profile_default">"استفاده از پروفایل فعلی"</string>
<!-- status -->
<string name="sent">"فرستاده‌شده:"</string>
<string name="received">"دریافت‌شده:"</string>
<string name="connecting">"در حال اتصال..."</string>
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
<string name="nat_connected">"اتصال برقرار است."</string>
<string name="not_connected">"اتصال برقرار نیست!"</string>
<string name="route_entry_all">"همه"</string>
</resources>
+8 -6
View File
@@ -2,8 +2,7 @@
<resources>
<string name="quick_toggle">"ON/OFF"</string>
<!-- global/misc category -->
<string name="misc_cat">"その他"</string>
<!-- misc -->
<string name="profile">"プロファイル"</string>
<string name="profile_summary">"新規作成または既存ファイルを開く"</string>
<string name="nat">"NAT モード (デバックのみ)"</string>
@@ -90,8 +89,6 @@
<string name="add_profile_dialog">"Shadowsocks用プロファイルを追加しますか"</string>
<string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string>
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
<plurals name="removed">
<item quantity="other">"削除済み"</item>
</plurals>
@@ -123,7 +120,7 @@
<string name="route_entry_bypass_lan_chn">"LAN 及び中国本土のアドレスを迂回する"</string>
<string name="route_entry_chinalist">"中国本土のアドレス以外を迂回する"</string>
<string name="proxied_apps_summary_v21">"選択したアプリにプロキシを設定する"</string>
<string name="acl_rule_templates_generic">"サブネット/ホスト名 PCRE パターン"</string>
<string name="acl_rule_templates_generic">"URL/サブネット/ホスト名 PCRE パターン"</string>
<string name="acl_rule_templates_domain">"ドメイン及び全てのサブドメイン"</string>
<!-- plugin -->
@@ -133,4 +130,9 @@
<string name="plugin_unknown">"不明なプラグイン"</string>
<string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string>
<string name="profile_plugin">"プラグイン"</string>
</resources>
<string name="add_profile_scanner_permission_required">"QRコードをスキャンするにはカメラ権限が必要です。"</string>
<!-- notification category -->
<string name="service_vpn">"VPNサービス"</string>
<string name="service_nat">"NATサービス"</string>
</resources>
+72 -8
View File
@@ -8,16 +8,11 @@
<string name="nat">"NAT 모드 (지원 중단)"</string>
<string name="nat_summary">"VPN 모드 대신 NAT 모드를 사용합니다. 루트 권한이 필요합니다."</string>
<string name="remote_dns">"원격 DNS"</string>
<string name="stat_summary">"송신: \t\t\t\t\t%3$s\t↑\t%1$s/s
수신: \t%4$s\t↓\t%2$s/s "</string>
<string name="stat_profiles">"%1$s↑\t%2$s↓"</string>
<string name="traffic_summary">"%1$s/s↑\t%2$s/s↓"</string>
<plurals name="bytes">
<item quantity="other">"바이트"</item>
</plurals>
<string name="stat_summary">"송신: \t%3$s\t↑\t%1$s/s
수신: \t%4$s\t↓\t%2$s/s"</string>
<string name="connection_test_pending">"인터넷 연결 상태 검사하기"</string>
<string name="connection_test_testing">"검사 중…"</string>
<string name="connection_test_available">"성공: %dms"</string>
<string name="connection_test_available">"성공: 지연시간 %dms"</string>
<string name="connection_test_error">"인터넷 연결을 찾을 수 없습니다: %s"</string>
<string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string>
<string name="connection_test_error_status_code">"오류 코드: #%d"</string>
@@ -45,4 +40,73 @@
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 포워딩"</string>
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
<!-- notification category -->
<string name="forward_success">"Shadowsocks가 시작되었습니다"</string>
<string name="invalid_server">"잘못된 서버 이름입니다"</string>
<string name="service_failed">"원격 서버에 접속하는 데 실패했습니다"</string>
<string name="nat_deprecated">"경고: NAT 모드는 Android 5.0부터 지원 중단되었습니다"</string>
<string name="nat_no_root">"NAT 모드는 루트 권한이 필요합니다"</string>
<string name="switch_to_vpn">"VPN 모드로 전환"</string>
<string name="stop">"중지"</string>
<string name="stopping">"종료 중…"</string>
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
<!-- alert category -->
<string name="close">"닫기"</string>
<string name="profile_empty">"프로필을 선택해 주세요"</string>
<string name="proxy_empty">"서버 주소와 비밀번호는 반드시 입력해야 합니다"</string>
<string name="connect">"접속"</string>
<string name="recovering">"재설정 중…"</string>
<string name="remove_profile">"'%s' 프로필을 삭제 하시겠습니까?"</string>
<!-- menu category -->
<string name="profiles">"프로필"</string>
<string name="settings">"설정"</string>
<string name="faq">"자주 묻는 질문"</string>
<string name="recovery">"재설정"</string>
<string name="about">"이 앱에 대하여"</string>
<string name="edit">"수정"</string>
<string name="share">"공유"</string>
<string name="add_profile">"프로필 추가"</string>
<string name="action_export">"클립보드로 내보내기"</string>
<string name="action_import">"클립보드에서 불러오기"</string>
<string name="action_export_msg">"성공적으로 내보냈습니다"</string>
<string name="action_export_err">"내보내기에 실패했습니다"</string>
<string name="action_import_msg">"성공적으로 불러왔습니다"</string>
<string name="action_import_err">"불러오는 데 실패했습니다"</string>
<!-- profile -->
<string name="profile_config">"프로필 설정"</string>
<string name="delete">"삭제"</string>
<string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</string>
<string name="share_qr_nfc">"QR 코드/NFC"</string>
<string name="add_profile_dialog">"이 Shadowsocks 프로필을 추가하시겠습니까?"</string>
<string name="add_profile_methods_scan_qr_code">"QR 코드 읽기"</string>
<string name="add_profile_methods_manual_settings">"손수 써넣기"</string>
<plurals name="removed">
<item quantity="other">"삭제했습니다"</item>
</plurals>
<string name="undo">"실행 취소"</string>
<!-- status -->
<string name="sent">"송신:"</string>
<string name="received">"수신:"</string>
<string name="connecting">"연결 중…"</string>
<string name="vpn_connected">"연결 완료. 탭 하면 연결 상태를 검사합니다."</string>
<string name="nat_connected">"연결 완료"</string>
<!-- acl -->
<string name="custom_rules">"사용자 정의 규칙"</string>
<string name="acl_rule_templates_generic">"URL, 서브넷 혹은 호스트 이름 PCRE 패턴"</string>
<!-- plugin -->
<string name="plugin">"플러그인"</string>
<string name="plugin_configure">"플러그인 설정…"</string>
<string name="plugin_disabled">"비활성화 됨"</string>
<string name="plugin_unknown">"알 수 없는 플러그인: %s"</string>
<string name="plugin_untrusted">"경고: 이 플러그인은 신뢰할 수 있는 출처에서 온 것이 아닌 것 같습니다"</string>
<string name="profile_plugin">"플러그인: %s"</string>
<string name="add_profile_scanner_permission_required">"QR 코드를 읽어 들이려면 카메라 권한이 필요합니다"</string>
</resources>
+3 -4
View File
@@ -95,8 +95,6 @@
<string name="add_profile_dialog">"Добавить этот Профиль Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string>
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
<string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string>
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
<plurals name="removed">
<item quantity="one">"Удалено"</item>
<item quantity="few">"Удалено %d элемента"</item>
@@ -131,7 +129,7 @@
<string name="route_entry_bypass_lan_chn">"Все, кроме LAN и Китая"</string>
<string name="route_entry_chinalist">"Список Китай"</string>
<string name="proxied_apps_summary_v21">"Установить прокси для выбранных приложений"</string>
<string name="acl_rule_templates_generic">"Подсеть/Регулярное выражение (PCRE) имени хоста"</string>
<string name="acl_rule_templates_generic">"URL/Подсеть/Регулярное выражение (PCRE) имени хоста"</string>
<string name="acl_rule_templates_domain">"Доменное имя и все его поддомены"</string>
<!-- plugin -->
@@ -141,4 +139,5 @@
<string name="plugin_unknown">"Неизвестный плагин %s"</string>
<string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string>
<string name="profile_plugin">"Плагин: %s"</string>
</resources>
<string name="add_profile_scanner_permission_required">"Разрешение камеры требуется для сканирования QR код."</string>
</resources>
+13 -8
View File
@@ -3,8 +3,7 @@
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string>
<!-- global/misc category -->
<string name="misc_cat">"其他"</string>
<!-- misc -->
<string name="profile">"配置文件"</string>
<string name="profile_summary">"新建或切换到其他配置文件"</string>
<string name="nat">"NAT 模式(仅限调试)"</string>
@@ -12,6 +11,9 @@
<string name="remote_dns">"远程 DNS"</string>
<string name="stat_summary">"发送: \t%3$s\t↑\t%1$s/s
接收: \t%4$s\t↓\t%2$s/s"</string>
<plurals name="bytes">
<item quantity="other">"字节"</item>
</plurals>
<string name="connection_test_pending">"检查网络连接"</string>
<string name="connection_test_testing">"测试中…"</string>
<string name="connection_test_available">"连接有效:延时 %d 毫秒"</string>
@@ -63,7 +65,7 @@
<string name="proxy_empty">"代理服务器地址及密码不能为空"</string>
<string name="connect">"连接"</string>
<string name="recovering">"重置中…"</string>
<string name="remove_profile">"删除此配置文件“%s”?"</string>
<string name="remove_profile">"删除此配置文件“%s”?"</string>
<!-- menu category -->
<string name="profiles">"配置文件"</string>
@@ -71,7 +73,7 @@
<string name="faq">"常见问题"</string>
<string name="recovery">"重置"</string>
<string name="about">"关于"</string>
<string name="about_title">"影梭 (shadowsocks) %s"</string>
<string name="about_title">"影梭 (Shadowsocks) %s"</string>
<string name="edit">"编辑"</string>
<string name="share">"分享"</string>
<string name="add_profile">"添加配置文件"</string>
@@ -91,8 +93,6 @@
<string name="add_profile_dialog">"为影梭添加此配置文件?"</string>
<string name="add_profile_methods_scan_qr_code">"扫描二维码"</string>
<string name="add_profile_methods_manual_settings">"手动设置"</string>
<string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string>
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
<plurals name="removed">
<item quantity="other">"已删除 %d 项"</item>
</plurals>
@@ -124,7 +124,7 @@
<string name="route_entry_bypass_lan_chn">"绕过局域网及中国大陆地址"</string>
<string name="route_entry_chinalist">"仅代理中国大陆地址"</string>
<string name="proxied_apps_summary_v21">"为应用程序分别设置代理"</string>
<string name="acl_rule_templates_generic">"子网/域名 PCRE 正则表达式"</string>
<string name="acl_rule_templates_generic">"URL/子网/域名 PCRE 正则表达式"</string>
<string name="acl_rule_templates_domain">"域名及其子域名"</string>
<!-- plugin -->
@@ -134,4 +134,9 @@
<string name="plugin_unknown">"未知插件 %s"</string>
<string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string>
<string name="profile_plugin">"插件:%s"</string>
</resources>
<string name="add_profile_scanner_permission_required">"扫描二维码需要获得使用相机的权限。"</string>
<!-- notification category -->
<string name="service_vpn">"VPN 服务"</string>
<string name="service_nat">"NAT 服务"</string>
</resources>
+12 -9
View File
@@ -89,8 +89,6 @@
<string name="add_profile_dialog">"為 Shadowsocks 新增此設定檔?"</string>
<string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string>
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
<plurals name="removed">
<item quantity="other">"已移除 %d 項"</item>
</plurals>
@@ -122,14 +120,19 @@
<string name="route_entry_bypass_lan_chn">"略過區域網路及中國大陸"</string>
<string name="route_entry_chinalist">"China List"</string>
<string name="proxied_apps_summary_v21">"為已選擇的應用程式設定 Proxy"</string>
<string name="acl_rule_templates_generic">"子網路/主機名稱 PCRE 模式"</string>
<string name="acl_rule_templates_generic">"URL/子網路/主機名稱 PCRE 模式"</string>
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
<!-- plugin -->
<string name="plugin">"插件"</string>
<string name="plugin_configure">"配置..."</string>
<string name="plugin_disabled">"未啟用"</string>
<string name="plugin">"外掛程式"</string>
<string name="plugin_configure">"設定..."</string>
<string name="plugin_disabled">"停用"</string>
<string name="plugin_unknown">"未知插件 %s"</string>
<string name="plugin_untrusted">"警告:該插件似乎不是來自一個已知的可信任源。"</string>
<string name="profile_plugin">"插件: %s"</string>
</resources>
<string name="plugin_untrusted">"警告:此外掛程式似乎不是來自一個已知的受信任來源。"</string>
<string name="profile_plugin">"外掛程式: %s"</string>
<string name="add_profile_scanner_permission_required">"掃描 QR 碼需要相機權限。"</string>
<!-- notification category -->
<string name="service_vpn">"VPN 服務"</string>
<string name="service_nat">"NAT 服務"</string>
</resources>
+1
View File
@@ -224,5 +224,6 @@
<string-array name="acl_rule_templates">
<item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item>
<item>URL</item>
</string-array>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#7488A1</color>
</resources>
+3 -1
View File
@@ -56,6 +56,8 @@
<string name="udp_dns_summary">Forward all DNS requests to remote</string>
<!-- notification category -->
<string name="service_vpn">VPN Service</string>
<string name="service_nat">NAT Service</string>
<string name="forward_success">Shadowsocks started.</string>
<string name="invalid_server">Invalid server name</string>
<string name="service_failed">Failed to connect the remote server</string>
@@ -130,7 +132,7 @@
<string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</string>
<string name="action_add_rule">Add rule(s)…</string>
<string name="acl_rule_templates_generic">Subnet/Hostname PCRE pattern</string>
<string name="acl_rule_templates_generic">Subnet or Hostname PCRE pattern</string>
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
<string name="edit_rule">Edit rule</string>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false"/>
</network-security-config>
@@ -34,7 +34,7 @@ class AboutFragment extends ToolbarFragment {
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(getString(R.string.about_title).formatLocal(Locale.ENGLISH, BuildConfig.VERSION_NAME))
val web = view.findViewById(R.id.web_view).asInstanceOf[WebView]
val web = view.findViewById[WebView](R.id.web_view)
web.loadUrl("file:///android_asset/pages/about.html")
web.setWebViewClient(new WebViewClient() {
override def shouldOverrideUrlLoading(view: WebView, url: String): Boolean = {
@@ -70,8 +70,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
import AppManager._
private final class AppViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
private val icon = itemView.findViewById(R.id.itemicon).asInstanceOf[ImageView]
private val check = itemView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
private val icon = itemView.findViewById[ImageView](R.id.itemicon)
private val check = itemView.findViewById[Switch](R.id.itemcheck)
private var item: ProxiedApp = _
itemView.setOnClickListener(this)
@@ -92,7 +92,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName)
check.setChecked(true)
}
if (!appsLoading.get) app.editor.putString(Key.individual, proxiedApps.mkString("\n")).apply()
if (!appsLoading.get) {
app.dataStore.individual = proxiedApps.mkString("\n")
app.dataStore.dirty = true
}
}
}
@@ -118,7 +121,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private val appsLoading = new AtomicBoolean
private var handler: Handler = _
private def initProxiedApps(str: String = app.settings.getString(Key.individual, null)) =
private def initProxiedApps(str: String = app.dataStore.individual) =
proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() {
@@ -136,7 +139,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = app.settings.getString(Key.individual, null)
val proxiedAppString = app.dataStore.individual
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
@@ -146,10 +149,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
}
return true
case R.id.action_export =>
val bypass = app.settings.getBoolean(Key.bypass, false)
val proxiedAppString = app.settings.getString(Key.individual, null)
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
clipboard.setPrimaryClip(ClipData.newPlainText(Key.individual,
app.dataStore.bypass + "\n" + app.dataStore.individual))
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
return true
case R.id.action_import =>
@@ -163,7 +164,8 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
app.editor.putString(Key.individual, apps).putBoolean(Key.dirty, true).apply()
app.dataStore.individual = apps
app.dataStore.dirty = true
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
fastScroller.setVisibility(View.GONE)
@@ -202,18 +204,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (!app.settings.getBoolean(Key.proxyApps, false))
app.editor.putBoolean(Key.proxyApps, true).putBoolean(Key.dirty, true).apply()
if (!app.dataStore.proxyApps) {
app.dataStore.proxyApps = true
app.dataStore.dirty = true
}
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.proxyApps, checked).putBoolean(Key.dirty, true).apply()
app.dataStore.proxyApps = checked
app.dataStore.dirty = true
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.settings.getBoolean(Key.bypass, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
app.editor.putBoolean(Key.bypass, checked).putBoolean(Key.dirty, true).apply())
bypassSwitch.setChecked(app.dataStore.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
app.dataStore.bypass = checked
app.dataStore.dirty = true
})
initProxiedApps()
loadingView = findViewById(R.id.loading)
@@ -21,15 +21,14 @@
package com.github.shadowsocks
import java.io.{File, IOException}
import java.net.{Inet6Address, InetAddress}
import java.util
import java.util.concurrent.TimeUnit
import java.util.{Timer, TimerTask}
import java.net.InetAddress
import java.net.Inet6Address
import android.app.Service
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.{Handler, IBinder, RemoteCallbackList}
import android.content._
import android.os.{Build, Handler, IBinder, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
@@ -65,9 +64,12 @@ trait BaseService extends Service {
lazy val handler = new Handler(getMainLooper)
lazy val restartHanlder = new Handler(getMainLooper)
private val closeReceiver: BroadcastReceiver = (context: Context, _: Intent) => {
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
private var notification: ShadowsocksNotification = _
private val closeReceiver: BroadcastReceiver = (context: Context, intent: Intent) => intent.getAction match {
case Action.RELOAD => forceLoad()
case _ =>
Toast.makeText(context, R.string.stopping, Toast.LENGTH_SHORT).show()
stopRunner(stopService = true)
}
var closeReceiverRegistered: Boolean = _
@@ -103,20 +105,6 @@ trait BaseService extends Service {
stopListeningForBandwidth(cb) // saves an RPC, and safer
callbacks.unregister(cb)
}
override def use(profileId: Int): Unit = synchronized(if (profileId < 0) stopRunner(stopService = true) else {
val profile = app.profileManager.getProfile(profileId).orNull
if (profile == null) stopRunner(stopService = true, getString(R.string.profile_empty)) else state match {
case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
stopRunner(stopService = false)
startRunner(profile)
}
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
}
})
override def useSync(profileId: Int): Unit = use(profileId)
}
def checkProfile(profile: Profile): Boolean = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
@@ -124,9 +112,18 @@ trait BaseService extends Service {
false
} else true
def connect() {
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
def forceLoad(): Unit = app.currentProfile.orNull match {
case null => stopRunner(stopService = true, getString(R.string.profile_empty))
case p => if (checkProfile(p)) state match {
case State.STOPPED => startRunner()
case State.CONNECTED =>
stopRunner(stopService = false)
startRunner()
case s => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + s)
}
}
def connect() {
if (profile.host == "198.199.101.152") {
val client = new OkHttpClient.Builder()
.dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
@@ -153,43 +150,15 @@ trait BaseService extends Service {
profile.method = proxy(3).trim
}
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
}
def startRunner(profile: Profile) {
this.profile = profile
startService(new Intent(this, getClass))
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
Utils.ThrowableFuture(try connect() catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
exc.printStackTrace()
app.track(exc)
})
}
def createNotification(): ShadowsocksNotification
def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
else startService(new Intent(this, getClass))
def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier
@@ -198,6 +167,8 @@ trait BaseService extends Service {
closeReceiverRegistered = false
}
if (notification != null) notification.destroy()
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
@@ -276,7 +247,49 @@ trait BaseService extends Service {
}
// Service of shadowsocks should always be started explicitly
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
state match {
case State.STOPPED | State.IDLE =>
case _ => return Service.START_NOT_STICKY // ignore request
}
profile = app.currentProfile.orNull
if (profile == null) {
stopRunner(stopService = true)
return Service.START_NOT_STICKY
}
profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
TrafficMonitor.reset()
trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
trafficMonitorThread.start()
if (!closeReceiverRegistered) {
// register close receiver
val filter = new IntentFilter()
filter.addAction(Action.RELOAD)
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
notification = createNotification()
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
Utils.ThrowableFuture(try connect() catch {
case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
case exc: Throwable =>
stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
exc.printStackTrace()
app.track(exc)
})
Service.START_NOT_STICKY
}
protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper)
@@ -341,17 +354,26 @@ trait BaseService extends Service {
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
case (dns, i) => makeDns("UserDef-" + i, dns.trim)
})
val localDns = new JSONArray(Array(
makeDns("Primary-1", "119.29.29.29", edns = false),
makeDns("Primary-2", "114.114.114.114", edns = false)
))
try {
val localLinkDns = com.github.shadowsocks.utils.Dns.getDnsResolver(this)
localDns.put(makeDns("Primary-3", localLinkDns, edns = false))
} catch {
case _: Exception => // Ignore
}
profile.route match {
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
.put("PrimaryDNS", new JSONArray(Array(
makeDns("Primary-1", "119.29.29.29", edns = false),
makeDns("Primary-2", "114.114.114.114", edns = false)
)))
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", "gfwlist.txt")
case Acl.CHINALIST => config
.put("PrimaryDNS", new JSONArray().put(makeDns("Primary", "119.29.29.29")))
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
case _ => config
.put("PrimaryDNS", remoteDns)
@@ -20,27 +20,24 @@
package com.github.shadowsocks
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.preference.PreferenceDataStore
import be.mygod.preference.PreferenceFragment
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.{Key, TcpFastOpen}
class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
class GlobalConfigFragment extends PreferenceFragment with OnPreferenceDataStoreChangeListener {
override def onCreatePreferences(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
addPreferencesFromResource(R.xml.pref_global)
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
BootReceiver.setEnabled(getActivity, value.asInstanceOf[Boolean])
true
})
if (getPreferenceManager.getSharedPreferences.getBoolean(Key.isAutoConnect, false)) {
BootReceiver.setEnabled(getActivity, true)
getPreferenceManager.getSharedPreferences.edit.remove(Key.isAutoConnect).apply()
}
switch.setChecked(BootReceiver.getEnabled(getActivity))
val tfo = findPreference(Key.tfo).asInstanceOf[SwitchPreference]
@@ -56,16 +53,16 @@ class GlobalConfigFragment extends PreferenceFragment with OnSharedPreferenceCha
tfo.setEnabled(false)
tfo.setSummary(getString(R.string.tcp_fastopen_summary_unsupported, java.lang.System.getProperty("os.version")))
}
app.settings.registerOnSharedPreferenceChangeListener(this)
app.dataStore.registerChangeListener(this)
}
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
super.onDestroy()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(pref.getBoolean(key, false))
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => findPreference(key).asInstanceOf[SwitchPreference].setChecked(store.getBoolean(key, false))
case _ =>
}
}
@@ -26,7 +26,6 @@ import java.util.Locale
import android.app.backup.BackupManager
import android.app.{Activity, ProgressDialog}
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.net.{Uri, VpnService}
import android.nfc.{NdefMessage, NfcAdapter}
@@ -36,6 +35,7 @@ import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.content.res.AppCompatResources
import android.support.v7.preference.PreferenceDataStore
import android.support.v7.widget.RecyclerView.ViewHolder
import android.text.TextUtils
import android.util.Log
@@ -45,6 +45,7 @@ import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, CustomRulesFragment}
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
import com.mikepenz.crossfader.Crossfader
@@ -67,7 +68,7 @@ object MainActivity {
}
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
with OnSharedPreferenceChangeListener {
with OnPreferenceDataStoreChangeListener {
import MainActivity._
// UI
@@ -165,7 +166,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onServiceDisconnected(): Unit = changeState(State.IDLE)
private def addDisableNatToSnackbar(snackbar: Snackbar) = snackbar.setAction(R.string.switch_to_vpn, (_ =>
if (state == State.STOPPED) app.editor.putBoolean(Key.isNAT, false).apply()): View.OnClickListener)
if (state == State.STOPPED) app.dataStore.isNAT = false): View.OnClickListener)
override def binderDied(): Unit = handler.post(() => {
detachService()
@@ -174,7 +175,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
})
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = resultCode match {
case Activity.RESULT_OK => bgService.use(app.profileId)
case Activity.RESULT_OK => Utils.startSsService(this)
case _ => Log.e(TAG, "Failed to start VpnService")
}
@@ -246,7 +247,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
} else drawer = drawerBuilder.build()
val header = drawer.getHeader
val title = header.findViewById(R.id.drawer_title).asInstanceOf[TextView]
val title = header.findViewById[TextView](R.id.drawer_title)
val tf = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
@@ -256,7 +257,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
txRateText = findViewById(R.id.txRate).asInstanceOf[TextView]
rxText = findViewById(R.id.rx).asInstanceOf[TextView]
rxRateText = findViewById(R.id.rxRate).asInstanceOf[TextView]
findViewById(R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
findViewById[View](R.id.stat).setOnClickListener(_ => if (state == State.CONNECTED && app.isVpnEnabled) {
testCount += 1
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
@@ -298,8 +299,8 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener(_ => if (state == State.CONNECTED) bgService.use(-1) else Utils.ThrowableFuture {
if (app.isNatEnabled) bgService.use(app.profileId) else {
fab.setOnClickListener(_ => if (state == State.CONNECTED) Utils.stopSsService(this) else Utils.ThrowableFuture {
if (app.isNatEnabled) Utils.startSsService(this) else {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
else handler.post(() => onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null))
@@ -313,7 +314,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
changeState(State.IDLE) // reset everything to init state
handler.post(() => attachService(callback))
app.settings.registerOnSharedPreferenceChangeListener(this)
app.dataStore.registerChangeListener(this)
val intent = getIntent
if (intent != null) handleShareIntent(intent)
@@ -350,7 +351,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.show()
}
def onSharedPreferenceChanged(pref: SharedPreferences, key: String): Unit = key match {
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit = key match {
case Key.isNAT => handler.post(() => {
detachService()
attachService(callback)
@@ -368,7 +369,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
case DRAWER_PROFILES => displayFragment(new ProfilesFragment)
case DRAWER_RECOVERY =>
app.track("GlobalConfigFragment", "reset")
if (bgService != null) bgService.use(-1)
Utils.stopSsService(this)
val dialog = ProgressDialog.show(this, "", getString(R.string.recovering), true, false)
val handler = new Handler {
override def handleMessage(msg: Message): Unit = if (dialog.isShowing && !isDestroyed) dialog.dismiss()
@@ -384,6 +385,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
displayFragment(new AboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
case _ => // Ignore
}
true // unexpected cases will throw exception
}
@@ -424,7 +426,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onDestroy() {
super.onDestroy()
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
@@ -49,7 +49,7 @@ class ProfileConfigActivity extends Activity {
toolbar.setOnMenuItemClickListener(child)
}
override def onBackPressed(): Unit = if (app.settings.getBoolean(Key.dirty, false)) new AlertDialog.Builder(this)
override def onBackPressed(): Unit = if (app.dataStore.dirty) new AlertDialog.Builder(this)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(R.string.yes, ((_, _) => child.saveAndExit()): DialogInterface.OnClickListener)
.setNegativeButton(R.string.no, ((_, _) => finish()): DialogInterface.OnClickListener)
@@ -21,13 +21,12 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content._
import android.os.{Build, Bundle, UserManager}
import android.support.design.widget.Snackbar
import android.support.v14.preference.SwitchPreference
import android.support.v7.app.AlertDialog
import android.support.v7.preference.Preference
import android.support.v7.preference.{Preference, PreferenceDataStore}
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.text.TextUtils
import android.view.MenuItem
@@ -35,7 +34,7 @@ import be.mygod.preference.{EditTextPreference, PreferenceFragment}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.plugin._
import com.github.shadowsocks.preference.{IconListPreference, PluginConfigurationDialogFragment}
import com.github.shadowsocks.preference.{IconListPreference, OnPreferenceDataStoreChangeListener, PluginConfigurationDialogFragment}
import com.github.shadowsocks.utils.{Action, Key, Utils}
object ProfileConfigFragment {
@@ -43,7 +42,7 @@ object ProfileConfigFragment {
}
class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListener
with OnSharedPreferenceChangeListener {
with OnPreferenceDataStoreChangeListener {
import ProfileConfigFragment._
private var profile: Profile = _
@@ -53,10 +52,11 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
private var pluginConfiguration: PluginConfiguration = _
override def onCreatePreferences(bundle: Bundle, key: String) {
getPreferenceManager.setPreferenceDataStore(app.dataStore)
app.profileManager.getProfile(getActivity.getIntent.getIntExtra(Action.EXTRA_PROFILE_ID, -1)) match {
case Some(p) =>
profile = p
profile.serialize(app.editor).apply()
profile.serialize(app.dataStore)
case None => getActivity.finish()
}
addPreferencesFromResource(R.xml.pref_profile)
@@ -78,7 +78,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setOnPreferenceChangeListener((_, value) => {
val selected = value.asInstanceOf[String]
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions, selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
pluginConfigure.setEnabled(!TextUtils.isEmpty(selected))
pluginConfigure.setText(pluginConfiguration.selectedOptions.toString)
if (!PluginManager.fetchPlugins()(selected).trusted)
@@ -88,7 +89,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
pluginConfigure.setOnPreferenceChangeListener(onPluginConfigureChanged)
initPlugins()
app.listenForPackageChanges(initPlugins())
app.settings.registerOnSharedPreferenceChangeListener(this)
app.dataStore.registerChangeListener(this)
}
def initPlugins() {
@@ -97,7 +98,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
plugin.setEntryValues(plugins.map(_._2.id.asInstanceOf[CharSequence]).toArray)
plugin.setEntryIcons(plugins.map(_._2.icon).toArray)
plugin.entryPackageNames = plugins.map(_._2.packageName).toArray
pluginConfiguration = new PluginConfiguration(app.settings.getString(Key.plugin, null))
pluginConfiguration = new PluginConfiguration(app.dataStore.plugin)
plugin.setValue(pluginConfiguration.selected)
plugin.init()
plugin.checkSummary()
@@ -109,7 +110,8 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
val selected = pluginConfiguration.selected
pluginConfiguration = new PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected -> new PluginOptions(selected, value.asInstanceOf[String])), selected)
app.editor.putString(Key.plugin, pluginConfiguration.toString).putBoolean(Key.dirty, true).apply()
app.dataStore.plugin = pluginConfiguration.toString
app.dataStore.dirty = true
true
} catch {
case exc: IllegalArgumentException =>
@@ -117,17 +119,17 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
false
}
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.editor.putBoolean(Key.dirty, true).apply()
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String): Unit =
if (key != Key.proxyApps && findPreference(key) != null) app.dataStore.dirty = true
override def onDestroy() {
app.settings.unregisterOnSharedPreferenceChangeListener(this)
app.dataStore.unregisterChangeListener(this)
super.onDestroy()
}
override def onResume() {
super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.proxyApps, false)) // fetch proxyApps updated by AppManager
isProxyApps.setChecked(app.dataStore.proxyApps) // fetch proxyApps updated by AppManager
}
private def showPluginEditor() {
@@ -176,7 +178,7 @@ class ProfileConfigFragment extends PreferenceFragment with OnMenuItemClickListe
}
def saveAndExit() {
profile.deserialize(app.settings)
profile.deserialize(app.dataStore)
app.profileManager.updateProfile(profile)
if (ProfilesFragment.instance != null) ProfilesFragment.instance.profilesAdapter.deepRefreshId(profile.id)
getActivity.finish()
@@ -68,7 +68,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
case _ => false
}
private def isProfileEditable(id: => Int) = getActivity.asInstanceOf[MainActivity].state match {
case State.CONNECTED => id != app.profileId
case State.CONNECTED => id != app.dataStore.profileId
case State.STOPPED => true
case _ => false
}
@@ -78,20 +78,20 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
var item: Profile = _
private val text1 = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = itemView.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val traffic = itemView.findViewById(R.id.traffic).asInstanceOf[TextView]
private val edit = itemView.findViewById(R.id.edit)
private val text1 = itemView.findViewById[TextView](android.R.id.text1)
private val text2 = itemView.findViewById[TextView](android.R.id.text2)
private val traffic = itemView.findViewById[TextView](R.id.traffic)
private val edit = itemView.findViewById[View](R.id.edit)
edit.setOnClickListener(_ => startConfig(item.id))
edit.setOnLongClickListener(cardButtonLongClickListener)
itemView.setOnClickListener(this)
// it will not take effect unless set in code
itemView.findViewById(R.id.indicator).setBackgroundResource(R.drawable.background_profile)
itemView.findViewById[View](R.id.indicator).setBackgroundResource(R.drawable.background_profile)
private var adView: NativeExpressAdView = _
{
val share = itemView.findViewById(R.id.share)
val share = itemView.findViewById[View](R.id.share)
share.setOnClickListener(_ => {
val popup = new PopupMenu(getActivity, share)
popup.getMenuInflater.inflate(R.menu.profile_share_popup, popup.getMenu)
@@ -129,7 +129,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
TrafficMonitor.formatTraffic(tx), TrafficMonitor.formatTraffic(rx)))
}
if (item.id == app.profileId) {
if (item.id == app.dataStore.profileId) {
itemView.setSelected(true)
selectedItem = this
} else {
@@ -147,7 +147,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
adView.setLayoutParams(params)
adView.setAdUnitId("ca-app-pub-9097031975646651/5224027521")
adView.setAdSize(new AdSize(328, 132))
itemView.findViewById(R.id.content).asInstanceOf[LinearLayout].addView(adView)
itemView.findViewById[LinearLayout](R.id.content).addView(adView)
// Demographics
val random = new Random()
@@ -166,11 +166,11 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onClick(v: View): Unit = if (isEnabled) {
val activity = getActivity.asInstanceOf[MainActivity]
val old = app.profileId
val old = app.dataStore.profileId
app.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.setSelected(true)
if (activity.state == State.CONNECTED) activity.bgService.use(item.id) // reconnect to new profile
if (activity.state == State.CONNECTED) Utils.reloadSsService(activity)
}
override def onMenuItemClick(menu: MenuItem): Boolean = menu.getItemId match {
@@ -251,7 +251,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
if (index >= 0) {
profiles.remove(index)
notifyItemRemoved(index)
if (id == app.profileId) app.profileId(0) // switch to null profile
if (id == app.dataStore.profileId) app.dataStore.profileId = 0 // switch to null profile
}
}
}
@@ -278,12 +278,12 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
if (app.profileManager.getFirstProfile.isEmpty) app.dataStore.profileId = app.profileManager.createProfile().id
val profilesList = view.findViewById[RecyclerView](R.id.list)
val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager)
layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
case (profile, i) if profile.id == app.profileId => i
case (profile, i) if profile.id == app.dataStore.profileId => i
}.getOrElse(-1))
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
@@ -24,6 +24,8 @@ import android.app.Activity
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.{Build, Bundle}
import android.support.v4.content.pm.{ShortcutInfoCompat, ShortcutManagerCompat}
import android.support.v4.graphics.drawable.IconCompat
import com.github.shadowsocks.utils.{State, Utils}
/**
@@ -34,11 +36,12 @@ class QuickToggleShortcut extends Activity with ServiceBoundContext {
super.onCreate(savedInstanceState)
getIntent.getAction match {
case Intent.ACTION_CREATE_SHORTCUT =>
setResult(Activity.RESULT_OK, new Intent()
.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(this, classOf[QuickToggleShortcut]))
.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.quick_toggle))
.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher)))
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
new ShortcutInfoCompat.Builder(this, "toggle")
.setIntent(new Intent(this, classOf[QuickToggleShortcut]).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
case _ =>
attachService()
@@ -24,16 +24,16 @@ import java.io.{File, FileOutputStream, IOException}
import java.util.Locale
import android.annotation.SuppressLint
import android.app.Application
import android.app.{Application, NotificationChannel, NotificationManager}
import android.content._
import android.content.res.Configuration
import android.os.{Build, LocaleList}
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
@@ -42,6 +42,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.j256.ormlite.logger.LocalLog
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
object ShadowsocksApplication {
@@ -59,13 +60,13 @@ object ShadowsocksApplication {
class ShadowsocksApplication extends Application {
import ShadowsocksApplication._
lazy val remoteConfig = FirebaseRemoteConfig.getInstance()
lazy val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()
lazy val tracker: Tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
lazy val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
lazy val editor: SharedPreferences.Editor = settings.edit
lazy val profileManager = new ProfileManager(new DBHelper(this))
private lazy val dbHelper = new DBHelper(this)
lazy val profileManager = new ProfileManager(dbHelper)
lazy val dataStore = new OrmLitePreferenceDataStore(dbHelper)
def isNatEnabled: Boolean = settings.getBoolean(Key.isNAT, false)
def isNatEnabled: Boolean = dataStore.isNAT
def isVpnEnabled: Boolean = !isNatEnabled
// send event
@@ -78,13 +79,11 @@ class ShadowsocksApplication extends Application {
.setFatal(false)
.build())
def profileId: Int = settings.getInt(Key.id, 0)
def profileId(i: Int): Unit = editor.putInt(Key.id, i).apply()
def currentProfile: Option[Profile] = profileManager.getProfile(profileId)
def currentProfile: Option[Profile] = profileManager.getProfile(dataStore.profileId)
def switchProfile(id: Int): Profile = {
val result = profileManager.getProfile(id) getOrElse profileManager.createProfile()
profileId(result.id)
dataStore.profileId = result.id
result
}
@@ -153,7 +152,12 @@ class ShadowsocksApplication extends Application {
JobManager.create(this).addJobCreator(DonaldTrump)
TcpFastOpen.enabled(settings.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
TcpFastOpen.enabled(dataStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
if (Build.VERSION.SDK_INT >= 26) getSystemService(classOf[NotificationManager]).createNotificationChannels(List(
new NotificationChannel("service-vpn", getText(R.string.service_vpn), NotificationManager.IMPORTANCE_MIN),
new NotificationChannel("service-nat", getText(R.string.service_nat), NotificationManager.IMPORTANCE_LOW)
))
}
def crashRecovery() {
@@ -186,10 +190,10 @@ class ShadowsocksApplication extends Application {
autoClose(new FileOutputStream(new File(getFilesDir, file)))(out =>
IOUtils.copy(in, out)))
}
editor.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE).apply()
dataStore.putInt(Key.currentVersionCode, BuildConfig.VERSION_CODE)
}
def updateAssets(): Unit = if (settings.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
def updateAssets(): Unit = if (dataStore.getInt(Key.currentVersionCode, -1) != BuildConfig.VERSION_CODE) copyAssets()
def listenForPackageChanges(callback: => Unit): BroadcastReceiver = {
val filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED)
@@ -20,26 +20,11 @@
package com.github.shadowsocks
import android.app.backup.{BackupAgentHelper, FileBackupHelper, SharedPreferencesBackupHelper}
import android.app.backup.{BackupAgentHelper, FileBackupHelper}
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.database.DBHelper
class ShadowsocksBackupAgent extends BackupAgentHelper {
// The names of the SharedPreferences groups that the application maintains. These
// are the same strings that are passed to getSharedPreferences(String, int).
val PREFS_DISPLAY = "com.github.shadowsocks_preferences"
// An arbitrary string used within the BackupAgentHelper implementation to
// identify the SharedPreferencesBackupHelper's data.
val MY_PREFS_BACKUP_KEY = "com.github.shadowsocks"
val DATABASE = "com.github.shadowsocks.database.profile"
override def onCreate() {
val helper = new SharedPreferencesBackupHelper(this, PREFS_DISPLAY)
addHelper(MY_PREFS_BACKUP_KEY, helper)
addHelper(DATABASE, new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE,
Acl.CUSTOM_RULES + ".acl"))
}
override def onCreate(): Unit = addHelper("com.github.shadowsocks.database.profile",
new FileBackupHelper(this, "../databases/" + DBHelper.PROFILE, Acl.CUSTOM_RULES + ".acl"))
}
@@ -24,11 +24,12 @@ import java.io.File
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.app.Service
import android.content._
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.acl.{Acl, AclSyncJob}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
val CMD_IPTABLES_DNAT_ADD_SOCKS =
"iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123"
private var notification: ShadowsocksNotification = _
val myUid: Int = android.os.Process.myUid()
var sslocalProcess: GuardedProcess = _
@@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService {
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath
cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
}
sslocalProcess = new GuardedProcess(cmd: _*).start()
@@ -177,15 +180,17 @@ class ShadowsocksNatService extends BaseService {
su.addCommand((init_sb ++ http_sb).toArray)
}
override def startRunner(profile: Profile): Unit = if (su == null)
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (su == null) {
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
if (exitCode == 0) super.startRunner(profile) else {
if (exitCode == 0) super.onStartCommand(intent, flags, startId) else {
if (su != null) {
su.close()
su = null
}
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
})
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
override def connect() {
super.connect()
@@ -204,12 +209,11 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name, true)
}
override def stopRunner(stopService: Boolean, msg: String = null) {
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-nat", true)
if (notification != null) notification.destroy()
override def stopRunner(stopService: Boolean, msg: String = null) {
// channge the state
changeState(State.STOPPING)
@@ -28,13 +28,15 @@ import android.os.{Build, PowerManager}
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
import com.github.shadowsocks.utils.{Action, State, TrafficMonitor, Utils}
/**
* @author Mygod
*/
class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
class ShadowsocksNotification(private val service: BaseService, profileName: String,
channel: String, visible: Boolean = false) {
private val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
private lazy val nm = service.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private lazy val callback = new Stub {
@@ -52,7 +54,7 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
private var lockReceiver: BroadcastReceiver = _
private var callbackRegistered: Boolean = _
private val builder = new NotificationCompat.Builder(service)
private val builder = new NotificationCompat.Builder(service, channel)
.setWhen(0)
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
.setTicker(service.getString(R.string.forward_success))
@@ -26,6 +26,7 @@ import android.net.VpnService
import android.os.{Bundle, Handler}
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Utils
object ShadowsocksRunnerActivity {
private final val TAG = "ShadowsocksRunnerActivity"
@@ -46,7 +47,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
def startBackgroundService() {
if (app.isNatEnabled) {
bgService.use(app.profileId)
Utils.startSsService(this)
finish()
} else {
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
@@ -73,7 +74,6 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
} else {
attachService()
}
finish()
}
override def onDestroy() {
@@ -87,10 +87,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
resultCode match {
case Activity.RESULT_OK =>
if (bgService != null) {
bgService.use(app.profileId)
}
case Activity.RESULT_OK => Utils.startSsService(this)
case _ =>
Log.e(TAG, "Failed to start VpnService")
}
@@ -1,57 +0,0 @@
/*******************************************************************************/
/* */
/* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> */
/* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*******************************************************************************/
package com.github.shadowsocks
import android.app.Service
import android.content.Intent
import android.net.VpnService
import android.os.{Handler, IBinder}
import com.github.shadowsocks.ShadowsocksApplication.app
class ShadowsocksRunnerService extends Service with ServiceBoundContext {
val handler = new Handler()
override def onBind(intent: Intent): IBinder = {
null
}
override def onServiceConnected() {
handler.postDelayed(() => {
if (bgService != null) {
if (app.isNatEnabled) startBackgroundService()
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
handler.postDelayed(() => stopSelf(), 3000)
}
}, 1000)
}
def startBackgroundService(): Unit = bgService.useSync(app.profileId)
override def onCreate() {
super.onCreate()
attachService()
}
override def onDestroy() {
super.onDestroy()
detachService()
}
}
@@ -23,6 +23,7 @@ package com.github.shadowsocks
import java.io.File
import java.util.Locale
import android.app.Service
import android.content._
import android.content.pm.PackageManager.NameNotFoundException
import android.net.VpnService
@@ -30,7 +31,6 @@ import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.acl.{Acl, AclSyncJob, Subnet}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import scala.collection.mutable.ArrayBuffer
@@ -42,7 +42,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
var conn: ParcelFileDescriptor = _
var vpnThread: ShadowsocksVpnThread = _
private var notification: ShadowsocksNotification = _
var sslocalProcess: GuardedProcess = _
var overtureProcess: GuardedProcess = _
@@ -69,8 +68,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = null
}
if (notification != null) notification.destroy()
// channge the state
changeState(State.STOPPING)
@@ -103,20 +100,19 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
override def startRunner(profile: Profile) {
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
// ensure the VPNService is prepared
if (VpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)
stopRunner(stopService = true)
return
}
super.startRunner(profile)
Service.START_NOT_STICKY
} else super.onStartCommand(intent, flags, startId)
}
override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-vpn")
override def connect() {
super.connect()
@@ -137,8 +133,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL && profile.route != Acl.CUSTOM_RULES)
AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.name)
}
/** Called when the activity is first created. */
@@ -167,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath
cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
@@ -42,7 +42,7 @@ class TaskerActivity extends AppCompatActivity {
typedArray.recycle()
}
private var item: Profile = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[CheckedTextView]
private val text = itemView.findViewById[CheckedTextView](android.R.id.text1)
itemView.setOnClickListener(this)
def bindDefault() {
@@ -30,10 +30,17 @@ import com.github.shadowsocks.ShadowsocksApplication.app
class TaskerReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent) {
val settings = TaskerSettings.fromIntent(intent)
var changed = false
app.profileManager.getProfile(settings.profileId) match {
case Some(_) => app.switchProfile(settings.profileId)
case Some(_) => if (app.dataStore.profileId != settings.profileId) {
app.switchProfile(settings.profileId)
changed = true
}
case _ =>
}
if (settings.switchOn) Utils.startSsService(context) else Utils.stopSsService(context)
if (settings.switchOn) {
Utils.startSsService(context)
if (changed) Utils.reloadSsService(context)
} else Utils.stopSsService(context)
}
}
@@ -30,7 +30,7 @@ class ToolbarFragment extends Fragment {
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar = view.findViewById[Toolbar](R.id.toolbar)
val activity = getActivity.asInstanceOf[MainActivity]
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
}
@@ -1,7 +1,9 @@
package com.github.shadowsocks.acl
import java.io.{File, FileNotFoundException}
import java.net.URL
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
@@ -17,17 +19,22 @@ import scala.io.Source
* @author Mygod
*/
class Acl {
import Acl._
@DatabaseField(generatedId = true)
var id: Int = _
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
val urls = new mutable.SortedList[URL]()(urlOrdering)
@DatabaseField
var bypass: Boolean = _
def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def getUrlsString: String = urls.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
@@ -40,6 +47,10 @@ class Acl {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def setUrlsString(value: String) {
urls.clear()
urls ++= value.split("\n").map(new URL(_))
}
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
@@ -48,6 +59,8 @@ class Acl {
proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
urls.clear()
urls ++= other.urls
bypass = other.bypass
this
}
@@ -55,6 +68,7 @@ class Acl {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
urls.clear()
bypass = defaultBypass
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
lazy val proxySubnets = new mutable.SortedList[Subnet]()
@@ -62,7 +76,12 @@ class Acl {
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => line
case index => line.substring(0, index) // trim comments
case index =>
line.substring(index + 1) match {
case networkAclParser(url) => urls.add(new URL(url))
case _ => // ignore
}
line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
@@ -85,6 +104,23 @@ class Acl {
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
def flatten(depth: Int): Acl = {
if (depth > 0) for (url <- urls) {
val child = new Acl().fromSource(Source.fromURL(url), bypass).flatten(depth - 1)
if (bypass != child.bypass) {
Log.w(TAG, "Imported network ACL has a conflicting mode set. This will probably not work as intended. URL: %s"
.format(url))
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
bypassHostnames ++= child.bypassHostnames
proxyHostnames ++= child.proxyHostnames
subnets ++= child.subnets
}
urls.clear()
this
}
override def toString: String = {
val result = new StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
@@ -101,11 +137,10 @@ class Acl {
result.append(proxyList.mkString("\n"))
result.append('\n')
}
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
result.toString
}
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
// Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol
@@ -120,6 +155,7 @@ class Acl {
}
object Acl {
final val TAG = "Acl"
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
@@ -127,6 +163,10 @@ object Acl {
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
final val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".r
private val urlOrdering: Ordering[URL] = Ordering.by(url => (url.getHost, url.getPort, url.getFile, url.getProtocol))
def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = {
@@ -1,6 +1,6 @@
package com.github.shadowsocks.acl
import java.net.IDN
import java.net.{IDN, URL}
import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
@@ -26,13 +26,15 @@ import scala.io.Source
* @author Mygod
*/
object CustomRulesFragment {
private final val GENERIC = 0
private final val DOMAIN = 1
private final val TEMPLATE_GENERIC = 0
private final val TEMPLATE_DOMAIN = 1
private final val TEMPLATE_URL = 2
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
private final val TEMPLATE_DOMAIN = "(^|\\.)%s$"
private final val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
private final val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
}
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
@@ -44,18 +46,26 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case _ => false
}
private def createAclRuleDialog(text: CharSequence = "") = {
private def createAclRuleDialog(item: AnyRef = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) =>
templateSelector.setSelection(DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
templateSelector.setSelection(GENERIC)
editText.setText(text)
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content)
item match {
case hostname: String => PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) =>
templateSelector.setSelection(TEMPLATE_DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(hostname)
}
case url: URL =>
templateSelector.setSelection(TEMPLATE_URL)
editText.setText(url.toString)
case i =>
templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(i.toString)
}
(templateSelector, editText, new AlertDialog.Builder(getActivity)
.setTitle(R.string.edit_rule)
@@ -70,7 +80,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _
private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text = itemView.findViewById[TextView](android.R.id.text1)
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable)
@@ -85,9 +95,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
text.setText(subnet.toString)
itemView.setSelected(selectedItems.contains(subnet))
}
def bind(url: URL) {
item = url
text.setText(url.toString)
itemView.setSelected(selectedItems.contains(url))
}
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
.setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item)
@@ -125,10 +140,13 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
})
}
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
val j = i - acl.subnets.size
if (j < 0) vh.bind(acl.subnets(i)) else vh.bind(acl.proxyHostnames(j))
if (j < 0) vh.bind(acl.subnets(i)) else {
val k = j - acl.proxyHostnames.size
if (k < 0) vh.bind(acl.proxyHostnames(j)) else vh.bind(acl.urls(k))
}
}
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false))
@@ -145,21 +163,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
apply()
index
} else -1
def addURL(url: URL): Int = if (acl.urls.add(url)) {
val index = acl.urls.indexOf(url) + acl.subnets.size + acl.proxyHostnames.size
notifyItemInserted(index)
apply()
index
} else -1
def addToProxy(input: String): Int = {
val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
for (url <- acl.urls) result = addURL(url)
result
}
def addFromTemplate(template: Int, text: CharSequence): Int = template match {
case GENERIC => addToProxy(text.toString)
case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
case TEMPLATE_GENERIC => addToProxy(text.toString)
case TEMPLATE_DOMAIN => try addHostname(TEMPLATE_REGEX_DOMAIN.formatLocal(Locale.ENGLISH,
IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
case exc: IllegalArgumentException =>
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
-1
}
case TEMPLATE_URL => addURL(new URL(text.toString))
case _ => -1
}
@@ -169,8 +195,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
undoManager.remove((i, acl.subnets(i)))
acl.subnets.remove(i)
} else {
undoManager.remove((j, acl.proxyHostnames(j)))
acl.proxyHostnames.remove(j)
val k = j - acl.proxyHostnames.size
if (k < 0) {
undoManager.remove((j, acl.proxyHostnames(j)))
acl.proxyHostnames.remove(j)
} else {
undoManager.remove((k, acl.urls(k)))
acl.urls.remove(k)
}
}
notifyItemRemoved(i)
apply()
@@ -181,9 +213,13 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
acl.subnets.remove(subnet)
apply()
case hostname: String =>
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
acl.proxyHostnames.remove(hostname)
apply()
case url: URL =>
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
acl.urls.remove(url)
apply()
}
def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
@@ -192,20 +228,25 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
onSelectedItemsUpdated()
}
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
apply()
}
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
notifyItemInserted(acl.subnets.indexOf(subnet))
apply()
}
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
apply()
}
case url: URL => if (acl.urls.insert(url)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
apply()
}
}
def selectAll() {
selectedItems.clear()
selectedItems ++= acl.subnets
selectedItems ++= acl.proxyHostnames
selectedItems ++= acl.urls
onSelectedItemsUpdated()
notifyDataSetChanged()
}
@@ -214,11 +255,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
val j = i - acl.subnets.size
try {
(if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else {
val hostname = acl.proxyHostnames(i)
PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
val k = j - acl.proxyHostnames.size
if (k < 0) {
val hostname = acl.proxyHostnames(j)
PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
} else acl.urls(k).getHost
}).substring(0, 1)
} catch {
case _: IndexOutOfBoundsException => " "
@@ -245,6 +289,10 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case null =>
case arr => selectedItems ++= arr
}
savedInstanceState.getStringArray(SELECTED_URLS) match {
case null =>
case arr => selectedItems ++= arr.map(new URL(_))
}
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
@@ -252,12 +300,11 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
toolbar.setOnMenuItemClickListener(this)
selectionItem = toolbar.getMenu.findItem(R.id.selection)
selectionItem.setVisible(selectedItems.nonEmpty)
list = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
list = view.findViewById(R.id.list)
list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
list.setItemAnimator(new DefaultItemAnimator)
list.setAdapter(adapter)
val fastScroller = view.findViewById(R.id.fastscroller).asInstanceOf[FastScroller]
fastScroller.setRecyclerView(list)
view.findViewById[FastScroller](R.id.fastscroller).setRecyclerView(list)
undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
@@ -278,18 +325,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
outState.putStringArray(SELECTED_URLS, selectedItems.filter(_.isInstanceOf[URL]).map(_.toString).toArray)
}
def copySelected() {
val acl = new Acl()
acl.bypass = true
selectedItems.foreach {
case subnet: Subnet => acl.subnets.add(subnet)
case hostname: String => acl.proxyHostnames.add(hostname)
case url: URL => acl.urls.add(url)
}
clipboard.setPrimaryClip(ClipData.newPlainText(null, acl.toString))
}
override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
case R.id.action_select_all =>
adapter.selectAll()
true
case R.id.action_cut =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
copySelected()
adapter.removeSelected()
true
case R.id.action_copy =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
copySelected()
true
case R.id.action_delete =>
adapter.removeSelected()
@@ -303,15 +361,18 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
.create().show()
true
case R.id.action_import =>
try if (adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1) return true catch {
case _: Exception =>
try adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1 catch {
case exc: Exception =>
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
exc.printStackTrace()
}
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show()
true
case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet)
acl.proxyHostnames.foreach(adapter.addHostname)
acl.urls.foreach(adapter.addURL)
true
case _ => false
}
@@ -20,10 +20,14 @@
package com.github.shadowsocks.database
import android.content.Context
import java.nio.ByteBuffer
import android.content.{Context, SharedPreferences}
import android.content.pm.ApplicationInfo
import android.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
@@ -46,17 +50,20 @@ object DBHelper {
}
class DBHelper(val context: Context)
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 22) {
extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 23) {
import DBHelper._
lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile])
lazy val kvPairDao: Dao[KeyValuePair, String] = getDao(classOf[KeyValuePair])
def onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.createTable(connectionSource, classOf[Profile])
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
}
def recreate(database: SQLiteDatabase, connectionSource: ConnectionSource) {
TableUtils.dropTable(connectionSource, classOf[Profile], true)
TableUtils.dropTable(connectionSource, classOf[KeyValuePair], true)
onCreate(database, connectionSource)
}
@@ -132,6 +139,22 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
}
if (oldVersion < 23) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
profileDao.executeRawNoArgs("COMMIT;")
import KeyValuePair._
val old = PreferenceManager.getDefaultSharedPreferences(app)
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.isNAT, TYPE_BOOLEAN,
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.isNAT, false)) 1 else 0).toByte).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN,
ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array()))
kvPairDao.createOrUpdate(new KeyValuePair(Key.currentVersionCode, TYPE_INT,
ByteBuffer.allocate(4).putInt(-1).array()))
}
} catch {
case ex: Exception =>
app.track(ex)
@@ -0,0 +1,32 @@
package com.github.shadowsocks.database
import com.j256.ormlite.field.{DataType, DatabaseField}
/**
* @author Mygod
*/
object KeyValuePair {
val TYPE_UNINITIALIZED = 0
val TYPE_BOOLEAN = 1
val TYPE_FLOAT = 2
val TYPE_INT = 3
val TYPE_LONG = 4
val TYPE_STRING = 5
val TYPE_STRING_SET = 6
}
class KeyValuePair {
@DatabaseField(id = true)
var key: String = _
@DatabaseField
var valueType: Int = _
@DatabaseField(dataType = DataType.BYTE_ARRAY)
var value: Array[Byte] = _
def this(key: String, valueType: Int, value: Array[Byte]) = {
this()
this.key = key
this.valueType = valueType
this.value = value
}
}
@@ -22,11 +22,11 @@ package com.github.shadowsocks.database
import java.util.Locale
import android.content.SharedPreferences
import android.net.Uri
import android.os.Binder
import android.util.Base64
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.OrmLitePreferenceDataStore
import com.github.shadowsocks.utils.Key
import com.j256.ormlite.field.{DataType, DatabaseField}
@@ -108,37 +108,38 @@ class Profile {
}
override def toString: String = toUri.toString
def serialize(editor: SharedPreferences.Editor): SharedPreferences.Editor = editor
.putString(Key.name, name)
.putString(Key.host, host)
.putInt(Key.localPort, localPort)
.putInt(Key.remotePort, remotePort)
.putString(Key.password, password)
.putString(Key.route, route)
.putString(Key.remoteDns, remoteDns)
.putString(Key.method, method)
.putBoolean(Key.proxyApps, proxyApps)
.putBoolean(Key.bypass, bypass)
.putBoolean(Key.udpdns, udpdns)
.putBoolean(Key.ipv6, ipv6)
.putString(Key.individual, individual)
.putString(Key.plugin, plugin)
.remove(Key.dirty)
def deserialize(pref: SharedPreferences) {
def serialize(store: OrmLitePreferenceDataStore) {
store.putString(Key.name, name)
store.putString(Key.host, host)
store.putInt(Key.localPort, localPort)
store.putInt(Key.remotePort, remotePort)
store.putString(Key.password, password)
store.putString(Key.route, route)
store.putString(Key.remoteDns, remoteDns)
store.putString(Key.method, method)
store.proxyApps = proxyApps
store.bypass = bypass
store.putBoolean(Key.udpdns, udpdns)
store.putBoolean(Key.ipv6, ipv6)
store.individual = individual
store.plugin = plugin
store.remove(Key.dirty)
}
def deserialize(store: OrmLitePreferenceDataStore) {
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = pref.getString(Key.name, null)
host = pref.getString(Key.host, null)
localPort = pref.getInt(Key.localPort, 0)
remotePort = pref.getInt(Key.remotePort, 0)
password = pref.getString(Key.password, null)
method = pref.getString(Key.method, null)
route = pref.getString(Key.route, null)
remoteDns = pref.getString(Key.remoteDns, null)
proxyApps = pref.getBoolean(Key.proxyApps, false)
bypass = pref.getBoolean(Key.bypass, false)
udpdns = pref.getBoolean(Key.udpdns, false)
ipv6 = pref.getBoolean(Key.ipv6, false)
individual = pref.getString(Key.individual, null)
plugin = pref.getString(Key.plugin, null)
name = store.getString(Key.name, null)
host = store.getString(Key.host, null)
localPort = store.getInt(Key.localPort, 0)
remotePort = store.getInt(Key.remotePort, 0)
password = store.getString(Key.password, null)
method = store.getString(Key.method, null)
route = store.getString(Key.route, null)
remoteDns = store.getString(Key.remoteDns, null)
proxyApps = store.proxyApps
bypass = store.bypass
udpdns = store.getBoolean(Key.udpdns, false)
ipv6 = store.getBoolean(Key.ipv6, false)
individual = store.individual
plugin = store.plugin
}
}
@@ -46,9 +46,9 @@ final class BottomSheetPreferenceDialogFragment extends PreferenceDialogFragment
private final class IconListViewHolder(val dialog: BottomSheetDialog, view: View) extends ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
private var index: Int = _
private val text1 = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
private val text2 = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
private val icon = view.findViewById(android.R.id.icon).asInstanceOf[ImageView]
private val text1 = view.findViewById[TextView](android.R.id.text1)
private val text2 = view.findViewById[TextView](android.R.id.text2)
private val icon = view.findViewById[ImageView](android.R.id.icon)
view.setOnClickListener(this)
view.setOnLongClickListener(this)
@@ -0,0 +1,10 @@
package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore
/**
* @author Mygod
*/
trait OnPreferenceDataStoreChangeListener {
def onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
}
@@ -0,0 +1,116 @@
package com.github.shadowsocks.preference
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.util
import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.{DBHelper, KeyValuePair}
import com.github.shadowsocks.utils.Key
import scala.collection.JavaConversions._
/**
* @author Mygod
*/
//noinspection AccessorLikeMethodIsUnit
final class OrmLitePreferenceDataStore(dbHelper: DBHelper) extends PreferenceDataStore {
import KeyValuePair._
override def getBoolean(key: String, defValue: Boolean = false): Boolean = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_BOOLEAN) ByteBuffer.wrap(pair.value).get() != 0 else defValue
case _ => defValue
}
override def getFloat(key: String, defValue: Float): Float = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_FLOAT) ByteBuffer.wrap(pair.value).getFloat() else defValue
case _ => defValue
}
override def getInt(key: String, defValue: Int): Int = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_INT) ByteBuffer.wrap(pair.value).getInt() else defValue
case _ => defValue
}
override def getLong(key: String, defValue: Long): Long = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_LONG) ByteBuffer.wrap(pair.value).getLong() else defValue
case _ => defValue
}
override def getString(key: String, defValue: String = null): String = dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair =>
if (pair.valueType == TYPE_STRING) new String(pair.value) else defValue
case _ => defValue
}
override def getStringSet(key: String, defValue: util.Set[String]): util.Set[String] =
dbHelper.kvPairDao.queryForId(key) match {
case pair: KeyValuePair => if (pair.valueType == TYPE_STRING_SET) {
val buffer = ByteBuffer.wrap(pair.value)
val result = new util.HashSet[String]()
while (buffer.hasRemaining) {
val chArr = new Array[Byte](buffer.getInt)
buffer.get(chArr)
result.add(new String(chArr))
}
result
} else defValue
case _ => defValue
}
override def putBoolean(key: String, value: Boolean) {
dbHelper.kvPairDao.createOrUpdate(
new KeyValuePair(key, TYPE_BOOLEAN, ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte).array()))
fireChangeListener(key)
}
override def putFloat(key: String, value: Float) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_FLOAT, ByteBuffer.allocate(4).putFloat(value).array()))
fireChangeListener(key)
}
override def putInt(key: String, value: Int) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_INT, ByteBuffer.allocate(4).putInt(value).array()))
fireChangeListener(key)
}
override def putLong(key: String, value: Long) {
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_LONG, ByteBuffer.allocate(8).putLong(value).array()))
fireChangeListener(key)
}
override def putString(key: String, value: String) {
value match {
case null => remove(key)
case _ => dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING, value.getBytes()))
}
fireChangeListener(key)
}
override def putStringSet(key: String, value: util.Set[String]) {
val stream = new ByteArrayOutputStream()
for (v <- value) {
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(v.getBytes())
}
dbHelper.kvPairDao.createOrUpdate(new KeyValuePair(key, TYPE_STRING_SET, stream.toByteArray))
fireChangeListener(key)
}
def remove(key: String): Int = dbHelper.kvPairDao.deleteById(key)
private var listeners: Set[OnPreferenceDataStoreChangeListener] = Set.empty
private def fireChangeListener(key: String) = listeners.foreach(_.onPreferenceDataStoreChanged(this, key))
def registerChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners += listener
def unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener): Unit = listeners -= listener
def profileId: Int = getInt(Key.id, 0)
def profileId_=(i: Int): Unit = putInt(Key.id, i)
def isNAT: Boolean = getBoolean(Key.isNAT)
def isNAT_=(value: Boolean): Unit = putBoolean(Key.isNAT, value)
def proxyApps: Boolean = getBoolean(Key.proxyApps)
def proxyApps_=(value: Boolean): Unit = putBoolean(Key.proxyApps, value)
def bypass: Boolean = getBoolean(Key.bypass)
def bypass_=(value: Boolean): Unit = putBoolean(Key.bypass, value)
def individual: String = getString(Key.individual)
def individual_=(value: String): Unit = putString(Key.individual, value)
def plugin: String = getString(Key.plugin)
def plugin_=(value: String): Unit = putString(Key.plugin, value)
def dirty: Boolean = getBoolean(Key.dirty)
def dirty_=(value: Boolean): Unit = putBoolean(Key.dirty, value)
}
@@ -94,6 +94,7 @@ object State {
object Action {
final val SERVICE = "com.github.shadowsocks.SERVICE"
final val CLOSE = "com.github.shadowsocks.CLOSE"
final val RELOAD = "com.github.shadowsocks.RELOAD"
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
@@ -33,7 +33,7 @@ import android.view.View.MeasureSpec
import android.view.{Gravity, View, Window}
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
import com.github.shadowsocks.{BuildConfig, ShadowsocksNatService, ShadowsocksVpnService}
import org.xbill.DNS._
import scala.collection.JavaConversions._
@@ -137,10 +137,11 @@ object Utils {
}
def startSsService(context: Context) {
val intent = new Intent(context, classOf[ShadowsocksRunnerService])
context.startService(intent)
val intent =
new Intent(context, if (app.dataStore.isNAT) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService])
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
}
def reloadSsService(context: Context): Unit = context.sendBroadcast(new Intent(Action.RELOAD))
def stopSsService(context: Context) {
val intent = new Intent(Action.CLOSE)
context.sendBroadcast(intent)
+3
View File
@@ -1,3 +1,6 @@
* 0.0.4:
* Enlarge text size of number pickers;
* Update support library version to 26.0.0.
* 0.0.3:
* Update support library version to 25.2.0.
* 0.0.2:
+1 -1
View File
@@ -36,7 +36,7 @@ First you need to add this library to your dependencies. This library is written
and it's most convenient to use it with SBT:
```scala
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.2"
libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.4"
```
### Native binary configuration
+1 -1
View File
@@ -2,7 +2,7 @@ enablePlugins(AndroidLib)
android.useSupportVectors
name := "plugin"
version := "0.0.3"
version := "0.0.4"
pomExtra in Global := {
<url>https://github.com/shadowsocks/shadowsocks-android</url>
+2 -2
View File
@@ -2,10 +2,10 @@
package="com.github.shadowsocks.plugin">
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="25"/>
android:targetSdkVersion="26"/>
<application
android:theme="@style/Theme.Shadowsocks">
<meta-data android:name="com.github.shadowsocks.plugin.version"
android:value="0.0.2"/>
android:value="0.0.4"/>
</application>
</manifest>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"تنظیمات سرور"</string>
<!-- alert category -->
<string name="yes">"بله"</string>
<string name="no">"خیر"</string>
<string name="apply">"تایید"</string>
</resources>
@@ -6,4 +6,10 @@
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<!-- alert category -->
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
</resources>
+1 -1
View File
@@ -40,6 +40,6 @@
<item name="android:scrollbars">vertical</item>
</style>
<style name="NumberPickerStyle">
<item name="android:textSize">14sp</item>
<item name="android:textSize">18sp</item>
</style>
</resources>
@@ -60,7 +60,8 @@ class EditTextPreference(context: Context, attrs: AttributeSet = null) extends P
}
override def setText(text: String): Unit = {
val old = getText
super.setText(text)
notifyChanged()
if (old != text) notifyChanged()
}
}
@@ -36,7 +36,7 @@ class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
val oldParent = editText.getParent.asInstanceOf[ViewGroup]
if (oldParent eq view) return
if (oldParent != null) oldParent.removeView(editText)
val oldEdit = view.findViewById(android.R.id.edit)
val oldEdit = view.findViewById[View](android.R.id.edit)
if (oldEdit == null) return
val container = oldEdit.getParent.asInstanceOf[ViewGroup]
if (container == null) return
@@ -77,7 +77,7 @@ class PreferenceGroupAdapter(group: PreferenceGroup) extends Old(group) {
view.setBackground(background)
ViewCompat.setPaddingRelative(view, s, t, e, b)
}
val widgetFrame = view.findViewById(android.R.id.widget_frame).asInstanceOf[ViewGroup]
val widgetFrame = view.findViewById[ViewGroup](android.R.id.widget_frame)
if (widgetFrame != null) {
val widgetResId = fieldWidgetResId.get(pl).asInstanceOf[Int]
if (widgetResId != 0) inflater.inflate(widgetResId, widgetFrame) else widgetFrame.setVisibility(View.GONE)
+1
View File
@@ -0,0 +1 @@
sbt.version=0.13.16
+1 -1
View File
@@ -1,4 +1,4 @@
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.9")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")