Fixed: Library language list is now retrieved dynamically from libkiwix.

* Previously, we used `Locale.getISOLanguages()` to display the language list. However, this did not include all the languages in which books are available. Now, we use the `OPDS` stream via libkiwix to fetch online books, and utilize a method in libkiwix that provides the list of available book languages. This dynamically retrieved list is now shown in the `LanguageFragment`, allowing users to select a language and download books accordingly.
* The `LanguageFragment` previously relied on `ObjectBox` for storing and managing selected languages. This has now been migrated to use `Room` database instead.
* A migration strategy has been added to move previously selected languages from ObjectBox to Room.
* Corresponding test cases for migration have been added, and existing tests have been refactored to reflect these changes.
* Since we now rely on `libkiwix` to retrieve the language list, the `ZimManageViewModelTest` initially failed due to native code dependencies (JNI) not loading in unit test environments. To address this, we refactored the code and introduced a `JNIModule` class in the app module (where `OnlineLibraryManager` resides). We now inject the `Library` object, allowing us to pass a mocked instance to `ZimManageViewModel` for proper unit testing.
This commit is contained in:
MohitMaliFtechiz 2025-07-02 01:02:33 +05:30
parent 85f11a71cc
commit e0849fa183
20 changed files with 348 additions and 61 deletions

View File

@ -5,7 +5,7 @@
<ID>EmptyFunctionBlock:None.kt$None${ }</ID> <ID>EmptyFunctionBlock:None.kt$None${ }</ID>
<ID>EmptyFunctionBlock:SimplePageChangeListener.kt$SimplePageChangeListener${ }</ID> <ID>EmptyFunctionBlock:SimplePageChangeListener.kt$SimplePageChangeListener${ }</ID>
<ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( booksOnFileSystem: List&lt;BookOnDisk>, activeDownloads: List&lt;DownloadModel>, allLanguages: List&lt;Language>, libraryNetworkEntity: LibraryNetworkEntity, filter: String, fileSystemState: FileSystemState )</ID> <ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( booksOnFileSystem: List&lt;BookOnDisk>, activeDownloads: List&lt;DownloadModel>, allLanguages: List&lt;Language>, libraryNetworkEntity: LibraryNetworkEntity, filter: String, fileSystemState: FileSystemState )</ID>
<ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( private val downloadDao: DownloadRoomDao, private val bookDao: NewBookDao, private val languageDao: NewLanguagesDao, private val storageObserver: StorageObserver, private var kiwixService: KiwixService, val context: Application, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val bookUtils: BookUtils, private val fat32Checker: Fat32Checker, private val defaultLanguageProvider: DefaultLanguageProvider, private val dataSource: DataSource, private val connectivityManager: ConnectivityManager, private val sharedPreferenceUtil: SharedPreferenceUtil )</ID> <ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( private val downloadDao: DownloadRoomDao, private val bookDao: NewBookDao, private val languageDao: NewLanguagesDao, private val storageObserver: StorageObserver, private var kiwixService: KiwixService, val context: Application, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val bookUtils: BookUtils, private val fat32Checker: Fat32Checker, private val defaultLanguageProvider: DefaultLanguageProvider, private val dataSource: DataSource, private val connectivityManager: ConnectivityManager, private val sharedPreferenceUtil: SharedPreferenceUtil, private val onlineLibraryManager: OnlineLibraryManager )</ID>
<ID>MagicNumber:LibraryListItem.kt$LibraryListItem.LibraryDownloadItem$1000L</ID> <ID>MagicNumber:LibraryListItem.kt$LibraryListItem.LibraryDownloadItem$1000L</ID>
<ID>MagicNumber:PeerGroupHandshake.kt$PeerGroupHandshake$15000</ID> <ID>MagicNumber:PeerGroupHandshake.kt$PeerGroupHandshake$15000</ID>
<ID>MagicNumber:ShareFiles.kt$ShareFiles$24</ID> <ID>MagicNumber:ShareFiles.kt$ShareFiles$24</ID>

View File

