Compare commits

...

17 Commits

Author SHA1 Message Date
17f7d58aed Don't send toast messages from the repository 2022-08-20 12:42:24 +02:00
ad2dfcae1b Remove unused function 2022-08-20 12:40:38 +02:00
ef0f326073 Prepare the repository functions for DB implementation 2022-08-20 12:37:11 +02:00
3aa6934f86 Don't create the mercury api if not connection is available 2022-08-20 12:17:15 +02:00
0cec538d18 Stop monitoring the network when the app goes in background 2022-08-20 12:15:36 +02:00
5eb46dc4e9 Do not change the network override from within the repository 2022-08-18 14:50:19 +02:00
a0d6159cf4 Reintroduce network checks where required 2022-08-18 14:48:27 +02:00
bea8f18db4 Refactor connectivity check 2022-08-18 14:45:10 +02:00
9712f2846f Show a message when the network connection is lost 2022-08-17 21:17:50 +02:00
c361819664 Update todo comments 2022-08-17 20:26:02 +02:00
0145349817 Remove all connectivity checks outside the repository 2022-08-17 20:20:44 +02:00
e71f8718d7 Remove network checks from the home activity 2022-08-17 20:12:45 +02:00
8f5cc6efa4 Do not fake offline mode when updating remote 2022-08-17 20:12:25 +02:00
0fa133d89e Handle the offline override in the repository 2022-08-17 20:04:31 +02:00
f54bdf93d3 Simplify network connectivity status check 2022-08-17 19:56:24 +02:00
f7a29d66ca Perform network connectivity checks in the repository 2022-08-17 17:58:16 +02:00
davidoskky
734b0b7112 Add multiplatform connectivity check 2022-08-17 17:47:34 +02:00
12 changed files with 357 additions and 318 deletions

View File

@ -190,13 +190,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.1.0")
} }

View File

@ -44,7 +44,6 @@ 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.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@ -126,7 +125,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 +151,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))
@ -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 {
@ -709,30 +707,26 @@ 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) { sources = response
sources = response val apiDrawerData = DrawerData(tags, sources)
val apiDrawerData = DrawerData(tags, sources) if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { handleDrawerData(apiDrawerData)
handleDrawerData(apiDrawerData) }
} } else {
} else { val apiDrawerData = DrawerData(tags, null)
val apiDrawerData = DrawerData(tags, null) if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { handleDrawerData(apiDrawerData)
handleDrawerData(apiDrawerData)
}
} }
} }
} }
} }
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) { CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { tags = repository.getTags()
tags = repository.getTags() sourcesApiCall()
sourcesApiCall()
}
} }
} }
@ -964,10 +958,8 @@ 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()
}
} }
} }
} }
@ -1041,61 +1033,57 @@ 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 { repository.offlineOverride = false
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
Toast.makeText( // TODO: Send toast messages from the repository
this@HomeActivity, Toast.makeText(
R.string.refresh_success_response, Toast.LENGTH_LONG this@HomeActivity,
) R.string.refresh_success_response, Toast.LENGTH_LONG
.show() )
} else { .show()
Toast.makeText( } else {
this@HomeActivity, Toast.makeText(
R.string.refresh_failer_message, this@HomeActivity,
Toast.LENGTH_SHORT R.string.refresh_failer_message,
).show() Toast.LENGTH_SHORT
} ).show()
} }
} }
return true
} else {
return false
} }
return true
} }
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) { Toast.makeText(
Toast.makeText( this@HomeActivity,
this@HomeActivity, R.string.all_posts_read,
R.string.all_posts_read, Toast.LENGTH_SHORT
Toast.LENGTH_SHORT ).show()
).show() tabNewBadge.removeBadge()
tabNewBadge.removeBadge()
handleDrawerItems() handleDrawerItems()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_not_read, R.string.all_posts_not_read,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
} }
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
} }
} }
} }
@ -1147,17 +1135,15 @@ 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()
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action) action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action) action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(repository.starrById(action.articleId.toInt()), action) action.starred -> doAndReportOnFail(repository.starrById(action.articleId.toInt()), action)
action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleId.toInt()), action) action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleId.toInt()), action)
}
} }
} }
} }

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,15 +201,13 @@ 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) { goToMain()
goToMain() } else {
} else { CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.Main).launch { preferenceError(Exception("Not success"))
preferenceError(Exception("Not success"))
}
} }
} }
} }

View File

