Refactor connection state
Until now, connection state was managed in VncClient. This was fine before introduction of SSH Tunnel, but now, state in VncClient may not fully reflect the state transitions. Moreover, most of the time we simply check whether we are connected or not. Now, simple boolean state is managed by VncClient, and more granular state is managed by VncViewModel.
This commit is contained in:
parent
781263fc5f
commit
26d303ebbd
9 changed files with 85 additions and 151 deletions
|
|
@ -10,7 +10,6 @@ package com.gaurav.avnc.vnc
|
|||
|
||||
import com.gaurav.avnc.TestServer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ class VncClientTest {
|
|||
override fun onCredentialRequired() = UserCredential()
|
||||
override fun onFramebufferUpdated() {}
|
||||
override fun onFramebufferSizeChanged(width: Int, height: Int) {}
|
||||
override fun onClientStateChanged(newState: VncClient.State) {}
|
||||
override fun onGotXCutText(text: String) {
|
||||
cutText = text
|
||||
}
|
||||
|
|
@ -47,8 +45,8 @@ class VncClientTest {
|
|||
|
||||
private fun connect() {
|
||||
server.start()
|
||||
assertTrue(client.connect(server.host, server.port))
|
||||
assertTrue(client.processServerMessage(10000))
|
||||
client.connect(server.host, server.port)
|
||||
client.processServerMessage(10000)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.gaurav.avnc.databinding.FragmentCredentialBinding
|
|||
import com.gaurav.avnc.model.LoginInfo
|
||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
||||
import com.gaurav.avnc.vnc.UserCredential
|
||||
import com.gaurav.avnc.vnc.VncClient
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
|
|
@ -79,8 +78,8 @@ class CredentialFragment : DialogFragment() {
|
|||
*/
|
||||
private fun scheduleCredentialSave(viewModel: VncViewModel, cred: UserCredential) {
|
||||
with(viewModel) {
|
||||
clientState.observeForever {
|
||||
if (it == VncClient.State.Connected) {
|
||||
state.observeForever {
|
||||
if (it == VncViewModel.State.Connected) {
|
||||
profile.username = cred.username
|
||||
profile.password = cred.password
|
||||
saveProfile()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.gaurav.avnc.ui.vnc.Dispatcher.SwipeAction
|
|||
import com.gaurav.avnc.viewmodel.VncViewModel
|
||||
import com.gaurav.avnc.vnc.Messenger
|
||||
import com.gaurav.avnc.vnc.PointerButton
|
||||
import com.gaurav.avnc.vnc.VncClient
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
|
|
@ -282,13 +281,13 @@ class Dispatcher(private val activity: VncActivity) {
|
|||
syncPending = true
|
||||
syncPosition()
|
||||
}
|
||||
viewModel.clientState.observe(activity) {
|
||||
viewModel.state.observe(activity) {
|
||||
syncPosition()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncPosition() {
|
||||
if (syncPending && viewModel.clientState.value == VncClient.State.Connected) {
|
||||
if (syncPending && viewModel.client.connected) {
|
||||
pointerPosition.apply {
|
||||
x = viewModel.frameState.fbWidth / 2
|
||||
y = viewModel.frameState.fbHeight / 2
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import com.gaurav.avnc.model.ServerProfile
|
|||
import com.gaurav.avnc.util.Experimental
|
||||
import com.gaurav.avnc.util.SamsungDex
|
||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
||||
import com.gaurav.avnc.vnc.VncClient
|
||||
import com.gaurav.avnc.vnc.VncUri
|
||||
import kotlinx.coroutines.delay
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
@ -91,7 +90,7 @@ class VncActivity : AppCompatActivity() {
|
|||
viewModel.frameViewRef = WeakReference(binding.frameView)
|
||||
viewModel.credentialRequest.observe(this) { showCredentialDialog() }
|
||||
viewModel.sshHostKeyVerifyRequest.observe(this) { showHostKeyDialog() }
|
||||
viewModel.clientState.observe(this) { onClientStateChanged(it) }
|
||||
viewModel.state.observe(this) { onClientStateChanged(it) }
|
||||
viewModel.initConnection(profile) //Should be called after observers has been setup
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +156,8 @@ class VncActivity : AppCompatActivity() {
|
|||
|
||||
private fun closeDrawers() = binding.drawerLayout.closeDrawers()
|
||||
|
||||
private fun onClientStateChanged(newState: VncClient.State) {
|
||||
if (newState == VncClient.State.Connected) {
|
||||
private fun onClientStateChanged(newState: VncViewModel.State) {
|
||||
if (newState == VncViewModel.State.Connected) {
|
||||
|
||||
if (!viewModel.pref.runInfo.hasConnectedSuccessfully) {
|
||||
viewModel.pref.runInfo.hasConnectedSuccessfully = true
|
||||
|
|
@ -254,7 +253,7 @@ class VncActivity : AppCompatActivity() {
|
|||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
|
||||
window.decorView.apply {
|
||||
if (viewModel.clientState.value == VncClient.State.Connected)
|
||||
if (viewModel.client.connected)
|
||||
systemUiVisibility = systemUiVisibility or flags
|
||||
else
|
||||
systemUiVisibility = systemUiVisibility and flags.inv()
|
||||
|
|
@ -286,7 +285,7 @@ class VncActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun enterPiPMode() {
|
||||
val canEnter = viewModel.pref.viewer.pipEnabled && viewModel.client.state == VncClient.State.Connected
|
||||
val canEnter = viewModel.pref.viewer.pipEnabled && viewModel.client.connected
|
||||
|
||||
if (canEnter && Build.VERSION.SDK_INT >= 26) {
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import android.opengl.GLES20.*
|
|||
import android.opengl.GLSurfaceView
|
||||
import android.opengl.Matrix
|
||||
import com.gaurav.avnc.viewmodel.VncViewModel
|
||||
import com.gaurav.avnc.vnc.VncClient
|
||||
import javax.microedition.khronos.egl.EGLConfig
|
||||
import javax.microedition.khronos.opengles.GL10
|
||||
|
||||
|
|
@ -72,7 +71,7 @@ class Renderer(val viewModel: VncViewModel) : GLSurfaceView.Renderer {
|
|||
override fun onDrawFrame(gl: GL10?) {
|
||||
glClear(GL_COLOR_BUFFER_BIT)
|
||||
|
||||
if (viewModel.client.state != VncClient.State.Connected)
|
||||
if (!viewModel.client.connected)
|
||||
return
|
||||
|
||||
if (state.vpWidth == 0f || state.vpHeight == 0f)
|
||||
|
|
|
|||
|
|
@ -65,24 +65,48 @@ import java.lang.ref.WeakReference
|
|||
*/
|
||||
class VncViewModel(app: Application) : BaseViewModel(app), VncClient.Observer {
|
||||
|
||||
/**
|
||||
* Connection lifecycle:
|
||||
*
|
||||
* Created
|
||||
* |
|
||||
* v
|
||||
* Connecting ----------+
|
||||
* | |
|
||||
* v |
|
||||
* Connected |
|
||||
* | |
|
||||
* v |
|
||||
* Disconnected <-------+
|
||||
*
|
||||
*/
|
||||
enum class State {
|
||||
Created,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
val client = VncClient(this)
|
||||
|
||||
/**
|
||||
* Client state is exposed as live data here to allow dynamic data binding
|
||||
* from layouts.
|
||||
*/
|
||||
val clientState = MutableLiveData(client.state)
|
||||
|
||||
/**
|
||||
* Reason for [clientState] being [VncClient.State.Disconnected].
|
||||
*/
|
||||
val disconnectReason = MutableLiveData("")
|
||||
|
||||
/**
|
||||
* [ServerProfile] used for current connection.
|
||||
*/
|
||||
var profile = ServerProfile()
|
||||
|
||||
/**
|
||||
* We have two places for connection state (both are synced):
|
||||
*
|
||||
* [VncClient.connected] - Simple boolean state, used most of the time
|
||||
* [state] - More granular, used by observers & data binding
|
||||
*/
|
||||
val state = MutableLiveData(State.Created)
|
||||
|
||||
/**
|
||||
* Reason for disconnecting.
|
||||
*/
|
||||
val disconnectReason = MutableLiveData("")
|
||||
|
||||
/**
|
||||
* Fired when [VncClient] has asked for credential. It is used to
|
||||
* show Credentials dialog to user and return the result to receiver
|
||||
|
|
@ -146,21 +170,16 @@ class VncViewModel(app: Application) : BaseViewModel(app), VncClient.Observer {
|
|||
* Connection management
|
||||
**************************************************************************/
|
||||
|
||||
/**
|
||||
* Because [initConnection] can be called multiple times due to activity restarts,
|
||||
* initialization inside it is performed only once.
|
||||
*/
|
||||
private var initialized = false
|
||||
|
||||
/**
|
||||
* Initialize VNC connection using given profile.
|
||||
* [initConnection] can be called multiple times due to activity restarts.
|
||||
*/
|
||||
fun initConnection(profile: ServerProfile) {
|
||||
if (initialized)
|
||||
if (state.value != State.Created)
|
||||
return
|
||||
|
||||
initialized = true
|
||||
this.profile = profile
|
||||
state.value = State.Connecting
|
||||
|
||||
launchConnection()
|
||||
}
|
||||
|
|
@ -175,12 +194,12 @@ class VncViewModel(app: Application) : BaseViewModel(app), VncClient.Observer {
|
|||
processMessages()
|
||||
|
||||
}.onFailure {
|
||||
client.disconnect()
|
||||
if (it is IOException)
|
||||
disconnectReason.postValue(it.message)
|
||||
if (it is IOException) disconnectReason.postValue(it.message)
|
||||
Log.e("ReceiverCoroutine", "Connection failed", it)
|
||||
}
|
||||
|
||||
state.postValue(State.Disconnected)
|
||||
|
||||
//Wait until activity is finished and viewmodel is cleaned up.
|
||||
runCatching { awaitCancellation() }
|
||||
cleanup()
|
||||
|
|
@ -208,16 +227,13 @@ class VncViewModel(app: Application) : BaseViewModel(app), VncClient.Observer {
|
|||
else -> throw IOException("Unknown Channel: ${profile.channelType}")
|
||||
}
|
||||
|
||||
if (client.state != VncClient.State.Connected)
|
||||
throw IOException(client.getLastErrorStr())
|
||||
state.postValue(State.Connected)
|
||||
sendClipboardText() //Initial sync
|
||||
}
|
||||
|
||||
private fun processMessages() {
|
||||
while (viewModelScope.isActive && client.processServerMessage()) {
|
||||
//Message Loop
|
||||
}
|
||||
|
||||
disconnectReason.postValue(client.getLastErrorStr())
|
||||
while (viewModelScope.isActive)
|
||||
client.processServerMessage()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
|
|
@ -320,13 +336,6 @@ class VncViewModel(app: Application) : BaseViewModel(app), VncClient.Observer {
|
|||
receiveClipboardText(text)
|
||||
}
|
||||
|
||||
override fun onClientStateChanged(newState: VncClient.State) {
|
||||
clientState.postValue(newState)
|
||||
|
||||
if (newState == VncClient.State.Connected)
|
||||
sendClipboardText() //Initial sync
|
||||
}
|
||||
|
||||
override fun onFramebufferSizeChanged(width: Int, height: Int) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
frameState.setFramebufferSize(width.toFloat(), height.toFloat())
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class Messenger(private val client: VncClient) {
|
|||
}
|
||||
|
||||
fun sendKey(keySym: Int, isDown: Boolean): Boolean {
|
||||
if (client.state != VncClient.State.Connected)
|
||||
if (!client.connected)
|
||||
return false
|
||||
|
||||
execute { client.sendKeyEvent(keySym, isDown) }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.gaurav.avnc.vnc
|
|||
|
||||
import androidx.annotation.Keep
|
||||
import com.gaurav.avnc.vnc.VncClient.Observer
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
|
|
@ -35,33 +36,9 @@ import java.nio.charset.StandardCharsets
|
|||
*/
|
||||
class VncClient(private val observer: Observer) {
|
||||
|
||||
/**
|
||||
* [VncClient] Lifecycle:
|
||||
*
|
||||
* Created ---------------+
|
||||
* | |
|
||||
* v |
|
||||
* Connecting ----------+ |
|
||||
* | | |
|
||||
* v | |
|
||||
* Connected | |
|
||||
* | | |
|
||||
* v | |
|
||||
* Disconnected <-------+ |
|
||||
* | |
|
||||
* v |
|
||||
* Destroyed <-------------+
|
||||
*/
|
||||
enum class State {
|
||||
Created,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
Destroyed
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for event observer.
|
||||
* DO NOT throw exceptions from these methods.
|
||||
*/
|
||||
interface Observer {
|
||||
fun onPasswordRequired(): String
|
||||
|
|
@ -69,7 +46,6 @@ class VncClient(private val observer: Observer) {
|
|||
fun onGotXCutText(text: String)
|
||||
fun onFramebufferUpdated()
|
||||
fun onFramebufferSizeChanged(width: Int, height: Int)
|
||||
fun onClientStateChanged(newState: State)
|
||||
|
||||
//fun onBell()
|
||||
//fun onCursorMoved(x: Int, y: Int): Boolean
|
||||
|
|
@ -86,14 +62,9 @@ class VncClient(private val observer: Observer) {
|
|||
throw RuntimeException("Could not create native rfbClient!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Current client state.
|
||||
*/
|
||||
var state: State = State.Created
|
||||
private set(value) {
|
||||
field = value
|
||||
observer.onClientStateChanged(value)
|
||||
}
|
||||
@Volatile
|
||||
var connected = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Name of remote desktop
|
||||
|
|
@ -112,14 +83,10 @@ class VncClient(private val observer: Observer) {
|
|||
|
||||
/**
|
||||
* Setup different properties for this client.
|
||||
* Only valid in [State.Connected] state.
|
||||
*
|
||||
* @param securityType RFB security type to use.
|
||||
*/
|
||||
fun configure(viewOnly: Boolean, securityType: Int, useLocalCursor: Boolean) {
|
||||
if (state != State.Created)
|
||||
return
|
||||
|
||||
viewOnlyMode = viewOnly
|
||||
nativeConfigure(nativePtr, securityType, useLocalCursor)
|
||||
}
|
||||
|
|
@ -130,42 +97,25 @@ class VncClient(private val observer: Observer) {
|
|||
|
||||
/**
|
||||
* Initializes VNC connection.
|
||||
*
|
||||
* @return true if initialization was successful
|
||||
*/
|
||||
fun connect(host: String, port: Int): Boolean {
|
||||
if (state != State.Created)
|
||||
return false
|
||||
|
||||
state = State.Connecting
|
||||
|
||||
if (nativeInit(nativePtr, host, port)) {
|
||||
state = State.Connected
|
||||
return true
|
||||
} else {
|
||||
state = State.Disconnected
|
||||
return false
|
||||
}
|
||||
fun connect(host: String, port: Int) {
|
||||
connected = nativeInit(nativePtr, host, port)
|
||||
if (!connected) throw IOException(nativeGetLastErrorStr())
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for incoming server message, parses it and then invokes appropriate
|
||||
* callbacks.
|
||||
* Waits for incoming server message, parses it and then invokes appropriate callbacks.
|
||||
*
|
||||
* @param uSecTimeout Timeout in microseconds.
|
||||
* @return true if message was successfully handled or no message was received within timeout,
|
||||
* false otherwise.
|
||||
*/
|
||||
fun processServerMessage(uSecTimeout: Int = 1000000): Boolean {
|
||||
if (state != State.Connected)
|
||||
return false
|
||||
fun processServerMessage(uSecTimeout: Int = 1000000) {
|
||||
if (!connected)
|
||||
return
|
||||
|
||||
if (!nativeProcessServerMessage(nativePtr, uSecTimeout)) {
|
||||
disconnect() //Reason: Msg processing will only fail on irrecoverable errors
|
||||
return false
|
||||
connected = false
|
||||
throw IOException(nativeGetLastErrorStr())
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -209,30 +159,11 @@ class VncClient(private val observer: Observer) {
|
|||
fun uploadFrameTexture() = nativeUploadFrameTexture(nativePtr)
|
||||
|
||||
/**
|
||||
* Marks this client as 'disconnected'.
|
||||
*
|
||||
* This doesn't actually release the resources from server but
|
||||
* simply updates the state. [cleanup] is used for that.
|
||||
*/
|
||||
fun disconnect() {
|
||||
state = State.Disconnected
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string describing last error.
|
||||
* Only valid in disconnected state.
|
||||
*/
|
||||
fun getLastErrorStr() = nativeGetLastErrorStr()
|
||||
|
||||
/**
|
||||
* Releases all resource (native & managed) currently held.
|
||||
* After cleanup, this client MUST NOT be used anymore.
|
||||
* Release all resources allocated by the client.
|
||||
* DO NOT use this client after [cleanup].
|
||||
*/
|
||||
fun cleanup() {
|
||||
if (state == State.Destroyed)
|
||||
return
|
||||
|
||||
state = State.Destroyed
|
||||
connected = false
|
||||
nativeCleanup(nativePtr)
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +172,7 @@ class VncClient(private val observer: Observer) {
|
|||
* events to remote server.
|
||||
*/
|
||||
private inline fun executeSend(block: () -> Unit) {
|
||||
if (state == State.Connected && !viewOnlyMode)
|
||||
if (connected && !viewOnlyMode)
|
||||
block()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<data>
|
||||
|
||||
<import type="com.gaurav.avnc.vnc.VncClient.State" />
|
||||
<import type="com.gaurav.avnc.viewmodel.VncViewModel.State" />
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:isVisible="@{viewModel.clientState == State.Connected}">
|
||||
app:isVisible="@{viewModel.state == State.Connected}">
|
||||
|
||||
|
||||
<!--FrameView-->
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurface"
|
||||
app:isVisible="@{viewModel.clientState != State.Connected}">
|
||||
app:isVisible="@{viewModel.state != State.Connected}">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
android:layout_height="60dp"
|
||||
android:indeterminate="true"
|
||||
android:padding="@dimen/padding_small"
|
||||
app:isVisible="@{viewModel.clientState != State.Disconnected}"
|
||||
app:isVisible="@{viewModel.state == State.Connecting}"
|
||||
app:layout_constraintBottom_toTopOf="@id/client_state"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
android:layout_height="60dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_warning"
|
||||
app:isVisible="@{viewModel.clientState == State.Disconnected}"
|
||||
app:isVisible="@{viewModel.state == State.Disconnected}"
|
||||
app:layout_constraintBottom_toTopOf="@id/client_state"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
@ -128,8 +128,8 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_normal"
|
||||
android:text="@{viewModel.clientState.toString()}"
|
||||
app:isVisible="@{viewModel.clientState != State.Created}"
|
||||
android:text="@{viewModel.state.toString()}"
|
||||
app:isVisible="@{viewModel.state != State.Created}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
android:gravity="center"
|
||||
android:text='@{ "( " + viewModel.disconnectReason + " )"}'
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||
app:isVisible="@{viewModel.clientState == State.Disconnected && !viewModel.disconnectReason.empty}"
|
||||
app:isVisible="@{viewModel.state == State.Disconnected && !viewModel.disconnectReason.empty}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/client_state"
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
android:background="@drawable/bg_circular_button"
|
||||
android:contentDescription="@string/desc_retry_connection_btn"
|
||||
android:src="@drawable/ic_refresh"
|
||||
app:isVisible="@{viewModel.clientState == State.Disconnected}"
|
||||
app:isVisible="@{viewModel.state == State.Disconnected}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue