mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-04 03:06:41 -04:00
Merge pull request #4328 from kiwix/Fixes#4302
Refactored `RxJava` to `coroutines` in `OnlineLibrary`.
This commit is contained in:
commit
10494cccf9
@ -26,7 +26,7 @@ import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.Language
|
||||
|
||||
@Suppress("InjectDispatcher", "TooGenericExceptionCaught")
|
||||
@Suppress("InjectDispatcher")
|
||||
data class SaveLanguagesAndFinish(
|
||||
private val languages: List<Language>,
|
||||
private val languageDao: NewLanguagesDao,
|
||||
@ -34,13 +34,13 @@ data class SaveLanguagesAndFinish(
|
||||
) : SideEffect<Unit> {
|
||||
override fun invokeWith(activity: AppCompatActivity) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
languageDao.insert(languages)
|
||||
}
|
||||
activity.onBackPressedDispatcher.onBackPressed()
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,7 +520,6 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mainRepositoryActions.dispose()
|
||||
actionMode = null
|
||||
coroutineJobs.forEach {
|
||||
it.cancel()
|
||||
|
@ -47,6 +47,8 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.tonyodev.fetch2.Status
|
||||
import eu.mhutti1.utils.storage.StorageDevice
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.R.drawable
|
||||
import org.kiwix.kiwixmobile.cachedComponent
|
||||
@ -123,13 +125,13 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
OnlineLibraryScreenState(
|
||||
onlineLibraryList = null,
|
||||
snackBarHostState = SnackbarHostState(),
|
||||
swipeRefreshItem = Pair(false, true),
|
||||
isRefreshing = false,
|
||||
scanningProgressItem = Pair(false, ""),
|
||||
noContentViewItem = Pair("", false),
|
||||
bottomNavigationHeight = ZERO,
|
||||
onBookItemClick = { onBookItemClick(it) },
|
||||
availableSpaceCalculator = availableSpaceCalculator,
|
||||
onRefresh = { refreshFragment() },
|
||||
onRefresh = { refreshFragment(true) },
|
||||
bookUtils = bookUtils,
|
||||
onPauseResumeButtonClick = { onPauseResumeButtonClick(it) },
|
||||
onStopButtonClick = { onStopButtonClick(it) },
|
||||
@ -146,7 +148,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
copy(searchText = "")
|
||||
}
|
||||
zimManageViewModel.onlineBooksSearchedQuery.value = null
|
||||
zimManageViewModel.requestFiltering.onNext("")
|
||||
zimManageViewModel.requestFiltering.tryEmit("")
|
||||
}
|
||||
|
||||
private fun onSearchValueChanged(searchText: String) {
|
||||
@ -159,7 +161,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(searchText = searchText)
|
||||
}
|
||||
zimManageViewModel.requestFiltering.onNext(searchText)
|
||||
zimManageViewModel.requestFiltering.tryEmit(searchText)
|
||||
}
|
||||
|
||||
private val noWifiWithWifiOnlyPreferenceSet
|
||||
@ -246,17 +248,39 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
)
|
||||
DialogHost(alertDialogShower)
|
||||
}
|
||||
zimManageViewModel.libraryItems.observe(viewLifecycleOwner, Observer(::onLibraryItemsChange))
|
||||
observeViewModelData()
|
||||
showPreviouslySearchedTextInSearchView()
|
||||
startDownloadingLibrary()
|
||||
}
|
||||
|
||||
private fun observeViewModelData() {
|
||||
zimManageViewModel.apply {
|
||||
// Observe when library items changes.
|
||||
libraryItems
|
||||
.onEach { onLibraryItemsChange(it) }
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
.also {
|
||||
coreMainActivity.navHostContainer
|
||||
.setBottomMarginToFragmentContainerView(0)
|
||||
}
|
||||
zimManageViewModel.libraryListIsRefreshing.observe(
|
||||
// Observe when online library downloading.
|
||||
onlineLibraryDownloading
|
||||
.onEach {
|
||||
if (it) {
|
||||
showProgressBarOfFetchingOnlineLibrary()
|
||||
} else {
|
||||
hideProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
}.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
// Observe when library list refreshing e.g. applying filters.
|
||||
libraryListIsRefreshing.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer { onRefreshStateChange(it, true) }
|
||||
Observer { onRefreshStateChange(it) }
|
||||
)
|
||||
zimManageViewModel.networkStates.observe(viewLifecycleOwner, Observer(::onNetworkStateChange))
|
||||
zimManageViewModel.shouldShowWifiOnlyDialog.observe(
|
||||
// Observe network changes.
|
||||
networkStates.observe(viewLifecycleOwner, Observer(::onNetworkStateChange))
|
||||
// Observe `shouldShowWifiOnlyDialog` should show.
|
||||
shouldShowWifiOnlyDialog.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it && !NetworkUtils.isWiFi(requireContext())) {
|
||||
@ -264,8 +288,9 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
hideProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
}
|
||||
zimManageViewModel.downloadProgress.observe(viewLifecycleOwner, ::onLibraryStatusChanged)
|
||||
showPreviouslySearchedTextInSearchView()
|
||||
// Observe the download progress.
|
||||
downloadProgress.observe(viewLifecycleOwner, ::onLibraryStatusChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPreviouslySearchedTextInSearchView() {
|
||||
@ -275,11 +300,11 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(isSearchActive = true, searchText = it)
|
||||
}
|
||||
zimManageViewModel.requestFiltering.onNext(it)
|
||||
zimManageViewModel.requestFiltering.tryEmit(it)
|
||||
} ?: run {
|
||||
// If no previously saved query found then normally initiate the search.
|
||||
zimManageViewModel.onlineBooksSearchedQuery.value = ""
|
||||
zimManageViewModel.requestFiltering.onNext("")
|
||||
zimManageViewModel.requestFiltering.tryEmit("")
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,6 +366,10 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
showRecyclerviewAndHideSwipeDownForLibraryErrorText()
|
||||
sharedPreferenceUtil.putPrefWifiOnly(false)
|
||||
zimManageViewModel.shouldShowWifiOnlyDialog.value = false
|
||||
// User allowed downloading over mobile data.
|
||||
// Since the download flow now triggers only when appropriate,
|
||||
// we start the library download explicitly after updating the preference.
|
||||
startDownloadingLibrary(true)
|
||||
},
|
||||
{
|
||||
context.toast(
|
||||
@ -356,32 +385,28 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(noContentViewItem = "" to false)
|
||||
}
|
||||
showProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
|
||||
private fun hideRecyclerviewAndShowSwipeDownForLibraryErrorText() {
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(noContentViewItem = getString(string.swipe_down_for_library) to true)
|
||||
}
|
||||
hideProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
|
||||
private fun showProgressBarOfFetchingOnlineLibrary() {
|
||||
onRefreshStateChange(isRefreshing = false, shouldShowScanningProgressItem = false)
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(
|
||||
noContentViewItem = "" to false,
|
||||
swipeRefreshItem = onlineLibraryScreenState.value.value.swipeRefreshItem.first to false,
|
||||
isRefreshing = false,
|
||||
scanningProgressItem = true to getString(string.reaching_remote_library)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideProgressBarOfFetchingOnlineLibrary() {
|
||||
onRefreshStateChange(isRefreshing = false, false)
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(
|
||||
swipeRefreshItem = onlineLibraryScreenState.value.value.swipeRefreshItem.first to true,
|
||||
isRefreshing = false,
|
||||
scanningProgressItem = false to getString(string.reaching_remote_library)
|
||||
)
|
||||
}
|
||||
@ -391,7 +416,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
synchronized(lock) {
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(
|
||||
scanningProgressItem = onlineLibraryScreenState.value.value.scanningProgressItem.first to libraryStatus
|
||||
scanningProgressItem = true to libraryStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -419,35 +444,30 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
onSearchClear()
|
||||
}
|
||||
|
||||
private fun onRefreshStateChange(
|
||||
isRefreshing: Boolean?,
|
||||
shouldShowScanningProgressItem: Boolean
|
||||
) {
|
||||
var refreshing = isRefreshing == true
|
||||
val onlineLibraryState = onlineLibraryScreenState.value.value
|
||||
// do not show the refreshing when the online library is downloading
|
||||
if (onlineLibraryState.scanningProgressItem.first ||
|
||||
onlineLibraryState.noContentViewItem.second
|
||||
) {
|
||||
refreshing = false
|
||||
}
|
||||
private fun onRefreshStateChange(isRefreshing: Boolean?) {
|
||||
val refreshing = isRefreshing == true
|
||||
onlineLibraryScreenState.value.update {
|
||||
copy(
|
||||
swipeRefreshItem = refreshing to onlineLibraryState.swipeRefreshItem.second,
|
||||
scanningProgressItem = shouldShowScanningProgressItem to onlineLibraryState.scanningProgressItem.second
|
||||
)
|
||||
copy(isRefreshing = refreshing)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkStateChange(networkState: NetworkState?) {
|
||||
when (networkState) {
|
||||
NetworkState.CONNECTED -> {
|
||||
if (NetworkUtils.isWiFi(requireContext())) {
|
||||
refreshFragment()
|
||||
} else if (noWifiWithWifiOnlyPreferenceSet) {
|
||||
when {
|
||||
NetworkUtils.isWiFi(requireContext()) -> {
|
||||
if (!zimManageViewModel.isOnlineLibraryDownloading) {
|
||||
refreshFragment(false)
|
||||
}
|
||||
}
|
||||
|
||||
noWifiWithWifiOnlyPreferenceSet -> {
|
||||
hideRecyclerviewAndShowSwipeDownForLibraryErrorText()
|
||||
} else if (!noWifiWithWifiOnlyPreferenceSet) {
|
||||
if (onlineLibraryScreenState.value.value.onlineLibraryList?.isEmpty() == true) {
|
||||
}
|
||||
|
||||
onlineLibraryScreenState.value.value.onlineLibraryList.isNullOrEmpty() &&
|
||||
!zimManageViewModel.isOnlineLibraryDownloading -> {
|
||||
startDownloadingLibrary(true)
|
||||
showProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
}
|
||||
@ -501,14 +521,20 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
hideProgressBarOfFetchingOnlineLibrary()
|
||||
}
|
||||
|
||||
private fun refreshFragment() {
|
||||
private fun refreshFragment(isExplicitRefresh: Boolean) {
|
||||
if (isNotConnected) {
|
||||
showNoInternetConnectionError()
|
||||
} else {
|
||||
zimManageViewModel.requestDownloadLibrary.onNext(Unit)
|
||||
startDownloadingLibrary(isExplicitRefresh)
|
||||
if (isExplicitRefresh) {
|
||||
showRecyclerviewAndHideSwipeDownForLibraryErrorText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDownloadingLibrary(isExplicitRefresh: Boolean = false) {
|
||||
zimManageViewModel.requestOnlineLibraryIfNeeded(isExplicitRefresh)
|
||||
}
|
||||
|
||||
private fun downloadFile() {
|
||||
downloadBookItem?.book?.let {
|
||||
|
@ -19,6 +19,7 @@
|
||||
package org.kiwix.kiwixmobile.nav.destination.library.online
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
@ -31,6 +32,8 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@ -109,8 +112,8 @@ fun OnlineLibraryScreen(
|
||||
.padding(bottom = bottomNavHeight.value)
|
||||
) { paddingValues ->
|
||||
SwipeRefreshLayout(
|
||||
isRefreshing = state.swipeRefreshItem.first,
|
||||
isEnabled = state.swipeRefreshItem.second,
|
||||
isRefreshing = state.isRefreshing && !state.scanningProgressItem.first,
|
||||
isEnabled = !state.scanningProgressItem.first,
|
||||
onRefresh = state.onRefresh,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@ -212,6 +215,13 @@ private fun ShowDividerItem(dividerItem: DividerItem) {
|
||||
|
||||
@Composable
|
||||
private fun NoContentView(noContentMessage: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = noContentMessage,
|
||||
textAlign = TextAlign.Center,
|
||||
@ -219,6 +229,7 @@ private fun NoContentView(noContentMessage: String) {
|
||||
.padding(horizontal = FOUR_DP)
|
||||
.semantics { testTag = NO_CONTENT_VIEW_TEXT_TESTING_TAG }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -37,11 +37,9 @@ data class OnlineLibraryScreenState(
|
||||
/**
|
||||
* Controls the visibility and behavior of the "Pull to refresh" animation.
|
||||
*
|
||||
* A [Pair] containing:
|
||||
* - [Boolean]: The first boolean triggers/hides the "pull to refresh" animation.
|
||||
* - [Boolean]: The second boolean enables/disables the "pull to refresh" gesture.
|
||||
*/
|
||||
val swipeRefreshItem: Pair<Boolean, Boolean>,
|
||||
val isRefreshing: Boolean,
|
||||
/**
|
||||
* Handles snack bar messages and displays.
|
||||
*/
|
||||
|
@ -24,32 +24,37 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.exceptions.UndeliverableException
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.functions.Function6
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.retry
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -75,11 +80,15 @@ import org.kiwix.kiwixmobile.core.di.modules.KIWIX_DOWNLOAD_URL
|
||||
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
|
||||
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.core.extensions.calculateSearchMatches
|
||||
import org.kiwix.kiwixmobile.core.extensions.registerReceiver
|
||||
import org.kiwix.kiwixmobile.core.ui.components.ONE
|
||||
import org.kiwix.kiwixmobile.core.ui.components.TWO
|
||||
import org.kiwix.kiwixmobile.core.utils.BookUtils
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
||||
@ -113,16 +122,16 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
|
||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem
|
||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
|
||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import javax.inject.Inject
|
||||
|
||||
const val DEFAULT_PROGRESS = 0
|
||||
const val MAX_PROGRESS = 100
|
||||
private const val TAG_RX_JAVA_DEFAULT_ERROR_HANDLER = "RxJavaDefaultErrorHandler"
|
||||
|
||||
const val THREE = 3
|
||||
const val FOUR = 4
|
||||
|
||||
class ZimManageViewModel @Inject constructor(
|
||||
private val downloadDao: DownloadRoomDao,
|
||||
@ -152,31 +161,45 @@ class ZimManageViewModel @Inject constructor(
|
||||
|
||||
private var isUnitTestCase: Boolean = false
|
||||
val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow()
|
||||
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
|
||||
private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList())
|
||||
val libraryItems: StateFlow<List<LibraryListItem>> = _libraryItems.asStateFlow()
|
||||
val fileSelectListStates: MutableLiveData<FileSelectListState> = MutableLiveData()
|
||||
val deviceListScanningProgress = MutableLiveData<Int>()
|
||||
val libraryListIsRefreshing = MutableLiveData<Boolean>()
|
||||
val onlineLibraryDownloading = MutableStateFlow(false)
|
||||
val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>()
|
||||
val networkStates = MutableLiveData<NetworkState>()
|
||||
|
||||
val networkLibrary = MutableSharedFlow<LibraryNetworkEntity>(replay = 0)
|
||||
val requestFileSystemCheck = MutableSharedFlow<Unit>(replay = 0)
|
||||
val fileSelectActions = MutableSharedFlow<FileSelectActions>()
|
||||
val requestDownloadLibrary = BehaviorProcessor.createDefault(Unit)
|
||||
val requestFiltering = BehaviorProcessor.createDefault("")
|
||||
val onlineBooksSearchedQuery = MutableLiveData<String>()
|
||||
private val requestDownloadLibrary = MutableSharedFlow<Unit>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
private var compositeDisposable: CompositeDisposable? = CompositeDisposable()
|
||||
@Volatile
|
||||
var isOnlineLibraryDownloading = false
|
||||
val requestFiltering = MutableStateFlow("")
|
||||
val onlineBooksSearchedQuery = MutableLiveData<String>()
|
||||
private val coroutineJobs: MutableList<Job> = mutableListOf()
|
||||
val downloadProgress = MutableLiveData<String>()
|
||||
|
||||
private lateinit var alertDialogShower: AlertDialogShower
|
||||
|
||||
init {
|
||||
compositeDisposable?.addAll(*disposables())
|
||||
observeCoroutineFlows()
|
||||
context.registerReceiver(connectivityBroadcastReceiver)
|
||||
}
|
||||
|
||||
fun requestOnlineLibraryIfNeeded(isExplicitRefresh: Boolean) {
|
||||
if (isOnlineLibraryDownloading && !isExplicitRefresh) return
|
||||
isOnlineLibraryDownloading = true
|
||||
viewModelScope.launch {
|
||||
requestDownloadLibrary.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun setIsUnitTestCase() {
|
||||
isUnitTestCase = true
|
||||
}
|
||||
@ -259,10 +282,17 @@ class ZimManageViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun observeCoroutineFlows(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
|
||||
val downloads = downloadDao.downloads()
|
||||
val booksFromDao = books()
|
||||
val languages = languageDao.languages()
|
||||
coroutineJobs.apply {
|
||||
add(scanBooksFromStorage(dispatcher))
|
||||
add(updateBookItems())
|
||||
add(fileSelectActions())
|
||||
add(updateLibraryItems(booksFromDao, downloads, networkLibrary, languages))
|
||||
add(updateLanguagesInDao(networkLibrary, languages))
|
||||
add(updateNetworkStates())
|
||||
add(requestsAndConnectivityChangesToLibraryRequests(networkLibrary))
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,49 +301,23 @@ class ZimManageViewModel @Inject constructor(
|
||||
it.cancel()
|
||||
}
|
||||
coroutineJobs.clear()
|
||||
compositeDisposable?.clear()
|
||||
context.unregisterReceiver(connectivityBroadcastReceiver)
|
||||
connectivityBroadcastReceiver.stopNetworkState()
|
||||
requestDownloadLibrary.onComplete()
|
||||
compositeDisposable = null
|
||||
appProgressListener = null
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun disposables(): Array<Disposable> {
|
||||
// temporary converting to flowable. TODO we will refactor this in upcoming issue.
|
||||
val downloads = downloadDao.downloads().asFlowable()
|
||||
val booksFromDao = books().asFlowable()
|
||||
val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>()
|
||||
val languages = languageDao.languages().asFlowable()
|
||||
return arrayOf(
|
||||
updateLibraryItems(booksFromDao, downloads, networkLibrary, languages),
|
||||
updateLanguagesInDao(networkLibrary, languages),
|
||||
updateNetworkStates(),
|
||||
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary)
|
||||
).also {
|
||||
setUpUncaughtErrorHandlerForOnlineLibrary(networkLibrary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanBooksFromStorage(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
|
||||
viewModelScope.launch {
|
||||
withContext(dispatcher) {
|
||||
books()
|
||||
.let { checkFileSystemForBooksOnRequest(it) }
|
||||
checkFileSystemForBooksOnRequest(books())
|
||||
.catch { it.printStackTrace() }
|
||||
.collect { books ->
|
||||
bookDao.insert(books)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEach { books -> bookDao.insert(books) }
|
||||
.flowOn(dispatcher)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun fileSelectActions() =
|
||||
viewModelScope.launch {
|
||||
fileSelectActions
|
||||
.collect { action ->
|
||||
try {
|
||||
.onEach { action ->
|
||||
runCatching {
|
||||
sideEffects.emit(
|
||||
when (action) {
|
||||
is RequestNavigateTo -> OpenFileWithNavigation(action.bookOnDisk)
|
||||
@ -326,11 +330,10 @@ class ZimManageViewModel @Inject constructor(
|
||||
UserClickedDownloadBooksButton -> NavigateToDownloads
|
||||
}
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
private fun startMultiSelectionAndSelectBook(
|
||||
bookOnDisk: BookOnDisk
|
||||
@ -385,117 +388,144 @@ class ZimManageViewModel @Inject constructor(
|
||||
return None
|
||||
}
|
||||
|
||||
@Suppress("NoNameShadowing")
|
||||
private fun requestsAndConnectivtyChangesToLibraryRequests(
|
||||
library: PublishProcessor<LibraryNetworkEntity>,
|
||||
) =
|
||||
Flowable.combineLatest(
|
||||
requestDownloadLibrary,
|
||||
connectivityBroadcastReceiver.networkStates.distinctUntilChanged().filter(
|
||||
CONNECTED::equals
|
||||
)
|
||||
) { _, _ -> }
|
||||
.switchMap {
|
||||
if (connectivityManager.isWifi()) {
|
||||
Flowable.just(Unit)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun requestsAndConnectivityChangesToLibraryRequests(
|
||||
library: MutableSharedFlow<LibraryNetworkEntity>,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) = requestDownloadLibrary.flatMapConcat {
|
||||
connectivityBroadcastReceiver.networkStates.asFlow()
|
||||
.distinctUntilChanged()
|
||||
.filter { networkState -> networkState == CONNECTED }
|
||||
.take(1)
|
||||
.flatMapConcat {
|
||||
shouldProceedWithDownload()
|
||||
.flatMapConcat { kiwixService ->
|
||||
downloadLibraryFlow(kiwixService).also {
|
||||
onlineLibraryDownloading.tryEmit(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.catch {
|
||||
it.printStackTrace().also {
|
||||
isOnlineLibraryDownloading = false
|
||||
onlineLibraryDownloading.tryEmit(false)
|
||||
}
|
||||
}
|
||||
.onEach {
|
||||
library.emit(it).also {
|
||||
// Setting this to true because once library downloaded we don't need to download again
|
||||
// until user wants to refresh the online library.
|
||||
isOnlineLibraryDownloading = true
|
||||
onlineLibraryDownloading.tryEmit(false)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatcher)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
private fun shouldProceedWithDownload(): Flow<KiwixService> {
|
||||
return if (connectivityManager.isWifi()) {
|
||||
flowOf(createKiwixServiceWithProgressListener())
|
||||
} else {
|
||||
sharedPreferenceUtil.prefWifiOnlys
|
||||
.asFlowable()
|
||||
.doOnNext {
|
||||
if (it) {
|
||||
flow {
|
||||
val wifiOnly = sharedPreferenceUtil.prefWifiOnlys.first()
|
||||
if (wifiOnly) {
|
||||
shouldShowWifiOnlyDialog.postValue(true)
|
||||
// Don't emit anything — just return
|
||||
return@flow
|
||||
}
|
||||
emit(createKiwixServiceWithProgressListener())
|
||||
}
|
||||
}
|
||||
.filter { !it }
|
||||
.map { }
|
||||
}
|
||||
|
||||
private fun downloadLibraryFlow(
|
||||
kiwixService: KiwixService
|
||||
): Flow<LibraryNetworkEntity?> = flow {
|
||||
downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library))
|
||||
val response = kiwixService.getLibrary()
|
||||
downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
|
||||
emit(response)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.concatMap {
|
||||
Flowable.fromCallable {
|
||||
synchronized(this, ::createKiwixServiceWithProgressListener)
|
||||
}
|
||||
}
|
||||
.concatMap {
|
||||
kiwixService.library
|
||||
.toFlowable()
|
||||
.retry(5)
|
||||
.doOnSubscribe {
|
||||
downloadProgress.postValue(
|
||||
context.getString(R.string.starting_downloading_remote_library)
|
||||
)
|
||||
}
|
||||
.map { response ->
|
||||
downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
|
||||
response
|
||||
}
|
||||
.doFinally {
|
||||
downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
|
||||
}
|
||||
.onErrorReturn {
|
||||
it.printStackTrace()
|
||||
LibraryNetworkEntity().apply { book = LinkedList() }
|
||||
}
|
||||
}
|
||||
.subscribe(library::onNext, Throwable::printStackTrace).also {
|
||||
compositeDisposable?.add(it)
|
||||
.catch { e ->
|
||||
e.printStackTrace()
|
||||
emit(LibraryNetworkEntity().apply { book = LinkedList() })
|
||||
}
|
||||
|
||||
private fun updateNetworkStates() =
|
||||
connectivityBroadcastReceiver.networkStates.subscribe(
|
||||
networkStates::postValue,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates
|
||||
.asFlow()
|
||||
.catch { it.printStackTrace() }
|
||||
.onEach { state -> networkStates.postValue(state) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun updateLibraryItems(
|
||||
booksFromDao: io.reactivex.rxjava3.core.Flowable<List<BookOnDisk>>,
|
||||
downloads: io.reactivex.rxjava3.core.Flowable<List<DownloadModel>>,
|
||||
library: Flowable<LibraryNetworkEntity>,
|
||||
languages: io.reactivex.rxjava3.core.Flowable<List<Language>>
|
||||
) = Flowable.combineLatest(
|
||||
booksFromDao: Flow<List<BookOnDisk>>,
|
||||
downloads: Flow<List<DownloadModel>>,
|
||||
library: MutableSharedFlow<LibraryNetworkEntity>,
|
||||
languages: Flow<List<Language>>,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) = viewModelScope.launch(dispatcher) {
|
||||
val requestFilteringFlow = merge(
|
||||
flowOf(""),
|
||||
requestFiltering
|
||||
.onEach { libraryListIsRefreshing.postValue(true) }
|
||||
.debounce(500)
|
||||
.flowOn(dispatcher)
|
||||
)
|
||||
|
||||
combine(
|
||||
booksFromDao,
|
||||
downloads,
|
||||
languages.filter(List<Language>::isNotEmpty),
|
||||
languages.filter { it.isNotEmpty() },
|
||||
library,
|
||||
Flowable.merge(
|
||||
Flowable.just(""),
|
||||
requestFiltering
|
||||
.doOnNext { libraryListIsRefreshing.postValue(true) }
|
||||
.debounce(500, MILLISECONDS)
|
||||
.observeOn(Schedulers.io())
|
||||
),
|
||||
fat32Checker.fileSystemStates.asFlowable(),
|
||||
Function6(::combineLibrarySources)
|
||||
requestFilteringFlow,
|
||||
fat32Checker.fileSystemStates
|
||||
) { args ->
|
||||
val books = args[ZERO] as List<BookOnDisk>
|
||||
val activeDownloads = args[ONE] as List<DownloadModel>
|
||||
val languageList = args[TWO] as List<Language>
|
||||
val libraryNetworkEntity = args[THREE] as LibraryNetworkEntity
|
||||
val filter = args[FOUR] as String
|
||||
val fileSystemState = args[FIVE] as FileSystemState
|
||||
combineLibrarySources(
|
||||
booksOnFileSystem = books,
|
||||
activeDownloads = activeDownloads,
|
||||
allLanguages = languageList,
|
||||
libraryNetworkEntity = libraryNetworkEntity,
|
||||
filter = filter,
|
||||
fileSystemState = fileSystemState
|
||||
)
|
||||
.doOnNext { libraryListIsRefreshing.postValue(false) }
|
||||
.doOnError { throwable ->
|
||||
if (throwable is OutOfMemoryError) {
|
||||
Log.e("ZimManageViewModel", "Error----${throwable.printStackTrace()}")
|
||||
}
|
||||
.onEach { libraryListIsRefreshing.postValue(false) }
|
||||
.catch { throwable ->
|
||||
libraryListIsRefreshing.postValue(false)
|
||||
throwable.printStackTrace()
|
||||
Log.e("ZimManageViewModel", "Error----$throwable")
|
||||
}
|
||||
.collect { _libraryItems.emit(it) }
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
libraryItems::postValue,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun updateLanguagesInDao(
|
||||
library: Flowable<LibraryNetworkEntity>,
|
||||
languages: io.reactivex.rxjava3.core.Flowable<List<Language>>
|
||||
) = library
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(LibraryNetworkEntity::book)
|
||||
.withLatestFrom(
|
||||
languages,
|
||||
BiFunction(::combineToLanguageList)
|
||||
)
|
||||
.map { it.sortedBy(Language::language) }
|
||||
.filter(List<Language>::isNotEmpty)
|
||||
.subscribe(
|
||||
languageDao::insert,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
library: MutableSharedFlow<LibraryNetworkEntity>,
|
||||
languages: Flow<List<Language>>,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) =
|
||||
combine(
|
||||
library.map { it.book }.filterNotNull(),
|
||||
languages
|
||||
) { books, existingLanguages ->
|
||||
combineToLanguageList(books, existingLanguages)
|
||||
}.map { it.sortedBy(Language::language) }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.catch { it.printStackTrace() }
|
||||
.onEach { languageDao.insert(it) }
|
||||
.flowOn(dispatcher)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
private fun combineToLanguageList(
|
||||
booksFromNetwork: List<Book>,
|
||||
@ -678,18 +708,16 @@ class ZimManageViewModel @Inject constructor(
|
||||
) = booksFromFileSystem.filterNot { idsInDao.contains(it.book.id) }
|
||||
|
||||
private fun updateBookItems() =
|
||||
viewModelScope.launch {
|
||||
dataSource.booksOnDiskAsListItems()
|
||||
.catch { it.printStackTrace() }
|
||||
.collect { newList ->
|
||||
.onEach { newList ->
|
||||
val currentState = fileSelectListStates.value
|
||||
val updatedState = currentState?.let {
|
||||
inheritSelections(it, newList.toMutableList())
|
||||
} ?: FileSelectListState(newList)
|
||||
|
||||
fileSelectListStates.postValue(updatedState)
|
||||
}
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
private fun inheritSelections(
|
||||
oldState: FileSelectListState,
|
||||
@ -706,35 +734,4 @@ class ZimManageViewModel @Inject constructor(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUpUncaughtErrorHandlerForOnlineLibrary(
|
||||
library: PublishProcessor<LibraryNetworkEntity>
|
||||
) {
|
||||
RxJavaPlugins.setErrorHandler { exception ->
|
||||
if (exception is RuntimeException && exception.cause == IOException()) {
|
||||
Log.i(
|
||||
TAG_RX_JAVA_DEFAULT_ERROR_HANDLER,
|
||||
"Caught undeliverable exception: ${exception.cause}"
|
||||
)
|
||||
}
|
||||
when (exception) {
|
||||
is UndeliverableException -> {
|
||||
library.onNext(
|
||||
LibraryNetworkEntity().apply { book = LinkedList() }
|
||||
).also {
|
||||
Log.i(
|
||||
TAG_RX_JAVA_DEFAULT_ERROR_HANDLER,
|
||||
"Caught undeliverable exception: ${exception.cause}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Thread.currentThread().also { thread ->
|
||||
thread.uncaughtExceptionHandler?.uncaughtException(thread, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,28 +23,32 @@ import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.asFlow
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.jraska.livedata.test
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
@ -91,11 +95,9 @@ import org.kiwix.sharedFunctions.bookOnDisk
|
||||
import org.kiwix.sharedFunctions.downloadModel
|
||||
import org.kiwix.sharedFunctions.language
|
||||
import org.kiwix.sharedFunctions.libraryNetworkEntity
|
||||
import org.kiwix.sharedFunctions.resetSchedulers
|
||||
import org.kiwix.sharedFunctions.setScheduler
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
class ZimManageViewModelTest {
|
||||
private val downloadRoomDao: DownloadRoomDao = mockk()
|
||||
@ -125,24 +127,20 @@ class ZimManageViewModelTest {
|
||||
MutableStateFlow<FileSystemState>(FileSystemState.DetectingFileSystem)
|
||||
private val networkStates: PublishProcessor<NetworkState> = PublishProcessor.create()
|
||||
private val booksOnDiskListItems = MutableStateFlow<List<BooksOnDiskListItem>>(emptyList())
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
private val testScheduler = TestScheduler()
|
||||
|
||||
init {
|
||||
setScheduler(testScheduler)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
Dispatchers.resetMain()
|
||||
viewModel.onClearedExposed()
|
||||
resetSchedulers()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
clearAllMocks()
|
||||
every { defaultLanguageProvider.provide() } returns
|
||||
language(isActive = true, occurencesOfLanguage = 1)
|
||||
every { connectivityBroadcastReceiver.action } returns "test"
|
||||
every { downloadRoomDao.downloads() } returns downloads
|
||||
every { newBookDao.books() } returns books
|
||||
@ -165,6 +163,13 @@ class ZimManageViewModelTest {
|
||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
} returns networkCapabilities
|
||||
every { networkCapabilities.hasTransport(TRANSPORT_WIFI) } returns true
|
||||
every { sharedPreferenceUtil.prefWifiOnly } returns true
|
||||
downloads.value = emptyList()
|
||||
booksOnFileSystem.value = emptyList()
|
||||
books.value = emptyList()
|
||||
languages.value = emptyList()
|
||||
fileSystemStates.value = FileSystemState.DetectingFileSystem
|
||||
booksOnDiskListItems.value = emptyList()
|
||||
viewModel =
|
||||
ZimManageViewModel(
|
||||
downloadRoomDao,
|
||||
@ -184,7 +189,8 @@ class ZimManageViewModelTest {
|
||||
setIsUnitTestCase()
|
||||
setAlertDialogShower(alertDialogShower)
|
||||
}
|
||||
testScheduler.triggerActions()
|
||||
viewModel.fileSelectListStates.value = FileSelectListState(emptyList())
|
||||
runBlocking { viewModel.networkLibrary.emit(libraryNetworkEntity()) }
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ -215,35 +221,29 @@ class ZimManageViewModelTest {
|
||||
|
||||
@Nested
|
||||
inner class Books {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `emissions from data source are observed`() = runTest {
|
||||
val expectedList = listOf(bookOnDisk())
|
||||
booksOnDiskListItems.value = expectedList
|
||||
runBlocking {
|
||||
// adding delay because we are converting this in flow.
|
||||
delay(3000)
|
||||
testFlow(
|
||||
viewModel.fileSelectListStates.asFlow(),
|
||||
triggerAction = { booksOnDiskListItems.emit(expectedList) },
|
||||
assert = {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(FileSelectListState(expectedList))
|
||||
}
|
||||
viewModel.fileSelectListStates.test()
|
||||
.assertValue(FileSelectListState(expectedList))
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
@Disabled(
|
||||
"this is flaky due to converting the `rxJava` to flow in ZimManageViewModel.\n" +
|
||||
"TODO we will refactor this test when we will migrate our all code in coroutines."
|
||||
)
|
||||
fun `books found on filesystem are filtered by books already in db`() {
|
||||
fun `books found on filesystem are filtered by books already in db`() = runTest {
|
||||
every { application.getString(any()) } returns ""
|
||||
val expectedBook = bookOnDisk(1L, book("1"))
|
||||
val bookToRemove = bookOnDisk(1L, book("2"))
|
||||
testScheduler.triggerActions()
|
||||
runBlocking { viewModel.requestFileSystemCheck.emit(Unit) }
|
||||
testScheduler.triggerActions()
|
||||
runBlocking { books.emit(listOf(bookToRemove)) }
|
||||
testScheduler.triggerActions()
|
||||
runBlocking {
|
||||
advanceUntilIdle()
|
||||
viewModel.requestFileSystemCheck.emit(Unit)
|
||||
advanceUntilIdle()
|
||||
books.emit(listOf(bookToRemove))
|
||||
advanceUntilIdle()
|
||||
booksOnFileSystem.emit(
|
||||
listOf(
|
||||
expectedBook,
|
||||
@ -251,9 +251,8 @@ class ZimManageViewModelTest {
|
||||
bookToRemove
|
||||
)
|
||||
)
|
||||
}
|
||||
runBlocking { delay(3000) }
|
||||
verify {
|
||||
advanceUntilIdle()
|
||||
coVerify {
|
||||
newBookDao.insert(listOf(expectedBook))
|
||||
}
|
||||
}
|
||||
@ -262,7 +261,7 @@ class ZimManageViewModelTest {
|
||||
@Nested
|
||||
inner class Languages {
|
||||
@Test
|
||||
fun `network no result & empty language db activates the default locale`() {
|
||||
fun `network no result & empty language db activates the default locale`() = runTest {
|
||||
val expectedLanguage =
|
||||
Language(
|
||||
active = true,
|
||||
@ -277,11 +276,12 @@ class ZimManageViewModelTest {
|
||||
listOf(),
|
||||
expectedLanguage
|
||||
)
|
||||
advanceUntilIdle()
|
||||
verify { newLanguagesDao.insert(listOf(expectedLanguage)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network no result & a language db result triggers nothing`() {
|
||||
fun `network no result & a language db result triggers nothing`() = runTest {
|
||||
expectNetworkDbAndDefault(
|
||||
listOf(),
|
||||
listOf(
|
||||
@ -296,11 +296,12 @@ class ZimManageViewModelTest {
|
||||
),
|
||||
language(isActive = true, occurencesOfLanguage = 1)
|
||||
)
|
||||
verify(exactly = 0) { newLanguagesDao.insert(any()) }
|
||||
verify { newLanguagesDao.insert(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network result & empty language db triggers combined result of default + network`() {
|
||||
fun `network result & empty language db triggers combined result of default + network`() =
|
||||
runTest {
|
||||
val defaultLanguage =
|
||||
Language(
|
||||
active = true,
|
||||
@ -337,7 +338,8 @@ class ZimManageViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network result & language db results activates a combined network + db result`() {
|
||||
fun `network result & language db results activates a combined network + db result`() =
|
||||
runTest {
|
||||
val dbLanguage =
|
||||
Language(
|
||||
active = true,
|
||||
@ -356,6 +358,7 @@ class ZimManageViewModelTest {
|
||||
listOf(dbLanguage),
|
||||
language(isActive = true, occurencesOfLanguage = 1)
|
||||
)
|
||||
advanceUntilIdle()
|
||||
verify {
|
||||
newLanguagesDao.insert(
|
||||
listOf(
|
||||
@ -364,59 +367,53 @@ class ZimManageViewModelTest {
|
||||
active = false,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "fra",
|
||||
languageLocalized = "",
|
||||
languageCode = "",
|
||||
languageCodeISO2 = ""
|
||||
languageLocalized = "fra",
|
||||
languageCode = "fra",
|
||||
languageCodeISO2 = "fra"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expectNetworkDbAndDefault(
|
||||
private suspend fun TestScope.expectNetworkDbAndDefault(
|
||||
networkBooks: List<Book>,
|
||||
dbBooks: List<Language>,
|
||||
defaultLanguage: Language
|
||||
) {
|
||||
every { application.getString(any()) } returns ""
|
||||
every { application.getString(any(), any()) } returns ""
|
||||
every { kiwixService.library } returns Single.just(libraryNetworkEntity(networkBooks))
|
||||
coEvery { kiwixService.getLibrary() } returns libraryNetworkEntity(networkBooks)
|
||||
every { defaultLanguageProvider.provide() } returns defaultLanguage
|
||||
viewModel.networkLibrary.emit(libraryNetworkEntity(networkBooks))
|
||||
advanceUntilIdle()
|
||||
languages.value = dbBooks
|
||||
testScheduler.triggerActions()
|
||||
advanceUntilIdle()
|
||||
networkStates.onNext(CONNECTED)
|
||||
testScheduler.triggerActions()
|
||||
advanceUntilIdle()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `network states observed`() {
|
||||
fun `network states observed`() = runTest {
|
||||
networkStates.offer(NOT_CONNECTED)
|
||||
advanceUntilIdle()
|
||||
viewModel.networkStates.test()
|
||||
.assertValue(NOT_CONNECTED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `library update removes from sources and maps to list items`() {
|
||||
fun `library update removes from sources and maps to list items`() = runTest {
|
||||
val bookAlreadyOnDisk = book(id = "0", url = "", language = Locale.ENGLISH.language)
|
||||
val bookDownloading = book(id = "1", url = "")
|
||||
val bookWithActiveLanguage = book(id = "3", language = "activeLanguage", url = "")
|
||||
val bookWithInactiveLanguage = book(id = "4", language = "inactiveLanguage", url = "")
|
||||
testFlow(
|
||||
flow = viewModel.libraryItems,
|
||||
triggerAction = {
|
||||
every { application.getString(any()) } returns ""
|
||||
every { application.getString(any(), any()) } returns ""
|
||||
every {
|
||||
kiwixService.library
|
||||
} returns
|
||||
Single.just(
|
||||
libraryNetworkEntity(
|
||||
listOf(
|
||||
bookAlreadyOnDisk,
|
||||
bookDownloading,
|
||||
bookWithActiveLanguage,
|
||||
bookWithInactiveLanguage
|
||||
)
|
||||
)
|
||||
)
|
||||
networkStates.onNext(CONNECTED)
|
||||
downloads.value = listOf(downloadModel(book = bookDownloading))
|
||||
books.value = listOf(bookOnDisk(book = bookAlreadyOnDisk))
|
||||
@ -426,10 +423,20 @@ class ZimManageViewModelTest {
|
||||
language(isActive = false, occurencesOfLanguage = 1, languageCode = "inactiveLanguage")
|
||||
)
|
||||
fileSystemStates.value = CanWrite4GbFile
|
||||
testScheduler.advanceTimeBy(500, MILLISECONDS)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.libraryItems.test()
|
||||
.assertValue(
|
||||
viewModel.networkLibrary.emit(
|
||||
libraryNetworkEntity(
|
||||
listOf(
|
||||
bookAlreadyOnDisk,
|
||||
bookDownloading,
|
||||
bookWithActiveLanguage,
|
||||
bookWithInactiveLanguage
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
assert = {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
listOf(
|
||||
LibraryListItem.DividerItem(Long.MAX_VALUE, R.string.downloading),
|
||||
LibraryListItem.LibraryDownloadItem(downloadModel(book = bookDownloading)),
|
||||
@ -440,9 +447,12 @@ class ZimManageViewModelTest {
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `library marks files over 4GB as can't download if file system state says to`() {
|
||||
fun `library marks files over 4GB as can't download if file system state says to`() = runTest {
|
||||
val bookOver4Gb =
|
||||
book(
|
||||
id = "0",
|
||||
@ -451,9 +461,9 @@ class ZimManageViewModelTest {
|
||||
)
|
||||
every { application.getString(any()) } returns ""
|
||||
every { application.getString(any(), any()) } returns ""
|
||||
every {
|
||||
kiwixService.library
|
||||
} returns Single.just(libraryNetworkEntity(listOf(bookOver4Gb)))
|
||||
testFlow(
|
||||
viewModel.libraryItems,
|
||||
triggerAction = {
|
||||
networkStates.onNext(CONNECTED)
|
||||
downloads.value = listOf()
|
||||
books.value = listOf()
|
||||
@ -462,16 +472,19 @@ class ZimManageViewModelTest {
|
||||
language(isActive = true, occurencesOfLanguage = 1, languageCode = "activeLanguage")
|
||||
)
|
||||
fileSystemStates.value = CannotWrite4GbFile
|
||||
testScheduler.advanceTimeBy(500, MILLISECONDS)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.libraryItems.test()
|
||||
.assertValue(
|
||||
viewModel.networkLibrary.emit(libraryNetworkEntity(listOf(bookOver4Gb)))
|
||||
},
|
||||
assert = {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
listOf(
|
||||
LibraryListItem.DividerItem(Long.MIN_VALUE, R.string.other_languages),
|
||||
LibraryListItem.BookItem(bookOver4Gb, CannotWrite4GbFile)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SideEffects {
|
||||
|
@ -12,7 +12,7 @@
|
||||
<ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList<KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID>
|
||||
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList<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:Repository.kt$Repository$( @param:IO private val ioThread: Scheduler, private val bookDao: NewBookDao, 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 bookDao: NewBookDao, 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:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup, videoView: ViewGroup, webViewClient: CoreWebViewClient, private val toolbarView: View, private val bottomBarView: View, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
|
||||
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
|
||||
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
|
||||
|
@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.core.data
|
||||
|
||||
import io.reactivex.Completable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
|
||||
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
|
||||
@ -33,21 +32,21 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListIte
|
||||
*/
|
||||
interface DataSource {
|
||||
fun getLanguageCategorizedBooks(): Flow<List<BooksOnDiskListItem>>
|
||||
fun saveBook(book: BookOnDisk): Completable
|
||||
fun saveBooks(book: List<BookOnDisk>): Completable
|
||||
fun saveLanguages(languages: List<Language>): Completable
|
||||
fun saveHistory(history: HistoryItem): Completable
|
||||
fun deleteHistory(historyList: List<HistoryListItem>): Completable
|
||||
fun clearHistory(): Completable
|
||||
suspend fun saveBook(book: BookOnDisk)
|
||||
suspend fun saveBooks(book: List<BookOnDisk>)
|
||||
suspend fun saveLanguages(languages: List<Language>)
|
||||
suspend fun saveHistory(history: HistoryItem)
|
||||
suspend fun deleteHistory(historyList: List<HistoryListItem>)
|
||||
suspend fun clearHistory()
|
||||
fun getBookmarks(): Flow<List<LibkiwixBookmarkItem>>
|
||||
suspend fun getCurrentZimBookmarksUrl(): List<String>
|
||||
suspend fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem)
|
||||
suspend fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>)
|
||||
suspend fun deleteBookmark(bookId: String, bookmarkUrl: String)
|
||||
fun booksOnDiskAsListItems(): Flow<List<BooksOnDiskListItem>>
|
||||
fun saveNote(noteListItem: NoteListItem): Completable
|
||||
fun deleteNote(noteTitle: String): Completable
|
||||
fun deleteNotes(noteList: List<NoteListItem>): Completable
|
||||
suspend fun saveNote(noteListItem: NoteListItem)
|
||||
suspend fun deleteNote(noteTitle: String)
|
||||
suspend fun deleteNotes(noteList: List<NoteListItem>)
|
||||
suspend fun insertWebViewPageHistoryItems(webViewHistoryEntityList: List<WebViewHistoryEntity>)
|
||||
fun getAllWebViewPagesHistory(): Flow<List<WebViewHistoryEntity>>
|
||||
suspend fun clearWebViewPagesHistory()
|
||||
|
@ -18,12 +18,11 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.data
|
||||
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Scheduler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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.LibkiwixBookmarks
|
||||
import org.kiwix.kiwixmobile.core.dao.NewBookDao
|
||||
@ -32,7 +31,6 @@ 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.WebViewHistoryEntity
|
||||
import org.kiwix.kiwixmobile.core.di.qualifiers.IO
|
||||
import org.kiwix.kiwixmobile.core.extensions.HeaderizableList
|
||||
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
|
||||
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
|
||||
@ -52,7 +50,6 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class Repository @Inject internal constructor(
|
||||
@param:IO private val ioThread: Scheduler,
|
||||
private val bookDao: NewBookDao,
|
||||
private val libkiwixBookmarks: LibkiwixBookmarks,
|
||||
private val historyRoomDao: HistoryRoomDao,
|
||||
@ -91,33 +88,38 @@ class Repository @Inject internal constructor(
|
||||
.map(MutableList<BooksOnDiskListItem>::toList)
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
override fun saveBooks(books: List<BookOnDisk>) =
|
||||
Completable.fromAction { bookDao.insert(books) }
|
||||
.subscribeOn(ioThread)
|
||||
|
||||
override fun saveBook(book: BookOnDisk) =
|
||||
Completable.fromAction { bookDao.insert(listOf(book)) }
|
||||
.subscribeOn(ioThread)
|
||||
|
||||
override fun saveLanguages(languages: List<Language>) =
|
||||
Completable.fromAction { languageDao.insert(languages) }
|
||||
.subscribeOn(ioThread)
|
||||
|
||||
override fun saveHistory(history: HistoryItem) =
|
||||
Completable.fromAction { historyRoomDao.saveHistory(history) }
|
||||
.subscribeOn(ioThread)
|
||||
|
||||
override fun deleteHistory(historyList: List<HistoryListItem>) =
|
||||
Completable.fromAction {
|
||||
historyRoomDao.deleteHistory(historyList.filterIsInstance(HistoryItem::class.java))
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun saveBooks(books: List<BookOnDisk>) = withContext(Dispatchers.IO) {
|
||||
bookDao.insert(books)
|
||||
}
|
||||
.subscribeOn(ioThread)
|
||||
|
||||
override fun clearHistory() =
|
||||
Completable.fromAction {
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun saveBook(book: BookOnDisk) = withContext(Dispatchers.IO) {
|
||||
bookDao.insert(listOf(book))
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun saveLanguages(languages: List<Language>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
languageDao.insert(languages)
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun saveHistory(history: HistoryItem) = withContext(Dispatchers.IO) {
|
||||
historyRoomDao.saveHistory(history)
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun deleteHistory(historyList: List<HistoryListItem>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
historyRoomDao.deleteHistory(historyList.filterIsInstance<HistoryItem>())
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun clearHistory() = withContext(Dispatchers.IO) {
|
||||
historyRoomDao.deleteAllHistory()
|
||||
recentSearchRoomDao.deleteSearchHistory()
|
||||
}.subscribeOn(ioThread)
|
||||
}
|
||||
|
||||
override fun getBookmarks() =
|
||||
libkiwixBookmarks.bookmarks() as Flow<List<LibkiwixBookmarkItem>>
|
||||
@ -134,13 +136,17 @@ class Repository @Inject internal constructor(
|
||||
override suspend fun deleteBookmark(bookId: String, bookmarkUrl: String) =
|
||||
libkiwixBookmarks.deleteBookmark(bookId, bookmarkUrl)
|
||||
|
||||
override fun saveNote(noteListItem: NoteListItem): Completable =
|
||||
Completable.fromAction { notesRoomDao.saveNote(noteListItem) }
|
||||
.subscribeOn(ioThread)
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun saveNote(noteListItem: NoteListItem) =
|
||||
withContext(Dispatchers.IO) {
|
||||
notesRoomDao.saveNote(noteListItem)
|
||||
}
|
||||
|
||||
override fun deleteNotes(noteList: List<NoteListItem>) =
|
||||
Completable.fromAction { notesRoomDao.deleteNotes(noteList) }
|
||||
.subscribeOn(ioThread)
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun deleteNotes(noteList: List<NoteListItem>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
notesRoomDao.deleteNotes(noteList)
|
||||
}
|
||||
|
||||
override suspend fun insertWebViewPageHistoryItems(
|
||||
webViewHistoryEntityList: List<WebViewHistoryEntity>
|
||||
@ -155,7 +161,8 @@ class Repository @Inject internal constructor(
|
||||
webViewHistoryRoomDao.clearWebViewPagesHistory()
|
||||
}
|
||||
|
||||
override fun deleteNote(noteTitle: String): Completable =
|
||||
Completable.fromAction { notesRoomDao.deleteNote(noteTitle) }
|
||||
.subscribeOn(ioThread)
|
||||
@Suppress("InjectDispatcher")
|
||||
override suspend fun deleteNote(noteTitle: String) = withContext(Dispatchers.IO) {
|
||||
notesRoomDao.deleteNote(noteTitle)
|
||||
}
|
||||
}
|
||||
|
@ -19,25 +19,22 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.data.remote
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okhttp3.OkHttpClient
|
||||
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface KiwixService {
|
||||
@get:GET(LIBRARY_NETWORK_PATH) val library: Single<LibraryNetworkEntity?>
|
||||
@GET(LIBRARY_NETWORK_PATH)
|
||||
suspend fun getLibrary(): LibraryNetworkEntity?
|
||||
|
||||
@GET
|
||||
fun getMetaLinks(
|
||||
suspend fun getMetaLinks(
|
||||
@Url url: String
|
||||
): Observable<MetaLinkNetworkEntity?>
|
||||
): MetaLinkNetworkEntity?
|
||||
|
||||
/******** Helper class that sets up new services */
|
||||
object ServiceCreator {
|
||||
@ -47,7 +44,6 @@ interface KiwixService {
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(SimpleXmlConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
|
||||
.build()
|
||||
return retrofit.create(KiwixService::class.java)
|
||||
}
|
||||
|
@ -24,15 +24,9 @@ import android.net.ConnectivityManager
|
||||
import android.os.storage.StorageManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.kiwix.kiwixmobile.core.DarkModeConfig
|
||||
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToLibkiwixMigrator
|
||||
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator
|
||||
import org.kiwix.kiwixmobile.core.di.qualifiers.Computation
|
||||
import org.kiwix.kiwixmobile.core.di.qualifiers.IO
|
||||
import org.kiwix.kiwixmobile.core.di.qualifiers.MainThread
|
||||
import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||
@ -68,18 +62,6 @@ class ApplicationModule {
|
||||
@Singleton
|
||||
fun provideObjectBoxToRoomMigrator() = ObjectBoxToRoomMigrator()
|
||||
|
||||
@IO
|
||||
@Provides
|
||||
fun provideIoThread(): Scheduler = Schedulers.io()
|
||||
|
||||
@MainThread
|
||||
@Provides
|
||||
fun provideMainThread(): Scheduler = AndroidSchedulers.mainThread()
|
||||
|
||||
@Computation
|
||||
@Provides
|
||||
fun provideComputationThread(): Scheduler = Schedulers.computation()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideDownloadMonitor(
|
||||
|
@ -18,8 +18,9 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.downloader
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
||||
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
|
||||
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
|
||||
@ -31,26 +32,25 @@ class DownloaderImpl @Inject constructor(
|
||||
private val downloadRoomDao: DownloadRoomDao,
|
||||
private val kiwixService: KiwixService
|
||||
) : Downloader {
|
||||
@Suppress("CheckResult", "IgnoredReturnValue")
|
||||
@Suppress("InjectDispatcher")
|
||||
override fun download(book: LibraryNetworkEntity.Book) {
|
||||
urlProvider(book)
|
||||
.take(1)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
urlProvider(book)?.let {
|
||||
downloadRoomDao.addIfDoesNotExist(it, book, downloadRequester)
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
private fun urlProvider(book: Book): Observable<String> =
|
||||
private suspend fun urlProvider(book: Book): String? =
|
||||
if (book.url?.endsWith("meta4") == true) {
|
||||
kiwixService.getMetaLinks(book.url!!)
|
||||
.map { it.relevantUrl.value }
|
||||
kiwixService.getMetaLinks(book.url!!)?.relevantUrl?.value
|
||||
} else {
|
||||
Observable.just(book.url)
|
||||
book.url
|
||||
}
|
||||
|
||||
override fun cancelDownload(downloadId: Long) {
|
||||
|
@ -41,6 +41,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent
|
||||
import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
@ -73,9 +74,6 @@ import javax.inject.Inject
|
||||
* Notes are saved as text files at location: "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
|
||||
*/
|
||||
|
||||
const val DISABLE_ICON_ITEM_ALPHA = 130
|
||||
const val ENABLE_ICON_ITEM_ALPHA = 255
|
||||
|
||||
class AddNoteDialog : DialogFragment() {
|
||||
private lateinit var zimId: String
|
||||
private var zimFileName: String? = null
|
||||
@ -401,6 +399,7 @@ class AddNoteDialog : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun addNoteToDao(noteFilePath: String?, title: String) {
|
||||
lifecycleScope.launch {
|
||||
noteFilePath?.let { filePath ->
|
||||
if (filePath.isNotEmpty() && zimFileUrl.isNotEmpty()) {
|
||||
val noteToSave = NoteListItem(
|
||||
@ -417,8 +416,10 @@ class AddNoteDialog : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteNote() {
|
||||
lifecycleScope.launch {
|
||||
val notesFolder = File(zimNotesDirectory)
|
||||
val noteFile =
|
||||
File(notesFolder.absolutePath, "$articleNoteFileName.txt")
|
||||
@ -438,6 +439,7 @@ class AddNoteDialog : DialogFragment() {
|
||||
context.toast(R.string.note_delete_unsuccessful, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreDeletedNote(text: String) {
|
||||
val restoreNoteTextFieldValue = TextFieldValue(
|
||||
@ -514,7 +516,6 @@ class AddNoteDialog : DialogFragment() {
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mainRepositoryActions.dispose()
|
||||
onBackPressedCallBack.remove()
|
||||
}
|
||||
|
||||
|
@ -1268,7 +1268,6 @@ abstract class CoreReaderFragment :
|
||||
if (sharedPreferenceUtil?.showIntro() == true) {
|
||||
(requireActivity() as? AppCompatActivity)?.setSupportActionBar(null)
|
||||
}
|
||||
repositoryActions?.dispose()
|
||||
safelyCancelBookmarkJob()
|
||||
unBindViewsAndBinding()
|
||||
tabCallback = null
|
||||
@ -2658,9 +2657,11 @@ abstract class CoreReaderFragment :
|
||||
timeStamp,
|
||||
zimFileReader!!
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
repositoryActions?.saveHistory(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateBottomToolbarVisibility()
|
||||
openFullScreenIfEnabled()
|
||||
updateNightMode()
|
||||
|
@ -17,9 +17,8 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.core.main
|
||||
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
|
||||
import org.kiwix.kiwixmobile.core.data.DataSource
|
||||
@ -36,57 +35,61 @@ private const val TAG = "MainPresenter"
|
||||
|
||||
@ActivityScope
|
||||
class MainRepositoryActions @Inject constructor(private val dataSource: DataSource) {
|
||||
private var saveHistoryDisposable: Disposable? = null
|
||||
private var saveNoteDisposable: Disposable? = null
|
||||
private var saveBookDisposable: Disposable? = null
|
||||
private var deleteNoteDisposable: Disposable? = null
|
||||
private var saveWebViewHistoryDisposable: Disposable? = null
|
||||
private var clearWebViewHistoryDisposable: Disposable? = null
|
||||
|
||||
fun saveHistory(history: HistoryItem) {
|
||||
saveHistoryDisposable =
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun saveHistory(history: HistoryItem) {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
dataSource.saveHistory(history)
|
||||
.subscribe({}, { e -> Log.e(TAG, "Unable to save history", e) })
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to save history", it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher", "TooGenericExceptionCaught")
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun saveBookmark(bookmark: LibkiwixBookmarkItem) {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
dataSource.saveBookmark(bookmark)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save bookmark", e)
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to save bookmark", it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher", "TooGenericExceptionCaught")
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun deleteBookmark(bookId: String, bookmarkUrl: String) {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
dataSource.deleteBookmark(bookId, bookmarkUrl)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete bookmark", e)
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to delete bookmark", it)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNote(note: NoteListItem) {
|
||||
saveNoteDisposable =
|
||||
suspend fun saveNote(note: NoteListItem) {
|
||||
runCatching {
|
||||
dataSource.saveNote(note)
|
||||
.subscribe({}, { e -> Log.e(TAG, "Unable to save note", e) })
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to save note", it)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNote(noteTitle: String) {
|
||||
deleteNoteDisposable =
|
||||
suspend fun deleteNote(noteTitle: String) {
|
||||
runCatching {
|
||||
dataSource.deleteNote(noteTitle)
|
||||
.subscribe({}, { e -> Log.e(TAG, "Unable to delete note", e) })
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to delete note", it)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveBook(book: BookOnDisk) {
|
||||
saveBookDisposable =
|
||||
suspend fun saveBook(book: BookOnDisk) {
|
||||
runCatching {
|
||||
dataSource.saveBook(book)
|
||||
.subscribe({}, { e -> Log.e(TAG, "Unable to save book", e) })
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Unable to save book", it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveWebViewPageHistory(webViewHistoryEntityList: List<WebViewHistoryEntity>) {
|
||||
@ -101,13 +104,4 @@ class MainRepositoryActions @Inject constructor(private val dataSource: DataSour
|
||||
dataSource.getAllWebViewPagesHistory()
|
||||
.first()
|
||||
.map(::WebViewHistoryItem)
|
||||
|
||||
fun dispose() {
|
||||
saveHistoryDisposable?.dispose()
|
||||
saveNoteDisposable?.dispose()
|
||||
deleteNoteDisposable?.dispose()
|
||||
saveBookDisposable?.dispose()
|
||||
saveWebViewHistoryDisposable?.dispose()
|
||||
clearWebViewHistoryDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +164,6 @@ abstract class CorePrefsFragment :
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter?.dispose()
|
||||
storagePermissionForNotesLauncher?.unregister()
|
||||
storagePermissionForNotesLauncher = null
|
||||
super.onDestroyView()
|
||||
@ -262,8 +261,10 @@ abstract class CorePrefsFragment :
|
||||
|
||||
private fun clearAllHistoryDialog() {
|
||||
alertDialogShower?.show(KiwixDialog.ClearAllHistory, {
|
||||
lifecycleScope.launch {
|
||||
presenter?.clearHistory()
|
||||
Snackbar.make(requireView(), R.string.all_history_cleared, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,6 @@ import org.kiwix.kiwixmobile.core.base.BaseContract
|
||||
interface SettingsContract {
|
||||
interface View : BaseContract.View<Presenter?>
|
||||
interface Presenter : BaseContract.Presenter<View?> {
|
||||
fun clearHistory()
|
||||
suspend fun clearHistory()
|
||||
}
|
||||
}
|
||||
|
@ -17,28 +17,20 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.core.settings
|
||||
|
||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||
import io.reactivex.disposables.Disposable
|
||||
import org.kiwix.kiwixmobile.core.base.BasePresenter
|
||||
import org.kiwix.kiwixmobile.core.data.DataSource
|
||||
import org.kiwix.kiwixmobile.core.settings.SettingsContract.Presenter
|
||||
import org.kiwix.kiwixmobile.core.settings.SettingsContract.View
|
||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SettingsPresenter @Inject constructor(private val dataSource: DataSource) :
|
||||
BasePresenter<View?>(), Presenter {
|
||||
private var dataSourceDisposable: Disposable? = null
|
||||
override fun clearHistory() {
|
||||
dataSourceDisposable =
|
||||
override suspend fun clearHistory() {
|
||||
runCatching {
|
||||
dataSource.clearHistory()
|
||||
.subscribe({
|
||||
// TODO
|
||||
}, { e ->
|
||||
Log.e("SettingsPresenter", e.message, e)
|
||||
})
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
dataSourceDisposable?.dispose()
|
||||
}.onFailure {
|
||||
Log.e("SettingsPresenter", it.message, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,13 +22,11 @@ import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
||||
@ -44,8 +42,6 @@ import org.kiwix.kiwixmobile.core.utils.files.testFlow
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
||||
import org.kiwix.sharedFunctions.book
|
||||
import org.kiwix.sharedFunctions.bookOnDisk
|
||||
import org.kiwix.sharedFunctions.resetSchedulers
|
||||
import org.kiwix.sharedFunctions.setScheduler
|
||||
import java.io.File
|
||||
|
||||
class StorageObserverTest {
|
||||
@ -65,15 +61,6 @@ class StorageObserverTest {
|
||||
|
||||
private lateinit var storageObserver: StorageObserver
|
||||
|
||||
init {
|
||||
setScheduler(Schedulers.trampoline())
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
resetSchedulers()
|
||||
}
|
||||
|
||||
@BeforeEach fun init() {
|
||||
clearAllMocks()
|
||||
every { sharedPreferenceUtil.prefStorage } returns "a"
|
||||
|
@ -27,7 +27,6 @@ import android.view.ViewGroup
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
@ -71,8 +70,6 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private var fragmentCustomDownloadBinding: FragmentCustomDownloadBinding? = null
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
override fun inject(baseActivity: BaseActivity) {
|
||||
baseActivity.customActivityComponent.inject(this)
|
||||
}
|
||||
@ -144,7 +141,6 @@ class CustomDownloadFragment : BaseFragment(), FragmentActivityExtensions {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
compositeDisposable.clear()
|
||||
activity?.finish()
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,8 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.custom.download.effects
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.downloader.Downloader
|
||||
import org.kiwix.sharedFunctions.book
|
||||
@ -29,7 +29,7 @@ internal class DownloadCustomTest {
|
||||
fun `invokeWith queues download with ZimUrl`() {
|
||||
val downloader = mockk<Downloader>()
|
||||
DownloadCustom(downloader).invokeWith(mockk())
|
||||
verify {
|
||||
coVerify {
|
||||
downloader.download(expectedBook())
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user