Compare commits

...

27 Commits

Author SHA1 Message Date
f670e222c6 Localize strings 2022-08-21 23:32:41 +02:00
2cee19cf4a Include the connectivity status library as a aar file 2022-08-21 21:30:07 +02:00
738e167904 Send toast messages at the application level and not on a per activity basis 2022-08-21 20:38:51 +02:00
24c22a72e6 Send a message regarding connectivity loss/retrieval on all activities 2022-08-21 19:52:53 +02:00
aff5729041 Include a local copy of the connectivity-status library to solve a bug 2022-08-21 19:47:24 +02:00
fbfb775f7f Send toast messages to the home activity on connectivity changes 2022-08-21 14:11:39 +02:00
9d0bb452e3 Add comment to remember the problem with the connectivity-status library 2022-08-20 21:28:21 +02:00
767805981f Don't reset offline override before updating remote 2022-08-20 21:14:13 +02:00
99b34cfa41 Consider offline override before updating remote 2022-08-20 21:12:39 +02:00
8949f78177 Refactor functions 2022-08-20 21:11:04 +02:00
49abc081b6 Don't send toast messages from the repository 2022-08-20 12:44:20 +02:00
1290d3c4dd Remove unused function 2022-08-20 12:44:20 +02:00
faac7fc621 Prepare the repository functions for DB implementation 2022-08-20 12:44:20 +02:00
59267e6759 Don't create the mercury api if not connection is available 2022-08-20 12:44:20 +02:00
9a33ba0dbb Stop monitoring the network when the app goes in background 2022-08-20 12:44:20 +02:00
e60ed92c6d Do not change the network override from within the repository 2022-08-20 12:44:20 +02:00
3a2fab8e68 Reintroduce network checks where required 2022-08-20 12:44:20 +02:00
e7172853dc Refactor connectivity check 2022-08-20 12:44:20 +02:00
1390ff264f Show a message when the network connection is lost 2022-08-20 12:44:20 +02:00
daaaf902f5 Update todo comments 2022-08-20 12:44:20 +02:00
f78ee18e79 Remove all connectivity checks outside the repository 2022-08-20 12:44:20 +02:00
86dbe06eaa Remove network checks from the home activity 2022-08-20 12:44:20 +02:00
c5826466ad Do not fake offline mode when updating remote 2022-08-20 12:44:20 +02:00
88debf068b Handle the offline override in the repository 2022-08-20 12:44:20 +02:00
5ab9db586d Simplify network connectivity status check 2022-08-20 12:44:20 +02:00
2105044920 Perform network connectivity checks in the repository 2022-08-20 12:44:20 +02:00
davidoskky
e1e43adde4 Add multiplatform connectivity check 2022-08-20 12:44:20 +02:00
15 changed files with 407 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")
@ -190,13 +191,21 @@ 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
// TODO: When updating this library, check if the todo in RepositoryImpl.startNetwork can be resolved
// TODO: Include this library once this is merged https://github.com/ln-12/multiplatform-connectivity-status/pull/4
// implementation("com.github.ln-12:multiplatform-connectivity-status:1.1.0")
implementation(files("../shared/libs/multiplatform-connectivity-status.aar"))
} }

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()
@ -1141,6 +1127,5 @@ 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,27 +7,40 @@ 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 org.kodein.di.* import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware { 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(applicationContext)) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private lateinit var config: Config private lateinit var config: Config
private lateinit var settings : Settings private lateinit var settings : Settings
@ -43,6 +56,18 @@ class MyApp : MultiDexApplication(), DIAware {
tryToHandleBug() tryToHandleBug()
handleNotificationChannels() handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(repository))
CoroutineScope(Dispatchers.Main).launch {
viewModel.toastMessageProvider.collect { toastMessage ->
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
} }
private fun handleNotificationChannels() { private fun handleNotificationChannels() {
@ -102,4 +127,17 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
} }
class AppLifeCycleObserver(val repository: Repository) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
repository.startNetwork()
}
override fun onPause(owner: LifecycleOwner) {
repository.stopNetwork()
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)
@ -97,5 +95,4 @@ 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,11 @@ kotlin {
//Logging //Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.6.1")
// Network information
// TODO: Include this library once this is merged https://github.com/ln-12/multiplatform-connectivity-status/pull/4
//implementation("com.github.ln-12:multiplatform-connectivity-status:1.1.0")
implementation(files("libs/multiplatform-connectivity-status.aar"))
} }
} }
val commonTest by getting { val commonTest by getting {

Binary file not shown.

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,45 +271,46 @@ 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,33 @@ 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: Start and stop the monitoring within Android and iOS code
// This should be performed in the architecture specific code
// However it is not possible on android due to the dash contained in the name of the library
// com.github.ln-12:multiplatform-connectivity-status:1.1.0
// https://github.com/ln-12/multiplatform-connectivity-status/issues/2
fun startNetwork() {
connectionMonitored = true
connectivityStatus.start()
}
fun stopNetwork() {
connectionMonitored = false
connectivityStatus.stop()
}
// TODO: Handle offline actions // TODO: Handle offline actions
} }