Refactored the RxJava code to use Kotlin Coroutines in WebServerHelper.

* Refactored the IP address polling logic using coroutine Flow. Now the IP check runs on the IO thread and the result is pushed to the main thread, because our callbacks need to work on the main thread. Also, this whole operation will automatically cancel if the user switches the screen or if the given coroutine scope gets cancelled, since the scope is lifecycle-aware. This helps to avoid using extra resources unnecessarily.
* Added clear and concise method-level comments to improve code readability and help others easily understand the purpose and behavior of each method.
This commit is contained in:
MohitMaliFtechiz 2025-05-12 14:45:04 +05:30 committed by Kelson
parent 2819c187ef
commit 42a1c46e19
2 changed files with 66 additions and 31 deletions

View File

@ -17,9 +17,16 @@
*/
package org.kiwix.kiwixmobile.webserver
import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.DEFAULT_PORT
import org.kiwix.kiwixmobile.core.utils.ServerUtils
@ -29,21 +36,24 @@ import org.kiwix.kiwixmobile.core.utils.ServerUtils.getIpAddress
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.webserver.wifi_hotspot.IpAddressCallbacks
import org.kiwix.kiwixmobile.webserver.wifi_hotspot.ServerStatus
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/**
* WebServerHelper class is used to set up the suitable environment i.e. getting the
* ip address and port no. before starting the WebServer
* Created by Adeel Zafar on 18/07/2019.
*/
const val FINDING_IP_ADDRESS_TIMEOUT = 15_000L
const val FINDING_IP_ADDRESS_RETRY_TIME = 1_000L
class WebServerHelper @Inject constructor(
private val kiwixServerFactory: KiwixServer.Factory,
private val ipAddressCallbacks: IpAddressCallbacks
) {
private var kiwixServer: KiwixServer? = null
private var isServerStarted = false
private var validIpAddressDisposable: Disposable? = null
suspend fun startServerHelper(
selectedBooksPath: ArrayList<String>,
@ -98,31 +108,53 @@ class WebServerHelper @Inject constructor(
ServerUtils.isServerStarted = isStarted
}
// Keeps checking if hotspot has been turned using the ip address with an interval of 1 sec
// If no ip is found after 15 seconds, dismisses the progress dialog
@Suppress("MagicNumber")
fun pollForValidIpAddress() {
validIpAddressDisposable =
Flowable.interval(1, TimeUnit.SECONDS)
.map { getIp() }
.filter { s: String? -> s != INVALID_IP }
.timeout(15, TimeUnit.SECONDS)
.take(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ s: String? ->
ipAddressCallbacks.onIpAddressValid()
Log.d(TAG, "onSuccess: $s")
}
) { e: Throwable? ->
Log.d(TAG, "Unable to turn on server", e)
/**
* Starts polling for a valid IP address using a [Flow].
* - Polls every [FINDING_IP_ADDRESS_RETRY_TIME] milliseconds.
* - If a valid IP is found, invokes [IpAddressCallbacks.onIpAddressInvalid].
* - If no valid IP is found within [FINDING_IP_ADDRESS_TIMEOUT] seconds,
* invokes [IpAddressCallbacks.onIpAddressInvalid].
* - The flow runs on [Dispatchers.IO] and results are collected on the Main thread.
* - Automatically cancels if [serviceScope] is cancelled (e.g. lifecycle aware).
*/
@OptIn(FlowPreview::class)
@Suppress("InjectDispatcher")
fun pollForValidIpAddress(serviceScope: CoroutineScope) {
serviceScope.launch(Dispatchers.Main) {
ipPollingFlow()
.timeout(FINDING_IP_ADDRESS_TIMEOUT.seconds)
.catch {
Log.d(TAG, "Unable to turn on server", it)
ipAddressCallbacks.onIpAddressInvalid()
}.collect {
ipAddressCallbacks.onIpAddressValid()
Log.d(TAG, "onSuccess: $it")
}
}
}
fun dispose() {
validIpAddressDisposable?.dispose()
/**
* Creates a [Flow] that emits the current IP address every [FINDING_IP_ADDRESS_RETRY_TIME] milliseconds.
* - If the returned IP is not [INVALID_IP], the flow completes.
* - The flow runs entirely on [Dispatchers.IO].
*/
@Suppress("InjectDispatcher", "TooGenericExceptionCaught")
private fun ipPollingFlow(): Flow<String?> = flow {
while (true) {
// if ip address is not found wait for 1 second to again getting the ip address.
// this is equivalent to our `rxJava` code.
delay(FINDING_IP_ADDRESS_RETRY_TIME)
val ip = try {
getIp()
} catch (e: Exception) {
Log.e(TAG, "Error getting IP address", e)
INVALID_IP
}
emit(ip)
if (ip != INVALID_IP) break
}
}.flowOn(Dispatchers.IO)
companion object {
private const val TAG = "WebServerHelper"

View File

@ -24,6 +24,8 @@ import android.os.IBinder
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.KiwixApp
@ -53,6 +55,7 @@ class HotspotService :
@set:Inject
var hotspotStateReceiver: HotspotStateReceiver? = null
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var zimHostCallbacks: ZimHostCallbacks? = null
private val serviceBinder: IBinder = HotspotBinder(this)
@ -64,13 +67,13 @@ class HotspotService :
.build()
.inject(this)
super.onCreate()
hotspotStateReceiver?.let(this::registerReceiver)
hotspotStateReceiver?.let(::registerReceiver)
}
override fun onDestroy() {
webServerHelper?.dispose()
hotspotStateReceiver?.let(this@HotspotService::unregisterReceiver)
hotspotStateReceiver?.let(::unregisterReceiver)
super.onDestroy()
serviceScope.cancel()
}
@Suppress("NestedBlockDepth", "InjectDispatcher")
@ -79,7 +82,7 @@ class HotspotService :
ACTION_START_SERVER -> {
val restartServer = intent.getBooleanExtra(RESTART_SERVER, false)
intent.getStringArrayListExtra(ZimHostFragment.SELECTED_ZIM_PATHS_KEY)?.let {
CoroutineScope(Dispatchers.Main).launch {
serviceScope.launch {
val serverStatus =
withContext(Dispatchers.IO) {
webServerHelper?.startServerHelper(it, restartServer)
@ -109,7 +112,7 @@ class HotspotService :
stopHotspotAndDismissNotification()
}
ACTION_CHECK_IP_ADDRESS -> webServerHelper?.pollForValidIpAddress()
ACTION_CHECK_IP_ADDRESS -> webServerHelper?.pollForValidIpAddress(serviceScope)
else -> {}
}
return START_NOT_STICKY