Compare commits

...

29 Commits

Author SHA1 Message Date
Amine Louveau
a9caaefb4d Merge pull request 'network' (#28) from davidoskky/ReaderForSelfoss-multiplatform:network into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/28
2022-08-22 19:01:15 +00:00
4c78b22614 Use the connectivity-status library from the repository rather than the local copy
Some checks failed
continuous-integration/drone/pr Build is failing
2022-08-22 19:33:58 +02:00
1d5ab3205e Localize strings
Some checks failed
continuous-integration/drone/pr Build is failing
2022-08-21 23:34:47 +02:00
df4903cae5 Include the connectivity status library as a aar file 2022-08-21 23:34:47 +02:00
2a78be69b5 Send toast messages at the application level and not on a per activity basis 2022-08-21 23:34:45 +02:00
8c69bb8c3c Send a message regarding connectivity loss/retrieval on all activities 2022-08-21 23:34:17 +02:00
9203012a97 Include a local copy of the connectivity-status library to solve a bug 2022-08-21 23:34:17 +02:00
2a44162c5a Send toast messages to the home activity on connectivity changes 2022-08-21 23:34:17 +02:00
20588aab81 Add comment to remember the problem with the connectivity-status library 2022-08-21 23:34:17 +02:00
0c8e49214f Don't reset offline override before updating remote 2022-08-21 23:34:17 +02:00
97d5063339 Consider offline override before updating remote 2022-08-21 23:34:17 +02:00
7c37b183d7 Refactor functions 2022-08-21 23:34:17 +02:00
82c4a5a1f9 Don't send toast messages from the repository 2022-08-21 23:34:17 +02:00
47b7062e16 Remove unused function 2022-08-21 23:34:17 +02:00
b9497ca939 Prepare the repository functions for DB implementation 2022-08-21 23:34:17 +02:00
1258ed3ad3 Don't create the mercury api if not connection is available 2022-08-21 23:34:17 +02:00
d838f509d4 Stop monitoring the network when the app goes in background 2022-08-21 23:34:17 +02:00
3c5b606a02 Do not change the network override from within the repository 2022-08-21 23:34:17 +02:00
d1481a1db6 Reintroduce network checks where required 2022-08-21 23:34:17 +02:00
d654b1b0bd Refactor connectivity check 2022-08-21 23:34:17 +02:00
f56861a3c2 Show a message when the network connection is lost 2022-08-21 23:34:17 +02:00
492e7e4aed Update todo comments 2022-08-21 23:34:17 +02:00
551a3e3caa Remove all connectivity checks outside the repository 2022-08-21 23:34:17 +02:00
c224b8a0b3 Remove network checks from the home activity 2022-08-21 23:34:17 +02:00
13ea7a693b Do not fake offline mode when updating remote 2022-08-21 23:34:17 +02:00
0f3c48dd8e Handle the offline override in the repository 2022-08-21 23:34:17 +02:00
d4c2373bac Simplify network connectivity status check 2022-08-21 23:34:17 +02:00
4f32097821 Perform network connectivity checks in the repository 2022-08-21 23:34:17 +02:00
davidoskky
37fa4a1a8e Add multiplatform connectivity check 2022-08-21 23:34:17 +02:00
14 changed files with 391 additions and 332 deletions

View File

@ -181,8 +181,9 @@ dependencies {
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01") implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0") implementation("org.kodein.di:kodein-di:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0") implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
//Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9") implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
@ -193,13 +194,18 @@ dependencies {
//PhotoView //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0") implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.room:room-ktx:2.4.0-beta01") implementation("androidx.room:room-ktx:2.4.0-beta01")
kapt("androidx.room:room-compiler:2.4.0-beta01") kapt("androidx.room:room-compiler:2.4.0-beta01")
implementation("android.arch.work:work-runtime-ktx:1.0.1") implementation("android.arch.work:work-runtime-ktx:1.0.1")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
} }

View File