@ -35,6 +35,7 @@ import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsEqual.equalTo
import org.junit.After import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -44,6 +45,7 @@ import org.junit.runner.RunWith
import org.kiwix.kiwixmobile.KiwixRoomDatabaseTest.Companion.getHistoryItem import org.kiwix.kiwixmobile.KiwixRoomDatabaseTest.Companion.getHistoryItem
import org.kiwix.kiwixmobile.KiwixRoomDatabaseTest.Companion.getNoteListItem import org.kiwix.kiwixmobile.KiwixRoomDatabaseTest.Companion.getNoteListItem
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.LanguageEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
import org.kiwix.kiwixmobile.core.data.KiwixRoomDatabase import org.kiwix.kiwixmobile.core.data.KiwixRoomDatabase
@ -52,6 +54,7 @@ import org.kiwix.kiwixmobile.core.di.modules.DatabaseModule
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
@ -212,10 +215,13 @@ class ObjectBoxToRoomMigratorTest {
private suspend fun <T> clearRoomAndBoxStoreDatabases(box: Box<T>) { private suspend fun <T> clearRoomAndBoxStoreDatabases(box: Box<T>) {
// delete history for testing other edge cases // delete history for testing other edge cases
kiwixRoomDatabase.recentSearchRoomDao().deleteSearchHistory() with(kiwixRoomDatabase) {
kiwixRoomDatabase.historyRoomDao().deleteAllHistory() recentSearchRoomDao().deleteSearchHistory()
kiwixRoomDatabase.notesRoomDao() historyRoomDao().deleteAllHistory()
.deletePages(kiwixRoomDatabase.notesRoomDao().notes().first()) notesRoomDao()
.deletePages(kiwixRoomDatabase.notesRoomDao().notes().first())
languageRoomDao().deleteAllLanguages()
}
box.removeAll() box.removeAll()
} }
@ -470,4 +476,53 @@ class ObjectBoxToRoomMigratorTest {
migrationTime < migrationMaxTime migrationTime < migrationMaxTime
) )
} }
@Test
fun migrateLanguages_shouldInsertDataIntoRoomDatabase() =
runBlocking {
val box = boxStore.boxFor(LanguageEntity::class.java)
// clear both databases for history to test more edge cases
clearRoomAndBoxStoreDatabases(box)
val language = Language("eng", true, 10)
val language1 = Language("fr", false, 1)
// insert into object box
box.put(LanguageEntity(language))
// migrate data into room database
objectBoxToRoomMigrator.migrateLanguages(box)
// check if data successfully migrated to room
var actualDataAfterMigration = kiwixRoomDatabase.languageRoomDao().languages().first()
assertEquals(actualDataAfterMigration.size, 1)
assertEquals(actualDataAfterMigration[0].language, language.language)
assertEquals(actualDataAfterMigration[0].occurencesOfLanguage, language.occurencesOfLanguage)
assertEquals(actualDataAfterMigration[0].active, language.active)
// clear both databases to test more edge cases
clearRoomAndBoxStoreDatabases(box)
// Migrate data from empty ObjectBox database
objectBoxToRoomMigrator.migrateLanguages(box)
val actualData = kiwixRoomDatabase.languageRoomDao().languages().first()
assertTrue(actualData.isEmpty())
// Test if data successfully migrated to Room and existing data is deleted.
kiwixRoomDatabase.languageRoomDao().insert(listOf(language))
box.put(LanguageEntity(language1))
objectBoxToRoomMigrator.migrateLanguages(box)
actualDataAfterMigration = kiwixRoomDatabase.languageRoomDao().languages().first()
assertEquals(1, actualDataAfterMigration.size)
val existingItem =
actualDataAfterMigration.find {
it.active == language.active && it.language == language.language
}
Assert.assertNull(existingItem)
val newItem =
actualDataAfterMigration.find {
it.active == language1.active && it.language == language1.language
}
assertNotNull(newItem)
clearRoomAndBoxStoreDatabases(box)
}
} }

View File

@ -22,17 +22,20 @@ import dagger.Component
import org.kiwix.kiwixmobile.core.di.components.CoreComponent import org.kiwix.kiwixmobile.core.di.components.CoreComponent
import org.kiwix.kiwixmobile.di.KiwixScope import org.kiwix.kiwixmobile.di.KiwixScope
import org.kiwix.kiwixmobile.di.components.ServiceComponent.Builder import org.kiwix.kiwixmobile.di.components.ServiceComponent.Builder
import org.kiwix.kiwixmobile.di.modules.JNIModule
import org.kiwix.kiwixmobile.di.modules.KiwixModule import org.kiwix.kiwixmobile.di.modules.KiwixModule
import org.kiwix.kiwixmobile.di.modules.KiwixViewModelModule import org.kiwix.kiwixmobile.di.modules.KiwixViewModelModule
import org.kiwix.kiwixmobile.storage.StorageSelectDialog import org.kiwix.kiwixmobile.storage.StorageSelectDialog
import org.kiwix.kiwixmobile.zimManager.OnlineLibraryManager
@KiwixScope @KiwixScope
@Component( @Component(
dependencies = [CoreComponent::class], dependencies = [CoreComponent::class],
modules = [KiwixViewModelModule::class, KiwixModule::class] modules = [KiwixViewModelModule::class, KiwixModule::class, JNIModule::class]
) )
interface KiwixComponent { interface KiwixComponent {
fun activityComponentBuilder(): KiwixActivityComponent.Builder fun activityComponentBuilder(): KiwixActivityComponent.Builder
fun serviceComponent(): Builder fun serviceComponent(): Builder
fun inject(storageSelectDialog: StorageSelectDialog) fun inject(storageSelectDialog: StorageSelectDialog)
fun providesOnlineLibraryManager(): OnlineLibraryManager
} }

