From 74717e2b935dd88e5e482591ff888e0225d6fa19 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 14 Oct 2022 17:04:04 +0300 Subject: [PATCH] Handle offline mode in shelf --- .../kotatsu/core/os/NetworkStateObserver.kt | 64 +++++++++++++++++++ .../kotatsu/explore/ui/model/ExploreItem.kt | 4 +- .../kotatsu/list/ui/adapter/EmptyHintAD.kt | 23 +++++++ .../kotatsu/list/ui/model/EmptyHint.kt | 14 ++++ .../kotatsu/shelf/ui/ShelfFragment.kt | 12 +++- .../kotatsu/shelf/ui/ShelfViewModel.kt | 55 ++++++++++++---- .../kotatsu/shelf/ui/adapter/ShelfAdapter.kt | 7 +- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 8 +++ app/src/main/res/values/strings.xml | 2 + 9 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt new file mode 100644 index 000000000..159ef8a4f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt @@ -0,0 +1,64 @@ +package org.koitharu.kotatsu.core.os + +import android.content.Context +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import org.koitharu.kotatsu.utils.ext.connectivityManager +import org.koitharu.kotatsu.utils.ext.isNetworkAvailable +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkStateObserver @Inject constructor( + @ApplicationContext context: Context, +) : StateFlow { + + private val connectivityManager = context.connectivityManager + + override val replayCache: List + get() = listOf(value) + + override var value: Boolean = connectivityManager.isNetworkAvailable + + override suspend fun collect(collector: FlowCollector): Nothing { + collector.emit(value) + while (true) { + observeImpl().collect(collector) + } + } + + private fun observeImpl() = callbackFlow { + val request = NetworkRequest.Builder().build() + val callback = FlowNetworkCallback(this) + connectivityManager.registerNetworkCallback(request, callback) + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + } + + inner class FlowNetworkCallback( + private val producerScope: ProducerScope, + ) : NetworkCallback() { + override fun onAvailable(network: Network) = update() + + override fun onLost(network: Network) = update() + + override fun onUnavailable() = update() + + private fun update() { + val newValue = connectivityManager.isNetworkAvailable + if (value != newValue) { + value = newValue + producerScope.trySendBlocking(newValue) + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt index dcf871c8d..4420a415d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.explore.ui.model -import android.net.Uri import androidx.annotation.DrawableRes import androidx.annotation.StringRes import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -73,6 +72,7 @@ sealed interface ExploreItem : ListModel { } } + @Deprecated("") class EmptyHint( @DrawableRes icon: Int, @StringRes textPrimary: Int, @@ -81,4 +81,4 @@ sealed interface ExploreItem : ListModel { ) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem object Loading : ExploreItem -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt new file mode 100644 index 000000000..aced05368 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding +import org.koitharu.kotatsu.list.ui.model.EmptyHint +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.utils.ext.setTextAndVisible + +fun emptyHintAD( + listener: ListStateHolderListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) }, +) { + + binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() } + + bind { + binding.icon.setImageResource(item.icon) + binding.textPrimary.setText(item.textPrimary) + binding.textSecondary.setTextAndVisible(item.textSecondary) + binding.buttonRetry.setTextAndVisible(item.actionStringRes) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt new file mode 100644 index 000000000..cc42528eb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.list.ui.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +class EmptyHint( + @DrawableRes icon: Int, + @StringRes textPrimary: Int, + @StringRes textSecondary: Int, + @StringRes actionStringRes: Int, +) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) { + + fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index b9769f1b7..19dd12912 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -1,6 +1,9 @@ package org.koitharu.kotatsu.shelf.ui +import android.content.Intent +import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -113,7 +116,14 @@ class ShelfFragment : override fun onRetryClick(error: Throwable) = Unit - override fun onEmptyActionClick() = Unit + override fun onEmptyActionClick() { + val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Settings.Panel.ACTION_INTERNET_CONNECTIVITY + } else { + Settings.ACTION_WIRELESS_SETTINGS + } + startActivity(Intent(action)) + } override fun onWindowInsetsChanged(insets: Insets) { binding.recyclerView.updatePadding( diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 15081f0a9..9577fd323 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.os.NetworkStateObserver import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.favourites.domain.FavouritesRepository @@ -19,6 +20,7 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ListExtraProvider +import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState @@ -26,6 +28,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.shelf.domain.ShelfRepository import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel @@ -41,17 +44,19 @@ class ShelfViewModel @Inject constructor( private val favouritesRepository: FavouritesRepository, private val trackingRepository: TrackingRepository, private val settings: AppSettings, + private val networkStateObserver: NetworkStateObserver, ) : BaseViewModel(), ListExtraProvider { val onActionDone = SingleLiveEvent() val content: LiveData> = combine( + networkStateObserver, historyRepository.observeAllWithHistory(), repository.observeLocalManga(SortOrder.UPDATED), repository.observeFavourites(), trackingRepository.observeUpdatedManga(), - ) { history, local, favourites, updated -> - mapList(history, favourites, updated, local) + ) { isConnected, history, local, favourites, updated -> + mapList(history, favourites, updated, local, isConnected) }.debounce(500) .catch { e -> emit(listOf(e.toErrorState(canRetry = false))) @@ -133,19 +138,36 @@ class ShelfViewModel @Inject constructor( favourites: Map>, updated: Map, local: List, + isNetworkAvailable: Boolean, ): List { - val result = ArrayList(favourites.keys.size + 2) - if (history.isNotEmpty()) { - mapHistory(result, history) - } - if (local.isNotEmpty()) { - mapLocal(result, local) - } - if (updated.isNotEmpty()) { - mapUpdated(result, updated) - } - if (favourites.isNotEmpty()) { - mapFavourites(result, favourites) + val result = ArrayList(favourites.keys.size + 3) + if (isNetworkAvailable) { + if (history.isNotEmpty()) { + mapHistory(result, history) + } + if (local.isNotEmpty()) { + mapLocal(result, local) + } + if (updated.isNotEmpty()) { + mapUpdated(result, updated) + } + if (favourites.isNotEmpty()) { + mapFavourites(result, favourites) + } + } else { + result += EmptyHint( + icon = R.drawable.ic_empty_suggestions, + textPrimary = R.string.network_unavailable, + textSecondary = R.string.network_unavailable_hint, + actionStringRes = R.string.manage, + ) + val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL } + if (offlineHistory.isNotEmpty()) { + mapHistory(result, offlineHistory) + } + if (local.isNotEmpty()) { + mapLocal(result, local) + } } if (result.isEmpty()) { result += EmptyState( @@ -154,6 +176,11 @@ class ShelfViewModel @Inject constructor( textSecondary = R.string.text_shelf_holder_secondary, actionStringRes = 0, ) + } else { + val one = result.singleOrNull() + if (one is EmptyHint) { + result[0] = one.toState() + } } return result } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt index 9afd2fdec..691db99ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt @@ -6,16 +6,17 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import kotlin.jvm.internal.Intrinsics import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller -import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.list.ui.ItemSizeResolver +import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel +import kotlin.jvm.internal.Intrinsics class ShelfAdapter( lifecycleOwner: LifecycleOwner, @@ -40,6 +41,7 @@ class ShelfAdapter( ) .addDelegate(loadingStateAD()) .addDelegate(loadingFooterAD()) + .addDelegate(emptyHintAD(listener)) .addDelegate(emptyStateListAD(coil, listener)) .addDelegate(errorStateListAD(listener)) } @@ -56,6 +58,7 @@ class ShelfAdapter( oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> { oldItem.key == newItem.key } + else -> oldItem.javaClass == newItem.javaClass } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index df12e5507..c5e8a34a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -44,6 +44,14 @@ val Context.activityManager: ActivityManager? val Context.connectivityManager: ConnectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager +val ConnectivityManager.isNetworkAvailable: Boolean + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activeNetwork != null + } else { + @Suppress("DEPRECATION") + activeNetworkInfo?.isConnectedOrConnecting == true + } + fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c074709fc..407cb7f37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -393,4 +393,6 @@ Webtoon zoom Allow zoom in/zoom out gesture in webtoon mode (beta) Different languages + Network is not available + Turn on Wi-Fi or mobile network to read manga online