@ -44,9 +44,9 @@ import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -126,7 +126,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var recyclerAdapter: RecyclerView.Adapter<*>? = null private var recyclerAdapter: RecyclerView.Adapter<*>? = null
private var fromTabShortcut: Boolean = false private var fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false
private lateinit var tagsBadge: Map<Long, Int> private lateinit var tagsBadge: Map<Long, Int>
@ -153,7 +152,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val view = binding.root val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1 fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false) repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) { if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
@ -175,7 +174,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
AppDatabase::class.java, "selfoss-database" AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build() ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
customTabActivityHelper = CustomTabActivityHelper() customTabActivityHelper = CustomTabActivityHelper()
dataBase = AndroidDeviceDatabase(applicationContext) dataBase = AndroidDeviceDatabase(applicationContext)
@ -197,7 +195,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.color.refresh_progress_3 R.color.refresh_progress_3
) )
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false repository.offlineOverride = false
lastFetchDone = false lastFetchDone = false
handleDrawerItems() handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -689,7 +687,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
var sources: List<SelfossModel.Source>? var sources: List<SelfossModel.Source>?
fun sourcesApiCall() { fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources() val response = repository.getSources()
if (response != null) { if (response != null) {
@ -706,15 +703,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
} }
}
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
tags = repository.getTags() tags = repository.getTags()
sourcesApiCall() sourcesApiCall()
} }
} }
}
binding.mainDrawer.itemAdapter.add( binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply { PrimaryDrawerItem().apply {
@ -944,13 +938,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() { private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) { if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (applicationContext.isNetworkAvailable()) {
repository.reloadBadges() repository.reloadBadges()
reloadBadgeContent() reloadBadgeContent()
} }
} }
} }
}
private fun reloadBadgeContent() { private fun reloadBadgeContent() {
if (displayUnreadCount) { if (displayUnreadCount) {
@ -1021,13 +1013,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.refresh -> { R.id.refresh -> {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO // TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
// TODO: Send toast messages from the repository
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG R.string.refresh_success_response, Toast.LENGTH_LONG
@ -1043,16 +1035,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
return true return true
} else {
return false
}
} }
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) { if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
@ -1078,7 +1066,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
} }
}
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
@ -1127,7 +1114,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions() val actions = db.actionsDao().actions()
@ -1142,5 +1128,4 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
} }
}

View File

@ -15,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
@ -202,7 +201,6 @@ class LoginActivity() : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert) repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val result = repository.login() val result = repository.login()
if (result) { if (result) {
@ -213,7 +211,6 @@ class LoginActivity() : AppCompatActivity(), DIAware {
} }
} }
} }
}
showProgress(false) showProgress(false)
} }
} }

View File

@ -7,18 +7,28 @@ import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.github.ln_12.library.ConnectivityStatus
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import org.kodein.di.* import org.kodein.di.*
@ -27,9 +37,14 @@ class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
import(networkModule) import(networkModule)
bind<Repository>() with singleton { Repository(instance(), instance()) } bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private lateinit var config: Config private lateinit var config: Config
private lateinit var settings : Settings private lateinit var settings : Settings
@ -46,6 +61,18 @@ class MyApp : MultiDexApplication(), DIAware {
tryToHandleBug() tryToHandleBug()
handleNotificationChannels() handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
CoroutineScope(Dispatchers.Main).launch {
viewModel.toastMessageProvider.collect { toastMessage ->
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
} }
private fun handleNotificationChannels() { private fun handleNotificationChannels() {
@ -105,4 +132,19 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
} }
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
repository.connectionMonitored = true
connectivityStatus.start()
}
override fun onPause(owner: LifecycleOwner) {
repository.connectionMonitored = false
connectivityStatus.stop()
super.onPause(owner)
}
}
} }

View File

@ -10,7 +10,6 @@ import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
@ -64,7 +63,6 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources() val response = repository.getSources()
if (response != null) { if (response != null) {
@ -89,7 +87,6 @@ class SourcesActivity : AppCompatActivity(), DIAware {
).show() ).show()
} }
} }
}
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))

View File

@ -16,7 +16,6 @@ import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
@ -110,7 +109,6 @@ class ItemCardAdapter(
binding.favButton.setOnClickListener { binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition] val item = items[bindingAdapterPosition]
if (c.isNetworkAvailable()) {
if (item.starred) { if (item.starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item) repository.unstarr(item)
@ -127,7 +125,6 @@ class ItemCardAdapter(
binding.favButton.isSelected = true binding.favButton.isSelected = true
} }
} }
}
binding.shareBtn.setOnClickListener { binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition] val item = items[bindingAdapterPosition]

View File

@ -14,7 +14,6 @@ import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
@ -78,7 +77,6 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition] val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id) val successfullyDeletedSource = repository.deleteSource(id)
@ -98,4 +96,3 @@ class SourcesListAdapter(
} }
} }
} }
}

View File

@ -21,7 +21,7 @@ import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATIO
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -44,9 +44,7 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
override fun doWork(): Result { override fun doWork(): Result {
val settings = Settings() val settings = Settings()
val periodicRefresh = settings.getBoolean("periodic_refresh", false) val periodicRefresh = settings.getBoolean("periodic_refresh", false)
if (periodicRefresh) { if (periodicRefresh && isNetworkAccessible(context)) {
if (context.isNetworkAvailable()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val notificationManager = val notificationManager =
@ -94,7 +92,6 @@ override fun doWork(): Result {
} }
} }
if (context.isNetworkAvailable()) {
launch { launch {
try { try {
val newItems = repository.allItems(ItemType.UNREAD) val newItems = repository.allItems(ItemType.UNREAD)
@ -106,8 +103,6 @@ override fun doWork(): Result {
} }
} }
} }
}
}
return Result.success() return Result.success()
} }

