diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index b79e5088..2a2b4c89 100755 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -78,6 +78,11 @@ 字节 + 点击检查是否接入互联网。 + 测试中…… + 已接入互联网。(%d 毫秒) + 失败:%s + 状态码无效。(#%d) 为影梭添加此配置文件? diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d3d7696f..177faac6 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -19,6 +19,11 @@ Byte Bytes + Tap to check Internet connection availability. + Testing… + Internet connection available. (time=%dms) + Fail: %s + Invalid status code. (#%d) Server Settings diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index 3e3d0a60..d1de1e87 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -152,6 +152,8 @@ class Shadowsocks var prepared = false var currentProfile = new Profile var vpnEnabled = -1 + var trafficCache: String = _ + var connectionTestResult: String = _ // Services var currentServiceName = classOf[ShadowsocksNatService].getName @@ -199,14 +201,17 @@ class Shadowsocks }) } def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) { - val trafficStat = getString(R.string.stat_summary) - .formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal) - handler.post(() => { - preferences.findPreference(Key.stat).setSummary(trafficStat) - }) + trafficCache = getString(R.string.stat_summary).formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal) + handler.post(updateTraffic) } } + def updateTraffic(): Unit = if (trafficCache == null) callback.trafficUpdated(TrafficMonitor.getTxRate, + TrafficMonitor.getRxRate, TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal) else { + if (connectionTestResult == null) connectionTestResult = getString(R.string.connection_test_pending) + preferences.stat.setSummary(trafficCache + '\n' + connectionTestResult) + } + def attachService: Unit = attachService(callback) override def onServiceConnected() { @@ -485,8 +490,7 @@ class Shadowsocks // Check if current profile changed if (ShadowsocksApplication.profileId != currentProfile.id) reloadProfile() - callback.trafficUpdated(TrafficMonitor.getTxRate, TrafficMonitor.getRxRate, - TrafficMonitor.getTxTotal, TrafficMonitor.getRxTotal) + updateTraffic() } private def setPreferenceEnabled(enabled: Boolean) { diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala index 464bb597..bfbc3d68 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala @@ -1,5 +1,8 @@ package com.github.shadowsocks +import java.lang.System.currentTimeMillis +import java.lang.{Long => jLong} +import java.net.{HttpURLConnection, URL} import java.util.Locale import android.content.SharedPreferences.OnSharedPreferenceChangeListener @@ -9,20 +12,60 @@ import android.os.{Build, Bundle} import android.preference.{Preference, PreferenceFragment, SwitchPreference} import android.support.v7.app.AlertDialog import android.webkit.{WebView, WebViewClient} +import com.github.shadowsocks.utils.CloseUtils._ import com.github.shadowsocks.utils.Key // TODO: Move related logic here class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener { private def activity = getActivity.asInstanceOf[Shadowsocks] lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference] + var stat: Preference = _ private var isProxyApps: SwitchPreference = _ + private var testCount: Int = _ override def onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) addPreferencesFromResource(R.xml.pref_all) getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this) + stat = findPreference(Key.stat) + stat.setOnPreferenceClickListener(_ => { + val id = synchronized { + testCount += 1 + activity.connectionTestResult = getString(R.string.connection_test_testing) + activity.updateTraffic() + testCount + } + ThrowableFuture { + // Based on: https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/connectivity/NetworkMonitor.java#640 + autoDisconnect(new URL("https", "www.google.com", "/generate_204").openConnection + .asInstanceOf[HttpURLConnection]) { conn => + conn.setInstanceFollowRedirects(false) + conn.setUseCaches(false) + if (testCount == id) { + var result: String = null + try { + val start = currentTimeMillis + conn.getInputStream + val elapsed = currentTimeMillis - start + val code = conn.getResponseCode + if (code == 204 || code == 200 && conn.getContentLength == 0) + result = getString(R.string.connection_test_available, elapsed: java.lang.Long) + else throw new Exception(getString(R.string.connection_test_error_status_code, code: Integer)) + } catch { + case e: Exception => result = getString(R.string.connection_test_error, e.getMessage) + } + synchronized(if (testCount == id) { + activity.connectionTestResult = result + activity.handler.post(activity.updateTraffic) + }) + } + } + } + true + }) + isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference] findPreference("recovery").setOnPreferenceClickListener((preference: Preference) => { diff --git a/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala b/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala new file mode 100644 index 00000000..edd85aeb --- /dev/null +++ b/src/main/scala/com/github/shadowsocks/utils/CloseUtils.scala @@ -0,0 +1,35 @@ +package com.github.shadowsocks.utils + +/** + * @author Mygod + */ +object CloseUtils { + type Disconnectable = { + def disconnect() + } + + def autoClose[A <: AutoCloseable, B](x: => A)(block: A => B): B = { + var a: Option[A] = None + try { + a = Some(x) + block(a.get) + } finally if (a.nonEmpty) try { + val v = a.get + if (v ne null) v.close + } catch { + case ignore: Exception => + } + } + def autoDisconnect[A <: Disconnectable, B](x: => A)(block: A => B): B = { + var a: Option[A] = None + try { + a = Some(x) + block(a.get) + } finally if (a.nonEmpty) try { + val v = a.get + if (v ne null) v.disconnect + } catch { + case ignore: Exception => + } + } +}