From ef7393169e82857089f478903a8bed785f097011 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Tue, 26 Sep 2017 14:00:34 +0200
Subject: [PATCH] Implemented proper offline functionality handling

---
 .../quasseldroid_ng/service/QuasselService.kt | 54 ++++++++++---------
 .../quasseldroid_ng/ui/MainActivity.kt        | 10 ++--
 .../de/kuschku/libquassel/session/Backend.kt  |  2 +-
 .../libquassel/session/CoreConnection.kt      | 11 ++--
 .../libquassel/session/ICoreConnection.kt     | 31 -----------
 .../de/kuschku/libquassel/session/ISession.kt | 19 +++++++
 .../libquassel/session/ProtocolHandler.kt     |  5 +-
 .../de/kuschku/libquassel/session/Session.kt  | 50 +++++++----------
 .../libquassel/session/SessionManager.kt      | 49 +++++++++++++++++
 .../kuschku/libquassel/ConnectionUnitTest.kt  |  7 +--
 10 files changed, 130 insertions(+), 108 deletions(-)
 delete mode 100644 lib/src/main/java/de/kuschku/libquassel/session/ICoreConnection.kt
 create mode 100644 lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
 create mode 100644 lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
index adb777f4f..b0e60d6a7 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/service/QuasselService.kt
@@ -7,7 +7,8 @@ import android.os.Handler
 import android.os.HandlerThread
 import de.kuschku.libquassel.protocol.*
 import de.kuschku.libquassel.session.Backend
-import de.kuschku.libquassel.session.Session
+import de.kuschku.libquassel.session.ISession
+import de.kuschku.libquassel.session.SessionManager
 import de.kuschku.libquassel.session.SocketAddress
 import de.kuschku.quasseldroid_ng.BuildConfig
 import de.kuschku.quasseldroid_ng.R
@@ -18,20 +19,31 @@ import java.security.cert.X509Certificate
 import javax.net.ssl.X509TrustManager
 
 class QuasselService : LifecycleService() {
-  private lateinit var session: Session
+  private lateinit var sessionManager: SessionManager
+
+  private lateinit var clientData: ClientData
+
+  private val trustManager = object : X509TrustManager {
+    override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
+    }
+
+    override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
+    }
+
+    override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
+  }
 
   private val backendImplementation = object : Backend {
-    override fun session() = session
+    override fun sessionManager() = sessionManager
 
     override fun connect(address: SocketAddress, user: String, pass: String) {
       disconnect()
       val handlerService = AndroidHandlerService()
-      session.connect(address, handlerService)
-      session.userData = user to pass
+      sessionManager.connect(clientData, trustManager, address, handlerService, user to pass)
     }
 
     override fun disconnect() {
-      session.cleanUp()
+      sessionManager.disconnect()
     }
   }
 
@@ -56,7 +68,7 @@ class QuasselService : LifecycleService() {
       }
     }
 
-    override fun session() = backendImplementation.session()
+    override fun sessionManager() = backendImplementation.sessionManager()
   }
 
   private lateinit var database: QuasselDatabase