View File

@ -35,7 +35,6 @@ import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
@ -276,7 +275,7 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) { private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
if ((context != null && requireContext().isNetworkAvailable(null)) || context == null) { if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi() val parser = MercuryApi()
@ -317,7 +316,10 @@ class ArticleFragment : Fragment(), DIAware {
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty()) .loadMaybeBasicAuth(
config,
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,52 +1,14 @@
package bou.amine.apps.readerforselfossv2.android.utils.network package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context import android.content.Context
import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.view.View
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.R
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
var snackBarShown = false
var view: View? = null
lateinit var s: Snackbar lateinit var s: Snackbar
fun Context.isNetworkAvailable( fun isNetworkAccessible(context: Context): Boolean {
v: View? = null,
overrideOffline: Boolean = false
): Boolean {
val networkIsAccessible = isNetworkAccessible(this)
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v
s = Snackbar
.make(
v,
R.string.no_network_connectivity,
Snackbar.LENGTH_INDEFINITE
)
s.setAction(android.R.string.ok) {
snackBarShown = false
s.dismiss()
}
val view = s.view
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
s.show()
snackBarShown = true
}
if (snackBarShown && networkIsAccessible && !overrideOffline) {
s.dismiss()
}
return if(overrideOffline) overrideOffline else networkIsAccessible
}
private fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

View File

@ -0,0 +1,29 @@
package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class AppViewModel(private val repository: Repository) : ViewModel() {
private val _toastMessageProvider = MutableSharedFlow<Int>()
val toastMessageProvider = _toastMessageProvider.asSharedFlow()
private var wasConnected = true
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
if (isConnected && !wasConnected && repository.connectionMonitored) {
_toastMessageProvider.emit(R.string.network_connectivity_retrieved)
wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored){
_toastMessageProvider.emit(R.string.network_connectivity_lost)
wasConnected = false
}
}
}
}
}

View File

@ -143,6 +143,8 @@
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string> <string name="no_network_connectivity">Not connected !</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string> <string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string> <string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string> <string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>

View File

