diff --git a/.travis.yml b/.travis.yml
index a9a15ed4..88ab7c8a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/README.md b/README.md
index f2ea0345..61f65178 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/build.sbt b/build.sbt
index ca3c9253..db86a192 100644
--- a/build.sbt
+++ b/build.sbt
@@ -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("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)
diff --git a/mobile/build.sbt b/mobile/build.sbt
index 13d16639..54c86540 100644
--- a/mobile/build.sbt
+++ b/mobile/build.sbt
@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
android.useSupportVectors
name := "shadowsocks"
-version := "4.1.8"
-versionCode := Some(188)
+version := "4.2.0"
+versionCode := Some(190)
proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" ::
@@ -23,11 +23,11 @@ proguardOptions ++=
"-keep public class com.evernote.android.job.JobRescheduleService" ::
Nil
-val playServicesVersion = "10.2.6"
+val playServicesVersion = "11.0.4"
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-alpha3" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
@@ -35,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.2" ::
+ "com.mikepenz" % "iconics-core" % "2.8.9" ::
+ "com.mikepenz" % "materialdrawer" % "5.9.4" ::
+ "com.mikepenz" % "materialize" % "1.0.3" ::
+ "com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
- "me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
+ "me.dm7.barcodescanner" % "zxing" % "1.9.5" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index c40c5e0b..4f610940 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -17,13 +17,17 @@
+ android:targetSdkVersion="26"/>
+
+
-
-
-
-
+
-
+
@@ -159,9 +159,6 @@
android:process=":bg"/>
-
-
-
diff --git a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl
index 17545289..5735de28 100644
--- a/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl
+++ b/mobile/src/main/aidl/com/github/shadowsocks/aidl/IShadowsocksService.aidl
@@ -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);
}
diff --git a/mobile/src/main/ic_launcher-web.png b/mobile/src/main/ic_launcher-web.png
index b394e2ae..26b9aecc 100644
Binary files a/mobile/src/main/ic_launcher-web.png and b/mobile/src/main/ic_launcher-web.png differ
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
index d203f978..921c652d 100644
--- a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
@@ -16,24 +16,18 @@
package com.github.shadowsocks.utils;
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
import android.annotation.TargetApi;
-import android.util.Log;
+import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.os.Build;
import java.lang.reflect.Method;
-import java.lang.reflect.InvocationTargetException;
+import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
-import java.net.InetAddress;
public class Dns {
-
public static String getDnsResolver(Context context) throws Exception {
Collection dnsResolvers = getDnsResolvers(context);
if (dnsResolvers.isEmpty()) {
@@ -46,25 +40,22 @@ public class Dns {
return dnsResolver;
}
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static Collection getDnsResolvers(Context context) throws Exception {
ArrayList addresses = new ArrayList();
ConnectivityManager connectivityManager =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
Class> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
- Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{});
+ 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", new Class []{});
+ Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses");
Collection> dnses = (Collection>)getDnsesMethod.invoke(linkProperties);
for (Object dns : dnses) {
addresses.add((InetAddress)dns);
}
} else {
- for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) {
- addresses.add(dns);
- }
+ addresses.addAll(((LinkProperties) linkProperties).getDnsServers());
}
}
diff --git a/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml b/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml
new file mode 100644
index 00000000..a6a7266d
--- /dev/null
+++ b/mobile/src/main/res/color-v24/ic_launcher_foreground_shadow.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml b/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..f5007c3d
--- /dev/null
+++ b/mobile/src/main/res/drawable-anydpi-v24/ic_launcher_foreground.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/mobile/src/main/res/layout/layout_profile.xml b/mobile/src/main/res/layout/layout_profile.xml
index aacb3bd8..3ba50070 100644
--- a/mobile/src/main/res/layout/layout_profile.xml
+++ b/mobile/src/main/res/layout/layout_profile.xml
@@ -37,7 +37,7 @@
+
+
+
+
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png
index 7aa45577..576274b8 100644
Binary files a/mobile/src/main/res/mipmap-hdpi/ic_launcher.png and b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png
index 7f252778..176e2c83 100644
Binary files a/mobile/src/main/res/mipmap-mdpi/ic_launcher.png and b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png
index f3011732..ba786d96 100644
Binary files a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png and b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d4f7d1a9..01c60ec3 100644
Binary files a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index da26ce19..0711dbd4 100644
Binary files a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml
new file mode 100644
index 00000000..64da4fad
--- /dev/null
+++ b/mobile/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #7488A1
+
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index d97b17f9..6de18162 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -56,6 +56,8 @@
Forward all DNS requests to remote
+ VPN Service
+ NAT Service
Shadowsocks started.
Invalid server name
Failed to connect the remote server
diff --git a/mobile/src/main/res/xml/network_security_config.xml b/mobile/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..c9828fa4
--- /dev/null
+++ b/mobile/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala
index bed140d3..785a2110 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/AboutFragment.scala
@@ -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 = {
diff --git a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala
index ceffb9d3..c4e97ae7 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/AppManager.scala
@@ -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)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
index efa325eb..cd08966d 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
@@ -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 {
@@ -160,36 +157,9 @@ trait BaseService extends Service {
pluginPath = PluginManager.init(plugin)
}
- def startRunner(profile: Profile) {
- this.profile = profile
-
- startService(new Intent(this, getClass))
- TrafficMonitor.reset()
- trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
- trafficMonitorThread.start()
-
- if (!closeReceiverRegistered) {
- // register close receiver
- val filter = new IntentFilter()
- filter.addAction(Intent.ACTION_SHUTDOWN)
- filter.addAction(Action.CLOSE)
- registerReceiver(closeReceiver, filter)
- closeReceiverRegistered = true
- }
-
- app.track(getClass.getSimpleName, "start")
-
- changeState(State.CONNECTING)
-
- Utils.ThrowableFuture(try connect() catch {
- case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
- case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
- case exc: Throwable =>
- stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
- exc.printStackTrace()
- app.track(exc)
- })
- }
+ def createNotification(): ShadowsocksNotification
+ def startRunner(): Unit = if (Build.VERSION.SDK_INT >= 26) startForegroundService(new Intent(this, getClass))
+ else startService(new Intent(this, getClass))
def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier
@@ -198,6 +168,8 @@ trait BaseService extends Service {
closeReceiverRegistered = false
}
+ if (notification != null) notification.destroy()
+
// Make sure update total traffic when stopping the runner
updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
@@ -276,7 +248,49 @@ trait BaseService extends Service {
}
// Service of shadowsocks should always be started explicitly
- override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
+ override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
+ state match {
+ case State.STOPPED | State.IDLE =>
+ case _ => return Service.START_NOT_STICKY // ignore request
+ }
+
+ profile = app.currentProfile.orNull
+ if (profile == null) {
+ stopRunner(stopService = true)
+ return Service.START_NOT_STICKY
+ }
+
+ profile.name = profile.getName // save original name before it's (possibly) overwritten by IP addresses
+
+ TrafficMonitor.reset()
+ trafficMonitorThread = new TrafficMonitorThread(getApplicationContext)
+ trafficMonitorThread.start()
+
+ if (!closeReceiverRegistered) {
+ // register close receiver
+ val filter = new IntentFilter()
+ filter.addAction(Action.RELOAD)
+ filter.addAction(Intent.ACTION_SHUTDOWN)
+ filter.addAction(Action.CLOSE)
+ registerReceiver(closeReceiver, filter)
+ closeReceiverRegistered = true
+ }
+
+ notification = createNotification()
+ app.track(getClass.getSimpleName, "start")
+
+ changeState(State.CONNECTING)
+
+ Utils.ThrowableFuture(try connect() catch {
+ case _: NameNotResolvedException => stopRunner(stopService = true, getString(R.string.invalid_server))
+ case _: NullConnectionException => stopRunner(stopService = true, getString(R.string.reboot_required))
+ case exc: Throwable =>
+ stopRunner(stopService = true, getString(R.string.service_failed) + ": " + exc.getMessage)
+ exc.printStackTrace()
+ app.track(exc)
+ })
+ Service.START_NOT_STICKY
+ }
protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala
index a3de92be..f60e3c79 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/GlobalConfigFragment.scala
@@ -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 _ =>
}
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala
index 61211364..b3768b3b 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/MainActivity.scala
@@ -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()
@@ -424,7 +425,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
override def onDestroy() {
super.onDestroy()
- app.settings.unregisterOnSharedPreferenceChangeListener(this)
+ app.dataStore.unregisterChangeListener(this)
detachService()
new BackupManager(this).dataChanged()
handler.removeCallbacksAndMessages(null)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala
index fd22a282..a99b3b53 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigActivity.scala
@@ -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)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala
index 506f459f..16cdf902 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ProfileConfigFragment.scala
@@ -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()
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
index 20f459e6..618a7931 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
@@ -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
diff --git a/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala b/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala
index 62d73999..eadc041a 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/QuickToggleShortcut.scala
@@ -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()
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
index 96840c4b..d8419e0c 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
@@ -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)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala
index 37e8657a..64441b6e 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksBackupAgent.scala
@@ -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"))
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index d5855834..a1db6d74 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -24,11 +24,12 @@ import java.io.File
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
+import android.app.Service
import android.content._
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
-import com.github.shadowsocks.acl.{AclSyncJob, Acl}
+import com.github.shadowsocks.acl.{Acl, AclSyncJob}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
@@ -43,7 +44,6 @@ class ShadowsocksNatService extends BaseService {
val CMD_IPTABLES_DNAT_ADD_SOCKS =
"iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123"
- private var notification: ShadowsocksNotification = _
val myUid: Int = android.os.Process.myUid()
var sslocalProcess: GuardedProcess = _
@@ -177,15 +177,17 @@ class ShadowsocksNatService extends BaseService {
su.addCommand((init_sb ++ http_sb).toArray)
}
- override def startRunner(profile: Profile): Unit = if (su == null)
+ override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = if (su == null) {
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
- if (exitCode == 0) super.startRunner(profile) else {
+ if (exitCode == 0) super.onStartCommand(intent, flags, startId) else {
if (su != null) {
su.close()
su = null
}
super.stopRunner(stopService = true, getString(R.string.nat_no_root))
})
+ Service.START_NOT_STICKY
+ } else super.onStartCommand(intent, flags, startId)
override def connect() {
super.connect()
@@ -204,12 +206,11 @@ class ShadowsocksNatService extends BaseService {
AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED)
- notification = new ShadowsocksNotification(this, profile.name, true)
}
- override def stopRunner(stopService: Boolean, msg: String = null) {
+ override def createNotification() = new ShadowsocksNotification(this, profile.name, "service-nat", true)
- if (notification != null) notification.destroy()
+ override def stopRunner(stopService: Boolean, msg: String = null) {
// channge the state
changeState(State.STOPPING)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala
index 3d81bd6c..6433d985 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala
@@ -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))
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
index 9e58c7ea..9fd86e8e 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
@@ -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")
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala
deleted file mode 100644
index 34a2e86a..00000000
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala
+++ /dev/null
@@ -1,57 +0,0 @@
-/*******************************************************************************/
-/* */
-/* Copyright (C) 2017 by Max Lv */
-/* Copyright (C) 2017 by Mygod Studio */
-/* */
-/* 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 . */
-/* */
-/*******************************************************************************/
-
-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()
- }
-}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index 81ac83ee..1f124776 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -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. */
diff --git a/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
index 36f405ae..944b27e9 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
@@ -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() {
diff --git a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala
index b015eb65..fa9017b8 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/TaskerReceiver.scala
@@ -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)
}
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala
index e21a6b80..e55d5eb7 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ToolbarFragment.scala
@@ -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)
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
index dc953f6c..26d0fe24 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
@@ -26,26 +26,6 @@ class Acl {
@DatabaseField
var bypass: Boolean = _
- def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
- def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
- def getSubnetsString: String = subnets.mkString("\n")
- def setBypassHostnamesString(value: String) {
- bypassHostnames.clear()
- bypassHostnames ++= value.split("\n")
- }
- def setProxyHostnamesString(value: String) {
- proxyHostnames.clear()
- proxyHostnames ++= value.split("\n")
- }
- def setSubnetsString(value: String) {
- subnets.clear()
- subnets ++= value.split("\n").map(Subnet.fromString)
- }
- def setUrlRules(value: String) {
- urls.clear()
- urls ++= value.split("\n")
- }
-
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
bypassHostnames ++= other.bypassHostnames
@@ -71,17 +51,10 @@ class Acl {
var in_urls = false
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => if (!in_urls) line else ""
- case index => {
- line.indexOf("URLS_BEGIN") match {
- case -1 =>
- case index => in_urls = true
- }
- line.indexOf("URLS_END") match {
- case -1 =>
- case index => in_urls = false
- }
- "" // ignore any comment lines
- }
+ case index =>
+ if (line.contains("URLS_BEGIN")) in_urls = true
+ if (line.contains("URLS_END")) in_urls = false
+ line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
@@ -95,10 +68,8 @@ class Acl {
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
- case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) {
- urls += input
- }
- hostnames += input
+ case _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://"))
+ urls += input else hostnames += input
}
case _ =>
}
@@ -116,7 +87,7 @@ class Acl {
try {
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
} catch {
- case e: IOException => // ignore
+ case _: IOException => // ignore
}
}
result.append("#URLS_END\n")
@@ -141,9 +112,7 @@ class Acl {
result.toString
}
- override def toString: String = {
- getAclString(false)
- }
+ override def toString: String = getAclString(false)
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
index 0bf6a523..74a6fdc4 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
@@ -46,8 +46,8 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private def createAclRuleDialog(text: CharSequence = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
- val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
- val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
+ val templateSelector = view.findViewById[Spinner](R.id.template_selector)
+ val editText = view.findViewById[EditText](R.id.content)
PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) =>
templateSelector.setSelection(DOMAIN)
@@ -70,7 +70,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with View.OnLongClickListener {
var item: AnyRef = _
- private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
+ private val text = itemView.findViewById[TextView](android.R.id.text1)
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
itemView.setBackgroundResource(R.drawable.background_selectable)
@@ -252,12 +252,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 =
diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala
index 155ddf28..68f176f4 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/database/DBHelper.scala
@@ -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,19 @@ class DBHelper(val context: Context)
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeRawNoArgs("COMMIT;")
}
+
+ if (oldVersion < 23) {
+ import KeyValuePair._
+ val old = PreferenceManager.getDefaultSharedPreferences(app)
+ kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
+ ByteBuffer.allocate(4).putInt(old.getInt(Key.id, 0)).array()))
+ kvPairDao.createOrUpdate(new KeyValuePair(Key.isNAT, TYPE_BOOLEAN,
+ ByteBuffer.allocate(1).put((if (old.getBoolean(Key.isNAT, false)) 1 else 0).toByte).array()))
+ kvPairDao.createOrUpdate(new KeyValuePair(Key.tfo, TYPE_BOOLEAN,
+ ByteBuffer.allocate(1).put((if (old.getBoolean(Key.tfo, false)) 1 else 0).toByte).array()))
+ kvPairDao.createOrUpdate(new KeyValuePair(Key.currentVersionCode, TYPE_INT,
+ ByteBuffer.allocate(4).putInt(-1).array()))
+ }
} catch {
case ex: Exception =>
app.track(ex)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala b/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala
new file mode 100644
index 00000000..625f8c41
--- /dev/null
+++ b/mobile/src/main/scala/com/github/shadowsocks/database/KeyValuePair.scala
@@ -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
+ }
+}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala
index fa347065..a6560814 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/database/Profile.scala
@@ -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
}
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala
index 1cb28118..11521105 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/preference/BottomSheetPreferenceDialogFragment.scala
@@ -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)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala
new file mode 100644
index 00000000..6e690b73
--- /dev/null
+++ b/mobile/src/main/scala/com/github/shadowsocks/preference/OnPreferenceDataStoreChangeListener.scala
@@ -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)
+}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala b/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala
new file mode 100644
index 00000000..0e48ccaa
--- /dev/null
+++ b/mobile/src/main/scala/com/github/shadowsocks/preference/OrmLitePreferenceDataStore.scala
@@ -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)
+}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala
index 80bd0f4a..e7e30a92 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Constants.scala
@@ -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"
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala
index 266703fb..eae40a19 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/utils/Utils.scala
@@ -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)
diff --git a/plugin/CHANGES.md b/plugin/CHANGES.md
index 9e277fee..e7250c3d 100644
--- a/plugin/CHANGES.md
+++ b/plugin/CHANGES.md
@@ -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:
diff --git a/plugin/README.md b/plugin/README.md
index cfd062f8..fbd519a0 100644
--- a/plugin/README.md
+++ b/plugin/README.md
@@ -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
diff --git a/plugin/build.sbt b/plugin/build.sbt
index 93e5f588..5e5df138 100644
--- a/plugin/build.sbt
+++ b/plugin/build.sbt
@@ -2,7 +2,7 @@ enablePlugins(AndroidLib)
android.useSupportVectors
name := "plugin"
-version := "0.0.3"
+version := "0.0.4"
pomExtra in Global := {
https://github.com/shadowsocks/shadowsocks-android
diff --git a/plugin/src/main/AndroidManifest.xml b/plugin/src/main/AndroidManifest.xml
index bef42e78..ca89bcc9 100644
--- a/plugin/src/main/AndroidManifest.xml
+++ b/plugin/src/main/AndroidManifest.xml
@@ -2,10 +2,10 @@
package="com.github.shadowsocks.plugin">
+ android:targetSdkVersion="26"/>
+ android:value="0.0.4"/>
diff --git a/plugin/src/main/res/values/styles.xml b/plugin/src/main/res/values/styles.xml
index 4db00a18..7f86d629 100644
--- a/plugin/src/main/res/values/styles.xml
+++ b/plugin/src/main/res/values/styles.xml
@@ -40,6 +40,6 @@
- vertical
diff --git a/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala
index e23d261a..a57fb98d 100644
--- a/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala
+++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreference.scala
@@ -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()
}
}
diff --git a/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala
index dadf7893..e8ed19e4 100644
--- a/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala
+++ b/plugin/src/main/scala/be/mygod/preference/EditTextPreferenceDialogFragment.scala
@@ -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
diff --git a/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala
index 8819ca37..8df8b6ef 100644
--- a/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala
+++ b/plugin/src/main/scala/be/mygod/preference/PreferenceGroupAdapter.scala
@@ -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)
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 8fd06146..8246189c 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,4 +1,4 @@
-addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.7")
+addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")