Commit f111add4 authored by Mygod's avatar Mygod

Prevent HTTPS test losing progress on configuration change

parent 28530d06
...@@ -9,6 +9,7 @@ buildscript { ...@@ -9,6 +9,7 @@ buildscript {
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.3' buildToolsVersion = '28.0.3'
lifecycleVersion = '2.0.0'
roomVersion = '2.0.0' roomVersion = '2.0.0'
workVersion = '1.0.0-beta01' workVersion = '1.0.0-beta01'
preferencexVersion = '1.0.0' preferencexVersion = '1.0.0'
......
...@@ -71,6 +71,8 @@ tasks.whenTaskAdded { task -> ...@@ -71,6 +71,8 @@ tasks.whenTaskAdded { task ->
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion" api "android.arch.work:work-runtime-ktx:$workVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api "androidx.preference:preference:1.0.0" api "androidx.preference:preference:1.0.0"
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.8' api 'com.crashlytics.sdk.android:crashlytics:2.9.8'
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
package com.github.shadowsocks.utils package com.github.shadowsocks.utils
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
...@@ -36,12 +38,46 @@ import java.net.URL ...@@ -36,12 +38,46 @@ import java.net.URL
/** /**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071 * Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
*/ */
class HttpsTest(private val setStatus: (CharSequence?) -> Unit, private val errorCallback: (CharSequence) -> Unit) { class HttpsTest : ViewModel() {
sealed class Status {
protected abstract val status: CharSequence
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = setStatus(status)
object Idle : Status() {
override val status get() = app.getText(R.string.vpn_connected)
}
object Testing : Status() {
override val status get() = app.getText(R.string.connection_test_testing)
}
class Success(private val elapsed: Long) : Status() {
override val status get() = app.getString(R.string.connection_test_available, elapsed)
}
sealed class Error : Status() {
override val status get() = app.getText(R.string.connection_test_fail)
protected abstract val error: String
private var shown = false
override fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) {
super.retrieve(setStatus, errorCallback)
if (shown) return
shown = true
errorCallback(error)
}
class UnexpectedResponseCode(private val code: Int) : Error() {
override val error get() = app.getString(R.string.connection_test_error_status_code, code)
}
class IOFailure(private val e: IOException) : Error() {
override val error get() = app.getString(R.string.connection_test_error, e.message)
}
}
}
private var testCount = 0 private var testCount = 0
val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() { fun testConnection() {
++testCount ++testCount
setStatus(app.getText(R.string.connection_test_testing)) status.value = Status.Testing
val id = testCount // it would change by other code val id = testCount // it would change by other code
thread("ConnectionTest") { thread("ConnectionTest") {
val url = URL("https", when (Core.currentProfile!!.route) { val url = URL("https", when (Core.currentProfile!!.route) {
...@@ -54,28 +90,23 @@ class HttpsTest(private val setStatus: (CharSequence?) -> Unit, private val erro ...@@ -54,28 +90,23 @@ class HttpsTest(private val setStatus: (CharSequence?) -> Unit, private val erro
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
val (success, result) = try { val result = try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
Pair(true, app.getString(R.string.connection_test_available, elapsed)) else Status.Error.UnexpectedResponseCode(code)
else throw IOException(app.getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) { } catch (e: IOException) {
Pair(false, app.getString(R.string.connection_test_error, e.message)) Status.Error.IOFailure(e)
} finally { } finally {
conn.disconnect() conn.disconnect()
} }
if (testCount == id) Core.handler.post { if (testCount == id) status.postValue(result)
if (success) setStatus(result) else {
setStatus(app.getText(R.string.connection_test_fail))
errorCallback(result)
}
}
} }
} }
fun invalidate() { fun invalidate() {
testCount += 1 ++testCount
status.value = Status.Idle
} }
} }
...@@ -26,6 +26,10 @@ import android.util.AttributeSet ...@@ -26,6 +26,10 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import com.github.shadowsocks.MainActivity import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R import com.github.shadowsocks.R
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
...@@ -40,7 +44,7 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -40,7 +44,7 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private lateinit var rxText: TextView private lateinit var rxText: TextView
private lateinit var txRateText: TextView private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView private lateinit var rxRateText: TextView
private val tester by lazy { HttpsTest(statusText::setText) { (context as MainActivity).snackbar(it).show() } } private val tester = ViewModelProviders.of(context as FragmentActivity).get<HttpsTest>()
private val behavior = object : Behavior() { private val behavior = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold) val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View, override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
...@@ -62,16 +66,22 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? ...@@ -62,16 +66,22 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
} }
fun changeState(state: Int) { fun changeState(state: Int) {
statusText.setText(when (state) { val activity = context as MainActivity
BaseService.CONNECTING -> R.string.connecting
BaseService.CONNECTED -> R.string.vpn_connected
BaseService.STOPPING -> R.string.stopping
else -> R.string.not_connected
})
if (state != BaseService.CONNECTED) { if (state != BaseService.CONNECTED) {
updateTraffic(0, 0, 0, 0) updateTraffic(0, 0, 0, 0)
tester.invalidate() tester.status.removeObservers(activity)
} else behavior.slideUp(this) if (state != BaseService.IDLE) tester.invalidate()
statusText.setText(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.STOPPING -> R.string.stopping
else -> R.string.not_connected
})
} else {
behavior.slideUp(this)
tester.status.observe(activity, Observer {
it.retrieve(statusText::setText) { activity.snackbar(it).show() }
})
}
} }
fun updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { fun updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
......
...@@ -30,7 +30,11 @@ import android.os.DeadObjectException ...@@ -30,7 +30,11 @@ import android.os.DeadObjectException
import android.text.format.Formatter import android.text.format.Formatter
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.leanback.preference.LeanbackPreferenceFragment import androidx.leanback.preference.LeanbackPreferenceFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
...@@ -79,9 +83,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -79,9 +83,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
portTransproxy.isEnabled = enabledTransproxy portTransproxy.isEnabled = enabledTransproxy
true true
} }
private val tester by lazy { private lateinit var tester: HttpsTest
HttpsTest(stats::setTitle) { Toast.makeText(activity, it, Toast.LENGTH_LONG).show() }
}
// service // service
var state = BaseService.IDLE var state = BaseService.IDLE
...@@ -111,10 +113,14 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -111,10 +113,14 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
}) })
stats.setTitle(R.string.connection_test_pending) stats.setTitle(R.string.connection_test_pending)
stats.isVisible = state == BaseService.CONNECTED stats.isVisible = state == BaseService.CONNECTED
val owner = activity as FragmentActivity // TODO: change to this when refactored to androidx
if (state != BaseService.CONNECTED) { if (state != BaseService.CONNECTED) {
serviceCallback.trafficUpdated(0, 0, 0, 0, 0) serviceCallback.trafficUpdated(0, 0, 0, 0, 0)
tester.invalidate() tester.status.removeObservers(owner)
} if (state != BaseService.IDLE) tester.invalidate()
} else tester.status.observe(owner, Observer {
it.retrieve(stats::setTitle) { Toast.makeText(activity, it, Toast.LENGTH_LONG).show() }
})
if (msg != null) Toast.makeText(activity, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show() if (msg != null) Toast.makeText(activity, getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state this.state = state
if (state == BaseService.STOPPED) { if (state == BaseService.STOPPED) {
...@@ -198,6 +204,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -198,6 +204,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
changeState(BaseService.IDLE) // reset everything to init state changeState(BaseService.IDLE) // reset everything to init state
connection.connect() connection.connect()
DataStore.publicStore.registerChangeListener(this) DataStore.publicStore.registerChangeListener(this)
tester = ViewModelProviders.of(activity as FragmentActivity).get()
} }
override fun onStart() { override fun onStart() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment