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 package org.kiwix.kiwixmobile.webserver
import io.reactivex.Flowable import kotlinx.coroutines.CoroutineScope
import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Dispatchers
import io.reactivex.disposables.Disposable 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.R
import org.kiwix.kiwixmobile.core.utils.DEFAULT_PORT import org.kiwix.kiwixmobile.core.utils.DEFAULT_PORT
import org.kiwix.kiwixmobile.core.utils.ServerUtils 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.core.utils.files.Log
import org.kiwix.kiwixmobile.webserver.wifi_hotspot.IpAddressCallbacks import org.kiwix.kiwixmobile.webserver.wifi_hotspot.IpAddressCallbacks
import org.kiwix.kiwixmobile.webserver.wifi_hotspot.ServerStatus import org.kiwix.kiwixmobile.webserver.wifi_hotspot.ServerStatus
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/** /**
* WebServerHelper class is used to set up the suitable environment i.e. getting the * WebServerHelper class is used to set up the suitable environment i.e. getting the
* ip address and port no. before starting the WebServer * ip address and port no. before starting the WebServer
* Created by Adeel Zafar on 18/07/2019. * 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( class WebServerHelper @Inject constructor(
private val kiwixServerFactory: KiwixServer.Factory, private val kiwixServerFactory: KiwixServer.Factory,
private val ipAddressCallbacks: IpAddressCallbacks private val ipAddressCallbacks: IpAddressCallbacks
) { ) {
private var kiwixServer: KiwixServer? = null private var kiwixServer: KiwixServer? = null
private var isServerStarted = false private var isServerStarted = false
private var validIpAddressDisposable: Disposable? = null
suspend fun startServerHelper( suspend fun startServerHelper(
selectedBooksPath: ArrayList<String>, selectedBooksPath: ArrayList<String>,
@ -98,31 +108,53 @@ class WebServerHelper @Inject constructor(
ServerUtils.isServerStarted = isStarted 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 * Starts polling for a valid IP address using a [Flow].
@Suppress("MagicNumber") * - Polls every [FINDING_IP_ADDRESS_RETRY_TIME] milliseconds.
fun pollForValidIpAddress() { * - If a valid IP is found, invokes [IpAddressCallbacks.onIpAddressInvalid].
validIpAddressDisposable = * - If no valid IP is found within [FINDING_IP_ADDRESS_TIMEOUT] seconds,
Flowable.interval(1, TimeUnit.SECONDS) * invokes [IpAddressCallbacks.onIpAddressInvalid].
.map { getIp() } * - The flow runs on [Dispatchers.IO] and results are collected on the Main thread.
.filter { s: String? -> s != INVALID_IP } * - Automatically cancels if [serviceScope] is cancelled (e.g. lifecycle aware).
.timeout(15, TimeUnit.SECONDS) */
.take(1) @OptIn(FlowPreview::class)
.observeOn(AndroidSchedulers.mainThread()) @Suppress("InjectDispatcher")
.subscribe( fun pollForValidIpAddress(serviceScope: CoroutineScope) {
{ s: String? -> serviceScope.launch(Dispatchers.Main) {
ipAddressCallbacks.onIpAddressValid() ipPollingFlow()
Log.d(TAG, "onSuccess: $s") .timeout(FINDING_IP_ADDRESS_TIMEOUT.seconds)
} .catch {
) { e: Throwable? -> Log.d(TAG, "Unable to turn on server", it)
Log.d(TAG, "Unable to turn on server", e)
ipAddressCallbacks.onIpAddressInvalid() 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 { companion object {
private const val TAG = "WebServerHelper" private const val TAG = "WebServerHelper"

View File

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