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")