@ -36,6 +36,9 @@ kotlin {
//Logging //Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.6.1")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
} }
} }
val commonTest by getting { val commonTest by getting {

View File

@ -5,16 +5,19 @@ import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.utils.DateUtils import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import com.github.ln_12.library.ConnectivityStatus
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService) { class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, val connectivityStatus: ConnectivityStatus) {
val settings = Settings() val settings = Settings()
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
val isConnectionAvailable = connectivityStatus.isNetworkConnected
var connectionMonitored = false
var baseUrl = apiDetails.getBaseUrl() var baseUrl = apiDetails.getBaseUrl()
lateinit var dateUtils: DateUtils lateinit var dateUtils: DateUtils
@ -26,6 +29,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
var searchFilter: String? = null var searchFilter: String? = null
var itemsCaching = settings.getBoolean("items_caching", false) var itemsCaching = settings.getBoolean("items_caching", false)
var offlineOverride = false
var apiMajorVersion = 0 var apiMajorVersion = 0
var badgeUnread = 0 var badgeUnread = 0
@ -45,14 +49,21 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity, use the updatedSince parameter // TODO: Use the updatedSince parameter
val fetchedItems = api.getItems(displayedItems.type, var fetchedItems: List<SelfossModel.Item>? = null
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(), settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0, offset = 0,
tagFilter?.tag, tagFilter?.tag,
sourceFilter?.id?.toLong(), sourceFilter?.id?.toLong(),
searchFilter, searchFilter,
null) null
)
} else {
// TODO: Get items from the database
}
if (fetchedItems != null) { if (fetchedItems != null) {
items = ArrayList(fetchedItems) items = ArrayList(fetchedItems)
@ -61,15 +72,21 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity var fetchedItems: List<SelfossModel.Item>? = null
if (isNetworkAvailable()) {
val offset = items.size val offset = items.size
val fetchedItems = api.getItems(displayedItems.type, fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(), settings.getString("prefer_api_items_number", "200").toInt(),
offset, offset,
tagFilter?.tag, tagFilter?.tag,
sourceFilter?.id?.toLong(), sourceFilter?.id?.toLong(),
searchFilter, searchFilter,
null) null
)
} else {
// TODO: Get items from the database
}
if (fetchedItems != null) { if (fetchedItems != null) {
appendItems(fetchedItems) appendItems(fetchedItems)
@ -77,8 +94,22 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return items return items
} }
suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? = suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? {
api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null) return if (isNetworkAvailable()) {
api.getItems(
itemType.type,
200,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
// TODO: Provide an error message
null
}
}
private fun appendItems(fetchedItems: List<SelfossModel.Item>) { private fun appendItems(fetchedItems: List<SelfossModel.Item>) {
// TODO: Store in DB if enabled by user // TODO: Store in DB if enabled by user
@ -94,8 +125,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
// TODO: Check connectivity, calculate from DB
var success = false var success = false
if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response != null) { if (response != null) {
badgeUnread = response.unread badgeUnread = response.unread
@ -103,26 +134,43 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
badgeStarred = response.starred badgeStarred = response.starred
success = true success = true
} }
} else {
// TODO: Compute badges from database
}
return success return success
} }
suspend fun getTags(): List<SelfossModel.Tag>? { suspend fun getTags(): List<SelfossModel.Tag>? {
// TODO: Check success, store in DB // TODO: Store in DB
return api.tags() return if (isNetworkAvailable()) {
api.tags()
} else {
// TODO: Compute from database
null
}
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? { suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
// TODO: Check success, store in DB // TODO: Store in DB
return api.spouts() return if (isNetworkAvailable()) {
api.spouts()
} else {
// TODO: Compute from database
null
}
} }
suspend fun getSources(): ArrayList<SelfossModel.Source>? { suspend fun getSources(): ArrayList<SelfossModel.Source>? {
// TODO: Check success // TODO: Store in DB
return api.sources() return if (isNetworkAvailable()) {
api.sources()
} else {
// TODO: Compute from database
null
}
} }
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
// TODO: Check internet connection
val success = markAsReadById(item.id) val success = markAsReadById(item.id)
if (success) { if (success) {
@ -131,10 +179,9 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success return success
} }
suspend fun markAsReadById(id: Int): Boolean { suspend fun markAsReadById(id: Int): Boolean =
// TODO: Check internet connection isNetworkAvailable() && api.markAsRead(id.toString())?.isSuccess == true
return api.markAsRead(id.toString())?.isSuccess == true
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -145,10 +192,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success return success
} }
suspend fun unmarkAsReadById(id: Int): Boolean { suspend fun unmarkAsReadById(id: Int): Boolean =
// TODO: Check internet connection isNetworkAvailable() && api.unmarkAsRead(id.toString())?.isSuccess == true
return api.unmarkAsRead(id.toString())?.isSuccess == true
}
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -159,10 +204,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success return success
} }
suspend fun starrById(id: Int): Boolean { suspend fun starrById(id: Int): Boolean =
// TODO: Check success, store in DB isNetworkAvailable() && api.starr(id.toString())?.isSuccess == true
return api.starr(id.toString())?.isSuccess == true
}
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -173,17 +216,15 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return success return success
} }
suspend fun unstarrById(id: Int): Boolean { suspend fun unstarrById(id: Int): Boolean =
// TODO: Check internet connection isNetworkAvailable() && api.unstarr(id.toString())?.isSuccess == true
return api.unstarr(id.toString())?.isSuccess == true
}
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
// TODO: Check Internet connectivity, store in DB var success = false
val success = api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true) {
success = true
if (success) {
for (item in items) { for (item in items) {
markAsReadLocally(item) markAsReadLocally(item)
} }
@ -230,46 +271,47 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
tags: String, tags: String,
filter: String filter: String
): Boolean { ): Boolean {
// TODO: Check connectivity var response = false
val response = api.createSourceForVersion( if (isNetworkAvailable()) {
response = api.createSourceForVersion(
title, title,
url, url,
spout, spout,
tags, tags,
filter, filter,
apiMajorVersion apiMajorVersion
) )?.isSuccess == true
}
return response != null return response
} }
suspend fun deleteSource(id: Int): Boolean { suspend fun deleteSource(id: Int): Boolean {
// TODO: Check connectivity, store in DB // TODO: Store in DB
var success = false var success = false
if (isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
if (response != null) { if (response != null) {
success = response.isSuccess success = response.isSuccess
} }
}
return success return success
} }
suspend fun updateRemote(): Boolean { suspend fun updateRemote(): Boolean =
// TODO: Handle connectivity issues isNetworkAvailable() && api.update()?.isSuccess == true
val response = api.update()
return response?.isSuccess ?: false
}
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
if (isNetworkAvailable()) {
try { try {
val response = api.login() val response = api.login()
if (response != null && response.isSuccess) { result = response?.isSuccess == true
result = true
}
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
} }
}
return result return result
} }
@ -287,15 +329,18 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
private suspend fun updateApiVersion() { private suspend fun updateApiVersion() {
// TODO: Handle connectivity issues apiMajorVersion = settings.getInt("apiVersionMajor", 0)
if (isNetworkAvailable()) {
val fetchedVersion = api.version() val fetchedVersion = api.version()
if (fetchedVersion != null) { if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion() apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion) settings.putInt("apiVersionMajor", apiMajorVersion)
} else { }
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
} }
} }
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
// TODO: Handle offline actions // TODO: Handle offline actions
} }