Commit dca0da09 authored by Mygod's avatar Mygod

Allow stop service while connecting

parent cf0ad396
......@@ -28,6 +28,7 @@ import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.ProxyService
import com.github.shadowsocks.bg.TransproxyService
import com.github.shadowsocks.bg.VpnService
......@@ -51,7 +52,7 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
}
interface Callback {
fun stateChanged(state: Int, profileName: String?, msg: String?)
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
fun trafficPersisted(profileId: Long) { }
......@@ -69,7 +70,7 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
val callback = callback ?: return
handler.post { callback.stateChanged(state, profileName, msg) }
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
val callback = callback ?: return
......
......@@ -53,20 +53,22 @@ import java.util.*
* This object uses WeakMap to simulate the effects of multi-inheritance.
*/
object BaseService {
/**
* IDLE state is only used by UI and will never be returned by BaseService.
*/
const val IDLE = 0
const val CONNECTING = 1
const val CONNECTED = 2
const val STOPPING = 3
const val STOPPED = 4
enum class State(val canStop: Boolean = false) {
/**
* Idle state is only used by UI and will never be returned by BaseService.
*/
Idle,
Connecting(true),
Connected(true),
Stopping,
Stopped,
}
const val CONFIG_FILE = "shadowsocks.conf"
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
class Data internal constructor(private val service: Interface) {
var state = STOPPED
var state = State.Stopped
var processes: GuardedProcessPool? = null
var proxy: ProxyInstance? = null
var udpFallback: ProxyInstance? = null
......@@ -83,7 +85,7 @@ object BaseService {
val binder = Binder(this)
var connectingJob: Job? = null
fun changeState(s: Int, msg: String? = null) {
fun changeState(s: State, msg: String? = null) {
if (state == s && msg == null) return
binder.stateChanged(s, msg)
state = s
......@@ -104,7 +106,7 @@ object BaseService {
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
private val handler = Handler()
override fun getState(): Int = data!!.state
override fun getState(): Int = data!!.state.ordinal
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
......@@ -126,12 +128,12 @@ object BaseService {
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
}
private fun onTimeout() {
val proxies = listOfNotNull(data!!.proxy, data!!.udpFallback)
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
val stats = proxies
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
.filter { it.second != null }
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
if (stats.any { it.third } && state == CONNECTED && bandwidthListeners.isNotEmpty()) {
if (stats.any { it.third } && data!!.state == State.Connected && bandwidthListeners.isNotEmpty()) {
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) {
......@@ -147,7 +149,7 @@ object BaseService {
val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
if (wasEmpty) registerTimeout()
if (state != CONNECTED) return
if (data!!.state != State.Connected) return
var sum = TrafficStats()
val proxy = data!!.proxy ?: return
proxy.trafficMonitor?.out.also { stats ->
......@@ -179,9 +181,9 @@ object BaseService {
callbacks.unregister(cb)
}
fun stateChanged(s: Int, msg: String?) {
fun stateChanged(s: State, msg: String?) {
val profileName = profileName
broadcast { it.stateChanged(s, profileName, msg) }
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
}
fun trafficPersisted(ids: List<Long>) {
......@@ -213,9 +215,9 @@ object BaseService {
return
}
val s = data.state
when (s) {
STOPPED -> startRunner()
CONNECTED -> stopRunner(true)
when {
s == State.Stopped -> startRunner()
s.canStop -> stopRunner(true)
else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
}
}
......@@ -251,11 +253,12 @@ object BaseService {
}
fun stopRunner(restart: Boolean = false, msg: String? = null) {
if (data.state == STOPPING) return
if (data.state == State.Stopping) return
// channge the state
data.changeState(STOPPING)
data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service
// we use a coroutineScope here to allow clean-up in parallel
coroutineScope {
......@@ -280,7 +283,7 @@ object BaseService {
}
// change the state
data.changeState(STOPPED, msg)
data.changeState(State.Stopped, msg)
// stop the service if nothing has bound to it
if (restart) startRunner() else stopSelf()
......@@ -293,7 +296,7 @@ object BaseService {
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != STOPPED) return Service.START_NOT_STICKY
if (data.state != State.Stopped) return Service.START_NOT_STICKY
val profilePair = Core.currentProfile
this as Context
if (profilePair == null) {
......@@ -320,7 +323,7 @@ object BaseService {
data.notification = createNotification(profile.formattedName)
Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(CONNECTING)
data.changeState(State.Connecting)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try {
Executable.killAll() // clean up old processes
......@@ -330,7 +333,6 @@ object BaseService {
data.processes = GuardedProcessPool {
printLog(it)
data.connectingJob?.cancelAndJoin()
stopRunner(false, it.readableMessage)
}
startProcesses()
......@@ -339,7 +341,7 @@ object BaseService {
data.udpFallback?.scheduleUpdate()
RemoteConfig.fetch()
data.changeState(CONNECTED)
data.changeState(State.Connected)
} catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) {
......
......@@ -94,7 +94,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
}
private fun update(action: String?, forceShow: Boolean = false) {
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
Intent.ACTION_SCREEN_OFF -> {
setVisible(false, forceShow)
unregisterCallback() // unregister callback to save battery
......
......@@ -83,8 +83,8 @@ class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
portTransproxy.isEnabled = enabledTransproxy
true
}
val listener: (Int) -> Unit = {
if (it == BaseService.STOPPED) {
val listener: (BaseService.State) -> Unit = {
if (it == BaseService.State.Stopped) {
tfo.isEnabled = true
serviceMode.isEnabled = true
portProxy.isEnabled = true
......
......@@ -70,7 +70,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private const val TAG = "ShadowsocksMainActivity"
private const val REQUEST_CONNECT = 1
var stateListener: ((Int) -> Unit)? = null
var stateListener: ((BaseService.State) -> Unit)? = null
}
@Parcelize
......@@ -107,12 +107,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
// service
var state = BaseService.IDLE
override fun stateChanged(state: Int, profileName: String?, msg: String?) = changeState(state, msg, true)
var state = BaseService.State.Idle
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg, true)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) this@MainActivity.stats.updateTraffic(
stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal)
if (state != BaseService.STOPPING) {
if (state != BaseService.State.Stopping) {
(supportFragmentManager.findFragmentById(R.id.fragment_holder) as? ToolbarFragment)
?.onTrafficUpdated(profileId, stats)
}
......@@ -121,7 +121,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
ProfilesFragment.instance?.onTrafficPersisted(profileId)
}
private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
private fun changeState(state: BaseService.State, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate)
stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
......@@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
private fun toggle() = when {
state == BaseService.CONNECTED -> Core.stopService()
state.canStop -> Core.stopService()
DataStore.serviceMode == Key.modeVpn -> {
val intent = VpnService.prepare(this)
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
......@@ -143,11 +143,11 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
BaseService.State.values()[service.state]
} catch (_: DeadObjectException) {
BaseService.IDLE
BaseService.State.Idle
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onBinderDied() {
connection.disconnect(this)
connection.connect(this, this)
......@@ -168,7 +168,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.CONNECTED) stats.testConnection() }
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)
navigation.setNavigationItemSelectedListener(this)
......@@ -180,7 +180,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
fab = findViewById(R.id.fab)
fab.setOnClickListener { toggle() }
changeState(BaseService.IDLE) // reset everything to init state
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this)
......
......@@ -72,15 +72,9 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
/**
* Is ProfilesFragment editable at all.
*/
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED, BaseService.STOPPED -> true
else -> false
}
private fun isProfileEditable(id: Long) = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> id !in Core.activeProfileIds
BaseService.STOPPED -> true
else -> false
}
private val isEnabled get() = (activity as MainActivity).state.let { it.canStop || it == BaseService.State.Stopped }
private fun isProfileEditable(id: Long) =
(activity as MainActivity).state == BaseService.State.Stopped || id !in Core.activeProfileIds
@SuppressLint("ValidFragment")
class QRCodeDialog() : DialogFragment() {
......@@ -203,7 +197,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
Core.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.isSelected = true
if (activity.state == BaseService.CONNECTED) Core.reloadService()
if (activity.state.canStop) Core.reloadService()
}
}
......
......@@ -55,14 +55,15 @@ class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
}
override fun onServiceConnected(service: IShadowsocksService) {
when (service.state) {
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
val state = BaseService.State.values()[service.state]
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
finish()
}
override fun stateChanged(state: Int, profileName: String?, msg: String?) { }
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) { }
override fun onDestroy() {
connection.disconnect(this)
......
......@@ -346,11 +346,8 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
}
}
private val isEnabled get() = when ((activity as MainActivity).state) {
BaseService.CONNECTED -> Core.currentProfile?.first?.route != Acl.CUSTOM_RULES
BaseService.STOPPED -> true
else -> false
}
private val isEnabled get() = (activity as MainActivity).state == BaseService.State.Stopped ||
Core.currentProfile?.first?.route != Acl.CUSTOM_RULES
private val selectedItems = HashSet<Any>()
private val adapter by lazy { AclRulesAdapter() }
......
......@@ -41,9 +41,9 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private var tapPending = false
private val connection = ShadowsocksConnection()
override fun stateChanged(state: Int, profileName: String?, msg: String?) = updateTile(state) { profileName }
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = updateTile(state) { profileName }
override fun onServiceConnected(service: IShadowsocksService) {
updateTile(service.state) { service.profileName }
updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) {
tapPending = false
onClick()
......@@ -63,23 +63,28 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
}
private fun updateTile(serviceState: Int, profileName: () -> String?) {
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
qsTile?.apply {
label = null
when (serviceState) {
BaseService.STOPPED -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
BaseService.State.Idle -> throw IllegalStateException("serviceState")
BaseService.State.Connecting -> {
icon = iconBusy
state = Tile.STATE_ACTIVE
}
BaseService.CONNECTED -> {
BaseService.State.Connected -> {
icon = iconConnected
if (!keyguard.isDeviceLocked) label = profileName()
state = Tile.STATE_ACTIVE
}
else -> {
BaseService.State.Stopping -> {
icon = iconBusy
state = Tile.STATE_UNAVAILABLE
}
BaseService.State.Stopped -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
}
}
label = label ?: getString(R.string.app_name)
updateTile()
......@@ -88,9 +93,11 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private fun toggle() {
val service = connection.service
if (service == null) tapPending = true else when (service.state) {
BaseService.STOPPED -> Core.startService()
BaseService.CONNECTED -> Core.stopService()
if (service == null) tapPending = true else BaseService.State.values()[service.state].let { state ->
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
}
}
}
......@@ -68,14 +68,14 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
return drawableState
}
fun changeState(state: Int, animate: Boolean) {
fun changeState(state: BaseService.State, animate: Boolean) {
when (state) {
BaseService.CONNECTING -> changeState(iconConnecting, animate)
BaseService.CONNECTED -> changeState(iconConnected, animate)
BaseService.STOPPING -> changeState(iconStopping, animate)
BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, animate)
BaseService.State.Stopping -> changeState(iconStopping, animate)
else -> changeState(iconStopped, animate)
}
if (state == BaseService.CONNECTED) {
if (state == BaseService.State.Connected) {
checked = true
TooltipCompat.setTooltipText(this, context.getString(R.string.stop))
} else {
......@@ -83,7 +83,7 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
}
refreshDrawableState()
isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED
isEnabled = state.canStop || state == BaseService.State.Stopped
}
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
......
......@@ -65,15 +65,15 @@ class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet?
super.setOnClickListener(l)
}
fun changeState(state: Int) {
fun changeState(state: BaseService.State) {
val activity = context as MainActivity
if (state != BaseService.CONNECTED) {
if (state != BaseService.State.Connected) {
updateTraffic(0, 0, 0, 0)
tester.status.removeObservers(activity)
if (state != BaseService.IDLE) tester.invalidate()
if (state != BaseService.State.Idle) tester.invalidate()
statusText.setText(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.STOPPING -> R.string.stopping
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Stopping -> R.string.stopping
else -> R.string.not_connected
})
} else {
......
......@@ -42,7 +42,7 @@ class MainFragment : LeanbackSettingsFragmentCompat() {
override fun onPreferenceDisplayDialog(caller: PreferenceFragmentCompat, pref: Preference?): Boolean {
if (pref?.key == Key.id) {
if ((childFragmentManager.findFragmentById(R.id.settings_preference_fragment_container)
as MainPreferenceFragment).state == BaseService.STOPPED) {
as MainPreferenceFragment).state == BaseService.State.Stopped) {
startPreferenceFragment(ProfilesDialogFragment().apply {
arguments = bundleOf(Pair(LeanbackPreferenceDialogFragmentCompat.ARG_KEY, Key.id))
setTargetFragment(caller, 0)
......
......@@ -88,9 +88,9 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private lateinit var tester: HttpsTest
// service
var state = BaseService.IDLE
var state = BaseService.State.Idle
private set
override fun stateChanged(state: Int, profileName: String?, msg: String?) = changeState(state, msg)
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) requireContext().let { context ->
this.stats.summary = getString(R.string.stat_summary,
......@@ -101,26 +101,26 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
}
private fun changeState(state: Int, msg: String? = null) {
fab.isEnabled = state == BaseService.STOPPED || state == BaseService.CONNECTED
private fun changeState(state: BaseService.State, msg: String? = null) {
fab.isEnabled = state.canStop || state == BaseService.State.Stopped
fab.setTitle(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.CONNECTED -> R.string.stop
BaseService.STOPPING -> R.string.stopping
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Connected -> R.string.stop
BaseService.State.Stopping -> R.string.stopping
else -> R.string.connect
})
stats.setTitle(R.string.connection_test_pending)
stats.isVisible = state == BaseService.CONNECTED
if (state != BaseService.CONNECTED) {
stats.isVisible = state == BaseService.State.Connected
if (state != BaseService.State.Connected) {
trafficUpdated(0, TrafficStats())
tester.status.removeObservers(this)
if (state != BaseService.IDLE) tester.invalidate()
if (state != BaseService.State.Idle) tester.invalidate()
} else tester.status.observe(this, Observer {
it.retrieve(stats::setTitle) { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
})
if (msg != null) Toast.makeText(requireContext(), getString(R.string.vpn_error, msg), Toast.LENGTH_SHORT).show()
this.state = state
if (state == BaseService.STOPPED) {
if (state == BaseService.State.Stopped) {
controlImport.isEnabled = true
tfo.isEnabled = true
serviceMode.isEnabled = true
......@@ -141,11 +141,11 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state
BaseService.State.values()[service.state]
} catch (_: DeadObjectException) {
BaseService.IDLE
BaseService.State.Idle
})
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onBinderDied() {
connection.disconnect(requireContext())
connection.connect(requireContext(), this)
......@@ -200,7 +200,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
}
tester = ViewModelProviders.of(this).get()
changeState(BaseService.IDLE) // reset everything to init state
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(requireContext(), this)
DataStore.publicStore.registerChangeListener(this)
}
......@@ -225,7 +225,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
fun startService() {
when {
state != BaseService.STOPPED -> return
state != BaseService.State.Stopped -> return
DataStore.serviceMode == Key.modeVpn -> {
val intent = VpnService.prepare(requireContext())
if (intent != null) startActivityForResult(intent, REQUEST_CONNECT)
......@@ -251,7 +251,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
override fun onPreferenceTreeClick(preference: Preference?) = when (preference?.key) {
Key.id -> {
if (state == BaseService.CONNECTED) Core.stopService()
if (state == BaseService.State.Connected) Core.stopService()
true
}
Key.controlStats -> {
......
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