@ -7,6 +7,9 @@ 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 androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
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
@ -16,6 +19,7 @@ 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
@ -25,9 +29,10 @@ 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)) }
} }
private val repository: Repository by instance()
private lateinit var config: Config private lateinit var config: Config
private lateinit var settings : Settings private lateinit var settings : Settings
@ -43,6 +48,8 @@ class MyApp : MultiDexApplication(), DIAware {
tryToHandleBug() tryToHandleBug()
handleNotificationChannels() handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(repository))
} }
private fun handleNotificationChannels() { private fun handleNotificationChannels() {
@ -102,4 +109,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,30 +63,28 @@ 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) { items = response
items = response val mAdapter = SourcesListAdapter(
val mAdapter = SourcesListAdapter( this@SourcesActivity, items
this@SourcesActivity, items )
) binding.recyclerView.adapter = mAdapter
binding.recyclerView.adapter = mAdapter mAdapter.notifyDataSetChanged()
mAdapter.notifyDataSetChanged() if (items.isEmpty()) {
if (items.isEmpty()) {
Toast.makeText(
this@SourcesActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText( Toast.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.nothing_here,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} else {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
} }
} }

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,22 +109,20 @@ 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) // TODO: Handle failure
// TODO: Handle failure
}
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
} }
item.starred = false
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
// TODO: Handle failure
}
item.starred = true
binding.favButton.isSelected = true
} }
} }

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,21 +77,19 @@ 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) if (successfullyDeletedSource) {
if (successfullyDeletedSource) { items.removeAt(adapterPosition)
items.removeAt(adapterPosition) notifyItemRemoved(adapterPosition)
notifyItemRemoved(adapterPosition) notifyItemRangeChanged(adapterPosition, itemCount)
notifyItemRangeChanged(adapterPosition, itemCount) } else {
} else { Toast.makeText(
Toast.makeText( app,
app, R.string.can_delete_source,
R.string.can_delete_source, Toast.LENGTH_SHORT
Toast.LENGTH_SHORT ).show()
).show()
}
} }
} }
} }

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,67 +44,62 @@ 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 {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CoroutineScope(Dispatchers.IO).launch { val notification =
val notificationManager = NotificationCompat.Builder(applicationContext, Config.syncChannelId)
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager .setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val notification = notificationManager.notify(1, notification.build())
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build()) val notifyNewItems = settings.getBoolean("notify_new_items", false)
val notifyNewItems = settings.getBoolean("notify_new_items", false) db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
db = Room.databaseBuilder( val actions = db.actionsDao().actions()
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val actions = db.actionsDao().actions() actions.forEach { action ->
when {
actions.forEach { action -> action.read -> doAndReportOnFail(
when { repository.markAsReadById(action.articleId.toInt()),
action.read -> doAndReportOnFail( action
repository.markAsReadById(action.articleId.toInt()), )
action action.unread -> doAndReportOnFail(
) repository.unmarkAsReadById(action.articleId.toInt()),
action.unread -> doAndReportOnFail( action
repository.unmarkAsReadById(action.articleId.toInt()), )
action action.starred -> doAndReportOnFail(
) repository.starrById(action.articleId.toInt()),
action.starred -> doAndReportOnFail( action
repository.starrById(action.articleId.toInt()), )
action action.unstarred -> doAndReportOnFail(
) repository.unstarrById(action.articleId.toInt()),
action.unstarred -> doAndReportOnFail( action
repository.unstarrById(action.articleId.toInt()), )
action
)
}
} }
}
if (context.isNetworkAvailable()) { launch {
launch { try {
try { val newItems = repository.allItems(ItemType.UNREAD)
val newItems = repository.allItems(ItemType.UNREAD) handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager) val readItems = repository.allItems(ItemType.ALL)
val readItems = repository.allItems(ItemType.ALL) val starredItems = repository.allItems(ItemType.STARRED)
val starredItems = repository.allItems(ItemType.STARRED) // TODO: save all to DB
// TODO: save all to DB } catch (e: Throwable) {}
} catch (e: Throwable) {}
}
}
} }
} }
} }

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,9 +275,9 @@ 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()
parser.parseUrl(url).enqueue( parser.parseUrl(url).enqueue(
object : Callback<ParsedContent> { object : Callback<ParsedContent> {
@ -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

@ -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.1.0")
} }
} }
val commonTest by getting { val commonTest by getting {

View File

@ -5,16 +5,18 @@ 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>()
private val isConnectionAvailable = connectivityStatus.isNetworkConnected
var baseUrl = apiDetails.getBaseUrl() var baseUrl = apiDetails.getBaseUrl()
lateinit var dateUtils: DateUtils lateinit var dateUtils: DateUtils
@ -26,6 +28,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 +48,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
settings.getString("prefer_api_items_number", "200").toInt(), if (isNetworkAvailable()) {
offset = 0, fetchedItems = api.getItems(
tagFilter?.tag, displayedItems.type,
sourceFilter?.id?.toLong(), settings.getString("prefer_api_items_number", "200").toInt(),
searchFilter, offset = 0,
null) tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
// TODO: Get items from the database
}
if (fetchedItems != null) { if (fetchedItems != null) {
items = ArrayList(fetchedItems) items = ArrayList(fetchedItems)
@ -61,15 +71,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
val offset = items.size if (isNetworkAvailable()) {
val fetchedItems = api.getItems(displayedItems.type, val offset = items.size
settings.getString("prefer_api_items_number", "200").toInt(), fetchedItems = api.getItems(
offset, displayedItems.type,
tagFilter?.tag, settings.getString("prefer_api_items_number", "200").toInt(),
sourceFilter?.id?.toLong(), offset,
searchFilter, tagFilter?.tag,
null) sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
// TODO: Get items from the database
}
if (fetchedItems != null) { if (fetchedItems != null) {
appendItems(fetchedItems) appendItems(fetchedItems)
@ -77,8 +93,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,35 +124,52 @@ 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
val response = api.stats() if (isNetworkAvailable()) {
if (response != null) { val response = api.stats()
badgeUnread = response.unread if (response != null) {
badgeAll = response.total badgeUnread = response.unread
badgeStarred = response.starred badgeAll = response.total
success = true badgeStarred = response.starred
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) {
@ -132,8 +179,11 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun markAsReadById(id: Int): Boolean { suspend fun markAsReadById(id: Int): Boolean {
// TODO: Check internet connection var success = false
return api.markAsRead(id.toString())?.isSuccess == true if (isNetworkAvailable()) {
success = api.markAsRead(id.toString())?.isSuccess == true
}
return success
} }
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
@ -146,8 +196,11 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun unmarkAsReadById(id: Int): Boolean { suspend fun unmarkAsReadById(id: Int): Boolean {
// TODO: Check internet connection var success = false
return api.unmarkAsRead(id.toString())?.isSuccess == true if (isNetworkAvailable()) {
success = api.unmarkAsRead(id.toString())?.isSuccess == true
}
return success
} }
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
@ -160,8 +213,11 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun starrById(id: Int): Boolean { suspend fun starrById(id: Int): Boolean {
// TODO: Check success, store in DB var success = false
return api.starr(id.toString())?.isSuccess == true if (isNetworkAvailable()) {
success = api.starr(id.toString())?.isSuccess == true
}
return success
} }
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
@ -174,14 +230,19 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun unstarrById(id: Int): Boolean { suspend fun unstarrById(id: Int): Boolean {
// TODO: Check internet connection var success = false
return api.unstarr(id.toString())?.isSuccess == true if (isNetworkAvailable()) {
success = api.unstarr(id.toString())?.isSuccess == true
}
return success
} }
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
// TODO: Check Internet connectivity, store in DB
val success = api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false
if (isNetworkAvailable()) {
success = api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true
}
if (success) { if (success) {
for (item in items) { for (item in items) {
@ -230,45 +291,51 @@ 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()) {
title, response = api.createSourceForVersion(
url, title,
spout, url,
tags, spout,
filter, tags,
apiMajorVersion filter,
) 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
val response = api.deleteSource(id) if (isNetworkAvailable()) {
if (response != null) { val response = api.deleteSource(id)
success = response.isSuccess if (response != null) {
success = response.isSuccess
}
} }
return success return success
} }
suspend fun updateRemote(): Boolean { suspend fun updateRemote(): Boolean {
// TODO: Handle connectivity issues var response = false
val response = api.update() if (isConnectionAvailable.value) {
return response?.isSuccess ?: false response = api.update()?.isSuccess == true
}
return response
} }
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
try { if (isNetworkAvailable()) {
val response = api.login() try {
if (response != null && response.isSuccess) { val response = api.login()
result = true result = response?.isSuccess == true
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
} }
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(),tag = "RepositoryImpl.updateRemote")
} }
return result return result
} }
@ -287,15 +354,26 @@ 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)
val fetchedVersion = api.version()
if (fetchedVersion != null) { if (isNetworkAvailable()) {
apiMajorVersion = fetchedVersion.getApiMajorVersion() val fetchedVersion = api.version()
settings.putInt("apiVersionMajor", apiMajorVersion) if (fetchedVersion != null) {
} else { apiMajorVersion = fetchedVersion.getApiMajorVersion()
apiMajorVersion = settings.getInt("apiVersionMajor", 0) settings.putInt("apiVersionMajor", apiMajorVersion)
}
} }
} }
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
fun startNetwork() {
connectivityStatus.start()
}
fun stopNetwork() {
connectivityStatus.stop()
}
// TODO: Handle offline actions // TODO: Handle offline actions
} }