Skip to content

Commit

Permalink
Merge pull request #2493 from quran/use_coroutines_for_translations
Browse files Browse the repository at this point in the history
Use coroutines instead of Rx for translations
  • Loading branch information
ahmedre authored Dec 10, 2023
2 parents e9ef991 + c76229c commit 54cf9f6
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +21,7 @@ class TranslationModel @Inject internal constructor(
private val ayahMapper: AyahMapper
) {

fun getArabicFromDatabase(verses: VerseRange): Single<List<QuranText>> {
suspend fun getArabicFromDatabase(verses: VerseRange): List<QuranText> {
return getVersesFromDatabase(
verses,
QuranDataProvider.QURAN_ARABIC_DATABASE,
Expand All @@ -29,17 +30,17 @@ class TranslationModel @Inject internal constructor(
)
}

fun getTranslationFromDatabase(verses: VerseRange, db: String): Single<List<QuranText>> {
suspend fun getTranslationFromDatabase(verses: VerseRange, db: String): List<QuranText> {
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<List<QuranText>> {
return Single.fromCallable {
): List<QuranText> {
return withContext(Dispatchers.IO) {
val databaseHandler = DatabaseHandler.getDatabaseHandler(appContext, database, quranFileUtils)

if (shouldMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<T : Any> internal constructor(
private val translationModel: TranslationModel,
Expand All @@ -30,46 +29,57 @@ open class BaseTranslationPresenter<T : Any> internal constructor(
private val translationMap: MutableMap<String, LocalTranslation> = HashMap()

var translationScreen: T? = null
var disposable: Disposable? = null

fun getVerses(getArabic: Boolean,
translationsFileNames: List<String>,
verseRange: VerseRange
): Single<ResultHolder> {

val translations = translationsAdapter.legacyGetTranslations()
val sortedTranslations: List<LocalTranslation> = 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<String>,
verseRange: VerseRange
): ResultHolder {
return withContext(Dispatchers.IO) {
val translations = translationsAdapter.getTranslations().first()
val sortedTranslations: List<LocalTranslation> = 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<QuranText>,
texts: List<List<QuranText>>,
map: Map<String, LocalTranslation> ->
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<String> {
Expand Down Expand Up @@ -174,18 +184,19 @@ open class BaseTranslationPresenter<T : Any> internal constructor(
return texts
}

private fun getTranslationMapSingle(): Single<Map<String, LocalTranslation>> {
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<String, LocalTranslation> {
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
}
}
}

Expand All @@ -198,6 +209,5 @@ open class BaseTranslationPresenter<T : Any> internal constructor(

override fun unbind(what: T) {
translationScreen = null
disposable?.dispose()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<ResultHolder>() {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TranslationPresenter.TranslationScreen>(
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<TranslationPresenter.TranslationScreen>(
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<ResultHolder>() {
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<QuranAyahInfo>): Int {
Expand All @@ -60,9 +78,12 @@ internal class TranslationPresenter @Inject internal constructor(translationMode
}

interface TranslationScreen {
fun setVerses(page: Int,
translations: Array<LocalTranslation>,
verses: List<@JvmSuppressWildcards QuranAyahInfo>)
fun setVerses(
page: Int,
translations: Array<LocalTranslation>,
verses: List<@JvmSuppressWildcards QuranAyahInfo>
)

fun updateScrollPosition()
}
}
Loading

0 comments on commit 54cf9f6

Please sign in to comment.