@@ -64,26 +76,16 @@ class QuasselService : LifecycleService() {
   override fun onCreate() {
     super.onCreate()
     database = QuasselDatabase.Creator.init(application)
-    session = Session(
-      clientData = ClientData(
-        identifier = "${resources.getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}",
-        buildDate = Instant.ofEpochSecond(BuildConfig.GIT_COMMIT_DATE),
-        clientFeatures = Quassel_Features.of(*Quassel_Feature.values()),
-        protocolFeatures = Protocol_Features.of(
-          Protocol_Feature.Compression,
-          Protocol_Feature.TLS
-        ),
-        supportedProtocols = byteArrayOf(0x02)
+    sessionManager = SessionManager(ISession.NULL)
+    clientData = ClientData(
+      identifier = "${resources.getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}",
+      buildDate = Instant.ofEpochSecond(BuildConfig.GIT_COMMIT_DATE),
+      clientFeatures = Quassel_Features.of(*Quassel_Feature.values()),
+      protocolFeatures = Protocol_Features.of(
+        Protocol_Feature.Compression,
+        Protocol_Feature.TLS
       ),
-      trustManager = object : X509TrustManager {
-        override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
-        }
-
-        override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {
-        }
-
-        override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
-      }
+      supportedProtocols = byteArrayOf(0x02)
     )
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/MainActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/MainActivity.kt
index 933598260..93f8a9a3c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/MainActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/MainActivity.kt
@@ -19,7 +19,6 @@ import de.kuschku.libquassel.util.compatibility.LoggingHandler
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.util.helper.stickyMapNotNull
 import de.kuschku.quasseldroid_ng.util.helper.stickySwitchMapNotNull
-import de.kuschku.quasseldroid_ng.util.helper.switchMapNullable
 import org.threeten.bp.ZoneOffset
 import org.threeten.bp.ZonedDateTime
 import org.threeten.bp.format.DateTimeFormatter
@@ -49,12 +48,9 @@ class MainActivity : ServiceBoundActivity() {
   @BindView(R.id.errorList)
   lateinit var errorList: TextView
 
-  private val state = backend.stickyMapNotNull(null, Backend::session)
-    .switchMapNullable(null) { session ->
-      LiveDataReactiveStreams.fromPublisher(session.connectionPublisher)
-    }
-    .stickySwitchMapNotNull(ConnectionState.DISCONNECTED) { connection ->
-      LiveDataReactiveStreams.fromPublisher(connection.state)
+  private val state = backend.stickyMapNotNull(null, Backend::sessionManager)
+    .stickySwitchMapNotNull(ConnectionState.DISCONNECTED) { session ->
+      LiveDataReactiveStreams.fromPublisher(session.state)
     }
 
   private var snackbar: Snackbar? = null
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
index 904e78e92..a0535645b 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Backend.kt
@@ -3,5 +3,5 @@ package de.kuschku.libquassel.session
 interface Backend {
   fun connect(address: SocketAddress, user: String, pass: String)
   fun disconnect()
-  fun session(): Session
+  fun sessionManager(): SessionManager
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt b/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt
index 9b26573ff..c778ad2c7 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/CoreConnection.kt
@@ -21,6 +21,7 @@ import io.reactivex.BackpressureStrategy
 import io.reactivex.subjects.BehaviorSubject
 import org.threeten.bp.ZoneOffset
 import org.threeten.bp.format.DateTimeFormatter
+import java.io.Closeable
 import java.lang.Thread.UncaughtExceptionHandler
 import java.net.Socket
 import java.net.SocketException
@@ -30,7 +31,7 @@ class CoreConnection(
   private val session: Session,
   private val address: SocketAddress,
   private val handlerService: HandlerService
-) : Thread(), ICoreConnection {
+) : Thread(), Closeable {
   companion object {
     private const val TAG = "CoreConnection"
   }
@@ -43,7 +44,7 @@ class CoreConnection(
   private val chainedBuffer = ChainedByteBuffer(direct = true)
 
   private val internalState = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED)
-  override val state = internalState.toFlowable(BackpressureStrategy.LATEST)
+  val state = internalState.toFlowable(BackpressureStrategy.LATEST)
 
   private var channel: WrappedChannel? = null
 
@@ -57,7 +58,7 @@ class CoreConnection(
     channel = WrappedChannel.ofSocket(socket)
   }
 
-  override fun setState(value: ConnectionState) {
+  fun setState(value: ConnectionState) {
     log(INFO, "CoreConnection", value.name)
     internalState.onNext(value)
   }
@@ -122,7 +123,7 @@ class CoreConnection(
     }
   }
 
-  override fun dispatch(message: HandshakeMessage) {
+  fun dispatch(message: HandshakeMessage) {
     handlerService.parse {
       try {
         val data = HandshakeMessage.serialize(message)
@@ -136,7 +137,7 @@ class CoreConnection(
     }
   }
 
-  override fun dispatch(message: SignalProxyMessage) {
+  fun dispatch(message: SignalProxyMessage) {
     handlerService.parse {
       try {
         val data = SignalProxyMessage.serialize(message)
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ICoreConnection.kt b/lib/src/main/java/de/kuschku/libquassel/session/ICoreConnection.kt
deleted file mode 100644
index e23d42b46..000000000
--- a/lib/src/main/java/de/kuschku/libquassel/session/ICoreConnection.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package de.kuschku.libquassel.session
-
-import de.kuschku.libquassel.protocol.message.HandshakeMessage
-import de.kuschku.libquassel.protocol.message.SignalProxyMessage
-import io.reactivex.BackpressureStrategy
-import io.reactivex.subjects.BehaviorSubject
-import org.reactivestreams.Publisher
-
-interface ICoreConnection {
-  val state: Publisher<ConnectionState>
-  fun close()
-  fun dispatch(message: HandshakeMessage)
-  fun dispatch(message: SignalProxyMessage)
-  fun start()
-  fun join()
-  fun setState(value: ConnectionState)
-
-  companion object {
-    val NULL = object : ICoreConnection {
-      override fun setState(value: ConnectionState) = Unit
-      override fun start() = Unit
-      override fun join() = Unit
-      override fun close() = Unit
-      override fun dispatch(message: HandshakeMessage) = Unit
-      override fun dispatch(message: SignalProxyMessage) = Unit
-      override val state: Publisher<ConnectionState>
-        get() = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED)
-          .toFlowable(BackpressureStrategy.LATEST)
-    }
-  }
-}
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
new file mode 100644
index 000000000..64054b6ea
--- /dev/null
+++ b/lib/src/main/java/de/kuschku/libquassel/session/ISession.kt
@@ -0,0 +1,19 @@
+package de.kuschku.libquassel.session
+
+import io.reactivex.BackpressureStrategy
+import io.reactivex.Flowable
+import io.reactivex.subjects.BehaviorSubject
+import java.io.Closeable
+
+interface ISession : Closeable {
+  val state: Flowable<ConnectionState>
+
+  companion object {
+    val NULL = object : ISession {
+      override fun close() = Unit
+      override val state: Flowable<ConnectionState>
+        = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED)
+        .toFlowable(BackpressureStrategy.LATEST)
+    }
+  }
+}
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt
index 5f364a5ee..c8fefde5c 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/ProtocolHandler.kt
@@ -11,8 +11,9 @@ import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.WARN
 import de.kuschku.libquassel.util.compatibility.log
 import org.threeten.bp.Instant
+import java.io.Closeable
 
-abstract class ProtocolHandler : SignalProxy, AuthHandler {
+abstract class ProtocolHandler : SignalProxy, AuthHandler, Closeable {
   private val objectStorage: ObjectStorage = ObjectStorage(this)
   private val rpcHandler: RpcHandler = RpcHandler(this)
 
@@ -170,7 +171,7 @@ abstract class ProtocolHandler : SignalProxy, AuthHandler {
     toInit.remove(syncableObject)
   }
 
-  open fun cleanUp() {
+  override fun close() {
     objectStorage.clear()
     toInit.clear()
   }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
index 180c9e307..d45a5f85d 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
@@ -5,25 +5,26 @@ import de.kuschku.libquassel.protocol.message.HandshakeMessage
 import de.kuschku.libquassel.protocol.message.SignalProxyMessage
 import de.kuschku.libquassel.quassel.QuasselFeature
 import de.kuschku.libquassel.quassel.syncables.*
-import de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invokers
 import de.kuschku.libquassel.util.compatibility.HandlerService
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.DEBUG
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel.INFO
 import de.kuschku.libquassel.util.compatibility.log
 import de.kuschku.libquassel.util.hasFlag
-import io.reactivex.BackpressureStrategy
 import io.reactivex.Flowable
-import io.reactivex.subjects.BehaviorSubject
 import org.threeten.bp.Instant
 import javax.net.ssl.X509TrustManager
 
 class Session(
   val clientData: ClientData,
-  val trustManager: X509TrustManager
-) : ProtocolHandler() {
+  val trustManager: X509TrustManager,
+  address: SocketAddress,
+  handlerService: HandlerService,
+  private val userData: Pair<String, String>
+) : ProtocolHandler(), ISession {
   var coreFeatures: Quassel_Features = Quassel_Feature.NONE
 
-  var userData: Pair<String, String>? = null
+  private val coreConnection = CoreConnection(this, address, handlerService)
+  override val state: Flowable<ConnectionState> = coreConnection.state
 
   private var aliasManager: AliasManager? = null
   private var backlogManager: BacklogManager? = null
@@ -38,34 +39,21 @@ class Session(
   private var networks = mutableMapOf<NetworkId, Network>()
   private var networkConfig: NetworkConfig? = null
 
-  private val connection = BehaviorSubject.createDefault(ICoreConnection.NULL)
-  val connectionPublisher: Flowable<ICoreConnection> = connection.toFlowable(
-    BackpressureStrategy.LATEST)
-
   init {
-    log(INFO, "Session", "Session created")
-
-    // This should preload them
-    Invokers
-  }
-
-  fun connect(address: SocketAddress, handlerService: HandlerService) {
-    val coreConnection = CoreConnection(this, address, handlerService)
-    connection.onNext(coreConnection)
     coreConnection.start()
   }
 
   override fun handle(f: HandshakeMessage.ClientInitAck): Boolean {
     coreFeatures = f.coreFeatures ?: Quassel_Feature.NONE
     dispatch(HandshakeMessage.ClientLogin(
-      user = userData?.first,
-      password = userData?.second
+      user = userData.first,
+      password = userData.second
     ))
     return true
   }
 
   override fun handle(f: HandshakeMessage.SessionInit): Boolean {
-    connection.value.setState(ConnectionState.INIT)
+    coreConnection.setState(ConnectionState.INIT)
 
     f.networkIds?.forEach {
       val network = Network(it.value(-1), this)
@@ -117,7 +105,7 @@ class Session(
   }
 
   override fun onInitDone() {
-    connection.value.setState(ConnectionState.CONNECTED)
+    coreConnection.setState(ConnectionState.CONNECTED)
     log(INFO, "Session", "Initialization finished")
   }
 
@@ -130,20 +118,19 @@ class Session(
 
   override fun dispatch(message: SignalProxyMessage) {
     log(DEBUG, "Session", "> $message")
-    connection.value.dispatch(message)
+    coreConnection.dispatch(message)
   }
 
   override fun dispatch(message: HandshakeMessage) {
     log(DEBUG, "Session", "> $message")
-    connection.value.dispatch(message)
+    coreConnection.dispatch(message)
   }
 
   override fun network(id: NetworkId): Network? = networks[id]
   override fun identity(id: IdentityId): Identity? = identities[id]
 
-  override fun cleanUp() {
-    connection.value.close()
-    connection.onNext(ICoreConnection.NULL)
+  override fun close() {
+    coreConnection.close()
 
     aliasManager = null
     backlogManager = null
@@ -159,9 +146,10 @@ class Session(
     identities.clear()
     networks.clear()
 
-    super.cleanUp()
+    super.close()
   }
 
-  fun connection(): ICoreConnection?
-    = connection.value
+  fun join() {
+    coreConnection.join()
+  }
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
new file mode 100644
index 000000000..f2e461552
--- /dev/null
+++ b/lib/src/main/java/de/kuschku/libquassel/session/SessionManager.kt
@@ -0,0 +1,49 @@
+package de.kuschku.libquassel.session
+
+import de.kuschku.libquassel.protocol.ClientData
+import de.kuschku.libquassel.quassel.syncables.interfaces.invokers.Invokers
+import de.kuschku.libquassel.util.compatibility.HandlerService
+import de.kuschku.libquassel.util.compatibility.LoggingHandler
+import de.kuschku.libquassel.util.compatibility.log
+import io.reactivex.BackpressureStrategy
+import io.reactivex.Flowable
+import io.reactivex.subjects.BehaviorSubject
+import javax.net.ssl.X509TrustManager
+
+class SessionManager(
+  private val offlineSession: ISession
+) {
+  init {
+    log(LoggingHandler.LogLevel.INFO, "Session", "Session created")
+
+    // This should preload them
+    Invokers
+  }
+
+  fun connect(
+    clientData: ClientData,
+    trustManager: X509TrustManager,
+    address: SocketAddress,
+    handlerService: HandlerService,
+    userData: Pair<String, String>
+  ) {
+    inProgressSession.value.close()
+    inProgressSession.onNext(Session(clientData, trustManager, address, handlerService, userData))
+  }
+
+  fun disconnect() {
+    inProgressSession.value.close()
+    inProgressSession.onNext(offlineSession)
+  }
+
+  private var inProgressSession = BehaviorSubject.createDefault(offlineSession)
+  private val inProgressSessionPublisher: Flowable<ISession>
+    = inProgressSession.toFlowable(BackpressureStrategy.LATEST)
+  val state = inProgressSessionPublisher.switchMap { it.state }
+  val session = state.map { connectionState ->
+    if (connectionState == ConnectionState.CONNECTED)
+      inProgressSession.value
+    else
+      offlineSession
+  }
+}
diff --git a/lib/src/test/java/de/kuschku/libquassel/ConnectionUnitTest.kt b/lib/src/test/java/de/kuschku/libquassel/ConnectionUnitTest.kt
index 7d7f5009f..a4af220db 100644
--- a/lib/src/test/java/de/kuschku/libquassel/ConnectionUnitTest.kt
+++ b/lib/src/test/java/de/kuschku/libquassel/ConnectionUnitTest.kt
@@ -46,10 +46,7 @@ class ConnectionUnitTest {
       }
 
       override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
-    })
-    session.userData = user to pass
-
-    session.connect(SocketAddress(host, port), JavaHandlerService())
-    session.connection()?.join()
+    }, SocketAddress(host, port), JavaHandlerService(), user to pass)
+    session.join()
   }
 }
-- 
GitLab