View File

@ -0,0 +1,52 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.di.modules
import dagger.Module
import dagger.Provides
import org.kiwix.kiwixmobile.di.KiwixScope
import org.kiwix.kiwixmobile.zimManager.OnlineLibraryManager
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
import javax.inject.Named
@Module
class JNIModule {
@Provides
@Named(ONLINE_BOOKS_LIBRARY)
@KiwixScope
fun provideOnlineBooksLibrary(): Library = Library()
@Provides
@Named(ONLINE_BOOKS_MANAGER)
@KiwixScope
fun providesOnlineBooksManager(
@Named(ONLINE_BOOKS_LIBRARY) library: Library
): Manager = Manager(library)
@Provides
@KiwixScope
fun provideOnlineLibraryManager(
@Named(ONLINE_BOOKS_LIBRARY) library: Library,
@Named(ONLINE_BOOKS_MANAGER) manager: Manager,
) = OnlineLibraryManager(library, manager)
}
const val ONLINE_BOOKS_LIBRARY = "onlineBookLibrary"
const val ONLINE_BOOKS_MANAGER = "onlineBookManager"

View File

@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll
@ -41,7 +41,7 @@ import org.kiwix.kiwixmobile.language.viewmodel.State.Saving
import javax.inject.Inject import javax.inject.Inject
class LanguageViewModel @Inject constructor( class LanguageViewModel @Inject constructor(
private val languageDao: NewLanguagesDao private val languageRoomDao: LanguageRoomDao
) : ViewModel() { ) : ViewModel() {
val state = MutableStateFlow<State>(Loading) val state = MutableStateFlow<State>(Loading)
val actions = MutableSharedFlow<Action>(extraBufferCapacity = Int.MAX_VALUE) val actions = MutableSharedFlow<Action>(extraBufferCapacity = Int.MAX_VALUE)
@ -63,7 +63,7 @@ class LanguageViewModel @Inject constructor(
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun observeLanguages() = private fun observeLanguages() =
languageDao.languages() languageRoomDao.languages()
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) } .onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
@ -112,7 +112,7 @@ class LanguageViewModel @Inject constructor(
effects.tryEmit( effects.tryEmit(
SaveLanguagesAndFinish( SaveLanguagesAndFinish(
currentState.items, currentState.items,
languageDao, languageRoomDao,
viewModelScope viewModelScope
) )
) )

View File

@ -23,20 +23,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
@Suppress("InjectDispatcher") @Suppress("InjectDispatcher")
data class SaveLanguagesAndFinish( data class SaveLanguagesAndFinish(
private val languages: List<Language>, private val languages: List<Language>,
private val languageDao: NewLanguagesDao, private val languageRoomDao: LanguageRoomDao,
private val lifecycleScope: CoroutineScope private val lifecycleScope: CoroutineScope
) : SideEffect<Unit> { ) : SideEffect<Unit> {
override fun invokeWith(activity: AppCompatActivity) { override fun invokeWith(activity: AppCompatActivity) {
lifecycleScope.launch { lifecycleScope.launch {
runCatching { runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
languageDao.insert(languages) languageRoomDao.insert(languages)
} }
activity.onBackPressedDispatcher.onBackPressed() activity.onBackPressedDispatcher.onBackPressed()
}.onFailure { }.onFailure {

View File

@ -19,10 +19,17 @@
package org.kiwix.kiwixmobile.zimManager package org.kiwix.kiwixmobile.zimManager
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.di.modules.ONLINE_BOOKS_LIBRARY
import org.kiwix.kiwixmobile.di.modules.ONLINE_BOOKS_MANAGER
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
import javax.inject.Inject
import javax.inject.Named
class OnlineLibraryManager(private val library: Library, private val manager: Manager) { class OnlineLibraryManager @Inject constructor(
@Named(ONLINE_BOOKS_LIBRARY) private val library: Library,
@Named(ONLINE_BOOKS_MANAGER) private val manager: Manager,
) {
suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean = suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean =
runCatching { runCatching {
manager.readOpds(content, urlHost) manager.readOpds(content, urlHost)
@ -40,4 +47,12 @@ class OnlineLibraryManager(private val library: Library, private val manager: Ma
}.onFailure { it.printStackTrace() } }.onFailure { it.printStackTrace() }
return onlineBooksList return onlineBooksList
} }
suspend fun getOnlineBooksLanguage(): List<String> {
return runCatching {
library.booksLanguages.distinct()
}.onFailure {
it.printStackTrace()
}.getOrDefault(emptyList())
}
} }

View File

@ -66,8 +66,8 @@ import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.isWifi import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.isWifi
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_NETWORK_PATH import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_NETWORK_PATH
@ -89,6 +89,7 @@ import org.kiwix.kiwixmobile.core.ui.components.ONE
import org.kiwix.kiwixmobile.core.ui.components.TWO import org.kiwix.kiwixmobile.core.ui.components.TWO
import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
@ -121,8 +122,6 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.Book
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem
import org.kiwix.libkiwix.Book import org.kiwix.libkiwix.Book
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject import javax.inject.Inject
@ -137,7 +136,7 @@ const val FOUR = 4
class ZimManageViewModel @Inject constructor( class ZimManageViewModel @Inject constructor(
private val downloadDao: DownloadRoomDao, private val downloadDao: DownloadRoomDao,
private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookOnDisk: LibkiwixBookOnDisk,
private val languageDao: NewLanguagesDao, private val languageRoomDao: LanguageRoomDao,
private val storageObserver: StorageObserver, private val storageObserver: StorageObserver,
private var kiwixService: KiwixService, private var kiwixService: KiwixService,
val context: Application, val context: Application,
@ -148,6 +147,7 @@ class ZimManageViewModel @Inject constructor(
private val dataSource: DataSource, private val dataSource: DataSource,
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val sharedPreferenceUtil: SharedPreferenceUtil, private val sharedPreferenceUtil: SharedPreferenceUtil,
private val onlineLibraryManager: OnlineLibraryManager
) : ViewModel() { ) : ViewModel() {
sealed class FileSelectActions { sealed class FileSelectActions {
data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions() data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions()
@ -160,12 +160,6 @@ class ZimManageViewModel @Inject constructor(
object UserClickedDownloadBooksButton : FileSelectActions() object UserClickedDownloadBooksButton : FileSelectActions()
} }
private val library by lazy { Library() }
private val manager by lazy { Manager(library) }
private val onlineLibraryManager by lazy {
OnlineLibraryManager(library, manager)
}
private var isUnitTestCase: Boolean = false private var isUnitTestCase: Boolean = false
val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow() val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow()
private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList()) private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList())
@ -294,7 +288,7 @@ class ZimManageViewModel @Inject constructor(
private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) { private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
val downloads = downloadDao.downloads() val downloads = downloadDao.downloads()
val booksFromDao = books() val booksFromDao = books()
val languages = languageDao.languages() val languages = languageRoomDao.languages()
coroutineJobs.apply { coroutineJobs.apply {
add(scanBooksFromStorage(dispatcher)) add(scanBooksFromStorage(dispatcher))
add(updateBookItems()) add(updateBookItems())
@ -541,11 +535,11 @@ class ZimManageViewModel @Inject constructor(
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.distinctUntilChanged() .distinctUntilChanged()
.catch { it.printStackTrace() } .catch { it.printStackTrace() }
.onEach { languageDao.insert(it) } .onEach { languageRoomDao.insert(it) }
.flowOn(dispatcher) .flowOn(dispatcher)
.launchIn(viewModelScope) .launchIn(viewModelScope)
private fun combineToLanguageList( private suspend fun combineToLanguageList(
booksFromNetwork: List<LibkiwixBook>, booksFromNetwork: List<LibkiwixBook>,
allLanguages: List<Language> allLanguages: List<Language>
) = when { ) = when {
@ -579,19 +573,28 @@ class ZimManageViewModel @Inject constructor(
private fun <K> MutableMap<K, Int>.increment(key: K) = private fun <K> MutableMap<K, Int>.increment(key: K) =
apply { set(key, getOrElse(key) { 0 } + 1) } apply { set(key, getOrElse(key) { 0 } + 1) }
private fun fromLocalesWithNetworkMatchesSetActiveBy( private suspend fun fromLocalesWithNetworkMatchesSetActiveBy(
networkLanguageCounts: MutableMap<String, Int>, networkLanguageCounts: MutableMap<String, Int>,
listToActivateBy: List<Language> listToActivateBy: List<Language>
) = Locale.getISOLanguages() ) = onlineLibraryManager.getOnlineBooksLanguage()
.map { it.convertToLocal() } .mapNotNull { code ->
.filter { networkLanguageCounts.containsKey(it.isO3Language) } runCatching {
.map { locale -> val locale = code.convertToLocal()
Language( val o3 = locale.isO3Language
locale.isO3Language, if (networkLanguageCounts.containsKey(o3)) {
languageIsActive(listToActivateBy, locale), Language(
networkLanguageCounts.getOrElse(locale.isO3Language) { 0 } o3,
) languageIsActive(listToActivateBy, locale),
networkLanguageCounts.getOrElse(o3) { 0 }
)
} else {
null
}
}.onFailure {
Log.w(TAG_KIWIX, "Unsupported locale code: $code", it)
}.getOrNull()
} }
.distinctBy { it.language }
private fun defaultLanguage() = private fun defaultLanguage() =
listOf( listOf(

View File

@ -28,7 +28,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.language.composables.LanguageListItem import org.kiwix.kiwixmobile.language.composables.LanguageListItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
@ -46,7 +46,7 @@ fun languageItem(language: Language = language()) =
@ExtendWith(InstantExecutorExtension::class) @ExtendWith(InstantExecutorExtension::class)
class LanguageViewModelTest { class LanguageViewModelTest {
private val newLanguagesDao: NewLanguagesDao = mockk() private val languageRoomDao: LanguageRoomDao = mockk()
private lateinit var languageViewModel: LanguageViewModel private lateinit var languageViewModel: LanguageViewModel
private lateinit var languages: MutableStateFlow<List<Language>> private lateinit var languages: MutableStateFlow<List<Language>>
@ -54,9 +54,9 @@ class LanguageViewModelTest {
fun init() { fun init() {
clearAllMocks() clearAllMocks()
languages = MutableStateFlow(emptyList()) languages = MutableStateFlow(emptyList())
every { newLanguagesDao.languages() } returns languages every { languageRoomDao.languages() } returns languages
languageViewModel = languageViewModel =
LanguageViewModel(newLanguagesDao) LanguageViewModel(languageRoomDao)
} }
@Test @Test
@ -183,7 +183,7 @@ class LanguageViewModelTest {
assertThat(awaitItem()).isEqualTo( assertThat(awaitItem()).isEqualTo(
SaveLanguagesAndFinish( SaveLanguagesAndFinish(
languages, languages,
newLanguagesDao, languageRoomDao,
languageViewModel.viewModelScope languageViewModel.viewModelScope
) )
) )

View File

@ -27,13 +27,13 @@ import io.mockk.verify
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
class SaveLanguagesAndFinishTest { class SaveLanguagesAndFinishTest {
@Test @Test
fun `invoke saves and finishes`() = runTest { fun `invoke saves and finishes`() = runTest {
val languageDao = mockk<NewLanguagesDao>() val languageDao = mockk<LanguageRoomDao>()
val activity = mockk<AppCompatActivity>() val activity = mockk<AppCompatActivity>()
val lifeCycleScope = TestScope(testScheduler) val lifeCycleScope = TestScope(testScheduler)
val onBackPressedDispatcher = mockk<OnBackPressedDispatcher>() val onBackPressedDispatcher = mockk<OnBackPressedDispatcher>()

View File

@ -28,6 +28,7 @@ import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test import app.cash.turbine.test
import com.jraska.livedata.test import com.jraska.livedata.test
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -53,8 +54,8 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.StorageObserver import org.kiwix.kiwixmobile.core.StorageObserver
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
@ -103,7 +104,7 @@ import kotlin.time.toDuration
class ZimManageViewModelTest { class ZimManageViewModelTest {
private val downloadRoomDao: DownloadRoomDao = mockk() private val downloadRoomDao: DownloadRoomDao = mockk()
private val libkiwixBookOnDisk: LibkiwixBookOnDisk = mockk() private val libkiwixBookOnDisk: LibkiwixBookOnDisk = mockk()
private val newLanguagesDao: NewLanguagesDao = mockk() private val languageRoomDao: LanguageRoomDao = mockk()
private val storageObserver: StorageObserver = mockk() private val storageObserver: StorageObserver = mockk()
private val kiwixService: KiwixService = mockk() private val kiwixService: KiwixService = mockk()
private val application: Application = mockk() private val application: Application = mockk()
@ -129,6 +130,7 @@ class ZimManageViewModelTest {
private val networkStates = MutableStateFlow(NetworkState.NOT_CONNECTED) private val networkStates = MutableStateFlow(NetworkState.NOT_CONNECTED)
private val booksOnDiskListItems = MutableStateFlow<List<BooksOnDiskListItem>>(emptyList()) private val booksOnDiskListItems = MutableStateFlow<List<BooksOnDiskListItem>>(emptyList())
private val testDispatcher = StandardTestDispatcher() private val testDispatcher = StandardTestDispatcher()
private val onlineLibraryManager = mockk<OnlineLibraryManager>()
@AfterAll @AfterAll
fun teardown() { fun teardown() {
@ -150,7 +152,7 @@ class ZimManageViewModelTest {
any<ScanningProgressListener>() any<ScanningProgressListener>()
) )
} returns booksOnFileSystem } returns booksOnFileSystem
every { newLanguagesDao.languages() } returns languages every { languageRoomDao.languages() } returns languages
every { fat32Checker.fileSystemStates } returns fileSystemStates every { fat32Checker.fileSystemStates } returns fileSystemStates
every { connectivityBroadcastReceiver.networkStates } returns networkStates every { connectivityBroadcastReceiver.networkStates } returns networkStates
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -176,7 +178,7 @@ class ZimManageViewModelTest {
ZimManageViewModel( ZimManageViewModel(
downloadRoomDao, downloadRoomDao,
libkiwixBookOnDisk, libkiwixBookOnDisk,
newLanguagesDao, languageRoomDao,
storageObserver, storageObserver,
kiwixService, kiwixService,
application, application,
@ -186,7 +188,8 @@ class ZimManageViewModelTest {
defaultLanguageProvider, defaultLanguageProvider,
dataSource, dataSource,
connectivityManager, connectivityManager,
sharedPreferenceUtil sharedPreferenceUtil,
onlineLibraryManager
).apply { ).apply {
setIsUnitTestCase() setIsUnitTestCase()
setAlertDialogShower(alertDialogShower) setAlertDialogShower(alertDialogShower)
@ -279,7 +282,7 @@ class ZimManageViewModelTest {
expectedLanguage expectedLanguage
) )
advanceUntilIdle() advanceUntilIdle()
verify { newLanguagesDao.insert(listOf(expectedLanguage)) } verify { languageRoomDao.insert(listOf(expectedLanguage)) }
} }
@Test @Test
@ -298,7 +301,7 @@ class ZimManageViewModelTest {
), ),
language(isActive = true, occurencesOfLanguage = 1) language(isActive = true, occurencesOfLanguage = 1)
) )
verify { newLanguagesDao.insert(any()) } verify { languageRoomDao.insert(any()) }
} }
@Test @Test
@ -323,7 +326,7 @@ class ZimManageViewModelTest {
defaultLanguage defaultLanguage
) )
verify { verify {
newLanguagesDao.insert( languageRoomDao.insert(
listOf( listOf(
defaultLanguage.copy(occurencesOfLanguage = 2), defaultLanguage.copy(occurencesOfLanguage = 2),
Language( Language(
@ -362,7 +365,7 @@ class ZimManageViewModelTest {
) )
advanceUntilIdle() advanceUntilIdle()
verify(timeout = MOCKK_TIMEOUT_FOR_VERIFICATION) { verify(timeout = MOCKK_TIMEOUT_FOR_VERIFICATION) {
newLanguagesDao.insert( languageRoomDao.insert(
listOf( listOf(
dbLanguage.copy(occurencesOfLanguage = 2), dbLanguage.copy(occurencesOfLanguage = 2),
Language( Language(
@ -386,6 +389,7 @@ class ZimManageViewModelTest {
every { application.getString(any()) } returns "" every { application.getString(any()) } returns ""
every { application.getString(any(), any()) } returns "" every { application.getString(any(), any()) } returns ""
every { defaultLanguageProvider.provide() } returns defaultLanguage every { defaultLanguageProvider.provide() } returns defaultLanguage
coEvery { onlineLibraryManager.getOnlineBooksLanguage() } returns networkBooks.map { it.language }
viewModel.networkLibrary.emit(networkBooks) viewModel.networkLibrary.emit(networkBooks)
advanceUntilIdle() advanceUntilIdle()
languages.value = dbBooks languages.value = dbBooks

View File

@ -12,7 +12,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID>
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID> <ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID> <ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageRoomDao: LanguageRoomDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil )</ID> <ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID> <ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID> <ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>

View File

@ -0,0 +1,50 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.entities.LanguageRoomEntity
import org.kiwix.kiwixmobile.core.zim_manager.Language
@Dao
abstract class LanguageRoomDao {
@Query("SELECT * FROM LanguageRoomEntity")
abstract fun languageAsEntity(): Flow<List<LanguageRoomEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun saveLanguages(languageRoomEntityList: List<LanguageRoomEntity>)
@Query("DELETE FROM LanguageRoomEntity")
abstract fun deleteAllLanguages()
fun languages(): Flow<List<Language>> =
languageAsEntity().map { it.map(LanguageRoomEntity::toLanguageModel) }
@Transaction
open fun insert(languages: List<Language>) {
deleteAllLanguages()
saveLanguages(languages.map(::LanguageRoomEntity))
}
}

View File

@ -0,0 +1,56 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.dao.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal
import org.kiwix.kiwixmobile.core.zim_manager.Language
import java.util.Locale
@Entity
data class LanguageRoomEntity(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
@TypeConverters(StringToLocalRoomConverter::class)
var locale: Locale = Locale.ENGLISH,
var active: Boolean = false,
var occurencesOfLanguage: Int = 0
) {
constructor(language: Language) : this(
0,
language.languageCode.convertToLocal(),
language.active,
language.occurencesOfLanguage
)
fun toLanguageModel() =
Language(locale, active, occurencesOfLanguage, id)
}
class StringToLocalRoomConverter {
@TypeConverter
fun convertToDatabaseValue(entityProperty: Locale?): String =
entityProperty?.isO3Language ?: Locale.ENGLISH.isO3Language
@TypeConverter
fun convertToEntityProperty(databaseValue: String?): Locale =
databaseValue?.convertToLocal() ?: Locale.ENGLISH
}

View File

@ -28,14 +28,17 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDaoCoverts import org.kiwix.kiwixmobile.core.dao.HistoryRoomDaoCoverts
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.entities.BundleRoomConverter import org.kiwix.kiwixmobile.core.dao.entities.BundleRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.LanguageRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.StringToLocalRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
@ -46,15 +49,17 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
HistoryRoomEntity::class, HistoryRoomEntity::class,
NotesRoomEntity::class, NotesRoomEntity::class,
DownloadRoomEntity::class, DownloadRoomEntity::class,
WebViewHistoryEntity::class WebViewHistoryEntity::class,
LanguageRoomEntity::class
], ],
version = 8, version = 9,
exportSchema = false exportSchema = false
) )
@TypeConverters( @TypeConverters(
HistoryRoomDaoCoverts::class, HistoryRoomDaoCoverts::class,
ZimSourceRoomConverter::class, ZimSourceRoomConverter::class,
BundleRoomConverter::class BundleRoomConverter::class,
StringToLocalRoomConverter::class
) )
abstract class KiwixRoomDatabase : RoomDatabase() { abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun recentSearchRoomDao(): RecentSearchRoomDao abstract fun recentSearchRoomDao(): RecentSearchRoomDao
@ -62,6 +67,7 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun notesRoomDao(): NotesRoomDao abstract fun notesRoomDao(): NotesRoomDao
abstract fun downloadRoomDao(): DownloadRoomDao abstract fun downloadRoomDao(): DownloadRoomDao
abstract fun webViewHistoryRoomDao(): WebViewHistoryRoomDao abstract fun webViewHistoryRoomDao(): WebViewHistoryRoomDao
abstract fun languageRoomDao(): LanguageRoomDao
companion object { companion object {
private var db: KiwixRoomDatabase? = null private var db: KiwixRoomDatabase? = null
@ -78,7 +84,8 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
MIGRATION_4_5, MIGRATION_4_5,
MIGRATION_5_6, MIGRATION_5_6,
MIGRATION_6_7, MIGRATION_6_7,
MIGRATION_7_8 MIGRATION_7_8,
MIGRATION_8_9
) )
.build().also { db = it } .build().also { db = it }
} }
@ -305,6 +312,23 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
} }
} }
@Suppress("MagicNumber")
private val MIGRATION_8_9 =
object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `LanguageRoomEntity` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`locale` TEXT NOT NULL DEFAULT 'eng',
`active` INTEGER NOT NULL DEFAULT 0,
`occurencesOfLanguage` INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
}
}
fun destroyInstance() { fun destroyInstance() {
db = null db = null
} }

View File

@ -24,9 +24,9 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
@ -56,7 +56,7 @@ class Repository @Inject internal constructor(
private val historyRoomDao: HistoryRoomDao, private val historyRoomDao: HistoryRoomDao,
private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao,
private val notesRoomDao: NotesRoomDao, private val notesRoomDao: NotesRoomDao,
private val languageDao: NewLanguagesDao, private val languageRoomDao: LanguageRoomDao,
private val recentSearchRoomDao: RecentSearchRoomDao, private val recentSearchRoomDao: RecentSearchRoomDao,
private val zimReaderContainer: ZimReaderContainer private val zimReaderContainer: ZimReaderContainer
) : DataSource { ) : DataSource {
@ -102,7 +102,7 @@ class Repository @Inject internal constructor(
@Suppress("InjectDispatcher") @Suppress("InjectDispatcher")
override suspend fun saveLanguages(languages: List<Language>) = override suspend fun saveLanguages(languages: List<Language>) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
languageDao.insert(languages) languageRoomDao.insert(languages)
} }
@Suppress("InjectDispatcher") @Suppress("InjectDispatcher")

View File

@ -23,6 +23,7 @@ import io.objectbox.BoxStore
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.LanguageEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
import org.kiwix.kiwixmobile.core.data.KiwixRoomDatabase import org.kiwix.kiwixmobile.core.data.KiwixRoomDatabase
@ -49,6 +50,9 @@ class ObjectBoxToRoomMigrator {
if (!sharedPreferenceUtil.prefIsNotesMigrated) { if (!sharedPreferenceUtil.prefIsNotesMigrated) {
migrateNotes(boxStore.boxFor()) migrateNotes(boxStore.boxFor())
} }
if (!sharedPreferenceUtil.prefLanguageListMigrated) {
migrateLanguages(boxStore.boxFor())
}
// TODO we will migrate here for other entities // TODO we will migrate here for other entities
} }
@ -87,4 +91,12 @@ class ObjectBoxToRoomMigrator {
} }
sharedPreferenceUtil.putPrefNotesMigrated(true) sharedPreferenceUtil.putPrefNotesMigrated(true)
} }
suspend fun migrateLanguages(box: Box<LanguageEntity>) {
kiwixRoomDatabase.languageRoomDao()
.insert(
box.all.map { it.toLanguageModel() }
)
sharedPreferenceUtil.putPrefLanguageListMigrated(true)
}
} }

View File

@ -30,6 +30,7 @@ import org.kiwix.kiwixmobile.core.StorageObserver
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryDao import org.kiwix.kiwixmobile.core.dao.HistoryDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
@ -107,6 +108,7 @@ interface CoreComponent {
fun historyRoomDao(): HistoryRoomDao fun historyRoomDao(): HistoryRoomDao
fun webViewHistoryRoomDao(): WebViewHistoryRoomDao fun webViewHistoryRoomDao(): WebViewHistoryRoomDao
fun noteRoomDao(): NotesRoomDao fun noteRoomDao(): NotesRoomDao
fun languageRoomDao(): LanguageRoomDao
fun objectBoxToRoomMigrator(): ObjectBoxToRoomMigrator fun objectBoxToRoomMigrator(): ObjectBoxToRoomMigrator
fun context(): Context fun context(): Context
fun downloader(): Downloader fun downloader(): Downloader

View File

@ -100,4 +100,8 @@ open class DatabaseModule {
db.downloadRoomDao().also { db.downloadRoomDao().also {
it.libkiwixBookOnDisk = libkiwixBookOnDisk it.libkiwixBookOnDisk = libkiwixBookOnDisk
} }
@Singleton
@Provides
fun provideLanguageRoomDao(db: KiwixRoomDatabase) = db.languageRoomDao()
} }

View File

@ -117,6 +117,9 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
val prefIsBookOnDiskMigrated: Boolean val prefIsBookOnDiskMigrated: Boolean
get() = sharedPreferences.getBoolean(PREF_BOOK_ON_DISK_MIGRATED, false) get() = sharedPreferences.getBoolean(PREF_BOOK_ON_DISK_MIGRATED, false)
val prefLanguageListMigrated: Boolean
get() = sharedPreferences.getBoolean(PREF_LANGUAGE_LIST_MIGRATED, false)
val prefStorage: String val prefStorage: String
get() { get() {
val storage = sharedPreferences.getString(PREF_STORAGE, null) val storage = sharedPreferences.getString(PREF_STORAGE, null)
@ -169,6 +172,9 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
fun putPrefBookOnDiskMigrated(isMigrated: Boolean) = fun putPrefBookOnDiskMigrated(isMigrated: Boolean) =
sharedPreferences.edit { putBoolean(PREF_BOOK_ON_DISK_MIGRATED, isMigrated) } sharedPreferences.edit { putBoolean(PREF_BOOK_ON_DISK_MIGRATED, isMigrated) }
fun putPrefLanguageListMigrated(isMigrated: Boolean) =
sharedPreferences.edit { putBoolean(PREF_LANGUAGE_LIST_MIGRATED, isMigrated) }
fun putPrefLanguage(language: String) = fun putPrefLanguage(language: String) =
sharedPreferences.edit { putString(PREF_LANG, language) } sharedPreferences.edit { putString(PREF_LANG, language) }
@ -352,6 +358,7 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
const val PREF_NOTES_MIGRATED = "pref_notes_migrated" const val PREF_NOTES_MIGRATED = "pref_notes_migrated"
const val PREF_APP_DIRECTORY_TO_PUBLIC_MIGRATED = "pref_app_directory_to_public_migrated" const val PREF_APP_DIRECTORY_TO_PUBLIC_MIGRATED = "pref_app_directory_to_public_migrated"
const val PREF_BOOK_ON_DISK_MIGRATED = "pref_book_on_disk_migrated" const val PREF_BOOK_ON_DISK_MIGRATED = "pref_book_on_disk_migrated"
const val PREF_LANGUAGE_LIST_MIGRATED = "pref_language_list_migrated"
const val PREF_SHOW_COPY_MOVE_STORAGE_SELECTION_DIALOG = "pref_show_copy_move_storage_dialog" const val PREF_SHOW_COPY_MOVE_STORAGE_SELECTION_DIALOG = "pref_show_copy_move_storage_dialog"
private const val PREF_LATER_CLICKED_MILLIS = "pref_later_clicked_millis" private const val PREF_LATER_CLICKED_MILLIS = "pref_later_clicked_millis"
const val PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS = const val PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS =