Skip to content

Commit

Permalink
Merge pull request #2485 from quran/translations_cleanup
Browse files Browse the repository at this point in the history
Use Sqldelight for translations
  • Loading branch information
ahmedre authored Dec 9, 2023
2 parents f43e882 + 294af52 commit a593617
Show file tree
Hide file tree
Showing 43 changed files with 465 additions and 323 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ dependencies {
implementation project(path: ':common:recitation')
implementation project(path: ':common:search')
implementation project(path: ':common:toolbar')
implementation project(path: ':common:translation')
implementation project(path: ':common:upgrade')
implementation project(path: ':common:ui:core')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.quran.labs.androidquran.common

import com.quran.mobile.translation.model.LocalTranslation

class LocalTranslationDisplaySort : Comparator<LocalTranslation> {
override fun compare(first: LocalTranslation, second: LocalTranslation): Int {
return first.displayOrder.compareTo(second.displayOrder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ data class Translation(val id: Int,
val saveTo: String,
val languageCode: String,
val translator: String? = "",
@Json(name = "translatorForeign") val translatorNameLocalized: String? = "",
val displayOrder: Int = -1) {
@Json(name = "translatorForeign") val translatorNameLocalized: String? = "") {

fun withSchema(schema: Int) = copy(minimumVersion = schema)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.quran.labs.androidquran.dao.translation

import com.quran.mobile.translation.model.LocalTranslation

data class TranslationItem @JvmOverloads constructor(val translation: Translation,
val localVersion: Int = 0,
val displayOrder: Int = -1) : TranslationRowData {
Expand All @@ -16,7 +18,22 @@ data class TranslationItem @JvmOverloads constructor(val translation: Translatio

fun withTranslationRemoved() = this.copy(localVersion = 0)

fun withTranslationVersion(version: Int) = this.copy(localVersion = version)

fun withDisplayOrder(newDisplayOrder: Int) = this.copy(displayOrder = newDisplayOrder)

fun withLocalVersionAndDisplayOrder(newVersion: Int, displayOrder: Int) = this.copy(localVersion = newVersion, displayOrder = displayOrder)

fun asLocalTranslation(): LocalTranslation {
return LocalTranslation(
id = translation.id.toLong(),
filename = translation.fileName,
name = translation.displayName,
translator = translation.translator,
translatorForeign = translation.translatorNameLocalized,
url = translation.fileUrl,
languageCode = translation.languageCode,
version = localVersion,
minimumVersion = translation.minimumVersion,
displayOrder = displayOrder
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@
import android.net.Uri;
import android.provider.BaseColumns;

import androidx.annotation.NonNull;

import com.quran.data.core.QuranInfo;
import com.quran.labs.androidquran.BuildConfig;
import com.quran.labs.androidquran.QuranApplication;
import com.quran.labs.androidquran.R;
import com.quran.labs.androidquran.common.LocalTranslation;
import com.quran.labs.androidquran.database.DatabaseHandler;
import com.quran.labs.androidquran.database.DatabaseUtils;
import com.quran.labs.androidquran.database.TranslationsDBAdapter;
import com.quran.labs.androidquran.util.QuranFileUtils;
import com.quran.labs.androidquran.util.QuranUtils;
import com.quran.mobile.translation.model.LocalTranslation;

import java.util.List;

import javax.inject.Inject;

import androidx.annotation.NonNull;
import timber.log.Timber;

public class QuranDataProvider extends ContentProvider {
Expand Down Expand Up @@ -107,7 +108,7 @@ private Cursor search(String query) {
}

private List<LocalTranslation> getAvailableTranslations() {
return translationsDBAdapter.getTranslations();
return translationsDBAdapter.legacyGetTranslations();
}

private Cursor getSuggestions(String query) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,163 +1,91 @@
package com.quran.labs.androidquran.database

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.util.SparseArray
import androidx.annotation.WorkerThread
import com.quran.labs.androidquran.common.LocalTranslation
import com.quran.labs.androidquran.dao.translation.TranslationItem
import com.quran.labs.androidquran.database.TranslationsDBHelper.TranslationsTable
import com.quran.labs.androidquran.util.QuranFileUtils
import com.quran.mobile.di.qualifier.ApplicationContext
import timber.log.Timber
import java.util.Collections
import com.quran.mobile.translation.data.TranslationsDataSource
import com.quran.mobile.translation.model.LocalTranslation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TranslationsDBAdapter @Inject constructor(
@ApplicationContext private val context: Context,
adapter: TranslationsDBHelper,
private val dataSource: TranslationsDataSource,
private val quranFileUtils: QuranFileUtils
) {
private val db: SQLiteDatabase = adapter.writableDatabase

@Volatile
private var cachedTranslations: List<LocalTranslation>? = null
fun getTranslations(): Flow<List<LocalTranslation>> {
return dataSource.translations()
.filterNotNull()
.map { translations ->
translations.filter { quranFileUtils.hasTranslation(context, it.filename) }
}
}

var lastWriteTime: Long = 0
private set
@WorkerThread
fun legacyGetTranslations(): List<LocalTranslation> {
return runBlocking { getTranslations().first() }
}

@WorkerThread
fun getTranslationsHash(): SparseArray<LocalTranslation> {
val result = SparseArray<LocalTranslation>()
for (item in getTranslations()) {
result.put(item.id, item)
for (item in legacyGetTranslations()) {
result.put(item.id.toInt(), item)
}
return result
}

// intentional, since cachedTranslations can be replaced by another thread, causing the check
// to be true, but the cached object returned to be null (or to change).
@WorkerThread
fun getTranslations(): List<LocalTranslation> {
// intentional, since cachedTranslations can be replaced by another thread, causing the check
// to be true, but the cached object returned to be null (or to change).
val cached = cachedTranslations
if (!cached.isNullOrEmpty()) {
return cached
}
var items: MutableList<LocalTranslation> = ArrayList()
var cursor: Cursor? = null
try {
cursor = db.query(TranslationsTable.TABLE_NAME,
null, null, null, null, null,
TranslationsTable.ID + " ASC")

while (cursor.moveToNext()) {
val id = cursor.getInt(0)
val name = cursor.getString(1)
val translator = cursor.getString(2)
val translatorForeign = cursor.getString(3)
val filename = cursor.getString(4)
val url = cursor.getString(5)
val languageCode = cursor.getString(6)
val version = cursor.getInt(7)
val minimumVersion = cursor.getInt(8)
val displayOrder = cursor.getInt(9)

if (quranFileUtils.hasTranslation(context, filename)) {
items.add(
LocalTranslation(
id, filename, name, translator,
translatorForeign, url, languageCode, version, minimumVersion, displayOrder
)
)
}
}
} finally {
cursor?.close()
}
suspend fun deleteTranslationByFileName(filename: String) {
dataSource.removeTranslation(filename)
}

items = Collections.unmodifiableList(items)
if (items.size > 0) {
cachedTranslations = items
@WorkerThread
fun legacyDeleteTranslationByFileName(filename: String) {
runBlocking {
deleteTranslationByFileName(filename)
}
return items
}

fun deleteTranslationByFile(filename: String) {
db.execSQL("DELETE FROM " + TranslationsTable.TABLE_NAME + " WHERE " +
TranslationsTable.FILENAME + " = ?", arrayOf<Any>(filename))
@WorkerThread
fun legacyWriteTranslationUpdates(updates: List<TranslationItem>): Boolean {
return runBlocking { writeTranslationUpdates(updates) }
}

fun writeTranslationUpdates(updates: List<TranslationItem>): Boolean {
var result = true
db.beginTransaction()
suspend fun writeTranslationUpdates(updates: List<TranslationItem>): Boolean {
val (available, unavailable) = updates.partition { it.exists() }

try {
var cachedNextOrder = -1
for (item in updates) {
if (item.exists()) {
var displayOrder = 0
val translation = item.translation
if (item.displayOrder > -1) {
displayOrder = item.displayOrder
} else {
var cursor: Cursor? = null
if (cachedNextOrder == -1) {
try {
// get next highest display order
cursor = db.query(
TranslationsTable.TABLE_NAME, arrayOf(TranslationsTable.DISPLAY_ORDER),
null, null, null, null,
TranslationsTable.DISPLAY_ORDER + " DESC",
"1"
)
if (cursor != null && cursor.moveToFirst()) {
cachedNextOrder = cursor.getInt(0) + 1
displayOrder = cachedNextOrder++
}
} finally {
cursor?.close()
}
} else {
displayOrder = cachedNextOrder++
}
}

val values = ContentValues()
values.put(TranslationsTable.ID, translation.id)
values.put(TranslationsTable.NAME, translation.displayName)
values.put(TranslationsTable.TRANSLATOR, translation.translator)
values.put(TranslationsTable.TRANSLATOR_FOREIGN,
translation.translatorNameLocalized)
values.put(TranslationsTable.FILENAME, translation.fileName)
values.put(TranslationsTable.URL, translation.fileUrl)
values.put(TranslationsTable.LANGUAGE_CODE, translation.languageCode)
values.put(TranslationsTable.VERSION, item.localVersion)
values.put(TranslationsTable.MINIMUM_REQUIRED_VERSION, translation.minimumVersion)
values.put(TranslationsTable.DISPLAY_ORDER, displayOrder)
val needNextOrder = available.any { it.displayOrder == -1 }
val nextOrder = if (needNextOrder) {
dataSource.maximumDisplayOrder().toInt() + 1
} else {
(available.maxOfOrNull { it.displayOrder } ?: 0) + 1
}

db.replace(TranslationsTable.TABLE_NAME, null, values)
val items = if (needNextOrder) {
var nextOrderNumber = nextOrder
available.map { item ->
if (item.displayOrder == -1) {
item.copy(displayOrder = nextOrderNumber++)
} else {
db.delete(TranslationsTable.TABLE_NAME,
TranslationsTable.ID + " = " + item.translation.id, null)
item
}
}
db.setTransactionSuccessful()

lastWriteTime = System.currentTimeMillis()
// clear the cached translations
cachedTranslations = null
} catch (e: Exception) {
result = false
Timber.d(e, "error writing translation updates")
} finally {
db.endTransaction()
} else {
available
}

return result
dataSource.updateTranslations(items.map { it.asLocalTranslation() })
dataSource.removeTranslationsById(unavailable.map { it.translation.id.toLong() })

return true
}
}
Loading

0 comments on commit a593617

Please sign in to comment.