Refactored the LanguageViewModel to fetch the language list from an online source.

This commit is contained in:
MohitMaliFtechiz 2025-07-05 00:01:10 +05:30
parent 8b02f96c1c
commit 014ef2bcef
23 changed files with 242 additions and 219 deletions

View File

@ -29,6 +29,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -37,6 +38,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.CollectSideEffectWithActivity
@ -45,11 +49,13 @@ import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.language.composables.LanguageList
import org.kiwix.kiwixmobile.language.viewmodel.Action
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
import org.kiwix.kiwixmobile.nav.destination.library.online.NO_CONTENT_VIEW_TEXT_TESTING_TAG
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("ComposableLambdaParameterNaming")
@ -115,11 +121,29 @@ fun LanguageScreen(
}
)
}
is State.Error -> ShowErrorMessage((state as State.Error).errorMessage)
}
}
}
}
@Composable
private fun ShowErrorMessage(errorMessage: String) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = errorMessage,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = FOUR_DP)
.semantics { testTag = NO_CONTENT_VIEW_TEXT_TESTING_TAG }
)
}
}
@Composable
fun LoadingScreen() {
Box(

View File

@ -25,5 +25,6 @@ sealed class Action {
data class UpdateLanguages(val languages: List<Language>) : Action()
data class Filter(val filter: String) : Action()
data class Select(val language: LanguageItem) : Action()
data class Error(val errorMessage: String) : Action()
object SaveAll : Action()
}

View File

@ -18,19 +18,40 @@
package org.kiwix.kiwixmobile.language.viewmodel
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC
import okhttp3.logging.HttpLoggingInterceptor.Level.NONE
import org.kiwix.kiwixmobile.core.BuildConfig
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor
import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.KIWIX_LANGUAGE_URL
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
import org.kiwix.kiwixmobile.core.extensions.registerReceiver
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Error
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll
import org.kiwix.kiwixmobile.language.viewmodel.Action.Select
@ -38,10 +59,14 @@ import org.kiwix.kiwixmobile.language.viewmodel.Action.UpdateLanguages
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
import org.kiwix.kiwixmobile.language.viewmodel.State.Loading
import org.kiwix.kiwixmobile.language.viewmodel.State.Saving
import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject
class LanguageViewModel @Inject constructor(
private val languageRoomDao: LanguageRoomDao
private val context: Application,
private val sharedPreferenceUtil: SharedPreferenceUtil,
private var kiwixService: KiwixService,
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver
) : ViewModel() {
val state = MutableStateFlow<State>(Loading)
val actions = MutableSharedFlow<Action>(extraBufferCapacity = Int.MAX_VALUE)
@ -49,6 +74,7 @@ class LanguageViewModel @Inject constructor(
private val coroutineJobs = mutableListOf<Job>()
init {
context.registerReceiver(connectivityBroadcastReceiver)
coroutineJobs.apply {
add(observeActions())
add(observeLanguages())
@ -62,17 +88,64 @@ class LanguageViewModel @Inject constructor(
.onEach { newState -> state.value = newState }
.launchIn(viewModelScope)
private fun observeLanguages() =
languageRoomDao.languages()
.filter { it.isNotEmpty() }
.onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) }
.launchIn(viewModelScope)
private suspend fun fetchLanguages(): List<Language>? =
runCatching {
kiwixService =
KiwixService.ServiceCreator.newHackListService(getOkHttpClient(), KIWIX_LANGUAGE_URL)
val feed = kiwixService.getLanguages()
buildList {
// Add default item to show all language.
add(
Language(
languageCode = "",
active = sharedPreferenceUtil.selectedOnlineContentLanguage.isEmpty(),
occurrencesOfLanguage = 0,
id = 0L
)
)
// Add the rest of the fetched languages
feed.entries.orEmpty().mapIndexedNotNull { index, languageEntry ->
runCatching {
Language(
languageCode = languageEntry.languageCode,
active = sharedPreferenceUtil.selectedOnlineContentLanguage == languageEntry.languageCode,
occurrencesOfLanguage = languageEntry.count,
id = (index + 1).toLong()
)
}.onFailure {
Log.w(TAG_KIWIX, "Unsupported locale code: ${languageEntry.languageCode}", it)
}.getOrNull()
}.forEach { add(it) }
}
}.onFailure { it.printStackTrace() }.getOrNull()
private fun observeLanguages() = viewModelScope.launch {
state.value = Loading
if (connectivityBroadcastReceiver.networkStates.value == NetworkState.NOT_CONNECTED) {
actions.emit(Error(context.getString(R.string.no_network_connection)))
return@launch
}
try {
val languages = fetchLanguages()
if (languages?.isNotEmpty() == true) {
actions.emit(UpdateLanguages(languages))
} else {
Log.w("LanguageViewModel", "Fetched empty language list.")
actions.emit(Error(context.getString(R.string.no_language_available)))
}
} catch (e: Exception) {
Log.e("LanguageViewModel", "Error fetching languages", e)
actions.emit(Error(context.getString(R.string.no_language_available)))
}
}
override fun onCleared() {
coroutineJobs.forEach {
it.cancel()
}
coroutineJobs.clear()
context.unregisterReceiver(connectivityBroadcastReceiver)
super.onCleared()
}
@ -81,6 +154,8 @@ class LanguageViewModel @Inject constructor(
currentState: State
): State {
return when (action) {
is Error -> State.Error(action.errorMessage)
is UpdateLanguages ->
when (currentState) {
Loading -> Content(action.languages)
@ -111,8 +186,8 @@ class LanguageViewModel @Inject constructor(
private fun saveAll(currentState: Content): State {
effects.tryEmit(
SaveLanguagesAndFinish(
currentState.items,
languageRoomDao,
currentState.items.first(),
sharedPreferenceUtil,
viewModelScope
)
)
@ -128,4 +203,18 @@ class LanguageViewModel @Inject constructor(
filter: String,
currentState: Content
) = currentState.updateFilter(filter)
private fun getOkHttpClient() = OkHttpClient().newBuilder()
.followRedirects(true)
.followSslRedirects(true)
.connectTimeout(CONNECTION_TIMEOUT, SECONDS)
.readTimeout(READ_TIMEOUT, SECONDS)
.callTimeout(CALL_TIMEOUT, SECONDS)
.addNetworkInterceptor(
HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BASIC else NONE
}
)
.addNetworkInterceptor(UserAgentInterceptor(USER_AGENT))
.build()
}

View File

@ -19,25 +19,21 @@ package org.kiwix.kiwixmobile.language.viewmodel
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.Language
@Suppress("InjectDispatcher")
data class SaveLanguagesAndFinish(
private val languages: List<Language>,
private val languageRoomDao: LanguageRoomDao,
private val languages: Language,
private val sharedPreferenceUtil: SharedPreferenceUtil,
private val lifecycleScope: CoroutineScope
) : SideEffect<Unit> {
override fun invokeWith(activity: AppCompatActivity) {
lifecycleScope.launch {
runCatching {
withContext(Dispatchers.IO) {
languageRoomDao.insert(languages)
}
sharedPreferenceUtil.selectedOnlineContentLanguage = languages.languageCode
activity.onBackPressedDispatcher.onBackPressed()
}.onFailure {
it.printStackTrace()

View File

@ -24,6 +24,7 @@ import org.kiwix.kiwixmobile.language.composables.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
sealed class State {
data class Error(val errorMessage: String) : State()
object Loading : State()
object Saving : State()
data class Content(

View File

@ -68,7 +68,6 @@ import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.isWifi
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.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
@ -139,7 +138,6 @@ const val FOUR = 4
class ZimManageViewModel @Inject constructor(
private val downloadDao: DownloadRoomDao,
private val libkiwixBookOnDisk: LibkiwixBookOnDisk,
private val languageRoomDao: LanguageRoomDao,
private val storageObserver: StorageObserver,
private var kiwixService: KiwixService,
val context: Application,
@ -164,9 +162,9 @@ class ZimManageViewModel @Inject constructor(
}
data class OnlineLibraryRequest(
val query: String?,
val category: String?,
val lang: String?,
val query: String? = null,
val category: String? = null,
val lang: String? = null,
val isLoadMoreItem: Boolean,
val page: Int
)
@ -327,16 +325,17 @@ class ZimManageViewModel @Inject constructor(
private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
val downloads = downloadDao.downloads()
val booksFromDao = books()
val languages = languageRoomDao.languages()
val selectedLanguage = sharedPreferenceUtil.onlineContentLanguage
coroutineJobs.apply {
add(scanBooksFromStorage(dispatcher))
add(updateBookItems())
add(fileSelectActions())
add(updateLibraryItems(booksFromDao, downloads, networkLibrary, languages))
add(updateLanguagesInDao(networkLibrary, languages))
add(updateLibraryItems(booksFromDao, downloads, networkLibrary, selectedLanguage))
// add(updateLanguagesInDao(networkLibrary, selectedLanguage))
add(updateNetworkStates())
add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary))
add(onlineLibraryRequest())
add(observeLanguageChanges())
}
}
@ -350,6 +349,16 @@ class ZimManageViewModel @Inject constructor(
super.onCleared()
}
private fun observeLanguageChanges(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
sharedPreferenceUtil.onlineContentLanguage
.onEach {
updateOnlineLibraryFilters(
OnlineLibraryRequest(lang = it, page = ZERO, isLoadMoreItem = false)
)
}
.flowOn(dispatcher)
.launchIn(viewModelScope)
fun updateOnlineLibraryFilters(newRequest: OnlineLibraryRequest) {
onlineLibraryRequest.update { current ->
current.copy(
@ -589,7 +598,7 @@ class ZimManageViewModel @Inject constructor(
localBooksFromLibkiwix: Flow<List<Book>>,
downloads: Flow<List<DownloadModel>>,
library: MutableStateFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>,
languages: StateFlow<String>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = viewModelScope.launch(dispatcher) {
val requestFilteringFlow = merge(
@ -632,23 +641,25 @@ class ZimManageViewModel @Inject constructor(
.collect { _libraryItems.emit(it) }
}
private fun updateLanguagesInDao(
library: MutableStateFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) =
combine(
library,
languages
) { books, existingLanguages ->
combineToLanguageList(books, existingLanguages)
}.map { it.sortedBy(Language::language) }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
.catch { it.printStackTrace() }
.onEach { languageRoomDao.insert(it) }
.flowOn(dispatcher)
.launchIn(viewModelScope)
// private fun updateLanguagesInDao(
// library: MutableStateFlow<List<LibkiwixBook>>,
// languages: StateFlow<String>,
// dispatcher: CoroutineDispatcher = Dispatchers.IO
// ) =
// combine(
// library,
// languages
// ) { books, existingLanguages ->
// combineToLanguageList(books, existingLanguages)
// }.map { it.sortedBy(Language::language) }
// .filter { it.isNotEmpty() }
// .distinctUntilChanged()
// .catch { it.printStackTrace() }
// .onEach {
// // languageRoomDao.insert(it)
// }
// .flowOn(dispatcher)
// .launchIn(viewModelScope)
private suspend fun combineToLanguageList(
booksFromNetwork: List<LibkiwixBook>,

View File

@ -28,7 +28,6 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.language.composables.LanguageListItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter

View File

@ -27,7 +27,6 @@ import io.mockk.verify
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.LanguageRoomDao
import org.kiwix.kiwixmobile.core.zim_manager.Language
class SaveLanguagesAndFinishTest {

View File

@ -54,7 +54,6 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.StorageObserver
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.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService

View File

@ -1,50 +0,0 @@
/*
* 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

@ -1,56 +0,0 @@
/*
* 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

@ -23,7 +23,6 @@ import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.libkiwix.Book
@ -34,7 +33,6 @@ interface DataSource {
fun getLanguageCategorizedBooks(): Flow<List<BooksOnDiskListItem>>
suspend fun saveBook(book: Book)
suspend fun saveBooks(book: List<Book>)
suspend fun saveLanguages(languages: List<Language>)
suspend fun saveHistory(history: HistoryItem)
suspend fun deleteHistory(historyList: List<HistoryListItem>)
suspend fun clearHistory()

View File

@ -28,17 +28,14 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
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.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.entities.BundleRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
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.RecentSearchRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.StringToLocalRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
@ -49,17 +46,15 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
HistoryRoomEntity::class,
NotesRoomEntity::class,
DownloadRoomEntity::class,
WebViewHistoryEntity::class,
LanguageRoomEntity::class
WebViewHistoryEntity::class
],
version = 9,
version = 8,
exportSchema = false
)
@TypeConverters(
HistoryRoomDaoCoverts::class,
ZimSourceRoomConverter::class,
BundleRoomConverter::class,
StringToLocalRoomConverter::class
BundleRoomConverter::class
)
abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun recentSearchRoomDao(): RecentSearchRoomDao
@ -67,7 +62,6 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun notesRoomDao(): NotesRoomDao
abstract fun downloadRoomDao(): DownloadRoomDao
abstract fun webViewHistoryRoomDao(): WebViewHistoryRoomDao
abstract fun languageRoomDao(): LanguageRoomDao
companion object {
private var db: KiwixRoomDatabase? = null
@ -84,8 +78,7 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
MIGRATION_7_8,
MIGRATION_8_9
MIGRATION_7_8
)
.build().also { db = it }
}
@ -312,23 +305,6 @@ 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() {
db = null
}

View File

@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
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.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
@ -37,7 +36,6 @@ import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.LanguageItem
@ -56,7 +54,6 @@ class Repository @Inject internal constructor(
private val historyRoomDao: HistoryRoomDao,
private val webViewHistoryRoomDao: WebViewHistoryRoomDao,
private val notesRoomDao: NotesRoomDao,
private val languageRoomDao: LanguageRoomDao,
private val recentSearchRoomDao: RecentSearchRoomDao,
private val zimReaderContainer: ZimReaderContainer
) : DataSource {
@ -99,12 +96,6 @@ class Repository @Inject internal constructor(
libkiwixBookOnDisk.insert(listOf(book))
}
@Suppress("InjectDispatcher")
override suspend fun saveLanguages(languages: List<Language>) =
withContext(Dispatchers.IO) {
languageRoomDao.insert(languages)
}
@Suppress("InjectDispatcher")
override suspend fun saveHistory(history: HistoryItem) = withContext(Dispatchers.IO) {
historyRoomDao.saveHistory(history)

View File

@ -39,6 +39,9 @@ interface KiwixService {
@Url url: String
): MetaLinkNetworkEntity?
@GET("catalog/v2/languages")
suspend fun getLanguages(): LanguageFeed
/******** Helper class that sets up new services */
object ServiceCreator {
@Suppress("DEPRECATION")

View File

@ -0,0 +1,48 @@
/*
* 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.data.remote
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.simpleframework.xml.Element
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Namespace
import org.simpleframework.xml.Root
@Root(name = "feed", strict = false)
@Namespace(reference = "http://www.w3.org/2005/Atom")
class LanguageFeed {
@field:ElementList(name = "entry", inline = true, required = false)
var entries: List<LanguageEntry>? = null
}
@Root(name = "entry", strict = false)
@Namespace(reference = "http://www.w3.org/2005/Atom")
class LanguageEntry {
@field:Element(name = "title", required = false)
var title: String = ""
@field:Element(name = "language", required = false)
@Namespace(prefix = "dc", reference = "http://purl.org/dc/terms/")
var languageCode: String = ""
@field:Element(name = "count", required = false)
@Namespace(prefix = "thr", reference = "http://purl.org/syndication/thread/1.0")
var count: Int = ZERO
}

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ const val READ_TIMEOUT = 300L
const val CALL_TIMEOUT = 300L
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/"
const val KIWIX_LANGUAGE_URL = "https://library.kiwix.org/"
@Module
class NetworkModule {

View File

@ -60,6 +60,9 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
val prefWifiOnly: Boolean
get() = sharedPreferences.getBoolean(PREF_WIFI_ONLY, true)
private val _onlineContentLanguage = MutableStateFlow("")
val onlineContentLanguage = _onlineContentLanguage.asStateFlow()
val prefIsFirstRun: Boolean
get() = sharedPreferences.getBoolean(PREF_IS_FIRST_RUN, true)
@ -117,9 +120,6 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
val prefIsBookOnDiskMigrated: Boolean
get() = sharedPreferences.getBoolean(PREF_BOOK_ON_DISK_MIGRATED, false)
val prefLanguageListMigrated: Boolean
get() = sharedPreferences.getBoolean(PREF_LANGUAGE_LIST_MIGRATED, false)
val prefStorage: String
get() {
val storage = sharedPreferences.getString(PREF_STORAGE, null)
@ -172,9 +172,6 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
fun putPrefBookOnDiskMigrated(isMigrated: Boolean) =
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) =
sharedPreferences.edit { putString(PREF_LANG, language) }
@ -308,6 +305,18 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
}
}
var selectedOnlineContentLanguage: String
get() = sharedPreferences.getString(SELECTED_ONLINE_CONTENT_LANGUAGE, "").orEmpty()
set(selectedOnlineContentLanguage) {
sharedPreferences.edit {
putString(
SELECTED_ONLINE_CONTENT_LANGUAGE,
selectedOnlineContentLanguage
)
}
_onlineContentLanguage.tryEmit(selectedOnlineContentLanguage)
}
fun getPublicDirectoryPath(path: String): String =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
path
@ -358,10 +367,10 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
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_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"
private const val PREF_LATER_CLICKED_MILLIS = "pref_later_clicked_millis"
const val PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS =
"pref_last_donation_shown_in_milliseconds"
private const val SELECTED_ONLINE_CONTENT_LANGUAGE = "selectedOnlineContentLanguage"
}
}

View File

@ -51,8 +51,9 @@ data class Language constructor(
constructor(
languageCode: String,
active: Boolean,
occurrencesOfLanguage: Int
) : this(languageCode.convertToLocal(), active, occurrencesOfLanguage)
occurrencesOfLanguage: Int,
id: Long = 0
) : this(languageCode.convertToLocal(), active, occurrencesOfLanguage, id)
override fun equals(other: Any?): Boolean =
(other as Language).language == language && other.active == active

View File

@ -210,6 +210,7 @@
<string name="table_of_contents">Table of contents</string>
<string name="select_languages" tools:keep="@string/select_languages">Select languages</string>
<string name="save_languages" tools:keep="@string/save_languages">Save languages</string>
<string name="no_language_available" tools:keep="@string/no_language_available">No languages available</string>
<string name="expand">Expand</string>
<string name="history">History</string>
<string name="history_from_current_book">View History From All Books</string>