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