diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt index 4ee833537..aa1af3587 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt @@ -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( diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt index 78fe3deef..74d2e9dac 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt @@ -25,5 +25,6 @@ sealed class Action { data class UpdateLanguages(val languages: List) : 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() } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt index 45d4e0570..03be7d9a4 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt @@ -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(Loading) val actions = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) @@ -49,6 +74,7 @@ class LanguageViewModel @Inject constructor( private val coroutineJobs = mutableListOf() 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? = + 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() } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt index c262cbe29..fde0ca385 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt @@ -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, - private val languageRoomDao: LanguageRoomDao, + private val languages: Language, + private val sharedPreferenceUtil: SharedPreferenceUtil, private val lifecycleScope: CoroutineScope ) : SideEffect { override fun invokeWith(activity: AppCompatActivity) { lifecycleScope.launch { runCatching { - withContext(Dispatchers.IO) { - languageRoomDao.insert(languages) - } + sharedPreferenceUtil.selectedOnlineContentLanguage = languages.languageCode activity.onBackPressedDispatcher.onBackPressed() }.onFailure { it.printStackTrace() diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt index 45ca13783..d6944cce1 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt @@ -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( diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt index 61d831c62..90aac7fff 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt @@ -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>, downloads: Flow>, library: MutableStateFlow>, - languages: Flow>, + languages: StateFlow, 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>, - languages: Flow>, - 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>, + // languages: StateFlow, + // 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, diff --git a/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModelTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModelTest.kt index aa13e4f60..c4fab5b9e 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModelTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModelTest.kt @@ -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 diff --git a/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinishTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinishTest.kt index 3f7384216..adf73ac0a 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinishTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinishTest.kt @@ -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 { diff --git a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt index f27cfdbab..5c1726585 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LanguageRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LanguageRoomDao.kt deleted file mode 100644 index 9f22ec821..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LanguageRoomDao.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2025 Kiwix - * 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 . - * - */ - -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> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveLanguages(languageRoomEntityList: List) - - @Query("DELETE FROM LanguageRoomEntity") - abstract fun deleteAllLanguages() - - fun languages(): Flow> = - languageAsEntity().map { it.map(LanguageRoomEntity::toLanguageModel) } - - @Transaction - open fun insert(languages: List) { - deleteAllLanguages() - saveLanguages(languages.map(::LanguageRoomEntity)) - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/LanguageRoomEntity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/LanguageRoomEntity.kt deleted file mode 100644 index fb9fc33b6..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/LanguageRoomEntity.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2025 Kiwix - * 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 . - * - */ - -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 -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt index ef6ba5ea4..d03d34760 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt @@ -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> suspend fun saveBook(book: Book) suspend fun saveBooks(book: List) - suspend fun saveLanguages(languages: List) suspend fun saveHistory(history: HistoryItem) suspend fun deleteHistory(historyList: List) suspend fun clearHistory() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt index 6865ce354..adb930319 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt @@ -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 } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt index 21662400b..ae24979b4 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt @@ -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) = - withContext(Dispatchers.IO) { - languageRoomDao.insert(languages) - } - @Suppress("InjectDispatcher") override suspend fun saveHistory(history: HistoryItem) = withContext(Dispatchers.IO) { historyRoomDao.saveHistory(history) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt index 808549b5f..523a7b4e0 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt @@ -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") diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/LanguageFeed.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/LanguageFeed.kt new file mode 100644 index 000000000..a9cd88cea --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/LanguageFeed.kt @@ -0,0 +1,48 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +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? = 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 +} + diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ObjectBoxToRoomMigrator.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ObjectBoxToRoomMigrator.kt index f6009f202..6b31f04b0 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ObjectBoxToRoomMigrator.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ObjectBoxToRoomMigrator.kt @@ -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) { - kiwixRoomDatabase.languageRoomDao() - .insert( - box.all.map { it.toLanguageModel() } - ) - sharedPreferenceUtil.putPrefLanguageListMigrated(true) - } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt index 30e5e5cfe..4dee3f60d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt index e30a4f7d1..0a121bd96 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt @@ -100,8 +100,4 @@ open class DatabaseModule { db.downloadRoomDao().also { it.libkiwixBookOnDisk = libkiwixBookOnDisk } - - @Singleton - @Provides - fun provideLanguageRoomDao(db: KiwixRoomDatabase) = db.languageRoomDao() } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt index e0d5a816e..7473ada89 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt @@ -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 { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt index b7d4a2db7..3fa98c041 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt @@ -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" } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/Language.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/Language.kt index b979fa463..8e21a6662 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/Language.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/Language.kt @@ -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 diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 0a5960d4d..e08359727 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -210,6 +210,7 @@ Table of contents Select languages Save languages + No languages available Expand History View History From All Books