From 42a1c46e1921324cc170922ffc4fca36b69a583a Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Mon, 12 May 2025 14:45:04 +0530 Subject: [PATCH] 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. --- .../kiwixmobile/webserver/WebServerHelper.kt | 84 +++++++++++++------ .../webserver/wifi_hotspot/HotspotService.kt | 13 +-- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.kt b/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.kt index 34086f027..58fedcc66 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/WebServerHelper.kt @@ -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, @@ -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 = 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" diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/wifi_hotspot/HotspotService.kt b/app/src/main/java/org/kiwix/kiwixmobile/webserver/wifi_hotspot/HotspotService.kt index 68014cb46..677bf145e 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/webserver/wifi_hotspot/HotspotService.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/wifi_hotspot/HotspotService.kt @@ -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