diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 88b1048657d490a939834364eb4e83a6915b434a..37e4dc846f8b25c54a3fda073f0d27441d562423 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -21,8 +21,8 @@ package foundation.e.apps.data.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData +import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App @@ -62,6 +62,7 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository +import foundation.e.apps.data.gplay.utils.runFlowWithTimeout import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule @@ -79,7 +80,7 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton -typealias GplaySearchResultFlow = Flow, Boolean>>> +typealias GplaySearchResultLiveData = LiveData, Boolean>>> typealias FusedHomeDeferred = Deferred>> @Singleton @@ -254,8 +255,9 @@ class FusedApiImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList() + fetchPackageSpecificResult(authData, query, packageSpecificResults).let { - if (it.data?.second == true) { // if there are no data to load + if (it.data?.second != true) { // if there are no data to load emit(it) return@liveData } @@ -289,7 +291,7 @@ class FusedApiImpl @Inject constructor( query, searchResult, packageSpecificResults - ).asLiveData() + ) ) } } @@ -333,20 +335,32 @@ class FusedApiImpl @Inject constructor( query: String, searchResult: MutableList, packageSpecificResults: ArrayList - ): GplaySearchResultFlow = getGplaySearchResult(query).map { - if (it.first.isNotEmpty()) { - searchResult.addAll(it.first) + ): GplaySearchResultLiveData { + return runFlowWithTimeout({ + getGplaySearchResult(query) + }, { + it.second + }, { + Pair(listOf(), false) // empty data for timeout + } + ).map { + if (it.isSuccess()) { + searchResult.addAll(it.data!!.first) + ResultSupreme.Success( + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + it.data!!.second + ) + ) + } else { + it + } + } - ResultSupreme.Success( - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - it.second - ) - ) } private suspend fun fetchOpenSourceSearchResult( @@ -409,9 +423,9 @@ class FusedApiImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (status != ResultStatus.OK) { - return ResultSupreme.create(status, Pair(packageSpecificResults, true)) + return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } - return ResultSupreme.create(status, Pair(packageSpecificResults, false)) + return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } /* diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt new file mode 100644 index 0000000000000000000000000000000000000000..4855aa13418a67804c05cdb42df296f620968f96 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019-2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.gplay.utils + +import android.os.CountDownTimer +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import foundation.e.apps.data.Constants +import foundation.e.apps.data.ResultSupreme +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Observing a LiveData/Flow for timeout is not easy. + * We use a [CountDownTimer] to keep track of intervals between two emissions of data. + * Also we had to collect items from Flow and emit it to a LiveData to avoid missing data emissions. + * + * @param block Code block producing the Flow + * @param moreItemsToLoad Code block evaluating if more items will be loaded in the Flow. + * Check if the item passed is the last item or not. If it is the last item, return false. + * @param timeoutBlock Mandatory code block to execute for timeout. + * Pass empty data from this block or any other data. + * @param exceptionBlock Optional code block to execute for any other error. + * + * @return LiveData containing items from the Flow from [block], each item + * wrapped in [ResultSupreme]. + */ +suspend fun runFlowWithTimeout( + block: suspend () -> Flow, + moreItemsToLoad: suspend (item: T) -> Boolean, + timeoutBlock: () -> T, + exceptionBlock: ((e: Exception) -> T?)? = null, +): LiveData> { + + return liveData { + withContext(Dispatchers.Main) { + + val timer = + Timer(this) { + emit(ResultSupreme.Timeout(timeoutBlock())) + cancel() + } + + + try { + withContext(Dispatchers.IO) { + timer.start() + block().collect { item -> + timer.cancel() + emit(ResultSupreme.Success(item)) + if (!moreItemsToLoad(item)) { + cancel() + } + timer.start() + } + } + } catch (e: Exception) { + if (e is CancellationException) { + return@withContext + } + runCatching { + emit( + ResultSupreme.Error(e.stackTraceToString()).apply { + exceptionBlock?.invoke(e)?.let { setData(it) } + } + ) + } + } + } + } + +} + +private class Timer( + private val scope: CoroutineScope, + private val onTimeout: suspend () -> Unit, +): CountDownTimer(Constants.timeoutDurationInMillis, 1000) { + + override fun onTick(millisUntilFinished: Long) {} + + override fun onFinish() { + scope.launch { + onTimeout() + } + } +} \ No newline at end of file