diff --git a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt index 12eb6cdca6..e1f9317494 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt +++ b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt @@ -10,7 +10,8 @@ import com.quran.labs.androidquran.database.DatabaseHandler import com.quran.labs.androidquran.database.DatabaseHandler.TextType import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext -import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject @ActivityScope @@ -20,7 +21,7 @@ class TranslationModel @Inject internal constructor( private val ayahMapper: AyahMapper ) { - fun getArabicFromDatabase(verses: VerseRange): Single> { + suspend fun getArabicFromDatabase(verses: VerseRange): List { return getVersesFromDatabase( verses, QuranDataProvider.QURAN_ARABIC_DATABASE, @@ -29,17 +30,17 @@ class TranslationModel @Inject internal constructor( ) } - fun getTranslationFromDatabase(verses: VerseRange, db: String): Single> { + suspend fun getTranslationFromDatabase(verses: VerseRange, db: String): List { return getVersesFromDatabase(verses, db, TextType.TRANSLATION, shouldMap = true) } - private fun getVersesFromDatabase( + private suspend fun getVersesFromDatabase( verses: VerseRange, database: String, @TextType type: Int, shouldMap: Boolean = false - ): Single> { - return Single.fromCallable { + ): List { + return withContext(Dispatchers.IO) { val databaseHandler = DatabaseHandler.getDatabaseHandler(appContext, database, quranFileUtils) if (shouldMap) { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt index 7d9474fc9e..12995febd4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt @@ -5,7 +5,6 @@ import com.quran.data.model.QuranText import com.quran.data.model.SuraAyah import com.quran.data.model.SuraAyahIterator import com.quran.data.model.VerseRange -import com.quran.labs.androidquran.common.LocalTranslationDisplaySort import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.common.TranslationMetadata import com.quran.labs.androidquran.database.TranslationsDBAdapter @@ -14,12 +13,12 @@ import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.translation.model.LocalTranslation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers -import java.util.Collections +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext open class BaseTranslationPresenter internal constructor( private val translationModel: TranslationModel, @@ -30,46 +29,57 @@ open class BaseTranslationPresenter internal constructor( private val translationMap: MutableMap = HashMap() var translationScreen: T? = null - var disposable: Disposable? = null - - fun getVerses(getArabic: Boolean, - translationsFileNames: List, - verseRange: VerseRange - ): Single { - - val translations = translationsAdapter.legacyGetTranslations() - val sortedTranslations: List = ArrayList(translations) - Collections.sort(sortedTranslations, LocalTranslationDisplaySort()) - - val orderedTranslationsFileNames = sortedTranslations - .filter { translationsFileNames.contains(it.filename) } - .map { it.filename } - - // get all the translations for these verses, using a source of the list of ordered active translations - val source = Observable.fromIterable(orderedTranslationsFileNames) - - val translationsObservable = - source.concatMapEager { db -> - translationModel.getTranslationFromDatabase(verseRange, db) - .map { texts -> ensureProperTranslations(verseRange, texts) } - .onErrorReturnItem(ArrayList()) - .toObservable() + + suspend fun getVerses( + getArabic: Boolean, + translationsFileNames: List, + verseRange: VerseRange + ): ResultHolder { + return withContext(Dispatchers.IO) { + val translations = translationsAdapter.getTranslations().first() + val sortedTranslations: List = translations.sortedBy { it.displayOrder } + + val orderedTranslationsFileNames = sortedTranslations + .filter { translationsFileNames.contains(it.filename) } + .map { it.filename } + + val job = SupervisorJob() + // get all the translations for these verses, using a source of the list of ordered active translations + val translationData = orderedTranslationsFileNames.map { + async(job) { + val initialTexts = translationModel.getTranslationFromDatabase(verseRange, it) + ensureProperTranslations(verseRange, initialTexts) + } + } + + val arabic = async(job) { + if (getArabic) { + translationModel.getArabicFromDatabase(verseRange) + } else { + emptyList() + } + } + + val arabicText = + try { + arabic.await() + } catch (e: Exception) { + emptyList() + } + + val translationTexts = translationData.map { deferred -> + try { + deferred.await() + } catch (e: Exception) { + emptyList() + } } - .toList() - val arabicObservable = if (!getArabic) - Single.just(ArrayList()) - else - translationModel.getArabicFromDatabase(verseRange).onErrorReturnItem(ArrayList()) - return Single.zip( - arabicObservable, translationsObservable, getTranslationMapSingle() - ) { arabic: List, - texts: List>, - map: Map -> - val translationInfos = getTranslations(orderedTranslationsFileNames, map) - val ayahInfo = combineAyahData(verseRange, arabic, texts, translationInfos) + val translationMap = getTranslationMap() + + val translationInfos = getTranslations(orderedTranslationsFileNames, translationMap) + val ayahInfo = combineAyahData(verseRange, arabicText, translationTexts, translationInfos) ResultHolder(translationInfos, ayahInfo) } - .subscribeOn(Schedulers.io()) } fun getTranslations(quranSettings: QuranSettings): List { @@ -174,18 +184,19 @@ open class BaseTranslationPresenter internal constructor( return texts } - private fun getTranslationMapSingle(): Single> { - return if (this.translationMap.isEmpty()) { - Single.fromCallable { translationsAdapter.legacyGetTranslations() } - .map { translations -> translations.associateBy { it.filename } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess { map -> - this.translationMap.clear() - this.translationMap.putAll(map) - } - } else { - Single.just(this.translationMap) + private suspend fun getTranslationMap(): Map { + val currentTranslationMap = translationMap + return withContext(Dispatchers.IO) { + if (currentTranslationMap.isEmpty()) { + val updatedTranslations = translationsAdapter.getTranslations() + .map { it.associateBy { it.filename } } + .first() + translationMap.clear() + translationMap.putAll(updatedTranslations) + updatedTranslations + } else { + currentTranslationMap + } } } @@ -198,6 +209,5 @@ open class BaseTranslationPresenter internal constructor( override fun unbind(what: T) { translationScreen = null - disposable?.dispose() } } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt index 714cf0876c..678451d325 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -9,11 +9,11 @@ import com.quran.labs.androidquran.presenter.translationlist.TranslationListPres import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.translation.model.LocalTranslation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.observers.DisposableSingleObserver +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext import javax.inject.Inject class InlineTranslationPresenter @Inject constructor( @@ -38,17 +38,11 @@ class InlineTranslationPresenter @Inject constructor( .launchIn(scope) } - fun refresh(verseRange: VerseRange) { - disposable?.dispose() - disposable = getVerses(false, getTranslations(quranSettings), verseRange) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableSingleObserver() { - override fun onSuccess(result: ResultHolder) { - translationScreen?.setVerses(result.translations, result.ayahInformation) - } - - override fun onError(e: Throwable) {} - }) + suspend fun refresh(verseRange: VerseRange) { + val result = withContext(Dispatchers.IO) { + getVerses(false, getTranslations(quranSettings), verseRange) + } + translationScreen?.setVerses(result.translations, result.ayahInformation) } override fun bind(what: TranslationScreen) { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt index 3c8a696f0f..ec7c04690a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt @@ -2,7 +2,6 @@ package com.quran.labs.androidquran.presenter.translation import android.content.Context import android.util.Pair -import androidx.annotation.VisibleForTesting import com.quran.labs.androidquran.dao.translation.Translation import com.quran.labs.androidquran.dao.translation.TranslationItem import com.quran.labs.androidquran.dao.translation.TranslationList @@ -43,8 +42,7 @@ open class TranslationManagerPresenter @Inject internal constructor( private val translationsDBAdapter: TranslationsDBAdapter, private val quranFileUtils: QuranFileUtils ) { - @VisibleForTesting - var host: String = Constants.HOST + internal var host: String = Constants.HOST private val scope = MainScope() diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index 8b7f3a9b95..a029cc5267 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt @@ -11,43 +11,61 @@ import com.quran.mobile.translation.model.LocalTranslation import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.observers.DisposableObserver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @QuranPageScope -internal class TranslationPresenter @Inject internal constructor(translationModel: TranslationModel, - private val quranSettings: QuranSettings, - translationsAdapter: TranslationsDBAdapter, - translationUtil: TranslationUtil, - private val quranInfo: QuranInfo, - private val pages: IntArray) : - BaseTranslationPresenter( - translationModel, translationsAdapter, translationUtil, quranInfo) { +internal class TranslationPresenter @Inject internal constructor( + translationModel: TranslationModel, + private val quranSettings: QuranSettings, + translationsAdapter: TranslationsDBAdapter, + translationUtil: TranslationUtil, + private val quranInfo: QuranInfo, + private val pages: IntArray +) : + BaseTranslationPresenter( + translationModel, translationsAdapter, translationUtil, quranInfo + ) { - fun refresh() { - disposable?.dispose() + private val scope = MainScope() - disposable = Observable.fromArray(*pages.toTypedArray()) - .flatMap { page -> - getVerses(quranSettings.wantArabicInTranslationView(), - getTranslations(quranSettings), quranInfo.getVerseRangeForPage(page)) - .toObservable() - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableObserver() { - override fun onNext(result: ResultHolder) { - val screen = translationScreen - if (screen != null && result.ayahInformation.isNotEmpty()) { - screen.setVerses( - getPage(result.ayahInformation), result.translations, - result.ayahInformation) - screen.updateScrollPosition() - } - } - - override fun onError(e: Throwable) {} + fun legacyRefresh() { + scope.launch { + refresh() + } + } - override fun onComplete() {} - }) + suspend fun refresh() { + pages + .map { + withContext(Dispatchers.IO) { + getVerses( + quranSettings.wantArabicInTranslationView(), + getTranslations(quranSettings), quranInfo.getVerseRangeForPage(it) + ) + } + } + .onEach { result -> + val screen = translationScreen + if (screen != null && result.ayahInformation.isNotEmpty()) { + screen.setVerses( + getPage(result.ayahInformation), result.translations, + result.ayahInformation + ) + screen.updateScrollPosition() + } + } } private fun getPage(result: List): Int { @@ -60,9 +78,12 @@ internal class TranslationPresenter @Inject internal constructor(translationMode } interface TranslationScreen { - fun setVerses(page: Int, - translations: Array, - verses: List<@JvmSuppressWildcards QuranAyahInfo>) + fun setVerses( + page: Int, + translations: Array, + verses: List<@JvmSuppressWildcards QuranAyahInfo> + ) + fun updateScrollPosition() } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 11248eeac5..077b427b5d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.abs @@ -48,6 +49,8 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { @Inject lateinit var translationPresenter: InlineTranslationPresenter + private val scope = MainScope() + object Provider : AyahActionFragmentProvider { override val order = SlidingPagerAdapter.TRANSLATION_PAGE override val iconResId = com.quran.labs.androidquran.common.toolbar.R.drawable.ic_translation @@ -59,6 +62,11 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { (activity as? PagerActivity)?.pagerActivityComponent?.inject(this) } + override fun onDetach() { + scope.cancel() + super.onDetach() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -151,7 +159,9 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { quranInfo.getAyahId(start.sura, start.ayah) - quranInfo.getAyahId(end.sura, end.ayah) ) val verseRange = VerseRange(start.sura, start.ayah, end.sura, end.ayah, verses) - translationPresenter.refresh(verseRange) + scope.launch { + translationPresenter.refresh(verseRange) + } } override fun setVerses(translations: Array, verses: List) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index 3a57d8347b..4e52ea55ee 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -408,7 +408,7 @@ public void hidePageDownloadError() { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mode == Mode.TRANSLATION) { - translationPresenter.get().refresh(); + translationPresenter.get().legacyRefresh(); } } @@ -438,7 +438,7 @@ public void updateScrollPosition() { public void refresh() { if (mode == Mode.TRANSLATION) { - translationPresenter.get().refresh(); + translationPresenter.get().legacyRefresh(); } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java index 5227d9f671..8eb1359803 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java @@ -157,7 +157,7 @@ public void updateScrollPosition() { } public void refresh() { - presenter.refresh(); + presenter.legacyRefresh(); } @Override